diff --git a/.circleci/config.yml b/.circleci/config.yml index 61cbe4b16a4..9c6c67f873f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,7 @@ version: 2.1 orbs: aws-cli: circleci/aws-cli@1.4.0 - browser-tools: circleci/browser-tools@1.1.1 + browser-tools: circleci/browser-tools@1.2.3 workflows: version: 2 @@ -245,8 +245,7 @@ jobs: steps: - attach_workspace: at: ~/ - - browser-tools/install-chrome: - chrome-version: 91.0.4472.164 + - browser-tools/install-chrome - run: yarn run test-render - store_test_results: path: test/integration/render-tests @@ -258,8 +257,7 @@ jobs: steps: - attach_workspace: at: ~/ - - browser-tools/install-chrome: - chrome-version: 91.0.4472.164 + - browser-tools/install-chrome - run: yarn run test-render-prod - store_test_results: path: test/integration/render-tests @@ -271,8 +269,7 @@ jobs: steps: - attach_workspace: at: ~/ - - browser-tools/install-chrome: - chrome-version: 91.0.4472.164 + - browser-tools/install-chrome - run: yarn run test-query - store_test_results: path: test/integration/query-tests @@ -299,8 +296,7 @@ jobs: steps: - attach_workspace: at: ~/ - - browser-tools/install-chrome: - chrome-version: 91.0.4472.164 + - browser-tools/install-chrome - run: name: Collect performance stats command: node bench/gl-stats.js diff --git a/CHANGELOG.md b/CHANGELOG.md index e293edb4623..7639c555494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.5.1 + +### 🐞 Bug fixes + +* Fix an iOS 15 issue where the iOS Safari tab bar interrupts touch interactions. ([#11084](https://github.com/mapbox/mapbox-gl-js/pull/11084)) + ## 2.5.0 ### Features ✨ and improvements 🏁 @@ -170,6 +176,12 @@ - Run render tests in browser. +## 1.13.2 + +### 🐞 Bug fixes + +* Backports a fix for an iOS 15 issue where the iOS Safari tab bar interrupts touch interactions. ([#11084](https://github.com/mapbox/mapbox-gl-js/pull/11084)) + ## 1.13.0 ### ✨ Features and improvements diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6ab915c1d9a..6113b2cf892 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,7 +78,7 @@ copy node_modules/headless-gl/deps/windows/dll/x64/*.dll c:\windows\system32 Start the debug server ```bash -MAPBOX_ACCESS_TOKEN={YOUR MAPBOX ACCESS TOKEN} yarn run start-debug +MAPBOX_ACCESS_TOKEN={YOUR_MAPBOX_ACCESS_TOKEN} yarn run start-debug ``` Open the debug page at [http://localhost:9966/debug](http://localhost:9966/debug) diff --git a/LICENSE.txt b/LICENSE.txt index 1054bedc9f8..e5424065cce 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -53,8 +53,10 @@ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- Contains a portion of d3-color https://github.com/d3/d3-color +Contains a portion of d3-geo https://github.com/d3/d3-geo +Contains a portion of d3-geo-projection https://github.com/d3/d3-geo-projection -Copyright 2010-2016 Mike Bostock +Copyright 2010-2021 Mike Bostock All rights reserved. Redistribution and use in source and binary forms, with or without modification, diff --git a/bench/README.md b/bench/README.md index 93bad575e05..e1647b40bc7 100644 --- a/bench/README.md +++ b/bench/README.md @@ -7,7 +7,7 @@ Benchmarks help us catch performance regressions and improve performance. Start the benchmark server ```bash -MAPBOX_ACCESS_TOKEN={YOUR MAPBOX ACCESS TOKEN} yarn start +MAPBOX_ACCESS_TOKEN={YOUR_MAPBOX_ACCESS_TOKEN} yarn start ``` To run all benchmarks, open [the benchmark page, `http://localhost:9966/bench/versions`](http://localhost:9966/bench/versions). @@ -21,7 +21,7 @@ By default, the benchmark page will compare the local branch against `main` and Start the benchmark server ```bash -MAPBOX_ACCESS_TOKEN={YOUR MAPBOX ACCESS TOKEN} MAPBOX_STYLES={YOUR STYLES HERE} yarn start +MAPBOX_ACCESS_TOKEN={YOUR_MAPBOX_ACCESS_TOKEN} MAPBOX_STYLES={YOUR STYLES HERE} yarn start ``` Note: `MAPBOX_STYLES` takes a comma-separated list of up to 3 Mapbox styles provided as a style URL or file system path (e.g. `./path/to/style.json,mapbox://styles/mapbox/streets-v10` or `mapbox://styles/mapbox/streets-v10,mapbox://styles/mapbox/streets-v9`) diff --git a/bench/benchmarks/symbol_layout.js b/bench/benchmarks/symbol_layout.js index 73e767a7239..d03ee8b4979 100644 --- a/bench/benchmarks/symbol_layout.js +++ b/bench/benchmarks/symbol_layout.js @@ -38,6 +38,7 @@ export default class SymbolLayout extends Layout { tileResult.iconMap, tileResult.imageAtlas.iconPositions, false, + this.parser.style.listImages(), tileResult.tileID.canonical, tileResult.tileZoom); } diff --git a/bench/versions/index.html b/bench/versions/index.html index 81007f00aa9..ad645d646b4 100644 --- a/bench/versions/index.html +++ b/bench/versions/index.html @@ -18,9 +18,16 @@ const params = new URLSearchParams(location.search.slice(1)); Promise.resolve(params.has('compare') ? params.getAll('compare').filter(Boolean) : - fetch('https://api.github.com/repos/mapbox/mapbox-gl-js/releases/latest') + fetch('https://api.github.com/repos/mapbox/mapbox-gl-js/releases') .then(response => response.json()) - .then(pkg => [pkg['tag_name'], 'main'])) + .then(releases => { + for (const release of releases) { + if (!release.prerelease && !release['tag_name'].includes('style-spec')) { + return [release['tag_name'], 'main']; + } + } + return ['main']; + })) .then(versions => { return versions .map(v => `https://s3.amazonaws.com/mapbox-gl-js/${v}/benchmarks.js`) diff --git a/build/generate-flow-typed-style-spec.js b/build/generate-flow-typed-style-spec.js index 2cc8ccf22ba..f174ba2bd6f 100644 --- a/build/generate-flow-typed-style-spec.js +++ b/build/generate-flow-typed-style-spec.js @@ -38,6 +38,8 @@ function flowType(property) { return 'TerrainSpecification'; case 'fog': return 'FogSpecification'; + case 'projection': + return 'ProjectionSpecification'; case 'sources': return '{[_: string]: SourceSpecification}'; case '*': @@ -185,6 +187,8 @@ ${flowObjectDeclaration('TerrainSpecification', spec.terrain)} ${flowObjectDeclaration('FogSpecification', spec.fog)} +${flowObjectDeclaration('ProjectionSpecification', spec.projection)} + ${spec.source.map(key => flowObjectDeclaration(flowSourceTypeName(key), spec[key])).join('\n\n')} export type SourceSpecification = diff --git a/build/generate-struct-arrays.js b/build/generate-struct-arrays.js index 6a3e8cf86ea..a6e14614fd4 100644 --- a/build/generate-struct-arrays.js +++ b/build/generate-struct-arrays.js @@ -118,10 +118,10 @@ function camelize (str) { global.camelize = camelize; import posAttributes from '../src/data/pos_attributes.js'; -import rasterBoundsAttributes from '../src/data/raster_bounds_attributes.js'; +import boundsAttributes from '../src/data/bounds_attributes.js'; createStructArrayType('pos', posAttributes); -createStructArrayType('raster_bounds', rasterBoundsAttributes); +createStructArrayType('raster_bounds', boundsAttributes); import circleAttributes from '../src/data/bucket/circle_attributes.js'; import fillAttributes from '../src/data/bucket/fill_attributes.js'; @@ -130,6 +130,7 @@ import lineAttributesExt from '../src/data/bucket/line_attributes_ext.js'; import patternAttributes from '../src/data/bucket/pattern_attributes.js'; import dashAttributes from '../src/data/bucket/dash_attributes.js'; import skyboxAttributes from '../src/render/skybox_attributes.js'; +import tileBoundsAttributes from '../src/data/bounds_attributes.js'; import {fillExtrusionAttributes, centroidAttributes} from '../src/data/bucket/fill_extrusion_attributes.js'; // layout vertex arrays @@ -208,6 +209,9 @@ createStructArrayType('line_strip_index', createLayout([ // skybox vertex array createStructArrayType(`skybox_vertex`, skyboxAttributes); +// tile bounds vertex array +createStructArrayType(`tile_bounds`, tileBoundsAttributes); + // paint vertex arrays // used by SourceBinder for float properties @@ -244,7 +248,6 @@ fs.writeFileSync('src/data/array_types.js', import assert from 'assert'; import {Struct, StructArray} from '../util/struct_array.js'; import {register} from '../util/web_worker_transfer.js'; -import Point from '@mapbox/point-geometry'; ${layouts.map(structArrayLayoutJs).join('\n')} ${arraysWithStructAccessors.map(structArrayJs).join('\n')} diff --git a/build/rollup_plugin_minify_style_spec.js b/build/rollup_plugin_minify_style_spec.js index c3554b4923a..10a30415590 100644 --- a/build/rollup_plugin_minify_style_spec.js +++ b/build/rollup_plugin_minify_style_spec.js @@ -16,7 +16,7 @@ export default function minifyStyleSpec() { delete spec['expression_name']; return { - code: JSON.stringify(spec, replacer, 0), + code: `export default JSON.parse('${JSON.stringify(spec, replacer, 0)}');`, map: {mappings: ''} }; } diff --git a/build/rollup_plugins.js b/build/rollup_plugins.js index dc084bea3ba..b2bdb4e8cae 100644 --- a/build/rollup_plugins.js +++ b/build/rollup_plugins.js @@ -16,7 +16,9 @@ import replace from '@rollup/plugin-replace'; export const plugins = (minified, production, test, bench) => [ flow(), minifyStyleSpec(), - json(), + json({ + exclude: 'src/style-spec/reference/v8.json' + }), production ? strip({ sourceMap: true, functions: ['PerformanceUtils.*', 'WorkerPerformanceUtils.*', 'Debug.*'] diff --git a/debug/dynamic-filter.html b/debug/dynamic-filter.html new file mode 100644 index 00000000000..b864fc07c91 --- /dev/null +++ b/debug/dynamic-filter.html @@ -0,0 +1,142 @@ + + + + Mapbox GL JS debug page + + + + + + + + + +
+
+
+ + + + + + diff --git a/debug/index.html b/debug/index.html index cffdd3eef64..9495e9cc7f2 100644 --- a/debug/index.html +++ b/debug/index.html @@ -22,7 +22,7 @@ container: 'map', zoom: 12.5, center: [-122.4194, 37.7749], - style: 'mapbox://styles/mapbox/streets-v10', + style: 'mapbox://styles/mapbox/streets-v11', hash: true }); diff --git a/debug/projections.html b/debug/projections.html new file mode 100644 index 00000000000..94754f0c925 --- /dev/null +++ b/debug/projections.html @@ -0,0 +1,189 @@ + + + + Mapbox GL JS debug page + + + + + + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + + + + diff --git a/debug/scroll_zoom_blocker.html b/debug/scroll_zoom_blocker.html index 5bb785705db..af73a356817 100644 --- a/debug/scroll_zoom_blocker.html +++ b/debug/scroll_zoom_blocker.html @@ -1,7 +1,7 @@ - Scroll Zoom Blocker Control + Cooperative Gestures @@ -12,7 +12,7 @@ -
+
@@ -24,7 +24,7 @@ zoom: 12.5, center: [-77.01866, 38.888], style: 'mapbox://styles/mapbox/streets-v10', - gestureHandling: true + cooperativeGestures: true }); map.addControl(new mapboxgl.FullscreenControl()); diff --git a/package.json b/package.json index b0c50ca5948..b3ad4d3786c 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "url": "git://github.com/mapbox/mapbox-gl-js.git" }, "dependencies": { - "@mapbox/geojson-rewind": "^0.5.0", + "@mapbox/geojson-rewind": "^0.5.1", "@mapbox/geojson-types": "^1.0.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", "@mapbox/mapbox-gl-supported": "^2.0.0", @@ -21,7 +21,7 @@ "@mapbox/vector-tile": "^1.3.1", "@mapbox/whoots-js": "^3.1.0", "csscolorparser": "~1.0.3", - "earcut": "^2.2.2", + "earcut": "^2.2.3", "geojson-vt": "^3.2.1", "gl-matrix": "^3.3.0", "grid-index": "^1.1.0", @@ -31,9 +31,9 @@ "potpack": "^1.0.1", "quickselect": "^2.0.0", "rw": "^1.3.3", - "supercluster": "^7.1.3", + "supercluster": "^7.1.4", "tinyqueue": "^2.0.3", - "vt-pbf": "^3.1.1" + "vt-pbf": "^3.1.3" }, "devDependencies": { "@babel/core": "^7.12.16", diff --git a/rollup/bundle_prelude.js b/rollup/bundle_prelude.js index fa62061ec4e..215f2a8405c 100644 --- a/rollup/bundle_prelude.js +++ b/rollup/bundle_prelude.js @@ -9,7 +9,7 @@ if (!shared) { } else if (!worker) { worker = chunk; } else { - var workerBundleString = "self.onerror = function() { console.error('An error occurred while parsing the WebWorker bundle. This is most likely due to improper transpilation by Babel; please see https://docs.mapbox.com/mapbox-gl-js/api/#transpiling-v2'); }; var sharedChunk = {}; (" + shared + ")(sharedChunk); (" + worker + ")(sharedChunk); self.onerror = null;" + var workerBundleString = "self.onerror = function() { console.error('An error occurred while parsing the WebWorker bundle. This is most likely due to improper transpilation by Babel; please see https://docs.mapbox.com/mapbox-gl-js/guides/install/#transpiling'); }; var sharedChunk = {}; (" + shared + ")(sharedChunk); (" + worker + ")(sharedChunk); self.onerror = null;" var sharedChunk = {}; shared(sharedChunk); diff --git a/src/css/mapbox-gl.css b/src/css/mapbox-gl.css index 62dbfd5142d..6a964f77476 100644 --- a/src/css/mapbox-gl.css +++ b/src/css/mapbox-gl.css @@ -770,6 +770,7 @@ a.mapboxgl-ctrl-logo.mapboxgl-compact { } } +.mapboxgl-touch-pan-blocker, .mapboxgl-scroll-zoom-blocker { color: #fff; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; @@ -789,7 +790,13 @@ a.mapboxgl-ctrl-logo.mapboxgl-compact { transition-delay: 1s; } +.mapboxgl-touch-pan-blocker-show, .mapboxgl-scroll-zoom-blocker-show { opacity: 1; transition: opacity 0.1s ease-in-out; } + +.mapboxgl-canvas-container.mapboxgl-touch-pan-blocker-override.mapboxgl-scrollable-page, +.mapboxgl-canvas-container.mapboxgl-touch-pan-blocker-override.mapboxgl-scrollable-page .mapboxgl-canvas { + touch-action: pan-x pan-y; +} diff --git a/src/data/array_types.js b/src/data/array_types.js index b14fc62d934..38d46b82217 100644 --- a/src/data/array_types.js +++ b/src/data/array_types.js @@ -815,41 +815,6 @@ class StructArrayLayout1ui2 extends StructArray { StructArrayLayout1ui2.prototype.bytesPerElement = 2; register('StructArrayLayout1ui2', StructArrayLayout1ui2); -/** - * Implementation of the StructArray layout: - * [0]: Float32[5] - * - * @private - */ -class StructArrayLayout5f20 extends StructArray { - uint8: Uint8Array; - float32: Float32Array; - - _refreshViews() { - this.uint8 = new Uint8Array(this.arrayBuffer); - this.float32 = new Float32Array(this.arrayBuffer); - } - - emplaceBack(v0: number, v1: number, v2: number, v3: number, v4: number) { - const i = this.length; - this.resize(i + 1); - return this.emplace(i, v0, v1, v2, v3, v4); - } - - emplace(i: number, v0: number, v1: number, v2: number, v3: number, v4: number) { - const o4 = i * 5; - this.float32[o4 + 0] = v0; - this.float32[o4 + 1] = v1; - this.float32[o4 + 2] = v2; - this.float32[o4 + 3] = v3; - this.float32[o4 + 4] = v4; - return i; - } -} - -StructArrayLayout5f20.prototype.bytesPerElement = 20; -register('StructArrayLayout5f20', StructArrayLayout5f20); - /** * Implementation of the StructArray layout: * [0]: Float32[2] @@ -1227,7 +1192,6 @@ export { StructArrayLayout1ul3ui12, StructArrayLayout2ui4, StructArrayLayout1ui2, - StructArrayLayout5f20, StructArrayLayout2f8, StructArrayLayout4f16, StructArrayLayout2i4 as PosArray, @@ -1251,6 +1215,6 @@ export { StructArrayLayout3ui6 as TriangleIndexArray, StructArrayLayout2ui4 as LineIndexArray, StructArrayLayout1ui2 as LineStripIndexArray, - StructArrayLayout5f20 as GlobeVertexArray, - StructArrayLayout3f12 as SkyboxVertexArray + StructArrayLayout3f12 as SkyboxVertexArray, + StructArrayLayout4i8 as TileBoundsArray }; diff --git a/src/data/raster_bounds_attributes.js b/src/data/bounds_attributes.js similarity index 100% rename from src/data/raster_bounds_attributes.js rename to src/data/bounds_attributes.js diff --git a/src/data/bucket.js b/src/data/bucket.js index b2c5c1a47d0..cc02377b070 100644 --- a/src/data/bucket.js +++ b/src/data/bucket.js @@ -9,6 +9,7 @@ import type {FeatureStates} from '../source/source_state.js'; import type {ImagePosition} from '../render/image_atlas.js'; import type LineAtlas from '../render/line_atlas.js'; import type {CanonicalTileID} from '../source/tile_id.js'; +import type {TileTransform} from '../geo/projection/tile_transform.js'; export type BucketParameters = { index: number, @@ -78,8 +79,8 @@ export interface Bucket { +layers: Array; +stateDependentLayers: Array; +stateDependentLayerIds: Array; - populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID): void; - update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: {[_: string]: ImagePosition}): void; + populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID, tileTransform: TileTransform): void; + update(states: FeatureStates, vtLayer: VectorTileLayer, availableImages: Array, imagePositions: {[_: string]: ImagePosition}): void; isEmpty(): boolean; upload(context: Context): void; diff --git a/src/data/bucket/circle_bucket.js b/src/data/bucket/circle_bucket.js index b350d64f0ff..8193e806376 100644 --- a/src/data/bucket/circle_bucket.js +++ b/src/data/bucket/circle_bucket.js @@ -28,6 +28,7 @@ import type VertexBuffer from '../../gl/vertex_buffer.js'; import type Point from '@mapbox/point-geometry'; import type {FeatureStates} from '../../source/source_state.js'; import type {ImagePosition} from '../../render/image_atlas.js'; +import type {TileTransform} from '../../geo/projection/tile_transform.js'; function addCircleVertex(layoutVertexArray, x, y, extrudeX, extrudeY) { layoutVertexArray.emplaceBack( @@ -77,7 +78,7 @@ class CircleBucket implements Bucke this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id); } - populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID) { + populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID, tileTransform: TileTransform) { const styleLayer = this.layers[0]; const bucketFeatures = []; let circleSortKey = null; @@ -103,7 +104,7 @@ class CircleBucket implements Bucke type: feature.type, sourceLayerIndex, index, - geometry: needGeometry ? evaluationFeature.geometry : loadGeometry(feature), + geometry: needGeometry ? evaluationFeature.geometry : loadGeometry(feature, canonical, tileTransform), patterns: {}, sortKey }; @@ -123,14 +124,14 @@ class CircleBucket implements Bucke const {geometry, index, sourceLayerIndex} = bucketFeature; const feature = features[index].feature; - this.addFeature(bucketFeature, geometry, index, canonical); + this.addFeature(bucketFeature, geometry, index, options.availableImages, canonical); options.featureIndex.insert(feature, geometry, index, sourceLayerIndex, this.index); } } - update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: {[_: string]: ImagePosition}) { + update(states: FeatureStates, vtLayer: VectorTileLayer, availableImages: Array, imagePositions: {[_: string]: ImagePosition}) { if (!this.stateDependentLayers.length) return; - this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers, imagePositions); + this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers, availableImages, imagePositions); } isEmpty() { @@ -158,7 +159,7 @@ class CircleBucket implements Bucke this.segments.destroy(); } - addFeature(feature: BucketFeature, geometry: Array>, index: number, canonical: CanonicalTileID) { + addFeature(feature: BucketFeature, geometry: Array>, index: number, availableImages: Array, canonical: CanonicalTileID) { for (const ring of geometry) { for (const point of ring) { const x = point.x; @@ -192,7 +193,7 @@ class CircleBucket implements Bucke } } - this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, {}, canonical); + this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, {}, availableImages, canonical); } } diff --git a/src/data/bucket/fill_bucket.js b/src/data/bucket/fill_bucket.js index bf73ff00393..57c9b84b356 100644 --- a/src/data/bucket/fill_bucket.js +++ b/src/data/bucket/fill_bucket.js @@ -31,6 +31,7 @@ import type VertexBuffer from '../../gl/vertex_buffer.js'; import type Point from '@mapbox/point-geometry'; import type {FeatureStates} from '../../source/source_state.js'; import type {ImagePosition} from '../../render/image_atlas.js'; +import type {TileTransform} from '../../geo/projection/tile_transform.js'; class FillBucket implements Bucket { index: number; @@ -75,7 +76,7 @@ class FillBucket implements Bucket { this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id); } - populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID) { + populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID, tileTransform: TileTransform) { this.hasPattern = hasPattern('fill', this.layers, options); const fillSortKey = this.layers[0].layout.get('fill-sort-key'); const bucketFeatures = []; @@ -96,7 +97,7 @@ class FillBucket implements Bucket { type: feature.type, sourceLayerIndex, index, - geometry: needGeometry ? evaluationFeature.geometry : loadGeometry(feature), + geometry: needGeometry ? evaluationFeature.geometry : loadGeometry(feature, canonical, tileTransform), patterns: {}, sortKey }; @@ -120,7 +121,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, canonical, {}); + this.addFeature(bucketFeature, geometry, index, canonical, {}, options.availableImages); } const feature = features[index].feature; @@ -128,14 +129,14 @@ class FillBucket implements Bucket { } } - update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: {[_: string]: ImagePosition}) { + update(states: FeatureStates, vtLayer: VectorTileLayer, availableImages: Array, imagePositions: {[_: string]: ImagePosition}) { if (!this.stateDependentLayers.length) return; - this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers, imagePositions); + this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers, availableImages, imagePositions); } - addFeatures(options: PopulateParameters, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) { + addFeatures(options: PopulateParameters, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}, availableImages: Array) { for (const feature of this.patternFeatures) { - this.addFeature(feature, feature.geometry, feature.index, canonical, imagePositions); + this.addFeature(feature, feature.geometry, feature.index, canonical, imagePositions, availableImages); } } @@ -166,7 +167,7 @@ class FillBucket implements Bucket { this.segments2.destroy(); } - addFeature(feature: BucketFeature, geometry: Array>, index: number, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) { + addFeature(feature: BucketFeature, geometry: Array>, index: number, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}, availableImages: Array = []) { for (const polygon of classifyRings(geometry, EARCUT_MAX_RINGS)) { let numVertices = 0; for (const ring of polygon) { @@ -220,7 +221,7 @@ class FillBucket implements Bucket { triangleSegment.vertexLength += numVertices; triangleSegment.primitiveLength += indices.length / 3; } - this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, imagePositions, canonical); + this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, imagePositions, availableImages, canonical); } } diff --git a/src/data/bucket/fill_extrusion_bucket.js b/src/data/bucket/fill_extrusion_bucket.js index 703a86c91de..3c2d38cd53b 100644 --- a/src/data/bucket/fill_extrusion_bucket.js +++ b/src/data/bucket/fill_extrusion_bucket.js @@ -36,6 +36,7 @@ import type IndexBuffer from '../../gl/index_buffer.js'; import type VertexBuffer from '../../gl/vertex_buffer.js'; import type {FeatureStates} from '../../source/source_state.js'; import type {ImagePosition} from '../../render/image_atlas.js'; +import type {TileTransform} from '../../geo/projection/tile_transform.js'; const FACTOR = Math.pow(2, 13); @@ -216,7 +217,7 @@ class FillExtrusionBucket implements Bucket { this.enableTerrain = options.enableTerrain; } - populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID) { + populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID, tileTransform: TileTransform) { this.features = []; this.hasPattern = hasPattern('fill-extrusion', this.layers, options); this.featuresOnBorder = []; @@ -234,7 +235,7 @@ class FillExtrusionBucket implements Bucket { id, sourceLayerIndex, index, - geometry: needGeometry ? evaluationFeature.geometry : loadGeometry(feature), + geometry: needGeometry ? evaluationFeature.geometry : loadGeometry(feature, canonical, tileTransform), properties: feature.properties, type: feature.type, patterns: {} @@ -244,7 +245,7 @@ class FillExtrusionBucket implements Bucket { if (this.hasPattern) { this.features.push(addPatternDependencies('fill-extrusion', this.layers, bucketFeature, this.zoom, options)); } else { - this.addFeature(bucketFeature, bucketFeature.geometry, index, canonical, {}); + this.addFeature(bucketFeature, bucketFeature.geometry, index, canonical, {}, options.availableImages); } options.featureIndex.insert(feature, bucketFeature.geometry, index, sourceLayerIndex, this.index, vertexArrayOffset); @@ -252,17 +253,17 @@ class FillExtrusionBucket implements Bucket { this.sortBorders(); } - addFeatures(options: PopulateParameters, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) { + addFeatures(options: PopulateParameters, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}, availableImages: Array) { for (const feature of this.features) { const {geometry} = feature; - this.addFeature(feature, geometry, feature.index, canonical, imagePositions); + this.addFeature(feature, geometry, feature.index, canonical, imagePositions, availableImages); } this.sortBorders(); } - update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: {[_: string]: ImagePosition}) { + update(states: FeatureStates, vtLayer: VectorTileLayer, availableImages: Array, imagePositions: {[_: string]: ImagePosition}) { if (!this.stateDependentLayers.length) return; - this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers, imagePositions); + this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers, availableImages, imagePositions); } isEmpty() { @@ -301,7 +302,7 @@ class FillExtrusionBucket implements Bucket { this.segments.destroy(); } - addFeature(feature: BucketFeature, geometry: Array>, index: number, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) { + addFeature(feature: BucketFeature, geometry: Array>, index: number, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}, availableImages: Array) { const metadata = this.enableTerrain ? new PartMetadata() : null; for (const polygon of classifyRings(geometry, EARCUT_MAX_RINGS)) { @@ -432,7 +433,7 @@ class FillExtrusionBucket implements Bucket { assert(!this.centroidVertexArray.length || this.centroidVertexArray.length === this.layoutVertexArray.length); } - this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, imagePositions, canonical); + this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, imagePositions, availableImages, canonical); } sortBorders() { diff --git a/src/data/bucket/line_bucket.js b/src/data/bucket/line_bucket.js index 4dfd299598c..99039cf969d 100644 --- a/src/data/bucket/line_bucket.js +++ b/src/data/bucket/line_bucket.js @@ -35,6 +35,7 @@ import type VertexBuffer from '../../gl/vertex_buffer.js'; import type {FeatureStates} from '../../source/source_state.js'; import type {ImagePosition} from '../../render/image_atlas.js'; import type LineAtlas from '../../render/line_atlas.js'; +import type {TileTransform} from '../../geo/projection/tile_transform.js'; // NOTE ON EXTRUDE SCALE: // scale the extrusion vector so that the normal length is this value. @@ -134,7 +135,7 @@ class LineBucket implements Bucket { this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id); } - populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID) { + populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID, tileTransform: TileTransform) { this.hasPattern = hasPattern('line', this.layers, options); const lineSortKey = this.layers[0].layout.get('line-sort-key'); const bucketFeatures = []; @@ -155,7 +156,7 @@ class LineBucket implements Bucket { type: feature.type, sourceLayerIndex, index, - geometry: needGeometry ? evaluationFeature.geometry : loadGeometry(feature), + geometry: needGeometry ? evaluationFeature.geometry : loadGeometry(feature, canonical, tileTransform), patterns: {}, sortKey }; @@ -187,7 +188,7 @@ class LineBucket implements Bucket { this.patternFeatures.push(patternBucketFeature); } else { - this.addFeature(bucketFeature, geometry, index, canonical, lineAtlas.positions); + this.addFeature(bucketFeature, geometry, index, canonical, lineAtlas.positions, options.availableImages); } const feature = features[index].feature; @@ -266,14 +267,14 @@ class LineBucket implements Bucket { } - update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: {[_: string]: ImagePosition}) { + update(states: FeatureStates, vtLayer: VectorTileLayer, availableImages: Array, imagePositions: {[_: string]: ImagePosition}) { if (!this.stateDependentLayers.length) return; - this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers, imagePositions); + this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers, availableImages, imagePositions); } - addFeatures(options: PopulateParameters, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) { + addFeatures(options: PopulateParameters, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}, availableImages: Array) { for (const feature of this.patternFeatures) { - this.addFeature(feature, feature.geometry, feature.index, canonical, imagePositions); + this.addFeature(feature, feature.geometry, feature.index, canonical, imagePositions, availableImages); } } @@ -313,7 +314,7 @@ class LineBucket implements Bucket { } } - addFeature(feature: BucketFeature, geometry: Array>, index: number, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) { + addFeature(feature: BucketFeature, geometry: Array>, index: number, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}, availableImages: Array) { const layout = this.layers[0].layout; const join = layout.get('line-join').evaluate(feature, {}); const cap = layout.get('line-cap').evaluate(feature, {}); @@ -325,7 +326,7 @@ class LineBucket implements Bucket { this.addLine(line, feature, join, cap, miterLimit, roundLimit); } - this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, imagePositions, canonical); + this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, imagePositions, availableImages, canonical); } addLine(vertices: Array, feature: BucketFeature, join: string, cap: string, miterLimit: number, roundLimit: number) { diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index b7e836e7792..77977ebcb81 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -61,6 +61,7 @@ import type {SymbolQuad} from '../../symbol/quads.js'; import type {SizeData} from '../../symbol/symbol_size.js'; import type {FeatureStates} from '../../source/source_state.js'; import type {ImagePosition} from '../../render/image_atlas.js'; +import type {TileTransform} from '../../geo/projection/tile_transform.js'; export type SingleCollisionBox = { x1: number; y1: number; @@ -425,7 +426,7 @@ class SymbolBucket implements Bucket { } } - populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID) { + populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID, tileTransform: TileTransform) { const layer = this.layers[0]; const layout = layer.layout; @@ -463,7 +464,7 @@ class SymbolBucket implements Bucket { continue; } - if (!needGeometry) evaluationFeature.geometry = loadGeometry(feature); + if (!needGeometry) evaluationFeature.geometry = loadGeometry(feature, canonical, tileTransform); let text: Formatted | void; if (hasText) { @@ -553,10 +554,10 @@ class SymbolBucket implements Bucket { } } - update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: {[_: string]: ImagePosition}) { + update(states: FeatureStates, vtLayer: VectorTileLayer, availableImages: Array, imagePositions: {[_: string]: ImagePosition}) { if (!this.stateDependentLayers.length) return; - this.text.programConfigurations.updatePaintArrays(states, vtLayer, this.layers, imagePositions); - this.icon.programConfigurations.updatePaintArrays(states, vtLayer, this.layers, imagePositions); + this.text.programConfigurations.updatePaintArrays(states, vtLayer, this.layers, availableImages, imagePositions); + this.icon.programConfigurations.updatePaintArrays(states, vtLayer, this.layers, availableImages, imagePositions); } isEmpty() { @@ -634,6 +635,7 @@ class SymbolBucket implements Bucket { lineStartIndex: number, lineLength: number, associatedIconIndex: number, + availableImages: Array, canonical: CanonicalTileID) { const indexArray = arrays.indexArray; const layoutVertexArray = arrays.layoutVertexArray; @@ -667,7 +669,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, {}, canonical, sections && sections[sectionIndex]); + arrays.programConfigurations.populatePaintArrays(layoutVertexArray.length, feature, feature.index, {}, availableImages, canonical, sections && sections[sectionIndex]); } } diff --git a/src/data/feature_index.js b/src/data/feature_index.js index f1bf5c0e639..8f13d52b33f 100644 --- a/src/data/feature_index.js +++ b/src/data/feature_index.js @@ -27,11 +27,13 @@ import type Transform from '../geo/transform.js'; import type {FilterSpecification, PromoteIdSpecification} from '../style-spec/types.js'; import type {TilespaceQueryGeometry} from '../style/query_geometry.js'; import type {FeatureIndex as FeatureIndexStruct} from './array_types.js'; +import type {TileTransform} from '../geo/projection/tile_transform.js'; type QueryParameters = { pixelPosMatrix: Float32Array, transform: Transform, tileResult: TilespaceQueryGeometry, + tileTransform: TileTransform, params: { filter: FilterSpecification, layers: Array, @@ -59,6 +61,7 @@ class FeatureIndex { bucketLayerIDs: Array>; vtLayers: {[_: string]: VectorTileLayer}; + vtFeatures: {[_: string]: VectorTileFeature[]}; sourceLayerCoder: DictionaryCoder; constructor(tileID: OverscaledTileID, promoteId?: ?PromoteIdSpecification) { @@ -102,6 +105,10 @@ class FeatureIndex { if (!this.vtLayers) { this.vtLayers = new vt.VectorTile(new Protobuf(this.rawTileData)).layers; this.sourceLayerCoder = new DictionaryCoder(this.vtLayers ? Object.keys(this.vtLayers).sort() : ['_geojsonTileLayer']); + this.vtFeatures = {}; + for (const layer in this.vtLayers) { + this.vtFeatures[layer] = []; + } } return this.vtLayers; } @@ -148,7 +155,7 @@ class FeatureIndex { sourceFeatureState, (feature: VectorTileFeature, styleLayer: StyleLayer, featureState: Object, layoutVertexArrayOffset: number = 0) => { if (!featureGeometry) { - featureGeometry = loadGeometry(feature); + featureGeometry = loadGeometry(feature, this.tileID.canonical, args.tileTransform); } return styleLayer.queryIntersectsFeature(tilespaceGeometry, feature, featureState, featureGeometry, this.z, args.transform, args.pixelPosMatrix, elevationHelper, layoutVertexArrayOffset); @@ -262,6 +269,23 @@ class FeatureIndex { return result; } + loadFeature(featureIndexData: FeatureIndices): VectorTileFeature { + const {featureIndex, sourceLayerIndex} = featureIndexData; + + this.loadVTLayers(); + const sourceLayerName = this.sourceLayerCoder.decode(sourceLayerIndex); + + const featureCache = this.vtFeatures[sourceLayerName]; + if (featureCache[featureIndex]) { + return featureCache[featureIndex]; + } + const sourceLayer = this.vtLayers[sourceLayerName]; + const feature = sourceLayer.feature(featureIndex); + featureCache[featureIndex] = feature; + + return feature; + } + hasLayer(id: string) { for (const layerIDs of this.bucketLayerIDs) { for (const layerID of layerIDs) { diff --git a/src/data/load_geometry.js b/src/data/load_geometry.js index e85cb7324c3..64c3ed93f60 100644 --- a/src/data/load_geometry.js +++ b/src/data/load_geometry.js @@ -3,8 +3,12 @@ import {warnOnce, clamp} from '../util/util.js'; import EXTENT from './extent.js'; +import {lngFromMercatorX, latFromMercatorY} from '../geo/mercator_coordinate.js'; +import resample from '../geo/projection/resample.js'; +import Point from '@mapbox/point-geometry'; -import type Point from '@mapbox/point-geometry'; +import type {CanonicalTileID} from '../source/tile_id.js'; +import type {TileTransform} from '../geo/projection/tile_transform.js'; // These bounds define the minimum and maximum supported coordinate values. // While visible coordinates are within [0, EXTENT], tiles may theoretically @@ -14,33 +18,61 @@ const BITS = 15; const MAX = Math.pow(2, BITS - 1) - 1; const MIN = -MAX - 1; +function clampPoint(point: Point) { + const {x, y} = point; + point.x = clamp(x, MIN, MAX); + point.y = clamp(y, MIN, MAX); + if (x < point.x || x > point.x + 1 || y < point.y || y > point.y + 1) { + // warn when exceeding allowed extent except for the 1-px-off case + // https://github.com/mapbox/mapbox-gl-js/issues/8992 + warnOnce('Geometry exceeds allowed extent, reduce your vector tile buffer size'); + } + return point; +} + +// a subset of VectorTileGeometry +type FeatureWithGeometry = { + extent: number; + type: 1 | 2 | 3; + loadGeometry(): Array>; +} + /** * Loads a geometry from a VectorTileFeature and scales it to the common extent * used internally. * @param {VectorTileFeature} feature * @private */ -export default function loadGeometry(feature: VectorTileFeature): Array> { - const scale = EXTENT / feature.extent; - const geometry = feature.loadGeometry(); - for (let r = 0; r < geometry.length; r++) { - const ring = geometry[r]; - for (let p = 0; p < ring.length; p++) { - const point = ring[p]; - // round here because mapbox-gl-native uses integers to represent - // points and we need to do the same to avoid rendering differences. - const x = Math.round(point.x * scale); - const y = Math.round(point.y * scale); - - point.x = clamp(x, MIN, MAX); - point.y = clamp(y, MIN, MAX); - - if (x < point.x || x > point.x + 1 || y < point.y || y > point.y + 1) { - // warn when exceeding allowed extent except for the 1-px-off case - // https://github.com/mapbox/mapbox-gl-js/issues/8992 - warnOnce('Geometry exceeds allowed extent, reduce your vector tile buffer size'); - } +export default function loadGeometry(feature: FeatureWithGeometry, canonical?: CanonicalTileID, tileTransform?: TileTransform): Array> { + const featureExtent = feature.extent; + const scale = EXTENT / featureExtent; + const projection = tileTransform ? tileTransform.projection : undefined; + const isMercator = !projection || projection.name === 'mercator'; + + function reproject(p) { + if (isMercator || !canonical || !tileTransform || !projection) { + return new Point(p.x * scale, p.y * scale); + } else { + const z2 = 1 << canonical.z; + const lng = lngFromMercatorX((canonical.x + p.x / featureExtent) / z2); + const lat = latFromMercatorY((canonical.y + p.y / featureExtent) / z2); + const {x, y} = projection.project(lng, lat); + return new Point( + (x * tileTransform.scale - tileTransform.x) * EXTENT, + (y * tileTransform.scale - tileTransform.y) * EXTENT + ); } } + + const geometry = feature.loadGeometry(); + + for (let i = 0; i < geometry.length; i++) { + geometry[i] = !isMercator && feature.type !== 1 ? + resample(geometry[i], reproject, 1) : + geometry[i].map(reproject); + + geometry[i].forEach(p => clampPoint(p._round())); + } + return geometry; } diff --git a/src/data/program_configuration.js b/src/data/program_configuration.js index dc8c0602ee2..f4605127351 100644 --- a/src/data/program_configuration.js +++ b/src/data/program_configuration.js @@ -80,8 +80,8 @@ function packColor(color: Color): [number, number] { */ interface AttributeBinder { - 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; + populatePaintArray(length: number, feature: Feature, imagePositions: {[_: string]: ImagePosition}, availableImages: Array, canonical?: CanonicalTileID, formattedSection?: FormattedSection): void; + updatePaintArray(start: number, length: number, feature: Feature, featureState: FeatureState, availableImages: Array, imagePositions: {[_: string]: ImagePosition}): void; upload(Context): void; destroy(): void; } @@ -174,15 +174,16 @@ class SourceExpressionBinder implements AttributeBinder { this.paintVertexArray = new PaintVertexArray(); } - populatePaintArray(newLength: number, feature: Feature, imagePositions: {[_: string]: ImagePosition}, canonical?: CanonicalTileID, formattedSection?: FormattedSection) { + populatePaintArray(newLength: number, feature: Feature, imagePositions: {[_: string]: ImagePosition}, availableImages: Array, canonical?: CanonicalTileID, formattedSection?: FormattedSection) { const start = this.paintVertexArray.length; - const value = this.expression.evaluate(new EvaluationParameters(0), feature, {}, canonical, [], formattedSection); + assert(Array.isArray(availableImages)); + const value = this.expression.evaluate(new EvaluationParameters(0), feature, {}, canonical, availableImages, formattedSection); this.paintVertexArray.resize(newLength); this._setPaintValue(start, newLength, value); } - updatePaintArray(start: number, end: number, feature: Feature, featureState: FeatureState) { - const value = this.expression.evaluate({zoom: 0}, feature, featureState); + updatePaintArray(start: number, end: number, feature: Feature, featureState: FeatureState, availableImages: Array) { + const value = this.expression.evaluate({zoom: 0}, feature, featureState, undefined, availableImages); this._setPaintValue(start, end, value); } @@ -245,17 +246,17 @@ class CompositeExpressionBinder implements AttributeBinder, UniformBinder { this.paintVertexArray = new PaintVertexArray(); } - 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); + populatePaintArray(newLength: number, feature: Feature, imagePositions: {[_: string]: ImagePosition}, availableImages: Array, canonical?: CanonicalTileID, formattedSection?: FormattedSection) { + const min = this.expression.evaluate(new EvaluationParameters(this.zoom), feature, {}, canonical, availableImages, formattedSection); + const max = this.expression.evaluate(new EvaluationParameters(this.zoom + 1), feature, {}, canonical, availableImages, formattedSection); const start = this.paintVertexArray.length; this.paintVertexArray.resize(newLength); this._setPaintValue(start, newLength, min, max); } - updatePaintArray(start: number, end: number, feature: Feature, featureState: FeatureState) { - const min = this.expression.evaluate({zoom: this.zoom}, feature, featureState); - const max = this.expression.evaluate({zoom: this.zoom + 1}, feature, featureState); + updatePaintArray(start: number, end: number, feature: Feature, featureState: FeatureState, availableImages: Array) { + const min = this.expression.evaluate({zoom: this.zoom}, feature, featureState, undefined, availableImages); + const max = this.expression.evaluate({zoom: this.zoom + 1}, feature, featureState, undefined, availableImages); this._setPaintValue(start, end, min, max); } @@ -337,7 +338,7 @@ class CrossFadedCompositeBinder implements AttributeBinder { this._setPaintValues(start, length, feature.patterns && feature.patterns[this.layerId], imagePositions); } - updatePaintArray(start: number, end: number, feature: Feature, featureState: FeatureState, imagePositions: {[_: string]: ImagePosition}) { + updatePaintArray(start: number, end: number, feature: Feature, featureState: FeatureState, availableImages: Array, imagePositions: {[_: string]: ImagePosition}) { this._setPaintValues(start, end, feature.patterns && feature.patterns[this.layerId], imagePositions); } @@ -455,11 +456,11 @@ export default class ProgramConfiguration { return binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder ? binder.maxValue : 0; } - populatePaintArrays(newLength: number, feature: Feature, imagePositions: {[_: string]: ImagePosition}, canonical?: CanonicalTileID, formattedSection?: FormattedSection) { + populatePaintArrays(newLength: number, feature: Feature, imagePositions: {[_: string]: ImagePosition}, availableImages: Array, 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, canonical, formattedSection); + (binder: AttributeBinder).populatePaintArray(newLength, feature, imagePositions, availableImages, canonical, formattedSection); } } setConstantPatternPositions(posTo: ImagePosition, posFrom: ImagePosition) { @@ -470,7 +471,7 @@ export default class ProgramConfiguration { } } - updatePaintArrays(featureStates: FeatureStates, featureMap: FeaturePositionMap, vtLayer: VectorTileLayer, layer: TypedStyleLayer, imagePositions: {[_: string]: ImagePosition}): boolean { + updatePaintArrays(featureStates: FeatureStates, featureMap: FeaturePositionMap, vtLayer: VectorTileLayer, layer: TypedStyleLayer, availableImages: Array, imagePositions: {[_: string]: ImagePosition}): boolean { let dirty: boolean = false; for (const id in featureStates) { const positions = featureMap.getPositions(id); @@ -485,7 +486,7 @@ export default class ProgramConfiguration { //AHM: Remove after https://github.com/mapbox/mapbox-gl-js/issues/6255 const value = layer.paint.get(property); (binder: any).expression = value.value; - (binder: AttributeBinder).updatePaintArray(pos.start, pos.end, feature, featureStates[id], imagePositions); + (binder: AttributeBinder).updatePaintArray(pos.start, pos.end, feature, featureStates[id], availableImages, imagePositions); dirty = true; } } @@ -608,9 +609,9 @@ export class ProgramConfigurationSet { this._bufferOffset = 0; } - populatePaintArrays(length: number, feature: Feature, index: number, imagePositions: {[_: string]: ImagePosition}, canonical: CanonicalTileID, formattedSection?: FormattedSection) { + populatePaintArrays(length: number, feature: Feature, index: number, imagePositions: {[_: string]: ImagePosition}, availableImages: Array, canonical: CanonicalTileID, formattedSection?: FormattedSection) { for (const key in this.programConfigurations) { - this.programConfigurations[key].populatePaintArrays(length, feature, imagePositions, canonical, formattedSection); + this.programConfigurations[key].populatePaintArrays(length, feature, imagePositions, availableImages, canonical, formattedSection); } if (feature.id !== undefined) { @@ -621,9 +622,9 @@ export class ProgramConfigurationSet { this.needsUpload = true; } - updatePaintArrays(featureStates: FeatureStates, vtLayer: VectorTileLayer, layers: $ReadOnlyArray, imagePositions: {[_: string]: ImagePosition}) { + updatePaintArrays(featureStates: FeatureStates, vtLayer: VectorTileLayer, layers: $ReadOnlyArray, availableImages: Array, imagePositions: {[_: string]: ImagePosition}) { for (const layer of layers) { - this.needsUpload = this.programConfigurations[layer.id].updatePaintArrays(featureStates, this._featureMap, vtLayer, layer, imagePositions) || this.needsUpload; + this.needsUpload = this.programConfigurations[layer.id].updatePaintArrays(featureStates, this._featureMap, vtLayer, layer, availableImages, imagePositions) || this.needsUpload; } } diff --git a/src/geo/lng_lat.js b/src/geo/lng_lat.js index 1315243fe75..f071ddb4af8 100644 --- a/src/geo/lng_lat.js +++ b/src/geo/lng_lat.js @@ -12,10 +12,9 @@ export const earthRadius = 6371008.8; /** * A `LngLat` object represents a given longitude and latitude coordinate, measured in degrees. - * These coordinates are based on the [WGS84 (EPSG:4326) standard](https://en.wikipedia.org/wiki/World_Geodetic_System#WGS84). - * - * Mapbox GL uses longitude, latitude coordinate order (as opposed to latitude, longitude) to match the - * [GeoJSON specification](https://tools.ietf.org/html/rfc7946). + * These coordinates use longitude, latitude coordinate order (as opposed to latitude, longitude) + * to match the [GeoJSON specification](https://datatracker.ietf.org/doc/html/rfc7946#section-4), + * which is equivalent to the OGC:CRS84 coordinate reference system. * * Note that any Mapbox GL method that accepts a `LngLat` object as an argument or option * can also accept an `Array` of two numbers and will perform an implicit conversion. diff --git a/src/geo/mercator_coordinate.js b/src/geo/mercator_coordinate.js index cb952ce89aa..1cc9d4f9aa8 100644 --- a/src/geo/mercator_coordinate.js +++ b/src/geo/mercator_coordinate.js @@ -6,13 +6,13 @@ import type {LngLatLike} from '../geo/lng_lat.js'; /* * The average circumference of the world in meters. */ -const earthCircumfrence = 2 * Math.PI * earthRadius; // meters +const earthCircumference = 2 * Math.PI * earthRadius; // meters /* * The circumference at a line of latitude in meters. */ function circumferenceAtLatitude(latitude: number) { - return earthCircumfrence * Math.cos(latitude * Math.PI / 180); + return earthCircumference * Math.cos(latitude * Math.PI / 180); } export function mercatorXfromLng(lng: number) { @@ -40,6 +40,8 @@ export function altitudeFromMercatorZ(z: number, y: number) { return z * circumferenceAtLatitude(latFromMercatorY(y)); } +export const MAX_MERCATOR_LATITUDE = 85.051129; + /** * Determine the Mercator scale factor for a given latitude, see * https://en.wikipedia.org/wiki/Mercator_projection#Scale_factor @@ -148,7 +150,7 @@ class MercatorCoordinate { */ meterInMercatorCoordinateUnits() { // 1 meter / circumference at equator in meters * Mercator projection scale factor at this latitude - return 1 / earthCircumfrence * mercatorScale(latFromMercatorY(this.y)); + return 1 / earthCircumference * mercatorScale(latFromMercatorY(this.y)); } } diff --git a/src/geo/projection/adjustments.js b/src/geo/projection/adjustments.js new file mode 100644 index 00000000000..2b834012114 --- /dev/null +++ b/src/geo/projection/adjustments.js @@ -0,0 +1,147 @@ +// @flow + +import LngLat from '../lng_lat.js'; +import MercatorCoordinate, {MAX_MERCATOR_LATITUDE} from '../mercator_coordinate.js'; +import {mat4, mat2} from 'gl-matrix'; +import {clamp} from '../../util/util.js'; +import type {Projection} from './index.js'; +import type Transform from '../transform.js'; + +export default function getProjectionAdjustments(transform: Transform, withoutRotation?: boolean) { + const projection = transform.projection; + + const interpT = getInterpolationT(transform); + + const zoomAdjustment = getZoomAdjustment(projection, transform.center); + const zoomAdjustmentOrigin = getZoomAdjustment(projection, LngLat.convert(projection.center)); + const scaleAdjustment = Math.pow(2, zoomAdjustment * interpT + (1 - interpT) * zoomAdjustmentOrigin); + + const matrix = getShearAdjustment(transform.projection, transform.zoom, transform.center, interpT, withoutRotation); + + mat4.scale(matrix, matrix, [scaleAdjustment, scaleAdjustment, 1]); + + return matrix; +} + +export function getProjectionAdjustmentInverted(transform: Transform) { + const m = getProjectionAdjustments(transform, true); + return mat2.invert([], [ + m[0], m[1], + m[4], m[5]]); +} + +function getInterpolationT(transform: Transform) { + const range = transform.projection.range; + if (!range) return 0; + + const size = Math.max(transform.width, transform.height); + // The interpolation ranges are manually defined based on what makes + // sense in a 1024px wide map. Adjust the ranges to the current size + // of the map. The smaller the map, the earlier you can start unskewing. + const rangeAdjustment = Math.log(size / 1024) / Math.LN2; + const zoomA = range[0] + rangeAdjustment; + const zoomB = range[1] + rangeAdjustment; + const t = clamp((transform.zoom - zoomA) / (zoomB - zoomA), 0, 1); + return t; +} + +// approx. kilometers per longitude degree at equator +const offset = 1 / 40000; + +/* + * Calculates the scale difference between Mercator and the given projection at a certain location. + */ +function getZoomAdjustment(projection: Projection, loc: LngLat) { + // make sure we operate within mercator space for adjustments (they can go over for other projections) + const lat = clamp(loc.lat, -MAX_MERCATOR_LATITUDE, MAX_MERCATOR_LATITUDE); + + const loc1 = new LngLat(loc.lng - 180 * offset, lat); + const loc2 = new LngLat(loc.lng + 180 * offset, lat); + + const p1 = projection.project(loc1.lng, lat); + const p2 = projection.project(loc2.lng, lat); + + const m1 = MercatorCoordinate.fromLngLat(loc1); + const m2 = MercatorCoordinate.fromLngLat(loc2); + + const pdx = p2.x - p1.x; + const pdy = p2.y - p1.y; + const mdx = m2.x - m1.x; + const mdy = m2.y - m1.y; + + const scale = Math.sqrt((mdx * mdx + mdy * mdy) / (pdx * pdx + pdy * pdy)); + + return Math.log(scale) / Math.LN2; +} + +function getShearAdjustment(projection, zoom, loc, interpT, withoutRotation?: boolean) { + + // create two locations a tiny amount (~1km) east and west of the given location + const locw = new LngLat(loc.lng - 180 * offset, loc.lat); + const loce = new LngLat(loc.lng + 180 * offset, loc.lat); + + const pw = projection.project(locw.lng, locw.lat); + const pe = projection.project(loce.lng, loce.lat); + + const pdx = pe.x - pw.x; + const pdy = pe.y - pw.y; + + // Calculate how much the map would need to be rotated to make east-west in + // projected coordinates be left-right + const angleAdjust = -Math.atan(pdy / pdx); + + // Pick a location identical to the original one except for poles to make sure we're within mercator bounds + const mc2 = MercatorCoordinate.fromLngLat(loc); + mc2.y = clamp(mc2.y, -1 + offset, 1 - offset); + const loc2 = mc2.toLngLat(); + const p2 = projection.project(loc2.lng, loc2.lat); + + // Find the projected coordinates of two locations, one slightly south and one slightly east. + // Then calculate the transform that would make the projected coordinates of the two locations be: + // - equal distances from the original location + // - perpendicular to one another + // + // Only the position of the coordinate to the north is adjusted. + // The coordinate to the east stays where it is. + const mc3 = MercatorCoordinate.fromLngLat(loc2); + mc3.x += offset; + const loc3 = mc3.toLngLat(); + const p3 = projection.project(loc3.lng, loc3.lat); + const pdx3 = p3.x - p2.x; + const pdy3 = p3.y - p2.y; + const delta3 = rotate(pdx3, pdy3, angleAdjust); + + const mc4 = MercatorCoordinate.fromLngLat(loc2); + mc4.y += offset; + const loc4 = mc4.toLngLat(); + const p4 = projection.project(loc4.lng, loc4.lat); + const pdx4 = p4.x - p2.x; + const pdy4 = p4.y - p2.y; + const delta4 = rotate(pdx4, pdy4, angleAdjust); + + const scale = Math.abs(delta3.x) / Math.abs(delta4.y); + + const unrotate = mat4.identity([]); + mat4.rotateZ(unrotate, unrotate, (-angleAdjust) * (1 - (withoutRotation ? 0 : interpT))); + + // unskew + const shear = mat4.identity([]); + mat4.scale(shear, shear, [1, 1 - (1 - scale) * interpT, 1]); + shear[4] = -delta4.x / delta4.y * interpT; + + // unrotate + mat4.rotateZ(shear, shear, angleAdjust); + + mat4.multiply(shear, unrotate, shear); + + return shear; +} + +function rotate(x, y, angle) { + const cos = Math.cos(angle); + const sin = Math.sin(angle); + return { + x: x * cos - y * sin, + y: x * sin + y * cos + }; +} diff --git a/src/geo/projection/albers.js b/src/geo/projection/albers.js new file mode 100644 index 00000000000..0b14cd609ee --- /dev/null +++ b/src/geo/projection/albers.js @@ -0,0 +1,44 @@ +// @flow +import LngLat from '../lng_lat.js'; +import {clamp} from '../../util/util.js'; + +export default { + name: 'albers', + range: [4, 7], + + center: [-96, 37.5], + parallels: [29.5, 45.5], + + conical: true, + + project(lng: number, lat: number) { + const p1 = this.parallels[0] / 180 * Math.PI; + const p2 = this.parallels[1] / 180 * Math.PI; + const n = 0.5 * (Math.sin(p1) + Math.sin(p2)); + const theta = n * ((lng - this.center[0]) / 180 * Math.PI); + const c = Math.pow(Math.cos(p1), 2) + 2 * n * Math.sin(p1); + const r = 0.5; + const a = r / n * Math.sqrt(c - 2 * n * Math.sin(lat / 180 * Math.PI)); + const b = r / n * Math.sqrt(c - 2 * n * Math.sin(0 / 180 * Math.PI)); + const x = a * Math.sin(theta); + const y = b - a * Math.cos(theta); + return {x: 1 + 0.5 * x, y: 1 - 0.5 * y}; + }, + unproject(x: number, y: number) { + const p1 = this.parallels[0] / 180 * Math.PI; + const p2 = this.parallels[1] / 180 * Math.PI; + const n = 0.5 * (Math.sin(p1) + Math.sin(p2)); + const c = Math.pow(Math.cos(p1), 2) + 2 * n * Math.sin(p1); + const r = 0.5; + const b = r / n * Math.sqrt(c - 2 * n * Math.sin(0 / 180 * Math.PI)); + const x_ = (x - 1) * 2; + const y_ = (y - 1) * -2; + const y2 = -(y_ - b); + const theta = Math.atan2(x_, y2); + const lng = clamp((theta / n * 180 / Math.PI) + this.center[0], -180, 180); + const a = x_ / Math.sin(theta); + const s = clamp((Math.pow(a / r * n, 2) - c) / (-2 * n), -1, 1); + const lat = clamp(Math.asin(s) * 180 / Math.PI, -90, 90); + return new LngLat(lng, lat); + } +}; diff --git a/src/geo/projection/equal_earth.js b/src/geo/projection/equal_earth.js new file mode 100644 index 00000000000..888db26f72b --- /dev/null +++ b/src/geo/projection/equal_earth.js @@ -0,0 +1,56 @@ +// @flow +import LngLat from '../lng_lat.js'; +import {clamp} from '../../util/util.js'; + +const a1 = 1.340264; +const a2 = -0.081106; +const a3 = 0.000893; +const a4 = 0.003796; +const M = Math.sqrt(3) / 2; + +export default { + name: 'equalEarth', + center: [0, 0], + range: [3.5, 7], + + project(lng: number, lat: number) { + // based on https://github.com/d3/d3-geo, MIT-licensed + lat = lat / 180 * Math.PI; + lng = lng / 180 * Math.PI; + const theta = Math.asin(M * Math.sin(lat)); + const theta2 = theta * theta; + const theta6 = theta2 * theta2 * theta2; + const x = lng * Math.cos(theta) / (M * (a1 + 3 * a2 * theta2 + theta6 * (7 * a3 + 9 * a4 * theta2))); + const y = theta * (a1 + a2 * theta2 + theta6 * (a3 + a4 * theta2)); + + return { + x: (x / Math.PI + 0.5) * 0.5, + y: 1 - (y / Math.PI + 1) * 0.5 + }; + }, + + unproject(x: number, y: number) { + // based on https://github.com/d3/d3-geo, MIT-licensed + x = (2 * x - 0.5) * Math.PI; + y = (2 * (1 - y) - 1) * Math.PI; + let theta = y; + let theta2 = theta * theta; + let theta6 = theta2 * theta2 * theta2; + + for (let i = 0, delta, fy, fpy; i < 12; ++i) { + fy = theta * (a1 + a2 * theta2 + theta6 * (a3 + a4 * theta2)) - y; + fpy = a1 + 3 * a2 * theta2 + theta6 * (7 * a3 + 9 * a4 * theta2); + theta -= delta = fy / fpy; + theta2 = theta * theta; + theta6 = theta2 * theta2 * theta2; + if (Math.abs(delta) < 1e-12) break; + } + + const lambda = M * x * (a1 + 3 * a2 * theta2 + theta6 * (7 * a3 + 9 * a4 * theta2)) / Math.cos(theta); + const phi = Math.asin(clamp(Math.sin(theta) / M, -1, 1)); + const lng = clamp(lambda * 180 / Math.PI, -180, 180); + const lat = clamp(phi * 180 / Math.PI, -90, 90); + + return new LngLat(lng, lat); + } +}; diff --git a/src/geo/projection/equirectangular.js b/src/geo/projection/equirectangular.js new file mode 100644 index 00000000000..b6f694f4b6f --- /dev/null +++ b/src/geo/projection/equirectangular.js @@ -0,0 +1,18 @@ +// @flow +import LngLat from '../lng_lat.js'; +import {clamp} from '../../util/util.js'; + +export default { + name: 'equirectangular', + center: [0, 0], + project(lng: number, lat: number) { + const x = 0.5 + lng / 360; + const y = 0.5 - lat / 360; + return {x, y}; + }, + unproject(x: number, y: number) { + const lng = (x - 0.5) * 360; + const lat = clamp((0.5 - y) * 360, -90, 90); + return new LngLat(lng, lat); + } +}; diff --git a/src/geo/projection/index.js b/src/geo/projection/index.js new file mode 100644 index 00000000000..304383d5e46 --- /dev/null +++ b/src/geo/projection/index.js @@ -0,0 +1,36 @@ +// @flow +import albers from './albers.js'; +import equalEarth from './equal_earth.js'; +import equirectangular from './equirectangular.js'; +import lambertConformalConic from './lambert.js'; +import mercator from './mercator.js'; +import naturalEarth from './natural_earth.js'; +import winkelTripel from './winkel_tripel.js'; +import LngLat from '../lng_lat.js'; +import type {ProjectionSpecification} from '../../style-spec/types.js'; + +export type Projection = { + name: string, + center: [number, number], + parallels?: [number, number], + range?: [number, number], + conical?: boolean, + project: (lng: number, lat: number) => {x: number, y: number}, + unproject: (x: number, y: number) => LngLat +}; + +const projections = { + albers, + equalEarth, + equirectangular, + lambertConformalConic, + mercator, + naturalEarth, + winkelTripel +}; + +export function getProjection(config: ProjectionSpecification) { + const projection = projections[config.name]; + if (!projection) throw new Error(`Invalid projection name: ${config.name}`); + return projection.conical ? {...projection, ...config} : projection; +} diff --git a/src/geo/projection/lambert.js b/src/geo/projection/lambert.js new file mode 100644 index 00000000000..eae0cc3ebee --- /dev/null +++ b/src/geo/projection/lambert.js @@ -0,0 +1,70 @@ +// @flow +import LngLat from '../lng_lat.js'; +import {clamp} from '../../util/util.js'; + +const halfPi = Math.PI / 2; + +function tany(y) { + return Math.tan((halfPi + y) / 2); +} + +function getParams([lat0, lat1]) { + const y0 = lat0 * Math.PI / 180; + const y1 = lat1 * Math.PI / 180; + const cy0 = Math.cos(y0); + const n = y0 === y1 ? Math.sin(y0) : Math.log(cy0 / Math.cos(y1)) / Math.log(tany(y1) / tany(y0)); + const f = cy0 * Math.pow(tany(y0), n) / n; + + return {n, f}; +} + +export default { + name: 'lambertConformalConic', + range: [3.5, 7], + + center: [0, 30], + parallels: [30, 30], + + conical: true, + + project(lng: number, lat: number) { + // based on https://github.com/d3/d3-geo, MIT-licensed + lat = lat / 180 * Math.PI; + lng = lng / 180 * Math.PI; + + const epsilon = 1e-6; + const {n, f} = getParams(this.parallels); + + if (f > 0) { + if (lat < -halfPi + epsilon) lat = -halfPi + epsilon; + } else { + if (lat > halfPi - epsilon) lat = halfPi - epsilon; + } + + const r = f / Math.pow(tany(lat), n); + const x = r * Math.sin(n * lng); + const y = f - r * Math.cos(n * lng); + + return { + x: (x / Math.PI + 0.5) * 0.5, + y: 1 - (y / Math.PI + 0.5) * 0.5 + }; + }, + + unproject(x: number, y: number) { + // based on https://github.com/d3/d3-geo, MIT-licensed + x = (2 * x - 0.5) * Math.PI; + y = (2 * (1 - y) - 0.5) * Math.PI; + const {n, f} = getParams(this.parallels); + const fy = f - y; + const r = Math.sign(n) * Math.sqrt(x * x + fy * fy); + let l = Math.atan2(x, Math.abs(fy)) * Math.sign(fy); + + if (fy * n < 0) l -= Math.PI * Math.sign(x) * Math.sign(fy); + + const lng = clamp((l / n) * 180 / Math.PI, -180, 180); + const lat = clamp((2 * Math.atan(Math.pow(f / r, 1 / n)) - halfPi) * 180 / Math.PI, -90, 90); + + return new LngLat(lng, lat); + } +}; diff --git a/src/geo/projection/mercator.js b/src/geo/projection/mercator.js new file mode 100644 index 00000000000..7c76340678e --- /dev/null +++ b/src/geo/projection/mercator.js @@ -0,0 +1,18 @@ +// @flow +import {mercatorXfromLng, mercatorYfromLat, lngFromMercatorX, latFromMercatorY} from '../mercator_coordinate.js'; +import LngLat from '../lng_lat.js'; + +export default { + name: 'mercator', + center: [0, 0], + project(lng: number, lat: number) { + const x = mercatorXfromLng(lng); + const y = mercatorYfromLat(lat); + return {x, y}; + }, + unproject(x: number, y: number) { + const lng = lngFromMercatorX(x); + const lat = latFromMercatorY(y); + return new LngLat(lng, lat); + } +}; diff --git a/src/geo/projection/natural_earth.js b/src/geo/projection/natural_earth.js new file mode 100644 index 00000000000..4223e7a00be --- /dev/null +++ b/src/geo/projection/natural_earth.js @@ -0,0 +1,51 @@ +// @flow +import LngLat from '../lng_lat.js'; +import {clamp} from '../../util/util.js'; + +export default { + name: 'naturalEarth', + center: [0, 0], + range: [3.5, 7], + + project(lng: number, lat: number) { + // based on https://github.com/d3/d3-geo, MIT-licensed + lat = lat / 180 * Math.PI; + lng = lng / 180 * Math.PI; + + const phi2 = lat * lat; + const phi4 = phi2 * phi2; + const x = lng * (0.8707 - 0.131979 * phi2 + phi4 * (-0.013791 + phi4 * (0.003971 * phi2 - 0.001529 * phi4))); + const y = lat * (1.007226 + phi2 * (0.015085 + phi4 * (-0.044475 + 0.028874 * phi2 - 0.005916 * phi4))); + + return { + x: (x / Math.PI + 0.5) * 0.5, + y: 1 - (y / Math.PI + 1) * 0.5 + }; + }, + + unproject(x: number, y: number) { + // based on https://github.com/d3/d3-geo, MIT-licensed + x = (2 * x - 0.5) * Math.PI; + y = (2 * (1 - y) - 1) * Math.PI; + const epsilon = 1e-6; + let phi = y; + let i = 25; + let delta = 0; + let phi2 = phi * phi; + + do { + phi2 = phi * phi; + const phi4 = phi2 * phi2; + phi -= delta = (phi * (1.007226 + phi2 * (0.015085 + phi4 * (-0.044475 + 0.028874 * phi2 - 0.005916 * phi4))) - y) / + (1.007226 + phi2 * (0.015085 * 3 + phi4 * (-0.044475 * 7 + 0.028874 * 9 * phi2 - 0.005916 * 11 * phi4))); + } while (Math.abs(delta) > epsilon && --i > 0); + + phi2 = phi * phi; + const lambda = x / (0.8707 + phi2 * (-0.131979 + phi2 * (-0.013791 + phi2 * phi2 * phi2 * (0.003971 - 0.001529 * phi2)))); + + const lng = clamp(lambda * 180 / Math.PI, -180, 180); + const lat = clamp(phi * 180 / Math.PI, -90, 90); + + return new LngLat(lng, lat); + } +}; diff --git a/src/geo/projection/resample.js b/src/geo/projection/resample.js new file mode 100644 index 00000000000..74389807236 --- /dev/null +++ b/src/geo/projection/resample.js @@ -0,0 +1,49 @@ +// @flow + +import Point from '@mapbox/point-geometry'; + +function pointToLineDist(px, py, ax, ay, bx, by) { + const dx = ax - bx; + const dy = ay - by; + return Math.abs((ay - py) * dx - (ax - px) * dy) / Math.hypot(dx, dy); +} + +function addResampled(resampled, startMerc, endMerc, startProj, endProj, reproject, tolerance) { + const midMerc = new Point( + (startMerc.x + endMerc.x) / 2, + (startMerc.y + endMerc.y) / 2); + + const midProj = reproject(midMerc); + const err = pointToLineDist(midProj.x, midProj.y, startProj.x, startProj.y, endProj.x, endProj.y); + + // if reprojected midPoint is too far from geometric midpoint, recurse into two halves + if (err >= tolerance) { + // we're very unlikely to hit max call stack exceeded here, + // but we might want to safeguard against it in the future + addResampled(resampled, startMerc, midMerc, startProj, midProj, reproject, tolerance); + addResampled(resampled, midMerc, endMerc, midProj, endProj, reproject, tolerance); + + } else { // otherwise, just add the point + resampled.push(endProj); + } +} + +export default function resample(line: Array, reproject: (Point) => Point, tolerance: number): Array { + const resampled = []; + let prevMerc, prevProj; + + for (const pointMerc of line) { + const pointProj = reproject(pointMerc); + + if (prevMerc && prevProj) { + addResampled(resampled, prevMerc, pointMerc, prevProj, pointProj, reproject, tolerance); + } else { + resampled.push(pointProj); + } + + prevMerc = pointMerc; + prevProj = pointProj; + } + + return resampled; +} diff --git a/src/geo/projection/tile_transform.js b/src/geo/projection/tile_transform.js new file mode 100644 index 00000000000..eddce3d9894 --- /dev/null +++ b/src/geo/projection/tile_transform.js @@ -0,0 +1,99 @@ +// @flow +import Point from '@mapbox/point-geometry'; +import MercatorCoordinate, {altitudeFromMercatorZ, lngFromMercatorX, latFromMercatorY} from '../mercator_coordinate.js'; +import EXTENT from '../../data/extent.js'; +import {vec3} from 'gl-matrix'; +import type {Projection} from './index.js'; + +export type TileTransform = { + scale: number, + x: number, + y: number, + x2: number, + y2: number, + projection: Projection +}; + +export default function tileTransform(id: Object, projection: Projection) { + const s = Math.pow(2, -id.z); + + const x1 = (id.x) * s; + const x2 = (id.x + 1) * s; + const y1 = (id.y) * s; + const y2 = (id.y + 1) * s; + + if (projection.name === 'mercator') { + return {scale: 1 << id.z, x: id.x, y: id.y, x2: id.x + 1, y2: id.y + 1, projection}; + } + + const lng1 = lngFromMercatorX(x1); + const lng2 = lngFromMercatorX(x2); + const lat1 = latFromMercatorY(y1); + const lat2 = latFromMercatorY(y2); + + const p0 = projection.project(lng1, lat1); + const p1 = projection.project(lng2, lat1); + const p2 = projection.project(lng2, lat2); + const p3 = projection.project(lng1, lat2); + + let minX = Math.min(p0.x, p1.x, p2.x, p3.x); + let minY = Math.min(p0.y, p1.y, p2.y, p3.y); + let maxX = Math.max(p0.x, p1.x, p2.x, p3.x); + let maxY = Math.max(p0.y, p1.y, p2.y, p3.y); + + // we pick an error threshold for calculating the bbox that balances between performance and precision + const maxErr = s / 16; + + function processSegment(pa, pb, ax, ay, bx, by) { + const mx = (ax + bx) / 2; + const my = (ay + by) / 2; + + const pm = projection.project(lngFromMercatorX(mx), latFromMercatorY(my)); + const err = Math.max(0, minX - pm.x, minY - pm.y, pm.x - maxX, pm.y - maxY); + + minX = Math.min(minX, pm.x); + maxX = Math.max(maxX, pm.x); + minY = Math.min(minY, pm.y); + maxY = Math.max(maxY, pm.y); + + if (err > maxErr) { + processSegment(pa, pm, ax, ay, mx, my); + processSegment(pm, pb, mx, my, bx, by); + } + } + + processSegment(p0, p1, x1, y1, x2, y1); + processSegment(p1, p2, x2, y1, x2, y2); + processSegment(p2, p3, x2, y2, x1, y2); + processSegment(p3, p0, x1, y2, x1, y1); + + // extend the bbox by max error to make sure coords don't go past tile extent + minX -= maxErr; + minY -= maxErr; + maxX += maxErr; + maxY += maxErr; + + const max = Math.max(maxX - minX, maxY - minY); + const scale = 1 / max; + + return { + scale, + x: minX * scale, + y: minY * scale, + x2: maxX * scale, + y2: maxY * scale, + projection + }; +} + +export function getTilePoint(tileTransform: TileTransform, {x, y}: {x: number, y: number}, wrap: number = 0) { + return new Point( + ((x - wrap) * tileTransform.scale - tileTransform.x) * EXTENT, + (y * tileTransform.scale - tileTransform.y) * EXTENT); +} + +export function getTileVec3(tileTransform: TileTransform, coord: MercatorCoordinate, wrap: number = 0): vec3 { + const x = ((coord.x - wrap) * tileTransform.scale - tileTransform.x) * EXTENT; + const y = (coord.y * tileTransform.scale - tileTransform.y) * EXTENT; + return vec3.fromValues(x, y, altitudeFromMercatorZ(coord.z, coord.y)); +} diff --git a/src/geo/projection/winkel_tripel.js b/src/geo/projection/winkel_tripel.js new file mode 100644 index 00000000000..8ce2d8616d7 --- /dev/null +++ b/src/geo/projection/winkel_tripel.js @@ -0,0 +1,64 @@ +// @flow +import LngLat from '../lng_lat.js'; +import {clamp} from '../../util/util.js'; + +export default { + name: 'winkelTripel', + center: [0, 0], + range: [3.5, 7], + + project(lng: number, lat: number) { + lat = lat / 180 * Math.PI; + lng = lng / 180 * Math.PI; + const phi1 = Math.acos(2 / Math.PI); + const alpha = Math.acos(Math.cos(lat) * Math.cos(lng / 2)); + const x = 0.5 * (lng * Math.cos(phi1) + (2 * Math.cos(lat) * Math.sin(lng / 2)) / (Math.sin(alpha) / alpha)) || 0; + const y = 0.5 * (lat + Math.sin(lat) / (Math.sin(alpha) / alpha)) || 0; + return { + x: (x / Math.PI + 0.5) * 0.5, + y: 1 - (y / Math.PI + 1) * 0.5 + }; + }, + + unproject(x: number, y: number) { + // based on https://github.com/d3/d3-geo-projection, MIT-licensed + x = (2 * x - 0.5) * Math.PI; + y = (2 * (1 - y) - 1) * Math.PI; + let lambda = x; + let phi = y; + let i = 25; + const epsilon = 1e-6; + let dlambda = 0, dphi = 0; + do { + const cosphi = Math.cos(phi), + sinphi = Math.sin(phi), + sinphi2 = 2 * sinphi * cosphi, + sin2phi = sinphi * sinphi, + cos2phi = cosphi * cosphi, + coslambda2 = Math.cos(lambda / 2), + sinlambda2 = Math.sin(lambda / 2), + sinlambda = 2 * coslambda2 * sinlambda2, + sin2lambda2 = sinlambda2 * sinlambda2, + C = 1 - cos2phi * coslambda2 * coslambda2, + F = C ? 1 / C : 0, + E = C ? Math.acos(cosphi * coslambda2) * Math.sqrt(1 / C) : 0, + fx = 0.5 * (2 * E * cosphi * sinlambda2 + lambda * 2 / Math.PI) - x, + fy = 0.5 * (E * sinphi + phi) - y, + dxdlambda = 0.5 * F * (cos2phi * sin2lambda2 + E * cosphi * coslambda2 * sin2phi) + 1 / Math.PI, + dxdphi = F * (sinlambda * sinphi2 / 4 - E * sinphi * sinlambda2), + dydlambda = 0.125 * F * (sinphi2 * sinlambda2 - E * sinphi * cos2phi * sinlambda), + dydphi = 0.5 * F * (sin2phi * coslambda2 + E * sin2lambda2 * cosphi) + 0.5, + denominator = dxdphi * dydlambda - dydphi * dxdlambda; + + dlambda = (fy * dxdphi - fx * dydphi) / denominator; + dphi = (fx * dydlambda - fy * dxdlambda) / denominator; + lambda -= dlambda; + phi -= dphi; + } while ((Math.abs(dlambda) > epsilon || Math.abs(dphi) > epsilon) && --i > 0); + + const lng = clamp(lambda * 180 / Math.PI, -180, 180); + const lat = clamp(phi * 180 / Math.PI, -90, 90); + + return new LngLat(lng, lat); + } +}; diff --git a/src/geo/transform.js b/src/geo/transform.js index ae1dc2f95e4..0ce5c87baf6 100644 --- a/src/geo/transform.js +++ b/src/geo/transform.js @@ -2,9 +2,11 @@ import LngLat from './lng_lat.js'; import LngLatBounds from './lng_lat_bounds.js'; -import MercatorCoordinate, {mercatorXfromLng, mercatorYfromLat, mercatorZfromAltitude, latFromMercatorY} from './mercator_coordinate.js'; +import MercatorCoordinate, {mercatorXfromLng, mercatorYfromLat, mercatorZfromAltitude, latFromMercatorY, MAX_MERCATOR_LATITUDE} from './mercator_coordinate.js'; +import {getProjection} from './projection/index.js'; +import tileTransform from '../geo/projection/tile_transform.js'; import Point from '@mapbox/point-geometry'; -import {wrap, clamp, radToDeg, degToRad, getAABBPointSquareDist, furthestTileCorner} from '../util/util.js'; +import {wrap, clamp, pick, radToDeg, degToRad, getAABBPointSquareDist, furthestTileCorner, warnOnce} from '../util/util.js'; import {number as interpolate} from '../style-spec/util/interpolate.js'; import EXTENT from '../data/extent.js'; import {vec4, mat4, mat2, vec3, quat} from 'gl-matrix'; @@ -12,10 +14,16 @@ import {Aabb, Frustum, Ray} from '../util/primitives.js'; import EdgeInsets from './edge_insets.js'; import {FreeCamera, FreeCameraOptions, orientationFromFrame} from '../ui/free_camera.js'; import assert from 'assert'; +import getProjectionAdjustments, {getProjectionAdjustmentInverted} from './projection/adjustments.js'; +import {getPixelsToTileUnitsMatrix} from '../source/pixels_to_tile_units.js'; import {UnwrappedTileID, OverscaledTileID, CanonicalTileID} from '../source/tile_id.js'; import type {Elevation} from '../terrain/elevation.js'; import type {PaddingOptions} from './edge_insets.js'; +import type {Projection} from './projection/index.js'; +import type Tile from '../source/tile.js'; +import type {ProjectionSpecification} from '../style-spec/types.js'; +import type {FeatureDistanceData} from '../style-spec/feature_filter/index.js'; const NUM_WORLD_COPIES = 3; const DEFAULT_MIN_ZOOM = 0; @@ -31,9 +39,7 @@ type ElevationReference = "sea" | "ground"; class Transform { tileSize: number; tileZoom: number; - lngRange: ?[number, number]; - latRange: ?[number, number]; - maxValidLatitude: number; + maxBounds: ?LngLatBounds; // 2^zoom (worldSize = tileSize * scale) scale: number; @@ -86,16 +92,26 @@ class Transform { // Inverse of glCoordMatrix, from NDC to screen coordinates, [-1, 1] x [-1, 1] --> [0, w] x [h, 0] labelPlaneMatrix: Float32Array; + inverseAdjustmentMatrix: Array; + + worldMinX: number; + worldMaxX: number; + worldMinY: number; + worldMaxY: number; + freezeTileCoverage: boolean; cameraElevationReference: ElevationReference; fogCullDistSq: ?number; _averageElevation: number; + projectionOptions: ProjectionSpecification; + projection: Projection; _elevation: ?Elevation; _fov: number; _pitch: number; _zoom: number; _cameraZoom: ?number; _unmodified: boolean; + _unmodifiedProjection: boolean; _renderWorldCopies: boolean; _minZoom: number; _maxZoom: number; @@ -106,14 +122,15 @@ class Transform { _constraining: boolean; _projMatrixCache: {[_: number]: Float32Array}; _alignedProjMatrixCache: {[_: number]: Float32Array}; + _pixelsToTileUnitsCache: {[_: number]: Float32Array}; _fogTileMatrixCache: {[_: number]: Float32Array}; + _distanceTileDataCache: {[_: number]: FeatureDistanceData}; _camera: FreeCamera; _centerAltitude: number; _horizonShift: number; constructor(minZoom: ?number, maxZoom: ?number, minPitch: ?number, maxPitch: ?number, renderWorldCopies: boolean | void) { this.tileSize = 512; // constant - this.maxValidLatitude = 85.051129; // constant this._renderWorldCopies = renderWorldCopies === undefined ? true : renderWorldCopies; this._minZoom = minZoom || DEFAULT_MIN_ZOOM; @@ -122,6 +139,7 @@ class Transform { this._minPitch = (minPitch === undefined || minPitch === null) ? 0 : minPitch; this._maxPitch = (maxPitch === undefined || maxPitch === null) ? 60 : maxPitch; + this.setProjection(); this.setMaxBounds(); this.width = 0; @@ -136,6 +154,7 @@ class Transform { this._projMatrixCache = {}; this._alignedProjMatrixCache = {}; this._fogTileMatrixCache = {}; + this._distanceTileDataCache = {}; this._camera = new FreeCamera(); this._centerAltitude = 0; this._averageElevation = 0; @@ -150,7 +169,7 @@ class Transform { clone._elevation = this._elevation; clone._centerAltitude = this._centerAltitude; clone.tileSize = this.tileSize; - clone.latRange = this.latRange; + clone.setMaxBounds(this.getMaxBounds()); clone.width = this.width; clone.height = this.height; clone.cameraElevationReference = this.cameraElevationReference; @@ -166,6 +185,7 @@ class Transform { clone._camera = this._camera.clone(); clone._calcMatrices(); clone.freezeTileCoverage = this.freezeTileCoverage; + if (!this._unmodifiedProjection) clone.setProjection(this.getProjection()); return clone; } @@ -193,6 +213,18 @@ class Transform { this._calcMatrices(); } + getProjection() { + return pick(this.projection, ['name', 'center', 'parallels']); + } + + setProjection(projection?: ?ProjectionSpecification) { + this._unmodifiedProjection = !projection; + if (projection === undefined || projection === null) projection = {name: 'mercator'}; + this.projectionOptions = projection; + this.projection = getProjection(projection); + this._calcMatrices(); + } + get minZoom(): number { return this._minZoom; } set minZoom(zoom: number) { if (this._minZoom === zoom) return; @@ -221,7 +253,9 @@ class Transform { this.pitch = Math.min(this.pitch, pitch); } - get renderWorldCopies(): boolean { return this._renderWorldCopies; } + get renderWorldCopies(): boolean { + return this.projection.name === 'mercator' && this._renderWorldCopies; + } set renderWorldCopies(renderWorldCopies?: ?boolean) { if (renderWorldCopies === undefined) { renderWorldCopies = true; @@ -258,10 +292,19 @@ class Transform { } get bearing(): number { - return -this.angle / Math.PI * 180; + return wrap(this.rotation, -180, 180); } + set bearing(bearing: number) { - const b = -wrap(bearing, -180, 180) * Math.PI / 180; + this.rotation = bearing; + } + + get rotation(): number { + return -this.angle / Math.PI * 180; + } + + set rotation(rotation: number) { + const b = -rotation * Math.PI / 180; if (this.angle === b) return; this._unmodified = false; this.angle = b; @@ -328,7 +371,7 @@ class Transform { // Camera zoom describes the distance of the camera to the sea level (altitude). It is used only for manipulating the camera location. // The standard zoom (this._zoom) defines the camera distance to the terrain (height). Its behavior and conceptual meaning in determining // which tiles to stream is same with or without the terrain. - const elevationAtCenter = this._elevation.getAtPointOrZero(MercatorCoordinate.fromLngLat(this.center), -1); + const elevationAtCenter = this._elevation.getAtPointOrZero(this.locationCoordinate(this.center), -1); if (elevationAtCenter === -1) { // Elevation data not loaded yet @@ -411,7 +454,7 @@ class Transform { // Compute zoom level from the height of the camera relative to the terrain const cameraZoom: number = this._cameraZoom; - const elevationAtCenter = this._elevation.getAtPointOrZero(MercatorCoordinate.fromLngLat(this.center)); + const elevationAtCenter = this._elevation.getAtPointOrZero(this.locationCoordinate(this.center)); const mercatorElevation = mercatorZfromAltitude(elevationAtCenter, this.center.lat); const altitude = this._mercatorZfromZoom(cameraZoom); const minHeight = this._mercatorZfromZoom(this._maxZoom); @@ -645,11 +688,12 @@ class Transform { const actualZ = z; const useElevationData = this.elevation && !options.isTerrainDEM; + const isMercator = this.projection.name === 'mercator'; if (options.minzoom !== undefined && z < options.minzoom) return []; if (options.maxzoom !== undefined && z > options.maxzoom) z = options.maxzoom; - const centerCoord = MercatorCoordinate.fromLngLat(this.center); + const centerCoord = this.locationCoordinate(this.center); const numTiles = 1 << z; const centerPoint = [numTiles * centerCoord.x, numTiles * centerCoord.y, 0]; const cameraFrustum = Frustum.fromInvProjectionMatrix(this.invProjMatrix, this.worldSize, z); @@ -664,19 +708,67 @@ class Transform { const zoomSplitDistance = this.cameraToCenterDistance / options.tileSize * (options.roundZoom ? 1 : 0.502); // No change of LOD behavior for pitch lower than 60 and when there is no top padding: return only tile ids from the requested zoom level - const minZoom = this.pitch <= 60.0 && this._edgeInsets.top <= this._edgeInsets.bottom && !this._elevation ? z : 0; + const minZoom = this.pitch <= 60.0 && this._edgeInsets.top <= this._edgeInsets.bottom && !this._elevation && isMercator ? z : 0; // When calculating tile cover for terrain, create deep AABB for nodes, to ensure they intersect frustum: for sources, // other than DEM, use minimum of visible DEM tiles and center altitude as upper bound (pitch is always less than 90°). const maxRange = options.isTerrainDEM && this._elevation ? this._elevation.exaggeration() * 10000 : this._centerAltitude; const minRange = options.isTerrainDEM ? -maxRange : this._elevation ? this._elevation.getMinElevationBelowMSL() : 0; + + const sizeAtMercatorCoord = mc => { + // Calculate how scale compares between projected coordinates and mercator coordinates. + // Returns a length. The units don't matter since the result is only + // used in a ratio with other values returned by this function. + + // Construct a small square in Mercator coordinates. + const offset = 1 / 40000; + const mcEast = new MercatorCoordinate(mc.x + offset, mc.y, mc.z); + const mcSouth = new MercatorCoordinate(mc.x, mc.y + offset, mc.z); + + // Convert the square to projected coordinates. + const ll = mc.toLngLat(); + const llEast = mcEast.toLngLat(); + const llSouth = mcSouth.toLngLat(); + const p = this.locationCoordinate(ll); + const pEast = this.locationCoordinate(llEast); + const pSouth = this.locationCoordinate(llSouth); + + // Calculate the size of each edge of the reprojected square + const dx = Math.hypot(pEast.x - p.x, pEast.y - p.y); + const dy = Math.hypot(pSouth.x - p.x, pSouth.y - p.y); + + // Calculate the size of a projected square that would have the + // same area as the reprojected square. + return Math.sqrt(dx * dy) / offset; + }; + + const centerSize = sizeAtMercatorCoord(MercatorCoordinate.fromLngLat(this.center)); + + const aabbForTile = (z, x, y, wrap, min, max) => { + const tt = tileTransform({z, x, y}, this.projection); + const tx = tt.x / tt.scale; + const ty = tt.y / tt.scale; + const tx2 = tt.x2 / tt.scale; + const ty2 = tt.y2 / tt.scale; + if (isNaN(tx) || isNaN(tx2) || isNaN(ty) || isNaN(ty2)) { + assert(false); + } + const ret = new Aabb( + [(wrap + tx) * numTiles, numTiles * ty, min], + [(wrap + tx2) * numTiles, numTiles * ty2, max]); + return ret; + }; + const newRootTile = (wrap: number): any => { const max = maxRange; const min = minRange; + const aabb = this.projection.name === 'mercator' ? + new Aabb([wrap * numTiles, 0, min], [(wrap + 1) * numTiles, numTiles, max]) : + aabbForTile(0, 0, 0, wrap, min, max); return { // With elevation, this._elevation provides z coordinate values. For 2D: // All tiles are on zero elevation plane => z difference is zero - aabb: new Aabb([wrap * numTiles, 0, min], [(wrap + 1) * numTiles, numTiles, max]), + aabb, zoom: 0, x: 0, y: 0, @@ -755,8 +847,21 @@ class Transform { dzSqr = square(it.aabb.distanceZ(cameraPoint) * meterToTile); } + let scaleAdjustment = 1; + if (!isMercator && actualZ <= 5) { + // In other projections, not all tiles are the same size. + // Account for the tile size difference by adjusting the distToSplit. + // Adjust by the ratio of the area at the tile center to the area at the map center. + // Adjustments are only needed at lower zooms where tiles are not similarly sized. + const numTiles = Math.pow(2, it.zoom); + const tileCenterSize = sizeAtMercatorCoord(new MercatorCoordinate((it.x + 0.5) / numTiles, (it.y + 0.5) / numTiles)); + const areaRatio = tileCenterSize / centerSize; + // Fudge the ratio slightly so that all tiles near the center have the same zoom level. + scaleAdjustment = areaRatio > 0.85 ? 1 : areaRatio; + } + const distanceSqr = dx * dx + dy * dy + dzSqr; - const distToSplit = (1 << maxZoom - it.zoom) * zoomSplitDistance; + const distToSplit = (1 << maxZoom - it.zoom) * zoomSplitDistance * scaleAdjustment; const distToSplitSqr = square(distToSplit * distToSplitScale(Math.max(dzSqr, cameraHeightSqr), distanceSqr)); return distanceSqr < distToSplitSqr; @@ -799,7 +904,6 @@ class Transform { const dx = centerPoint[0] - ((0.5 + x + (it.wrap << it.zoom)) * (1 << (z - it.zoom))); const dy = centerPoint[1] - 0.5 - y; const id = it.tileID ? it.tileID : new OverscaledTileID(tileZoom, it.wrap, it.zoom, x, y); - result.push({tileID: id, distanceSq: dx * dx + dy * dy}); continue; } @@ -808,7 +912,7 @@ class Transform { const childX = (x << 1) + (i % 2); const childY = (y << 1) + (i >> 1); - const aabb = it.aabb.quadrant(i); + const aabb = this.projection.name === 'mercator' ? it.aabb.quadrant(i) : aabbForTile(it.zoom + 1, childX, childY, it.wrap, 0, 0); const child = {aabb, zoom: it.zoom + 1, x: childX, y: childY, wrap: it.wrap, fullyVisible, tileID: undefined, shouldSplit: undefined}; if (useElevationData) { child.tileID = new OverscaledTileID(it.zoom + 1 === maxZoom ? overscaledZ : it.zoom + 1, it.wrap, it.zoom + 1, childX, childY); @@ -854,7 +958,8 @@ class Transform { if (!minmax) { minmax = {min: minRange, max: maxRange}; } - const cornerFar = furthestTileCorner(this.bearing); + // ensure that we want `this.rotation` instead of `this.bearing` here + const cornerFar = furthestTileCorner(this.rotation); const farX = cornerFar[0] * EXTENT; const farY = cornerFar[1] * EXTENT; @@ -879,7 +984,7 @@ class Transform { const cover = result.sort((a, b) => a.distanceSq - b.distanceSq).map(a => a.tileID); // Relax the assertion on terrain, on high zoom we use distance to center of tile // while camera might be closer to selected center of map. - assert(!cover.length || this.elevation || cover[0].overscaledZ === overscaledZ); + assert(!cover.length || this.elevation || cover[0].overscaledZ === overscaledZ || !isMercator); return cover; } @@ -899,15 +1004,16 @@ class Transform { // Transform from LngLat to Point in world coordinates [-180, 180] x [90, -90] --> [0, this.worldSize] x [0, this.worldSize] project(lnglat: LngLat) { - const lat = clamp(lnglat.lat, -this.maxValidLatitude, this.maxValidLatitude); + const lat = clamp(lnglat.lat, -MAX_MERCATOR_LATITUDE, MAX_MERCATOR_LATITUDE); + const projectedLngLat = this.projection.project(lnglat.lng, lat); return new Point( - mercatorXfromLng(lnglat.lng) * this.worldSize, - mercatorYfromLat(lat) * this.worldSize); + projectedLngLat.x * this.worldSize, + projectedLngLat.y * this.worldSize); } // Transform from Point in world coordinates to LngLat [0, this.worldSize] x [0, this.worldSize] --> [-180, 180] x [90, -90] unproject(point: Point): LngLat { - return new MercatorCoordinate(point.x / this.worldSize, point.y / this.worldSize).toLngLat(); + return this.projection.unproject(point.x / this.worldSize, point.y / this.worldSize); } // Point at center in world coordinates. @@ -917,18 +1023,14 @@ class Transform { const a = this.pointCoordinate(point); const b = this.pointCoordinate(this.centerPoint); const loc = this.locationCoordinate(lnglat); - const newCenter = new MercatorCoordinate( - loc.x - (a.x - b.x), - loc.y - (a.y - b.y)); - this.center = this.coordinateLocation(newCenter); - if (this._renderWorldCopies) { - this.center = this.center.wrap(); - } + this.setLocation(new MercatorCoordinate( + loc.x - (a.x - b.x), + loc.y - (a.y - b.y))); } setLocation(location: MercatorCoordinate) { this.center = this.coordinateLocation(location); - if (this._renderWorldCopies) { + if (this.renderWorldCopies) { this.center = this.center.wrap(); } } @@ -981,24 +1083,31 @@ class Transform { } /** - * Given a geographical lnglat, return an unrounded + * Given a geographical lngLat, return an unrounded * coordinate that represents it at this transform's zoom level. - * @param {LngLat} lnglat + * @param {LngLat} lngLat * @returns {Coordinate} * @private */ - locationCoordinate(lnglat: LngLat) { - return MercatorCoordinate.fromLngLat(lnglat); + locationCoordinate(lngLat: LngLat, altitude?: number) { + const z = altitude ? + mercatorZfromAltitude(altitude, lngLat.lat) : + undefined; + const projectedLngLat = this.projection.project(lngLat.lng, lngLat.lat); + return new MercatorCoordinate( + projectedLngLat.x, + projectedLngLat.y, + z); } /** * Given a Coordinate, return its geographical position. * @param {Coordinate} coord - * @returns {LngLat} lnglat + * @returns {LngLat} lngLat * @private */ coordinateLocation(coord: MercatorCoordinate) { - return coord.toLngLat(); + return this.projection.unproject(coord.x, coord.y); } /** @@ -1224,11 +1333,8 @@ class Transform { * Returns the maximum geographical bounds the map is constrained to, or `null` if none set. * @returns {LngLatBounds} {@link LngLatBounds}. */ - getMaxBounds(): LngLatBounds | null { - if (!this.latRange || this.latRange.length !== 2 || - !this.lngRange || this.lngRange.length !== 2) return null; - - return new LngLatBounds([this.lngRange[0], this.latRange[0]], [this.lngRange[1], this.latRange[1]]); + getMaxBounds(): ?LngLatBounds { + return this.maxBounds; } /** @@ -1236,32 +1342,87 @@ class Transform { * * @param {LngLatBounds} bounds A {@link LngLatBounds} object describing the new geographic boundaries of the map. */ - setMaxBounds(bounds?: LngLatBounds) { + setMaxBounds(bounds: ?LngLatBounds) { + this.maxBounds = bounds; + + let minLat = -MAX_MERCATOR_LATITUDE; + let maxLat = MAX_MERCATOR_LATITUDE; + let minLng = -180; + let maxLng = 180; + if (bounds) { - const eastBound = bounds.getEast(); - const westBound = bounds.getWest(); - // Unwrap bounds if they cross the 180th meridian - this.lngRange = [westBound, eastBound > westBound ? eastBound : eastBound + 360]; - this.latRange = [bounds.getSouth(), bounds.getNorth()]; - this._constrain(); - } else { - this.lngRange = null; - this.latRange = [-this.maxValidLatitude, this.maxValidLatitude]; + minLat = bounds.getSouth(); + maxLat = bounds.getNorth(); + minLng = bounds.getWest(); + maxLng = bounds.getEast(); + if (maxLng < minLng) maxLng += 360; } + + this.worldMinX = mercatorXfromLng(minLng) * this.tileSize; + this.worldMaxX = mercatorXfromLng(maxLng) * this.tileSize; + this.worldMinY = mercatorYfromLat(maxLat) * this.tileSize; + this.worldMaxY = mercatorYfromLat(minLat) * this.tileSize; + + this._constrain(); } calculatePosMatrix(unwrappedTileID: UnwrappedTileID, worldSize: number): Float32Array { + let scale, scaledX, scaledY; const canonical = unwrappedTileID.canonical; - const scale = worldSize / this.zoomScale(canonical.z); - const unwrappedX = canonical.x + Math.pow(2, canonical.z) * unwrappedTileID.wrap; - const posMatrix = mat4.identity(new Float64Array(16)); - mat4.translate(posMatrix, posMatrix, [unwrappedX * scale, canonical.y * scale, 0]); + + if (this.projection.name === 'mercator') { + scale = worldSize / this.zoomScale(canonical.z); + const unwrappedX = canonical.x + Math.pow(2, canonical.z) * unwrappedTileID.wrap; + scaledX = unwrappedX * scale; + scaledY = canonical.y * scale; + } else { + const cs = tileTransform(canonical, this.projection); + scale = 1; + scaledX = cs.x; + scaledY = cs.y; + mat4.scale(posMatrix, posMatrix, [scale / cs.scale, scale / cs.scale, this.pixelsPerMeter / this.worldSize]); + } + + mat4.translate(posMatrix, posMatrix, [scaledX, scaledY, 0]); mat4.scale(posMatrix, posMatrix, [scale / EXTENT, scale / EXTENT, 1]); return posMatrix; } + calculateDistanceTileData(unwrappedTileID: UnwrappedTileID): FeatureDistanceData { + const distanceDataKey = unwrappedTileID.key; + const cache = this._distanceTileDataCache; + if (cache[distanceDataKey]) { + return cache[distanceDataKey]; + } + + //Calculate the offset of the tile + const canonical = unwrappedTileID.canonical; + const windowScaleFactor = 1 / this.height; + const scale = this.cameraWorldSize / this.zoomScale(canonical.z); + const unwrappedX = canonical.x + Math.pow(2, canonical.z) * unwrappedTileID.wrap; + const tX = unwrappedX * scale; + const tY = canonical.y * scale; + + const center = this.point; + + // Calculate the bearing vector by rotating unit vector [0, -1] clockwise + const angle = this.angle; + const bX = Math.sin(-angle); + const bY = -Math.cos(-angle); + + const cX = (center.x - tX) * windowScaleFactor; + const cY = (center.y - tY) * windowScaleFactor; + cache[distanceDataKey] = { + bearing: [bX, bY], + center: [cX, cY], + scale: (scale / EXTENT) * windowScaleFactor + }; + + return cache[distanceDataKey]; + } + /** * Calculate the fogTileMatrix that, given a tile coordinate, can be used to * calculate its position relative to the camera in units of pixels divided @@ -1298,12 +1459,25 @@ class Transform { } const posMatrix = this.calculatePosMatrix(unwrappedTileID, this.worldSize); - mat4.multiply(posMatrix, aligned ? this.alignedProjMatrix : this.projMatrix, posMatrix); + const projMatrix = this.projection.name === 'mercator' ? aligned ? this.alignedProjMatrix : this.projMatrix : this.mercatorMatrix; + mat4.multiply(posMatrix, projMatrix, posMatrix); cache[projMatrixKey] = new Float32Array(posMatrix); return cache[projMatrixKey]; } + calculatePixelsToTileUnitsMatrix(tile: Tile): Float32Array { + const key = tile.tileID.key; + const cache = this._pixelsToTileUnitsCache; + if (cache[key]) { + return cache[key]; + } + + const matrix = getPixelsToTileUnitsMatrix(tile, this); + cache[key] = matrix; + return cache[key]; + } + customLayerMatrix(): Array { return this.mercatorMatrix.slice(); } @@ -1341,7 +1515,7 @@ class Transform { // Camera zoom has to be updated as the orbit distance might have changed this._cameraZoom = this._zoomFromMercatorZ(maxAltitude); this._centerAltitude = newCenter.toAltitude(); - this._center = newCenter.toLngLat(); + this._center = this.coordinateLocation(newCenter); this._updateZoomFromElevation(); this._constrain(); this._calcMatrices(); @@ -1361,7 +1535,7 @@ class Transform { const cameraHeight = this._camera.position[2] - terrainElevation; if (cameraHeight < minHeight) { - const center = MercatorCoordinate.fromLngLat(this._center, this._centerAltitude); + const center = this.locationCoordinate(this._center, this._centerAltitude); const cameraPos = this._camera.mercatorPosition; const cameraToCenter = [center.x - cameraPos.x, center.y - cameraPos.y, center.z - cameraPos.z]; const prevDistToCamera = vec3.length(cameraToCenter); @@ -1386,106 +1560,80 @@ class Transform { this._constraining = true; - let minY = Infinity; - let maxY = -Infinity; - let minX, maxX, sy, sx, y2; - const size = this.size, - unmodified = this._unmodified; - - if (this.latRange) { - const latRange = this.latRange; - minY = mercatorYfromLat(latRange[1]) * this.worldSize; - maxY = mercatorYfromLat(latRange[0]) * this.worldSize; - sy = maxY - minY < size.y ? size.y / (maxY - minY) : 0; - } - - if (this.lngRange) { - const lngRange = this.lngRange; - minX = mercatorXfromLng(lngRange[0]) * this.worldSize; - maxX = mercatorXfromLng(lngRange[1]) * this.worldSize; - sx = maxX - minX < size.x ? size.x / (maxX - minX) : 0; - } - - const point = this.point; - - // how much the map should scale to fit the screen into given latitude/longitude ranges - const s = Math.max(sx || 0, sy || 0); - - if (s) { - this.center = this.unproject(new Point( - sx ? (maxX + minX) / 2 : point.x, - sy ? (maxY + minY) / 2 : point.y)); - this.zoom += this.scaleZoom(s); - this._unmodified = unmodified; + // alternate constraining for non-Mercator projections + const maxBounds = this.maxBounds; + if (this.projection.name !== 'mercator' && maxBounds) { + const center = this.center; + center.lat = clamp(center.lat, maxBounds.getSouth(), maxBounds.getNorth()); + center.lng = clamp(center.lng, maxBounds.getWest(), maxBounds.getEast()); + this.center = center; this._constraining = false; return; } - if (this.latRange) { - const y = point.y, - h2 = size.y / 2; - - if (y - h2 < minY) y2 = minY + h2; - if (y + h2 > maxY) y2 = maxY - h2; + const unmodified = this._unmodified; + const {x, y} = this.point; + let s = 0; + let x2 = x; + let y2 = y; + const w2 = this.width / 2; + const h2 = this.height / 2; + + const minY = this.worldMinY * this.scale; + const maxY = this.worldMaxY * this.scale; + if (y - h2 < minY) y2 = minY + h2; + if (y + h2 > maxY) y2 = maxY - h2; + if (maxY - minY < this.height) { + s = Math.max(s, this.height / (maxY - minY)); + y2 = (maxY + minY) / 2; } - let x = point.x; + if (this.maxBounds) { + const minX = this.worldMinX * this.scale; + const maxX = this.worldMaxX * this.scale; - if (this.lngRange) { // Translate to positive positions with the map center in the center position. // This ensures that the map snaps to the correct edge. const shift = this.worldSize / 2 - (minX + maxX) / 2; - x = (x + shift + this.worldSize) % this.worldSize; - minX += shift; - maxX += shift; - - const w2 = size.x / 2; - if (x - w2 < minX) x = minX + w2; - if (x + w2 > maxX) x = maxX - w2; + x2 = (x + shift + this.worldSize) % this.worldSize - shift; - x -= shift; + if (x2 - w2 < minX) x2 = minX + w2; + if (x2 + w2 > maxX) x2 = maxX - w2; + if (maxX - minX < this.width) { + s = Math.max(s, this.width / (maxX - minX)); + x2 = (maxX + minX) / 2; + } } - // pan the map if the screen goes off the range - if (x !== point.x || y2 !== undefined) { - this.center = this.unproject(new Point( - x, - y2 !== undefined ? y2 : point.y)); + if (x2 !== x || y2 !== y) { // pan the map to fit the range + this.center = this.unproject(new Point(x2, y2)); + } + if (s) { // scale the map to fit the range + this.zoom += this.scaleZoom(s); } this._constrainCameraAltitude(); - this._unmodified = unmodified; this._constraining = false; } /** - * Returns the minimum zoom at which `this.width` can fit `this.lngRange` - * and `this.height` can fit `this.latRange`. + * Returns the minimum zoom at which `this.width` can fit max longitude range + * and `this.height` can fit max latitude range. * * @returns {number} The zoom value. */ _minZoomForBounds(): number { - const minZoomForDim = (dim: number, range: [number, number]): number => { - return Math.log2(dim / (this.tileSize * Math.abs(range[1] - range[0]))); - }; - let minLatZoom = DEFAULT_MIN_ZOOM; - if (this.latRange) { - const latRange = this.latRange; - minLatZoom = minZoomForDim(this.height, [mercatorYfromLat(latRange[0]), mercatorYfromLat(latRange[1])]); - } - let minLngZoom = DEFAULT_MIN_ZOOM; - if (this.lngRange) { - const lngRange = this.lngRange; - minLngZoom = minZoomForDim(this.width, [mercatorXfromLng(lngRange[0]), mercatorXfromLng(lngRange[1])]); + let minZoom = Math.max(0, this.scaleZoom(this.height / (this.worldMaxY - this.worldMinY))); + if (this.maxBounds) { + minZoom = Math.max(minZoom, this.scaleZoom(this.width / (this.worldMaxX - this.worldMinX))); } - - return Math.max(minLatZoom, minLngZoom); + return minZoom; } /** * Returns the maximum distance of the camera from the center of the bounds, such that - * `this.width` can fit `this.lngRange` and `this.height` can fit `this.latRange`. + * `this.width` can fit max longitude range and `this.height` can fit max latitude range. * In mercator units. * * @returns {number} The mercator z coordinate. @@ -1547,6 +1695,20 @@ class Transform { let m = mat4.mul([], cameraToClip, worldToCamera); + if (this.projection.name !== 'mercator') { + // Projections undistort as you zoom in (shear, scale, rotate). + // Apply the undistortion around the center of the map. + const mc = this.locationCoordinate(this.center); + const adjustments = mat4.identity([]); + mat4.translate(adjustments, adjustments, [mc.x * this.worldSize, mc.y * this.worldSize, 0]); + mat4.multiply(adjustments, adjustments, getProjectionAdjustments(this)); + mat4.translate(adjustments, adjustments, [-mc.x * this.worldSize, -mc.y * this.worldSize, 0]); + mat4.multiply(m, m, adjustments); + this.inverseAdjustmentMatrix = getProjectionAdjustmentInverted(this); + } else { + this.inverseAdjustmentMatrix = [1, 0, 0, 1]; + } + // The mercatorMatrix can be used to transform points from mercator coordinates // ([0, 0] nw, [1, 1] se) to GL coordinates. this.mercatorMatrix = mat4.scale([], m, [this.worldSize, this.worldSize, this.worldSize / pixelsPerMeter]); @@ -1600,6 +1762,7 @@ class Transform { this.pixelMatrix = mat4.multiply(new Float64Array(16), this.labelPlaneMatrix, this.projMatrix); this._calcFogMatrices(); + this._distanceTileDataCache = {}; // inverse matrix for conversion from screen coordinates to location m = mat4.invert(new Float64Array(16), this.pixelMatrix); @@ -1608,6 +1771,7 @@ class Transform { this._projMatrixCache = {}; this._alignedProjMatrixCache = {}; + this._pixelsToTileUnitsCache = {}; } _calcFogMatrices() { @@ -1666,7 +1830,7 @@ class Transform { /** * Apply a 3d translation to the camera position, but clamping it so that - * it respects the bounds set by `this.latRange` and `this.lngRange`. + * it respects the maximum longitude and latitude range set. * * @param {vec3} translation The translation vector. */ @@ -1707,7 +1871,7 @@ class Transform { if (this._terrainEnabled()) this._updateCameraOnTerrain(); - this._center = new MercatorCoordinate(position[0], position[1], position[2]).toLngLat(); + this._center = this.coordinateLocation(new MercatorCoordinate(position[0], position[1], position[2])); this._unmodified = false; this._constrain(); this._calcMatrices(); @@ -1736,7 +1900,12 @@ class Transform { } _terrainEnabled(): boolean { - return !!this._elevation; + if (!this._elevation) return false; + if (this.projection.name !== 'mercator') { + warnOnce('Terrain is not yet supported with alternate projections. Use mercator to enable terrain.'); + return false; + } + return true; } // Check if any of the four corners are off the edge of the rendered map diff --git a/src/render/draw_background.js b/src/render/draw_background.js index d2b15f51e7c..bd786aa8889 100644 --- a/src/render/draw_background.js +++ b/src/render/draw_background.js @@ -3,6 +3,7 @@ import StencilMode from '../gl/stencil_mode.js'; import DepthMode from '../gl/depth_mode.js'; import CullFaceMode from '../gl/cull_face_mode.js'; +import Tile from '../source/tile.js'; import { backgroundUniformValues, backgroundPatternUniformValues @@ -37,7 +38,12 @@ function drawBackground(painter: Painter, sourceCache: SourceCache, layer: Backg const program = painter.useProgram(image ? 'backgroundPattern' : 'background'); - const tileIDs = coords ? coords : transform.coveringTiles({tileSize}); + let tileIDs = coords; + let backgroundTiles; + if (!tileIDs) { + backgroundTiles = painter.getBackgroundTiles(); + tileIDs = Object.values(backgroundTiles).map(tile => (tile: any).tileID); + } if (image) { context.activeTexture.set(gl.TEXTURE0); @@ -50,14 +56,19 @@ function drawBackground(painter: Painter, sourceCache: SourceCache, layer: Backg const matrix = coords ? tileID.projMatrix : painter.transform.calculateProjMatrix(unwrappedTileID); painter.prepareDrawTile(tileID); + const tile = sourceCache ? sourceCache.getTile(tileID) : + backgroundTiles ? backgroundTiles[tileID.key] : new Tile(tileID, tileSize, transform.zoom, painter); + const uniformValues = image ? backgroundPatternUniformValues(matrix, opacity, painter, image, {tileID, tileSize}, crossfade) : backgroundUniformValues(matrix, opacity, color); painter.prepareDrawProgram(context, program, unwrappedTileID); + const {tileBoundsBuffer, tileBoundsIndexBuffer, tileBoundsSegments} = painter.getTileBoundsBuffers(tile); + program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.disabled, - uniformValues, layer.id, painter.tileExtentBuffer, - painter.quadTriangleIndexBuffer, painter.tileExtentSegments); + uniformValues, layer.id, tileBoundsBuffer, + tileBoundsIndexBuffer, tileBoundsSegments); } } diff --git a/src/render/draw_debug.js b/src/render/draw_debug.js index 18101641c1f..63ba163c6e6 100644 --- a/src/render/draw_debug.js +++ b/src/render/draw_debug.js @@ -136,9 +136,15 @@ function drawDebugTile(painter, sourceCache, coord: OverscaledTileID) { // Bind the empty texture for drawing outlines painter.emptyTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); + tile._makeDebugTileBoundsBuffers(painter.context, painter.transform.projection); + + const debugBuffer = tile._tileDebugBuffer || painter.debugBuffer; + const debugIndexBuffer = tile._tileDebugIndexBuffer || painter.debugIndexBuffer; + const debugSegments = tile._tileDebugSegments || painter.debugSegments; + program.draw(context, gl.LINE_STRIP, depthMode, stencilMode, colorMode, CullFaceMode.disabled, debugUniformValues(posMatrix, Color.red), id, - painter.debugBuffer, painter.tileBorderIndexBuffer, painter.debugSegments); + debugBuffer, debugIndexBuffer, debugSegments); const tileRawData = tile.latestRawTileData; const tileByteLength = (tileRawData && tileRawData.byteLength) || 0; diff --git a/src/render/draw_hillshade.js b/src/render/draw_hillshade.js index dc4ea0aaf9f..ef678574693 100644 --- a/src/render/draw_hillshade.js +++ b/src/render/draw_hillshade.js @@ -63,9 +63,11 @@ function renderHillshade(painter, coord, tile, layer, depthMode, stencilMode, co painter.prepareDrawProgram(context, program, coord.toUnwrapped()); + const {tileBoundsBuffer, tileBoundsIndexBuffer, tileBoundsSegments} = painter.getTileBoundsBuffers(tile); + program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.disabled, - uniformValues, layer.id, painter.rasterBoundsBuffer, - painter.quadTriangleIndexBuffer, painter.rasterBoundsSegments); + uniformValues, layer.id, tileBoundsBuffer, + tileBoundsIndexBuffer, tileBoundsSegments); } export function prepareDEMTexture(painter: Painter, tile: Tile, dem: DEMData) { @@ -114,11 +116,13 @@ function prepareHillshade(painter, tile, layer, depthMode, stencilMode, colorMod context.bindFramebuffer.set(fbo.framebuffer); context.viewport.set([0, 0, tileSize, tileSize]); + const {tileBoundsBuffer, tileBoundsIndexBuffer, tileBoundsSegments} = painter.getTileBoundsBuffers(tile); + painter.useProgram('hillshadePrepare').draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.disabled, hillshadeUniformPrepareValues(tile.tileID, dem), - layer.id, painter.rasterBoundsBuffer, - painter.quadTriangleIndexBuffer, painter.rasterBoundsSegments); + layer.id, tileBoundsBuffer, + tileBoundsIndexBuffer, tileBoundsSegments); tile.needsHillshadePrepare = false; } diff --git a/src/render/draw_raster.js b/src/render/draw_raster.js index 19b1caaef50..beaea4fc0a6 100644 --- a/src/render/draw_raster.js +++ b/src/render/draw_raster.js @@ -87,9 +87,11 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty uniformValues, layer.id, source.boundsBuffer, painter.quadTriangleIndexBuffer, source.boundsSegments); } else { + const {tileBoundsBuffer, tileBoundsIndexBuffer, tileBoundsSegments} = painter.getTileBoundsBuffers(tile); + program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.disabled, - uniformValues, layer.id, painter.rasterBoundsBuffer, - painter.quadTriangleIndexBuffer, painter.rasterBoundsSegments); + uniformValues, layer.id, tileBoundsBuffer, + tileBoundsIndexBuffer, tileBoundsSegments); } } } diff --git a/src/render/draw_symbol.js b/src/render/draw_symbol.js index dc94dbfeb38..e796564df82 100644 --- a/src/render/draw_symbol.js +++ b/src/render/draw_symbol.js @@ -4,7 +4,6 @@ import Point from '@mapbox/point-geometry'; import drawCollisionDebug from './draw_collision_debug.js'; import SegmentVector from '../data/segment.js'; -import pixelsToTileUnits from '../source/pixels_to_tile_units.js'; import * as symbolProjection from '../symbol/projection.js'; import * as symbolSize from '../symbol/symbol_size.js'; import {mat4} from 'gl-matrix'; @@ -126,8 +125,8 @@ function updateVariableAnchors(coords, painter, layer, sourceCache, rotationAlig const sizeData = bucket.textSizeData; const size = symbolSize.evaluateSizeForZoom(sizeData, tr.zoom); - const pixelToTileScale = pixelsToTileUnits(tile, 1, painter.transform.zoom); - const labelPlaneMatrix = symbolProjection.getLabelPlaneMatrix(coord.projMatrix, pitchWithMap, rotateWithMap, painter.transform, pixelToTileScale); + const pixelsToTileUnits = painter.transform.calculatePixelsToTileUnitsMatrix(tile); + const labelPlaneMatrix = symbolProjection.getLabelPlaneMatrix(coord.projMatrix, pitchWithMap, rotateWithMap, painter.transform, pixelsToTileUnits); const updateTextFitIcon = layer.layout.get('icon-text-fit') !== 'none' && bucket.hasIconData(); if (size) { @@ -298,7 +297,7 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate texSize = tile.imageAtlasTexture.size; } - const s = pixelsToTileUnits(tile, 1, painter.transform.zoom); + const s = painter.transform.calculatePixelsToTileUnitsMatrix(tile); const labelPlaneMatrix = symbolProjection.getLabelPlaneMatrix(coord.projMatrix, pitchWithMap, rotateWithMap, painter.transform, s); // labelPlaneMatrixInv is used for converting vertex pos to tile coordinates needed for sampling elevation. const labelPlaneMatrixInv = painter.terrain && pitchWithMap && alongLine ? mat4.invert(new Float32Array(16), labelPlaneMatrix) : identityMat4; diff --git a/src/render/painter.js b/src/render/painter.js index 88de16aec79..7ad554175ef 100644 --- a/src/render/painter.js +++ b/src/render/painter.js @@ -8,11 +8,11 @@ import SourceCache from '../source/source_cache.js'; import EXTENT from '../data/extent.js'; import pixelsToTileUnits from '../source/pixels_to_tile_units.js'; import SegmentVector from '../data/segment.js'; -import {RasterBoundsArray, PosArray, TriangleIndexArray, LineStripIndexArray} from '../data/array_types.js'; +import {PosArray, TileBoundsArray, TriangleIndexArray, LineStripIndexArray} from '../data/array_types.js'; import {values, MAX_SAFE_INTEGER} from '../util/util.js'; import {isMapAuthenticated} from '../util/mapbox.js'; -import rasterBoundsAttributes from '../data/raster_bounds_attributes.js'; import posAttributes from '../data/pos_attributes.js'; +import boundsAttributes from '../data/bounds_attributes.js'; import ProgramConfiguration from '../data/program_configuration.js'; import CrossTileSymbolIndex from '../symbol/cross_tile_symbol_index.js'; import shaders from '../shaders/shaders.js'; @@ -42,6 +42,7 @@ import custom from './draw_custom.js'; import sky from './draw_sky.js'; import {Terrain} from '../terrain/terrain.js'; import {Debug} from '../util/debug.js'; +import Tile from '../source/tile.js'; const draw = { symbol, @@ -59,7 +60,6 @@ const draw = { }; import type Transform from '../geo/transform.js'; -import type Tile from '../source/tile.js'; import type {OverscaledTileID, UnwrappedTileID} from '../source/tile_id.js'; import type Style from '../style/style.js'; import type StyleLayer from '../style/style_layer.js'; @@ -112,13 +112,13 @@ class Painter { tileExtentBuffer: VertexBuffer; tileExtentSegments: SegmentVector; debugBuffer: VertexBuffer; + debugIndexBuffer: IndexBuffer; debugSegments: SegmentVector; - rasterBoundsBuffer: VertexBuffer; - rasterBoundsSegments: SegmentVector; viewportBuffer: VertexBuffer; viewportSegments: SegmentVector; quadTriangleIndexBuffer: IndexBuffer; - tileBorderIndexBuffer: IndexBuffer; + mercatorBoundsBuffer: VertexBuffer; + mercatorBoundsSegments: SegmentVector; _tileClippingMaskIDs: {[_: number]: number }; stencilClearMode: StencilMode; style: Style; @@ -147,6 +147,7 @@ class Painter { tileLoaded: boolean; frameCopies: Array; loadTimeStamps: Array; + _backgroundTiles: {[_: number | string]: Tile}; constructor(gl: WebGLRenderingContext, transform: Transform) { this.context = new Context(gl); @@ -166,10 +167,11 @@ class Painter { this.gpuTimers = {}; this.frameCounter = 0; + this._backgroundTiles = {}; } updateTerrain(style: Style, cameraChanging: boolean) { - const enabled = !!style && !!style.terrain; + const enabled = !!style && !!style.terrain && this.transform.projection.name === 'mercator'; if (!enabled && (!this._terrain || !this._terrain.enabled)) return; if (!this._terrain) { this._terrain = new Terrain(this, style); @@ -202,7 +204,7 @@ class Painter { } get terrain(): ?Terrain { - return this._terrain && this._terrain.enabled ? this._terrain : null; + return this.transform._terrainEnabled() && this._terrain && this._terrain.enabled ? this._terrain : null; } /* @@ -240,14 +242,6 @@ class Painter { this.debugBuffer = context.createVertexBuffer(debugArray, posAttributes.members); this.debugSegments = SegmentVector.simpleSegment(0, 0, 4, 5); - const rasterBoundsArray = new RasterBoundsArray(); - rasterBoundsArray.emplaceBack(0, 0, 0, 0); - rasterBoundsArray.emplaceBack(EXTENT, 0, EXTENT, 0); - rasterBoundsArray.emplaceBack(0, EXTENT, 0, EXTENT); - rasterBoundsArray.emplaceBack(EXTENT, EXTENT, EXTENT, EXTENT); - this.rasterBoundsBuffer = context.createVertexBuffer(rasterBoundsArray, rasterBoundsAttributes.members); - this.rasterBoundsSegments = SegmentVector.simpleSegment(0, 0, 4, 2); - const viewportArray = new PosArray(); viewportArray.emplaceBack(-1, -1); viewportArray.emplaceBack(1, -1); @@ -256,19 +250,23 @@ class Painter { this.viewportBuffer = context.createVertexBuffer(viewportArray, posAttributes.members); this.viewportSegments = SegmentVector.simpleSegment(0, 0, 4, 2); - const tileLineStripIndices = new LineStripIndexArray(); - tileLineStripIndices.emplaceBack(0); - tileLineStripIndices.emplaceBack(1); - tileLineStripIndices.emplaceBack(3); - tileLineStripIndices.emplaceBack(2); - tileLineStripIndices.emplaceBack(0); - this.tileBorderIndexBuffer = context.createIndexBuffer(tileLineStripIndices); + const tileBoundsArray = new TileBoundsArray(); + tileBoundsArray.emplaceBack(0, 0, 0, 0); + tileBoundsArray.emplaceBack(EXTENT, 0, EXTENT, 0); + tileBoundsArray.emplaceBack(0, EXTENT, 0, EXTENT); + tileBoundsArray.emplaceBack(EXTENT, EXTENT, EXTENT, EXTENT); + this.mercatorBoundsBuffer = context.createVertexBuffer(tileBoundsArray, boundsAttributes.members); + this.mercatorBoundsSegments = SegmentVector.simpleSegment(0, 0, 4, 2); const quadTriangleIndices = new TriangleIndexArray(); quadTriangleIndices.emplaceBack(0, 1, 2); quadTriangleIndices.emplaceBack(2, 1, 3); this.quadTriangleIndexBuffer = context.createIndexBuffer(quadTriangleIndices); + const tileLineStripIndices = new LineStripIndexArray(); + for (const i of [0, 1, 3, 2, 0]) tileLineStripIndices.emplaceBack(i); + this.debugIndexBuffer = context.createIndexBuffer(tileLineStripIndices); + this.emptyTexture = new Texture(context, { width: 1, height: 1, @@ -282,6 +280,21 @@ class Painter { this.loadTimeStamps.push(window.performance.now()); } + getTileBoundsBuffers(tile: Tile) { + let tileBoundsBuffer, tileBoundsIndexBuffer, tileBoundsSegments; + if (tile._tileBoundsBuffer) { + tileBoundsBuffer = tile._tileBoundsBuffer; + tileBoundsIndexBuffer = tile._tileBoundsIndexBuffer; + tileBoundsSegments = tile._tileBoundsSegments; + } else { + tileBoundsBuffer = this.mercatorBoundsBuffer; + tileBoundsIndexBuffer = this.quadTriangleIndexBuffer; + tileBoundsSegments = this.mercatorBoundsSegments; + } + + return {tileBoundsBuffer, tileBoundsIndexBuffer, tileBoundsSegments}; + } + /* * Reset the drawing canvas by clearing the stencil buffer so that we can draw * new tiles at the same location, while retaining previously drawn pixels. @@ -325,14 +338,16 @@ class Painter { this._tileClippingMaskIDs = {}; for (const tileID of tileIDs) { + const tile = sourceCache.getTile(tileID); const id = this._tileClippingMaskIDs[tileID.key] = this.nextStencilID++; + const {tileBoundsBuffer, tileBoundsIndexBuffer, tileBoundsSegments} = this.getTileBoundsBuffers(tile); program.draw(context, gl.TRIANGLES, DepthMode.disabled, // Tests will always pass, and ref value will be written to stencil buffer. new StencilMode({func: gl.ALWAYS, mask: 0}, id, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE), ColorMode.disabled, CullFaceMode.disabled, clippingMaskUniformValues(tileID.projMatrix), - '$clipping', this.tileExtentBuffer, - this.quadTriangleIndexBuffer, this.tileExtentSegments); + '$clipping', tileBoundsBuffer, + tileBoundsIndexBuffer, tileBoundsSegments); } } @@ -890,6 +905,22 @@ class Painter { return true; } + + getBackgroundTiles() { + const oldTiles = this._backgroundTiles; + const newTiles = this._backgroundTiles = {}; + + const tileSize = 512; + const tileIDs = this.transform.coveringTiles({tileSize}); + for (const tileID of tileIDs) { + newTiles[tileID.key] = oldTiles[tileID.key] || new Tile(tileID, tileSize, this.transform.tileZoom, this); + } + return newTiles; + } + + clearBackgroundTiles() { + this._backgroundTiles = {}; + } } export default Painter; diff --git a/src/render/program/circle_program.js b/src/render/program/circle_program.js index 2e07158f3e9..552ac9cc60d 100644 --- a/src/render/program/circle_program.js +++ b/src/render/program/circle_program.js @@ -2,10 +2,9 @@ import { Uniform1f, - Uniform2f, + UniformMatrix2f, UniformMatrix4f } from '../uniform_binding.js'; -import pixelsToTileUnits from '../../source/pixels_to_tile_units.js'; import type Context from '../../gl/context.js'; import type {UniformValues, UniformLocations} from '../uniform_binding.js'; @@ -17,7 +16,7 @@ import browser from '../../util/browser.js'; export type CircleUniformsType = {| 'u_camera_to_center_distance': Uniform1f, - 'u_extrude_scale': Uniform2f, + 'u_extrude_scale': UniformMatrix2f, 'u_device_pixel_ratio': Uniform1f, 'u_matrix': UniformMatrix4f |}; @@ -26,7 +25,7 @@ export type CircleDefinesType = 'PITCH_WITH_MAP' | 'SCALE_WITH_MAP'; const circleUniforms = (context: Context, locations: UniformLocations): CircleUniformsType => ({ 'u_camera_to_center_distance': new Uniform1f(context, locations.u_camera_to_center_distance), - 'u_extrude_scale': new Uniform2f(context, locations.u_extrude_scale), + 'u_extrude_scale': new UniformMatrix2f(context, locations.u_extrude_scale), 'u_device_pixel_ratio': new Uniform1f(context, locations.u_device_pixel_ratio), 'u_matrix': new UniformMatrix4f(context, locations.u_matrix) }); @@ -39,12 +38,15 @@ const circleUniformValues = ( ): UniformValues => { const transform = painter.transform; - let extrudeScale: [number, number]; + let extrudeScale; if (layer.paint.get('circle-pitch-alignment') === 'map') { - const pixelRatio = pixelsToTileUnits(tile, 1, transform.zoom); - extrudeScale = [pixelRatio, pixelRatio]; + extrudeScale = transform.calculatePixelsToTileUnitsMatrix(tile); } else { - extrudeScale = transform.pixelsToGLUnits; + extrudeScale = new Float32Array([ + transform.pixelsToGLUnits[0], + 0, + 0, + transform.pixelsToGLUnits[1]]); } return { diff --git a/src/render/program/line_program.js b/src/render/program/line_program.js index 804e2b1b672..685bbe524c4 100644 --- a/src/render/program/line_program.js +++ b/src/render/program/line_program.js @@ -5,6 +5,7 @@ import { Uniform1f, Uniform2f, Uniform3f, + UniformMatrix2f, UniformMatrix4f } from '../uniform_binding.js'; import pixelsToTileUnits from '../../source/pixels_to_tile_units.js'; @@ -20,7 +21,7 @@ import type {CrossfadeParameters} from '../../style/evaluation_parameters.js'; export type LineUniformsType = {| 'u_matrix': UniformMatrix4f, - 'u_ratio': Uniform1f, + 'u_pixels_to_tile_units': UniformMatrix2f, 'u_device_pixel_ratio': Uniform1f, 'u_units_to_pixels': Uniform2f, 'u_dash_image': Uniform1i, @@ -34,7 +35,7 @@ export type LineUniformsType = {| export type LinePatternUniformsType = {| 'u_matrix': UniformMatrix4f, 'u_texsize': Uniform2f, - 'u_ratio': Uniform1f, + 'u_pixels_to_tile_units': UniformMatrix2f, 'u_device_pixel_ratio': Uniform1f, 'u_units_to_pixels': Uniform2f, 'u_image': Uniform1i, @@ -46,7 +47,7 @@ export type LineDefinesType = 'RENDER_LINE_GRADIENT' | 'RENDER_LINE_DASH'; const lineUniforms = (context: Context, locations: UniformLocations): LineUniformsType => ({ 'u_matrix': new UniformMatrix4f(context, locations.u_matrix), - 'u_ratio': new Uniform1f(context, locations.u_ratio), + 'u_pixels_to_tile_units': new UniformMatrix2f(context, locations.u_pixels_to_tile_units), 'u_device_pixel_ratio': new Uniform1f(context, locations.u_device_pixel_ratio), 'u_units_to_pixels': new Uniform2f(context, locations.u_units_to_pixels), 'u_dash_image': new Uniform1i(context, locations.u_dash_image), @@ -60,7 +61,7 @@ const lineUniforms = (context: Context, locations: UniformLocations): LineUnifor const linePatternUniforms = (context: Context, locations: UniformLocations): LinePatternUniformsType => ({ 'u_matrix': new UniformMatrix4f(context, locations.u_matrix), 'u_texsize': new Uniform2f(context, locations.u_texsize), - 'u_ratio': new Uniform1f(context, locations.u_ratio), + 'u_pixels_to_tile_units': new UniformMatrix2f(context, locations.u_pixels_to_tile_units), 'u_device_pixel_ratio': new Uniform1f(context, locations.u_device_pixel_ratio), 'u_image': new Uniform1i(context, locations.u_image), 'u_units_to_pixels': new Uniform2f(context, locations.u_units_to_pixels), @@ -77,9 +78,11 @@ const lineUniformValues = ( imageHeight: number ): UniformValues => { const transform = painter.transform; + const pixelsToTileUnits = transform.calculatePixelsToTileUnitsMatrix(tile); + const values = { 'u_matrix': calculateMatrix(painter, tile, layer, matrix), - 'u_ratio': 1 / pixelsToTileUnits(tile, 1, transform.zoom), + 'u_pixels_to_tile_units': pixelsToTileUnits, 'u_device_pixel_ratio': browser.devicePixelRatio, 'u_units_to_pixels': [ 1 / transform.pixelsToGLUnits[0], @@ -114,7 +117,7 @@ const linePatternUniformValues = ( 'u_matrix': calculateMatrix(painter, tile, layer, matrix), 'u_texsize': tile.imageAtlasTexture.size, // camera zoom ratio - 'u_ratio': 1 / pixelsToTileUnits(tile, 1, transform.zoom), + 'u_pixels_to_tile_units': transform.calculatePixelsToTileUnitsMatrix(tile), 'u_device_pixel_ratio': browser.devicePixelRatio, 'u_image': 0, 'u_scale': [tileZoomRatio, crossfade.fromScale, crossfade.toScale], diff --git a/src/render/uniform_binding.js b/src/render/uniform_binding.js index 86ea382d792..8fb623ca890 100644 --- a/src/render/uniform_binding.js +++ b/src/render/uniform_binding.js @@ -151,6 +151,24 @@ class UniformMatrix3f extends Uniform { } } +const emptyMat2 = new Float32Array(4); +class UniformMatrix2f extends Uniform { + constructor(context: Context, location: WebGLUniformLocation) { + super(context, location); + this.current = emptyMat2; + } + + set(v: Float32Array): void { + for (let i = 0; i < 4; i++) { + if (v[i] !== this.current[i]) { + this.current = v; + this.gl.uniformMatrix2fv(this.location, false, v); + break; + } + } + } +} + export { Uniform, Uniform1i, @@ -159,6 +177,7 @@ export { Uniform3f, Uniform4f, UniformColor, + UniformMatrix2f, UniformMatrix3f, UniformMatrix4f }; diff --git a/src/shaders/circle.vertex.glsl b/src/shaders/circle.vertex.glsl index f9a1942276a..171b964a54e 100644 --- a/src/shaders/circle.vertex.glsl +++ b/src/shaders/circle.vertex.glsl @@ -5,7 +5,7 @@ #define NUM_SAMPLES_PER_RING 16 uniform mat4 u_matrix; -uniform vec2 u_extrude_scale; +uniform mat2 u_extrude_scale; uniform lowp float u_device_pixel_ratio; uniform highp float u_camera_to_center_distance; diff --git a/src/shaders/heatmap.vertex.glsl b/src/shaders/heatmap.vertex.glsl index dda815d3973..a2308f86f01 100644 --- a/src/shaders/heatmap.vertex.glsl +++ b/src/shaders/heatmap.vertex.glsl @@ -6,6 +6,19 @@ uniform float u_intensity; attribute vec2 a_pos; +#ifdef PROJECTION_GLOBE_VIEW +attribute vec3 a_pos_3; // Projected position on the globe +attribute vec3 a_pos_normal_3; // Surface normal at the position +attribute float a_scale; + +// Uniforms required for transition between globe and mercator +uniform mat4 u_inv_rot_matrix; +uniform vec2 u_merc_center; +uniform vec3 u_tile_id; +uniform float u_zoom_transition; +uniform vec3 u_up_dir; +#endif + varying vec2 v_extrude; #pragma mapbox: define highp float weight @@ -48,7 +61,26 @@ void main(void) { // multiply a_pos by 0.5, since we had it * 2 in order to sneak // in extrusion data - vec3 pos = vec3(floor(a_pos * 0.5) + extrude, elevation(floor(a_pos * 0.5))); + vec2 tilePos = floor(a_pos * 0.5); + +#ifdef PROJECTION_GLOBE_VIEW + // Apply extra scaling to extrusion to cover different pixel space ratios (which is dependant on the latitude) + extrude *= a_scale; + + vec3 normal = normalize(mix(a_pos_normal_3 / 16384.0, u_up_dir, u_zoom_transition)); + + // Coordinate frame for the extrusion is the tangent plane at the point location on the globe surface + vec3 xAxis = normalize(vec3(normal.z, 0.0, -normal.x)); + vec3 yAxis = normalize(cross(normal, xAxis)); + + // Compute positions on both globe and mercator plane to support transition between the two modes + vec3 globePos = a_pos_3 + xAxis * extrude.x + yAxis * extrude.y + elevationVector(tilePos) * elevation(tilePos); + vec3 mercPos = mercator_tile_position(u_inv_rot_matrix, tilePos, u_tile_id, u_merc_center) + xAxis * extrude.x + yAxis * extrude.y; + + vec3 pos = mix_globe_mercator(globePos, mercPos, u_zoom_transition); +#else + vec3 pos = vec3(tilePos + extrude, elevation(tilePos)); +#endif gl_Position = u_matrix * vec4(pos, 1); diff --git a/src/shaders/line.vertex.glsl b/src/shaders/line.vertex.glsl index 06287c1fd2a..14977f808f6 100644 --- a/src/shaders/line.vertex.glsl +++ b/src/shaders/line.vertex.glsl @@ -18,7 +18,7 @@ attribute float a_linesofar; #endif uniform mat4 u_matrix; -uniform mediump float u_ratio; +uniform mat2 u_pixels_to_tile_units; uniform vec2 u_units_to_pixels; uniform lowp float u_device_pixel_ratio; @@ -95,8 +95,8 @@ void main() { mediump float t = 1.0 - abs(u); mediump vec2 offset2 = offset * a_extrude * EXTRUDE_SCALE * normal.y * mat2(t, -u, u, t); - vec4 projected_extrude = u_matrix * vec4(dist / u_ratio, 0.0, 0.0); - gl_Position = u_matrix * vec4(pos + offset2 / u_ratio, 0.0, 1.0) + projected_extrude; + vec4 projected_extrude = u_matrix * vec4(dist * u_pixels_to_tile_units, 0.0, 0.0); + gl_Position = u_matrix * vec4(pos + offset2 * u_pixels_to_tile_units, 0.0, 1.0) + projected_extrude; #ifndef RENDER_TO_TEXTURE // calculate how much the perspective view squishes or stretches the extrude diff --git a/src/shaders/line_pattern.vertex.glsl b/src/shaders/line_pattern.vertex.glsl index 89994fb972b..dcf94d5afdf 100644 --- a/src/shaders/line_pattern.vertex.glsl +++ b/src/shaders/line_pattern.vertex.glsl @@ -12,7 +12,7 @@ attribute float a_linesofar; uniform mat4 u_matrix; uniform vec2 u_units_to_pixels; -uniform mediump float u_ratio; +uniform mat2 u_pixels_to_tile_units; uniform lowp float u_device_pixel_ratio; varying vec2 v_normal; @@ -82,8 +82,8 @@ void main() { mediump float t = 1.0 - abs(u); mediump vec2 offset2 = offset * a_extrude * scale * normal.y * mat2(t, -u, u, t); - vec4 projected_extrude = u_matrix * vec4(dist / u_ratio, 0.0, 0.0); - gl_Position = u_matrix * vec4(pos + offset2 / u_ratio, 0.0, 1.0) + projected_extrude; + vec4 projected_extrude = u_matrix * vec4(dist * u_pixels_to_tile_units, 0.0, 0.0); + gl_Position = u_matrix * vec4(pos + offset2 * u_pixels_to_tile_units, 0.0, 1.0) + projected_extrude; #ifndef RENDER_TO_TEXTURE // calculate how much the perspective view squishes or stretches the extrude diff --git a/src/source/canvas_source.js b/src/source/canvas_source.js index 49f11c3dae6..7f5dc9ecda2 100644 --- a/src/source/canvas_source.js +++ b/src/source/canvas_source.js @@ -3,7 +3,7 @@ import ImageSource from './image_source.js'; import window from '../util/window.js'; -import rasterBoundsAttributes from '../data/raster_bounds_attributes.js'; +import boundsAttributes from '../data/bounds_attributes.js'; import SegmentVector from '../data/segment.js'; import Texture from '../render/texture.js'; import {ErrorEvent} from '../util/evented.js'; @@ -211,8 +211,12 @@ class CanvasSource extends ImageSource { const context = this.map.painter.context; const gl = context.gl; + if (!this._boundsArray) { + this._makeBoundsArray(); + } + if (!this.boundsBuffer) { - this.boundsBuffer = context.createVertexBuffer(this._boundsArray, rasterBoundsAttributes.members); + this.boundsBuffer = context.createVertexBuffer(this._boundsArray, boundsAttributes.members); } if (!this.boundsSegments) { diff --git a/src/source/image_source.js b/src/source/image_source.js index d76737bcc0c..1ebf0dcd230 100644 --- a/src/source/image_source.js +++ b/src/source/image_source.js @@ -5,11 +5,12 @@ import {Event, ErrorEvent, Evented} from '../util/evented.js'; import {getImage, ResourceType} from '../util/ajax.js'; import EXTENT from '../data/extent.js'; import {RasterBoundsArray} from '../data/array_types.js'; -import rasterBoundsAttributes from '../data/raster_bounds_attributes.js'; +import boundsAttributes from '../data/bounds_attributes.js'; import SegmentVector from '../data/segment.js'; import Texture from '../render/texture.js'; import MercatorCoordinate from '../geo/mercator_coordinate.js'; import browser from '../util/browser.js'; +import tileTransform, {getTilePoint} from '../geo/projection/tile_transform.js'; import type {Source} from './source.js'; import type {CanvasSourceSpecification} from './canvas_source.js'; @@ -219,6 +220,7 @@ class ImageSource extends Evented implements Source { */ setCoordinates(coordinates: Coordinates) { this.coordinates = coordinates; + delete this._boundsArray; // Calculate which mercator tile is suitable for rendering the video in // and create a buffer with the corner coordinates. These coordinates @@ -236,9 +238,19 @@ class ImageSource extends Evented implements Source { // level) this.minzoom = this.maxzoom = this.tileID.z; + this.fire(new Event('data', {dataType:'source', sourceDataType: 'content'})); + return this; + } + + _makeBoundsArray() { + const tileTr = tileTransform(this.tileID, this.map.transform.projection); + // Transform the corner coordinates into the coordinate space of our // tile. - const tileCoords = cornerCoords.map((coord) => this.tileID.getTilePoint(coord)._round()); + const tileCoords = this.coordinates.map((coord) => { + const projectedCoord = tileTr.projection.project(coord[0], coord[1]); + return getTilePoint(tileTr, projectedCoord)._round(); + }); this._boundsArray = new RasterBoundsArray(); this._boundsArray.emplaceBack(tileCoords[0].x, tileCoords[0].y, 0, 0); @@ -251,7 +263,6 @@ class ImageSource extends Evented implements Source { delete this.boundsBuffer; } - this.fire(new Event('data', {dataType:'source', sourceDataType: 'content'})); return this; } @@ -263,8 +274,12 @@ class ImageSource extends Evented implements Source { const context = this.map.painter.context; const gl = context.gl; + if (!this._boundsArray) { + this._makeBoundsArray(); + } + if (!this.boundsBuffer) { - this.boundsBuffer = context.createVertexBuffer(this._boundsArray, rasterBoundsAttributes.members); + this.boundsBuffer = context.createVertexBuffer(this._boundsArray, boundsAttributes.members); } if (!this.boundsSegments) { diff --git a/src/source/pixels_to_tile_units.js b/src/source/pixels_to_tile_units.js index 33fd3bfa8eb..35d863e93e3 100644 --- a/src/source/pixels_to_tile_units.js +++ b/src/source/pixels_to_tile_units.js @@ -1,8 +1,12 @@ // @flow +import {mat2} from 'gl-matrix'; + import EXTENT from '../data/extent.js'; import type {OverscaledTileID} from './tile_id.js'; +import type Transform from '../geo/transform.js'; +import type {TileTransform} from '../geo/projection/tile_transform.js'; /** * Converts a pixel value at a the given zoom level to tile units. @@ -19,3 +23,9 @@ import type {OverscaledTileID} from './tile_id.js'; export default function(tile: {tileID: OverscaledTileID, tileSize: number}, pixelValue: number, z: number): number { return pixelValue * (EXTENT / (tile.tileSize * Math.pow(2, z - tile.tileID.overscaledZ))); } + +export function getPixelsToTileUnitsMatrix(tile: {tileID: OverscaledTileID, tileSize: number, tileTransform: TileTransform}, transform: Transform): Float32Array { + const {scale} = tile.tileTransform; + const s = scale * EXTENT / (tile.tileSize * Math.pow(2, transform.zoom - tile.tileID.overscaledZ + tile.tileID.canonical.z)); + return mat2.scale(new Float32Array(4), transform.inverseAdjustmentMatrix, [s, s]); +} diff --git a/src/source/source_cache.js b/src/source/source_cache.js index 730a5f98beb..a9c47bdf5a7 100644 --- a/src/source/source_cache.js +++ b/src/source/source_cache.js @@ -415,7 +415,7 @@ class SourceCache extends Evented { handleWrapJump(lng: number) { // On top of the regular z/x/y values, TileIDs have a `wrap` value that specify - // which cppy of the world the tile belongs to. For example, at `lng: 10` you + // which copy of the world the tile belongs to. For example, at `lng: 10` you // might render z/x/y/0 while at `lng: 370` you would render z/x/y/1. // // When lng values get wrapped (going from `lng: 370` to `long: 10`) you expect @@ -595,7 +595,7 @@ class SourceCache extends Evented { if (idealTileIDs.length === 0) { return retain; } const checked: {[_: number | string]: boolean } = {}; - const minZoom = idealTileIDs[idealTileIDs.length - 1].overscaledZ; + const minZoom = idealTileIDs.reduce((min, id) => Math.min(min, id.overscaledZ), Infinity); const maxZoom = idealTileIDs[0].overscaledZ; assert(minZoom <= maxZoom); const minCoveringZoom = Math.max(maxZoom - SourceCache.maxOverzooming, this._source.minzoom); @@ -737,7 +737,8 @@ class SourceCache extends Evented { const cached = Boolean(tile); if (!cached) { - tile = new Tile(tileID, this._source.tileSize * tileID.overscaleFactor(), this.transform.tileZoom); + const painter = this.map ? this.map.painter : null; + tile = new Tile(tileID, this._source.tileSize * tileID.overscaleFactor(), this.transform.tileZoom, painter, this._source.type === 'raster'); this._loadTile(tile, this._tileLoaded.bind(this, tile, tileID.key, tile.state)); } diff --git a/src/source/source_state.js b/src/source/source_state.js index f41fea5783c..ceb78178b7e 100644 --- a/src/source/source_state.js +++ b/src/source/source_state.js @@ -3,6 +3,7 @@ import {extend} from '../util/util.js'; import Tile from './tile.js'; import type {FeatureState} from '../style-spec/expression/index.js'; +import type Painter from '../render/painter.js'; export type FeatureStates = {[feature_id: string]: FeatureState}; export type LayerFeatureStates = {[layer: string]: FeatureStates}; @@ -99,7 +100,7 @@ class SourceFeatureState { return reconciledState; } - initializeTileState(tile: Tile, painter: any) { + initializeTileState(tile: Tile, painter: ?Painter) { tile.setFeatureState(this.state, painter); } diff --git a/src/source/tile.js b/src/source/tile.js index a29dc1629ee..00a13ebee35 100644 --- a/src/source/tile.js +++ b/src/source/tile.js @@ -6,7 +6,7 @@ import FeatureIndex from '../data/feature_index.js'; import GeoJSONFeature from '../util/vectortile_to_geojson.js'; import featureFilter from '../style-spec/feature_filter/index.js'; import SymbolBucket from '../data/bucket/symbol_bucket.js'; -import {CollisionBoxArray} from '../data/array_types.js'; +import {CollisionBoxArray, TileBoundsArray, PosArray, TriangleIndexArray, LineStripIndexArray} from '../data/array_types.js'; import Texture from '../render/texture.js'; import browser from '../util/browser.js'; import {Debug} from '../util/debug.js'; @@ -16,6 +16,15 @@ import SourceFeatureState from '../source/source_state.js'; import {lazyLoadRTLTextPlugin} from './rtl_text_plugin.js'; import {TileSpaceDebugBuffer} from '../data/debug_viz.js'; import Color from '../style-spec/util/color.js'; +import loadGeometry from '../data/load_geometry.js'; +import earcut from 'earcut'; +import getTileMesh from './tile_mesh.js'; +import tileTransform from '../geo/projection/tile_transform.js'; + +import boundsAttributes from '../data/bounds_attributes.js'; +import EXTENT from '../data/extent.js'; +import Point from '@mapbox/point-geometry'; +import SegmentVector from '../data/segment.js'; const CLOCK_SKEW_RETRY_TIMEOUT = 30000; @@ -36,6 +45,11 @@ import type {LayerFeatureStates} from './source_state.js'; import type {Cancelable} from '../types/cancelable.js'; import type {FilterSpecification} from '../style-spec/types.js'; import type {TilespaceQueryGeometry} from '../style/query_geometry.js'; +import type VertexBuffer from '../gl/vertex_buffer.js'; +import type IndexBuffer from '../gl/index_buffer.js'; +import type {Projection} from '../geo/projection/index.js'; +import type {TileTransform} from '../geo/projection/tile_transform.js'; +import type Painter from '../render/painter.js'; export type TileState = | 'loading' // Tile data is in the process of loading. @@ -46,6 +60,20 @@ export type TileState = | 'expired'; /* Tile data was previously loaded, but has expired per its * HTTP headers and is in the process of refreshing. */ +// a tile bounds outline used for getting reprojected tile geometry in non-mercator projections +const BOUNDS_FEATURE = (() => { + const c0 = new Point(0, 0); + const c1 = new Point(EXTENT + 1, 0); + const c2 = new Point(EXTENT + 1, EXTENT + 1); + const c3 = new Point(0, EXTENT + 1); + const coords = [[c0, c1, c2, c3, c0]]; + return { + type: 2, + extent: EXTENT, + loadGeometry() { return coords.slice(); } + }; +})(); + /** * A tile object is the combination of a Coordinate, which defines * its place, as well as a unique ID and data tracking for its content @@ -79,6 +107,8 @@ class Tile { actor: ?Actor; vtLayers: {[_: string]: VectorTileLayer}; isSymbolTile: ?boolean; + isRaster: ?boolean; + tileTransform: TileTransform; neighboringTiles: ?Object; dem: ?DEMData; @@ -101,12 +131,20 @@ class Tile { queryGeometryDebugViz: TileSpaceDebugBuffer; queryBoundsDebugViz: TileSpaceDebugBuffer; + + _tileDebugBuffer: ?VertexBuffer; + _tileBoundsBuffer: ?VertexBuffer; + _tileDebugIndexBuffer: IndexBuffer; + _tileBoundsIndexBuffer: IndexBuffer; + _tileDebugSegments: SegmentVector; + _tileBoundsSegments: SegmentVector; + /** * @param {OverscaledTileID} tileID * @param size * @private */ - constructor(tileID: OverscaledTileID, size: number, tileZoom: number) { + constructor(tileID: OverscaledTileID, size: number, tileZoom: number, painter: any, isRaster?: boolean) { this.tileID = tileID; this.uid = uniqueId(); this.uses = 0; @@ -118,6 +156,7 @@ class Tile { this.hasSymbolBuckets = false; this.hasRTLText = false; this.dependencies = {}; + this.isRaster = isRaster; // Counts the number of times a response was already expired when // received. We're using this to add a delay when making a new request @@ -126,6 +165,14 @@ class Tile { this.expiredRequestCount = 0; this.state = 'loading'; + + if (painter) { + const {projection} = painter.transform; + this.tileTransform = tileTransform(tileID.canonical, projection); + if (painter.context) { + this._makeTileBoundsBuffers(painter.context, projection); + } + } } registerFadeDuration(duration: number) { @@ -254,6 +301,20 @@ class Tile { this.lineAtlasTexture.destroy(); } + if (this._tileBoundsBuffer) { + this._tileBoundsBuffer.destroy(); + this._tileBoundsIndexBuffer.destroy(); + this._tileBoundsSegments.destroy(); + this._tileBoundsBuffer = null; + } + + if (this._tileDebugBuffer) { + this._tileDebugBuffer.destroy(); + this._tileDebugIndexBuffer.destroy(); + this._tileDebugSegments.destroy(); + this._tileDebugBuffer = null; + } + Debug.run(() => { if (this.queryGeometryDebugViz) { this.queryGeometryDebugViz.unload(); @@ -334,7 +395,8 @@ class Tile { tileResult, pixelPosMatrix, transform, - params + params, + tileTransform: this.tileTransform }, layers, serializedLayers, sourceFeatureState); } @@ -436,14 +498,16 @@ class Tile { } } - setFeatureState(states: LayerFeatureStates, painter: any) { + setFeatureState(states: LayerFeatureStates, painter: ?Painter) { if (!this.latestFeatureIndex || !this.latestFeatureIndex.rawTileData || - Object.keys(states).length === 0) { + Object.keys(states).length === 0 || + !painter) { return; } const vtLayers = this.latestFeatureIndex.loadVTLayers(); + const availableImages = painter.style.listImages(); for (const id in this.buckets) { if (!painter.style.hasLayer(id)) continue; @@ -455,7 +519,7 @@ class Tile { const sourceLayerStates = states[sourceLayerId]; if (!sourceLayer || !sourceLayerStates || Object.keys(sourceLayerStates).length === 0) continue; - bucket.update(sourceLayerStates, sourceLayer, this.imageAtlas && this.imageAtlas.patternPositions || {}); + bucket.update(sourceLayerStates, sourceLayer, availableImages, this.imageAtlas && this.imageAtlas.patternPositions || {}); const layer = painter && painter.style && painter.style.getLayer(id); if (layer) { this.queryPadding = Math.max(this.queryPadding, layer.queryRadius(bucket)); @@ -511,6 +575,60 @@ class Tile { } }); } + + _makeDebugTileBoundsBuffers(context: Context, projection: Projection) { + if (!projection || projection.name === 'mercator' || this._tileDebugBuffer) return; + + // reproject tile outline with adaptive resampling + const boundsLine = loadGeometry(BOUNDS_FEATURE, this.tileID.canonical, this.tileTransform)[0]; + + // generate vertices for debugging tile boundaries + const debugVertices = new PosArray(); + const debugIndices = new LineStripIndexArray(); + + for (let i = 0; i < boundsLine.length; i++) { + const {x, y} = boundsLine[i]; + debugVertices.emplaceBack(x, y); + debugIndices.emplaceBack(i); + } + debugIndices.emplaceBack(0); + + this._tileDebugIndexBuffer = context.createIndexBuffer(debugIndices); + this._tileDebugBuffer = context.createVertexBuffer(debugVertices, boundsAttributes.members); + this._tileDebugSegments = SegmentVector.simpleSegment(0, 0, debugVertices.length, debugIndices.length); + } + + _makeTileBoundsBuffers(context: Context, projection: Projection) { + if (this._tileBoundsBuffer || !projection || projection.name === 'mercator') return; + + // reproject tile outline with adaptive resampling + const boundsLine = loadGeometry(BOUNDS_FEATURE, this.tileID.canonical, this.tileTransform)[0]; + + let boundsVertices, boundsIndices; + if (this.isRaster) { + // for raster tiles, generate an adaptive MARTINI mesh + const mesh = getTileMesh(this.tileID.canonical, projection); + boundsVertices = mesh.vertices; + boundsIndices = mesh.indices; + + } else { + // for vector tiles, generate an Earcut triangulation of the outline + boundsVertices = new TileBoundsArray(); + boundsIndices = new TriangleIndexArray(); + + for (const {x, y} of boundsLine) { + boundsVertices.emplaceBack(x, y, 0, 0); + } + const indices = earcut(boundsVertices.int16, undefined, 4); + for (let i = 0; i < indices.length; i += 3) { + boundsIndices.emplaceBack(indices[i], indices[i + 1], indices[i + 2]); + } + } + + this._tileBoundsBuffer = context.createVertexBuffer(boundsVertices, boundsAttributes.members); + this._tileBoundsIndexBuffer = context.createIndexBuffer(boundsIndices); + this._tileBoundsSegments = SegmentVector.simpleSegment(0, 0, boundsVertices.length, boundsIndices.length); + } } export default Tile; diff --git a/src/source/tile_id.js b/src/source/tile_id.js index bae57ca5cf0..69e7b690cb0 100644 --- a/src/source/tile_id.js +++ b/src/source/tile_id.js @@ -1,13 +1,9 @@ // @flow import {getTileBBox} from '@mapbox/whoots-js'; -import EXTENT from '../data/extent.js'; -import Point from '@mapbox/point-geometry'; -import MercatorCoordinate, {altitudeFromMercatorZ} from '../geo/mercator_coordinate.js'; import {MAX_SAFE_INTEGER} from '../util/util.js'; import assert from 'assert'; import {register} from '../util/web_worker_transfer.js'; -import {vec3} from 'gl-matrix'; export class CanonicalTileID { z: number; @@ -43,20 +39,6 @@ export class CanonicalTileID { .replace('{bbox-epsg-3857}', bbox); } - getTilePoint(coord: MercatorCoordinate) { - const tilesAtZoom = Math.pow(2, this.z); - return new Point( - (coord.x * tilesAtZoom - this.x) * EXTENT, - (coord.y * tilesAtZoom - this.y) * EXTENT); - } - - getTileVec3(coord: MercatorCoordinate): vec3 { - const tilesAtZoom = Math.pow(2, this.z); - const x = (coord.x * tilesAtZoom - this.x) * EXTENT; - const y = (coord.y * tilesAtZoom - this.y) * EXTENT; - return vec3.fromValues(x, y, altitudeFromMercatorZ(coord.z, coord.y)); - } - toString() { return `${this.z}/${this.x}/${this.y}`; } @@ -181,14 +163,6 @@ export class OverscaledTileID { toString() { return `${this.overscaledZ}/${this.canonical.x}/${this.canonical.y}`; } - - getTilePoint(coord: MercatorCoordinate) { - return this.canonical.getTilePoint(new MercatorCoordinate(coord.x - this.wrap, coord.y)); - } - - getTileVec3(coord: MercatorCoordinate) { - return this.canonical.getTileVec3(new MercatorCoordinate(coord.x - this.wrap, coord.y, coord.z)); - } } function calculateKey(wrap: number, overscaledZ: number, z: number, x: number, y: number): number { diff --git a/src/source/tile_mesh.js b/src/source/tile_mesh.js new file mode 100644 index 00000000000..eefbf5cac9f --- /dev/null +++ b/src/source/tile_mesh.js @@ -0,0 +1,162 @@ +// @flow +// logic for generating non-Mercator adaptive raster tile reprojection meshes with MARTINI + +import tileTransform from '../geo/projection/tile_transform.js'; +import EXTENT from '../data/extent.js'; +import {lngFromMercatorX, latFromMercatorY} from '../geo/mercator_coordinate.js'; +import {TileBoundsArray, TriangleIndexArray} from '../data/array_types.js'; + +import type {CanonicalTileID} from './tile_id.js'; +import type {Projection} from '../geo/projection/index.js'; + +const meshSize = 32; +const gridSize = meshSize + 1; + +const numTriangles = meshSize * meshSize * 2 - 2; +const numParentTriangles = numTriangles - meshSize * meshSize; + +const coords = new Uint16Array(numTriangles * 4); + +// precalculate RTIN triangle coordinates +for (let i = 0; i < numTriangles; i++) { + let id = i + 2; + let ax = 0, ay = 0, bx = 0, by = 0, cx = 0, cy = 0; + + if (id & 1) { + bx = by = cx = meshSize; // bottom-left triangle + + } else { + ax = ay = cy = meshSize; // top-right triangle + } + + while ((id >>= 1) > 1) { + const mx = (ax + bx) >> 1; + const my = (ay + by) >> 1; + + if (id & 1) { // left half + bx = ax; by = ay; + ax = cx; ay = cy; + + } else { // right half + ax = bx; ay = by; + bx = cx; by = cy; + } + + cx = mx; cy = my; + } + + const k = i * 4; + coords[k + 0] = ax; + coords[k + 1] = ay; + coords[k + 2] = bx; + coords[k + 3] = by; +} + +// temporary arrays we'll reuse for MARTINI mesh code +const reprojectedCoords = new Uint16Array(gridSize * gridSize * 2); +const used = new Uint8Array(gridSize * gridSize); +const indexMap = new Uint16Array(gridSize * gridSize); + +type TileMesh = { + vertices: TileBoundsArray, + indices: TriangleIndexArray +}; + +export default function getTileMesh(canonical: CanonicalTileID, projection: Projection): TileMesh { + const cs = tileTransform(canonical, projection); + const z2 = Math.pow(2, canonical.z); + + for (let y = 0; y < gridSize; y++) { + for (let x = 0; x < gridSize; x++) { + const lng = lngFromMercatorX((canonical.x + x / meshSize) / z2); + const lat = latFromMercatorY((canonical.y + y / meshSize) / z2); + const p = projection.project(lng, lat); + const k = y * gridSize + x; + reprojectedCoords[2 * k + 0] = Math.round((p.x * cs.scale - cs.x) * EXTENT); + reprojectedCoords[2 * k + 1] = Math.round((p.y * cs.scale - cs.y) * EXTENT); + } + } + + used.fill(0); + indexMap.fill(0); + + // iterate over all possible triangles, starting from the smallest level + for (let i = numTriangles - 1; i >= 0; i--) { + const k = i * 4; + const ax = coords[k + 0]; + const ay = coords[k + 1]; + const bx = coords[k + 2]; + const by = coords[k + 3]; + const mx = (ax + bx) >> 1; + const my = (ay + by) >> 1; + const cx = mx + my - ay; + const cy = my + ax - mx; + + const aIndex = ay * gridSize + ax; + const bIndex = by * gridSize + bx; + const mIndex = my * gridSize + mx; + + // calculate error in the middle of the long edge of the triangle + const rax = reprojectedCoords[2 * aIndex + 0]; + const ray = reprojectedCoords[2 * aIndex + 1]; + const rbx = reprojectedCoords[2 * bIndex + 0]; + const rby = reprojectedCoords[2 * bIndex + 1]; + const rmx = reprojectedCoords[2 * mIndex + 0]; + const rmy = reprojectedCoords[2 * mIndex + 1]; + + // raster tiles are typically 512px, and we use 1px as an error threshold; 8192 / 512 = 16 + const isUsed = Math.hypot((rax + rbx) / 2 - rmx, (ray + rby) / 2 - rmy) >= 16; + + used[mIndex] = used[mIndex] || (isUsed ? 1 : 0); + + if (i < numParentTriangles) { // bigger triangles; accumulate error with children + const leftChildIndex = ((ay + cy) >> 1) * gridSize + ((ax + cx) >> 1); + const rightChildIndex = ((by + cy) >> 1) * gridSize + ((bx + cx) >> 1); + used[mIndex] = used[mIndex] || used[leftChildIndex] || used[rightChildIndex]; + } + } + + const vertices = new TileBoundsArray(); + const indices = new TriangleIndexArray(); + + let numVertices = 0; + + function addVertex(x, y) { + const k = y * gridSize + x; + + if (indexMap[k] === 0) { + vertices.emplaceBack( + reprojectedCoords[2 * k + 0], + reprojectedCoords[2 * k + 1], + x * EXTENT / meshSize, + y * EXTENT / meshSize); + + // save new vertex index so that we can reuse it + indexMap[k] = ++numVertices; + } + + return indexMap[k] - 1; + } + + function addTriangles(ax, ay, bx, by, cx, cy) { + const mx = (ax + bx) >> 1; + const my = (ay + by) >> 1; + + if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && used[my * gridSize + mx]) { + // triangle doesn't approximate the surface well enough; drill down further + addTriangles(cx, cy, ax, ay, mx, my); + addTriangles(bx, by, cx, cy, mx, my); + + } else { + const ai = addVertex(ax, ay); + const bi = addVertex(bx, by); + const ci = addVertex(cx, cy); + indices.emplaceBack(ai, bi, ci); + } + } + + addTriangles(0, 0, meshSize, meshSize, meshSize, 0); + addTriangles(meshSize, meshSize, 0, 0, 0, meshSize); + + return {vertices, indices}; +} diff --git a/src/source/video_source.js b/src/source/video_source.js index 3bf4e04807e..0ed5cc59991 100644 --- a/src/source/video_source.js +++ b/src/source/video_source.js @@ -3,7 +3,7 @@ import {getVideo, ResourceType} from '../util/ajax.js'; import ImageSource from './image_source.js'; -import rasterBoundsAttributes from '../data/raster_bounds_attributes.js'; +import boundsAttributes from '../data/bounds_attributes.js'; import SegmentVector from '../data/segment.js'; import Texture from '../render/texture.js'; import {ErrorEvent} from '../util/evented.js'; @@ -208,8 +208,12 @@ class VideoSource extends ImageSource { const context = this.map.painter.context; const gl = context.gl; + if (!this._boundsArray) { + this._makeBoundsArray(); + } + if (!this.boundsBuffer) { - this.boundsBuffer = context.createVertexBuffer(this._boundsArray, rasterBoundsAttributes.members); + this.boundsBuffer = context.createVertexBuffer(this._boundsArray, boundsAttributes.members); } if (!this.boundsSegments) { diff --git a/src/source/worker.js b/src/source/worker.js index b1336bec6bd..715fd101117 100644 --- a/src/source/worker.js +++ b/src/source/worker.js @@ -12,6 +12,7 @@ import {enforceCacheSizeLimit} from '../util/tile_request_cache.js'; import {extend} from '../util/util.js'; import {PerformanceUtils} from '../util/performance.js'; import {Event} from '../util/evented.js'; +import {getProjection} from '../geo/projection/index.js'; import type { WorkerSource, @@ -24,8 +25,9 @@ import type { import type {WorkerGlobalScopeInterface} from '../util/web_worker.js'; import type {Callback} from '../types/callback.js'; -import type {LayerSpecification} from '../style-spec/types.js'; +import type {LayerSpecification, ProjectionSpecification} from '../style-spec/types.js'; import type {PluginState} from './rtl_text_plugin.js'; +import type {Projection} from '../geo/projection/index.js'; /** * @private @@ -38,7 +40,9 @@ export default class Worker { workerSourceTypes: {[_: string]: Class }; workerSources: {[_: string]: {[_: string]: {[_: string]: WorkerSource } } }; demWorkerSources: {[_: string]: {[_: string]: RasterDEMTileWorkerSource } }; - isSpriteLoaded: boolean; + projections: {[_: string]: Projection }; + defaultProjection: Projection; + isSpriteLoaded: {[_: string]: boolean }; referrer: ?string; terrain: ?boolean; @@ -49,7 +53,10 @@ export default class Worker { this.layerIndexes = {}; this.availableImages = {}; - this.isSpriteLoaded = false; + this.isSpriteLoaded = {}; + + this.projections = {}; + this.defaultProjection = getProjection({name: 'mercator'}); this.workerSourceTypes = { vector: VectorTileWorkerSource, @@ -78,6 +85,14 @@ export default class Worker { }; } + clearCaches(mapId: string, unused: mixed, callback: WorkerTileCallback) { + delete this.layerIndexes[mapId]; + delete this.availableImages[mapId]; + delete this.workerSources[mapId]; + delete this.demWorkerSources[mapId]; + callback(); + } + checkIfReady(mapID: string, unused: mixed, callback: WorkerTileCallback) { // noop, used to check if a worker is fully set up and ready to receive messages callback(); @@ -88,7 +103,7 @@ export default class Worker { } spriteLoaded(mapId: string, bool: boolean) { - this.isSpriteLoaded = bool; + this.isSpriteLoaded[mapId] = bool; for (const workerSource in this.workerSources[mapId]) { const ws = this.workerSources[mapId][workerSource]; for (const source in ws) { @@ -116,6 +131,10 @@ export default class Worker { callback(); } + setProjection(mapId: string, config: ProjectionSpecification) { + this.projections[mapId] = getProjection(config); + } + setLayers(mapId: string, layers: Array, callback: WorkerTileCallback) { this.getLayerIndex(mapId).replace(layers); callback(); @@ -129,6 +148,7 @@ export default class Worker { loadTile(mapId: string, params: WorkerTileParameters & {type: string}, callback: WorkerTileCallback) { assert(params.type); const p = this.enableTerrain ? extend({enableTerrain: this.terrain}, params) : params; + p.projection = this.projections[mapId] || this.defaultProjection; this.getWorkerSource(mapId, params.type, params.source).loadTile(p, callback); } @@ -140,6 +160,7 @@ export default class Worker { reloadTile(mapId: string, params: WorkerTileParameters & {type: string}, callback: WorkerTileCallback) { assert(params.type); const p = this.enableTerrain ? extend({enableTerrain: this.terrain}, params) : params; + p.projection = this.projections[mapId] || this.defaultProjection; this.getWorkerSource(mapId, params.type, params.source).reloadTile(p, callback); } @@ -240,7 +261,7 @@ export default class Worker { }, scheduler: this.actor.scheduler }; - this.workerSources[mapId][type][source] = new (this.workerSourceTypes[type]: any)((actor: any), this.getLayerIndex(mapId), this.getAvailableImages(mapId), this.isSpriteLoaded); + this.workerSources[mapId][type][source] = new (this.workerSourceTypes[type]: any)((actor: any), this.getLayerIndex(mapId), this.getAvailableImages(mapId), this.isSpriteLoaded[mapId]); } return this.workerSources[mapId][type][source]; diff --git a/src/source/worker_source.js b/src/source/worker_source.js index 781a181b627..4f0791be448 100644 --- a/src/source/worker_source.js +++ b/src/source/worker_source.js @@ -13,6 +13,7 @@ import type DEMData from '../data/dem_data.js'; import type {StyleGlyph} from '../style/style_glyph.js'; import type {StyleImage} from '../style/style_image.js'; import type {PromoteIdSpecification} from '../style-spec/types.js'; +import type {Projection} from '../geo/projection/index.js'; import window from '../util/window.js'; const {ImageBitmap} = window; @@ -38,7 +39,8 @@ export type WorkerTileParameters = RequestedTileParameters & { showCollisionBoxes: boolean, collectResourceTiming?: boolean, returnDependencies?: boolean, - enableTerrain?: boolean + enableTerrain?: boolean, + projection?: Projection }; export type WorkerDEMTileParameters = TileParameters & { diff --git a/src/source/worker_tile.js b/src/source/worker_tile.js index 4831ddb792f..b1ca42a983c 100644 --- a/src/source/worker_tile.js +++ b/src/source/worker_tile.js @@ -15,8 +15,9 @@ import LineAtlas from '../render/line_atlas.js'; import ImageAtlas from '../render/image_atlas.js'; import GlyphAtlas from '../render/glyph_atlas.js'; import EvaluationParameters from '../style/evaluation_parameters.js'; -import {OverscaledTileID} from './tile_id.js'; +import {CanonicalTileID, OverscaledTileID} from './tile_id.js'; import {PerformanceUtils} from '../util/performance.js'; +import tileTransform from '../geo/projection/tile_transform.js'; import type {Bucket} from '../data/bucket.js'; import type Actor from '../util/actor.js'; @@ -29,12 +30,14 @@ import type { WorkerTileCallback, } from '../source/worker_source.js'; import type {PromoteIdSpecification} from '../style-spec/types.js'; +import type {TileTransform} from '../geo/projection/tile_transform.js'; class WorkerTile { tileID: OverscaledTileID; uid: number; zoom: number; tileZoom: number; + canonical: CanonicalTileID; pixelRatio: number; tileSize: number; source: string; @@ -45,6 +48,7 @@ class WorkerTile { returnDependencies: boolean; enableTerrain: boolean; isSymbolTile: ?boolean; + tileTransform: TileTransform; status: 'parsing' | 'done'; data: VectorTile; @@ -59,6 +63,7 @@ class WorkerTile { this.tileZoom = params.tileZoom; this.uid = params.uid; this.zoom = params.zoom; + this.canonical = params.tileID.canonical; this.pixelRatio = params.pixelRatio; this.tileSize = params.tileSize; this.source = params.source; @@ -69,6 +74,12 @@ class WorkerTile { this.promoteId = params.promoteId; this.enableTerrain = !!params.enableTerrain; this.isSymbolTile = params.isSymbolTile; + if (params.projection) { + this.tileTransform = tileTransform(params.tileID.canonical, params.projection); + } else { + // silence flow + assert(params.projection); + } } parse(data: VectorTile, layerIndex: StyleLayerIndex, availableImages: Array, actor: Actor, callback: WorkerTileCallback) { @@ -147,15 +158,17 @@ class WorkerTile { index: featureIndex.bucketLayerIDs.length, layers: family, zoom: this.zoom, + canonical: this.canonical, pixelRatio: this.pixelRatio, overscaling: this.overscaling, collisionBoxArray: this.collisionBoxArray, sourceLayerIndex, sourceID: this.source, - enableTerrain: this.enableTerrain + enableTerrain: this.enableTerrain, + availableImages }); - bucket.populate(features, options, this.tileID.canonical); + bucket.populate(features, options, this.tileID.canonical, this.tileTransform); featureIndex.bucketLayerIDs.push(family.map((l) => l.id)); } } @@ -229,6 +242,7 @@ class WorkerTile { iconMap, imageAtlas.iconPositions, this.showCollisionBoxes, + availableImages, this.tileID.canonical, this.tileZoom); } else if (bucket.hasPattern && @@ -236,7 +250,7 @@ class WorkerTile { bucket instanceof FillBucket || bucket instanceof FillExtrusionBucket)) { recalculateLayers(bucket.layers, this.zoom, availableImages); - bucket.addFeatures(options, this.tileID.canonical, imageAtlas.patternPositions); + bucket.addFeatures(options, this.tileID.canonical, imageAtlas.patternPositions, availableImages); } } diff --git a/src/style-spec/diff.js b/src/style-spec/diff.js index 4baa19f7997..44dc24f449a 100644 --- a/src/style-spec/diff.js +++ b/src/style-spec/diff.js @@ -106,8 +106,12 @@ const operations = { /* * { command: 'setFog', args: [fogProperties] } */ - setFog: 'setFog' + setFog: 'setFog', + /* + * { command: 'setProjection', args: [projectionProperties] } + */ + setProjection: 'setProjection' }; function addSource(sourceId, after, commands) { @@ -363,6 +367,9 @@ function diffStyles(before, after) { if (!isEqual(before.fog, after.fog)) { commands.push({command: operations.setFog, args: [after.fog]}); } + if (!isEqual(before.projection, after.projection)) { + commands.push({command: operations.setProjection, args: [after.projection]}); + } // Handle changes to `sources` // If a source is to be removed, we also--before the removeSource diff --git a/src/style-spec/expression/definitions/index.js b/src/style-spec/expression/definitions/index.js index 434cf776f1f..914ece11b6c 100644 --- a/src/style-spec/expression/definitions/index.js +++ b/src/style-spec/expression/definitions/index.js @@ -201,6 +201,16 @@ CompoundExpression.register(expressions, { [], (ctx) => ctx.globals.zoom ], + 'pitch': [ + NumberType, + [], + (ctx) => ctx.globals.pitch || 0 + ], + 'distance-from-center': [ + NumberType, + [], + (ctx) => ctx.distanceFromCenter() + ], 'heatmap-density': [ NumberType, [], diff --git a/src/style-spec/expression/evaluation_context.js b/src/style-spec/expression/evaluation_context.js index f851dcf9c62..dd1fe1641ff 100644 --- a/src/style-spec/expression/evaluation_context.js +++ b/src/style-spec/expression/evaluation_context.js @@ -1,9 +1,12 @@ // @flow import {Color} from './values.js'; + +import type Point from '@mapbox/point-geometry'; import type {FormattedSection} from './types/formatted.js'; import type {GlobalProperties, Feature, FeatureState} from './index.js'; import type {CanonicalTileID} from '../../source/tile_id.js'; +import type {FeatureDistanceData} from '../feature_filter/index.js'; const geometryTypes = ['Unknown', 'Point', 'LineString', 'Polygon']; @@ -14,6 +17,8 @@ class EvaluationContext { formattedSection: ?FormattedSection; availableImages: ?Array; canonical: ?CanonicalTileID; + featureTileCoord: ?Point; + featureDistanceData: ?FeatureDistanceData; _parseColorCache: {[_: string]: ?Color}; @@ -25,6 +30,8 @@ class EvaluationContext { this._parseColorCache = {}; this.availableImages = null; this.canonical = null; + this.featureTileCoord = null; + this.featureDistanceData = null; } id() { @@ -47,6 +54,29 @@ class EvaluationContext { return this.feature && this.feature.properties || {}; } + distanceFromCenter() { + if (this.featureTileCoord && this.featureDistanceData) { + + const c = this.featureDistanceData.center; + const scale = this.featureDistanceData.scale; + const {x, y} = this.featureTileCoord; + + // Calculate the distance vector `d` (left handed) + const dX = x * scale - c[0]; + const dY = y * scale - c[1]; + + // The bearing vector `b` (left handed) + const bX = this.featureDistanceData.bearing[0]; + const bY = this.featureDistanceData.bearing[1]; + + // Distance is calculated as `dot(d, v)` + const dist = (bX * dX + bY * dY); + return dist; + } + + return 0; + } + parseColor(input: string): ?Color { let cached = this._parseColorCache[input]; if (!cached) { diff --git a/src/style-spec/expression/index.js b/src/style-spec/expression/index.js index d4156d81600..4d3f5335668 100644 --- a/src/style-spec/expression/index.js +++ b/src/style-spec/expression/index.js @@ -27,6 +27,7 @@ import type {PropertyValueSpecification} from '../types.js'; import type {FormattedSection} from './types/formatted.js'; import type Point from '@mapbox/point-geometry'; import type {CanonicalTileID} from '../../source/tile_id.js'; +import type {FeatureDistanceData} from '../feature_filter/index.js'; export type Feature = { +type: 1 | 2 | 3 | 'Unknown' | 'Point' | 'MultiPoint' | 'LineString' | 'MultiLineString' | 'Polygon' | 'MultiPolygon', @@ -40,6 +41,7 @@ export type FeatureState = {[_: string]: any}; export type GlobalProperties = $ReadOnly<{ zoom: number, + pitch?: number, heatmapDensity?: number, lineProgress?: number, skyRadialProgress?: number, @@ -63,24 +65,28 @@ export class StyleExpression { this._enumValues = propertySpec && propertySpec.type === 'enum' ? propertySpec.values : null; } - evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection): any { + evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection, featureTileCoord?: Point, featureDistanceData?: FeatureDistanceData): 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; + this._evaluator.featureTileCoord = featureTileCoord || null; + this._evaluator.featureDistanceData = featureDistanceData || null; return this.expression.evaluate(this._evaluator); } - evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection): any { + evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array, formattedSection?: FormattedSection, featureTileCoord?: Point, featureDistanceData?: FeatureDistanceData): any { this._evaluator.globals = globals; this._evaluator.feature = feature || null; this._evaluator.featureState = featureState || null; this._evaluator.canonical = canonical; this._evaluator.availableImages = availableImages || null; this._evaluator.formattedSection = formattedSection || null; + this._evaluator.featureTileCoord = featureTileCoord || null; + this._evaluator.featureDistanceData = featureDistanceData || null; try { const val = this.expression.evaluate(this._evaluator); @@ -233,7 +239,7 @@ export function createPropertyExpression(expression: mixed, propertySpec: StyleP return error([new ParsingError('', 'data expressions not supported')]); } - const isZoomConstant = isConstant.isGlobalPropertyConstant(parsed, ['zoom']); + const isZoomConstant = isConstant.isGlobalPropertyConstant(parsed, ['zoom', 'pitch', 'distance-from-center']); if (!isZoomConstant && !supportsZoomExpression(propertySpec)) { return error([new ParsingError('', 'zoom expressions not supported')]); } diff --git a/src/style-spec/expression/parsing_context.js b/src/style-spec/expression/parsing_context.js index 60df487c5ae..d6525276a30 100644 --- a/src/style-spec/expression/parsing_context.js +++ b/src/style-spec/expression/parsing_context.js @@ -229,5 +229,5 @@ function isConstant(expression: Expression) { } return isFeatureConstant(expression) && - isGlobalPropertyConstant(expression, ['zoom', 'heatmap-density', 'line-progress', 'sky-radial-progress', 'accumulated', 'is-supported-script']); + isGlobalPropertyConstant(expression, ['zoom', 'heatmap-density', 'line-progress', 'sky-radial-progress', 'accumulated', 'is-supported-script', 'pitch', 'distance-from-center']); } diff --git a/src/style-spec/feature_filter/index.js b/src/style-spec/feature_filter/index.js index 9868c32bf09..9e7d970fdbc 100644 --- a/src/style-spec/feature_filter/index.js +++ b/src/style-spec/feature_filter/index.js @@ -1,14 +1,18 @@ // @flow import {createExpression} from '../expression/index.js'; +import {isFeatureConstant} from '../expression/is_constant.js'; +import latest from '../reference/latest.js'; import type {GlobalProperties, Feature} from '../expression/index.js'; import type {CanonicalTileID} from '../../source/tile_id.js'; +import type Point from '@mapbox/point-geometry'; -type FilterExpression = (globalProperties: GlobalProperties, feature: Feature, canonical?: CanonicalTileID) => boolean; -export type FeatureFilter ={filter: FilterExpression, needGeometry: boolean}; +export type FeatureDistanceData = {bearing: [number, number], center: [number, number], scale: number}; +type FilterExpression = (globalProperties: GlobalProperties, feature: Feature, canonical?: CanonicalTileID, featureTileCoord?: Point, featureDistanceData?: FeatureDistanceData) => boolean; +export type FeatureFilter = {filter: FilterExpression, dynamicFilter?: FilterExpression, needGeometry: boolean, needFeature: boolean}; export default createFilter; -export {isExpressionFilter}; +export {isExpressionFilter, isDynamicFilter, extractStaticFilter}; function isExpressionFilter(filter: any) { if (filter === true || filter === false) { @@ -52,17 +56,6 @@ function isExpressionFilter(filter: any) { } } -const filterSpec = { - 'type': 'boolean', - 'default': false, - 'transition': false, - 'property-type': 'data-driven', - 'expression': { - 'interpolated': false, - 'parameters': ['zoom', 'feature'] - } -}; - /** * Given a filter expressed as nested arrays, return a new function * that evaluates whether a given feature (with a .properties or .tags property) @@ -70,25 +63,192 @@ const filterSpec = { * * @private * @param {Array} filter mapbox gl filter + * @param {string} layerType the type of the layer this filter will be applied to. * @returns {Function} filter-evaluating function */ -function createFilter(filter: any): FeatureFilter { +function createFilter(filter: any, layerType?: string = 'fill'): FeatureFilter { if (filter === null || filter === undefined) { - return {filter: () => true, needGeometry: false}; + return {filter: () => true, needGeometry: false, needFeature: false}; } if (!isExpressionFilter(filter)) { filter = convertFilter(filter); } + const filterExp = ((filter: any): string[] | string | boolean); + + let staticFilter = true; + try { + staticFilter = extractStaticFilter(filterExp); + } catch (e) { + console.warn( +`Failed to extract static filter. Filter will continue working, but at higher memory usage and slower framerate. +This is most likely a bug, please report this via https://github.com/mapbox/mapbox-gl-js/issues/new?assignees=&labels=&template=Bug_report.md +and paste the contents of this message in the report. +Thank you! +Filter Expression: +${JSON.stringify(filterExp, null, 2)} + `); + } - const compiled = createExpression(filter, filterSpec); - if (compiled.result === 'error') { - throw new Error(compiled.value.map(err => `${err.key}: ${err.message}`).join(', ')); + // Compile the static component of the filter + const filterSpec = latest[`filter_${layerType}`]; + const compiledStaticFilter = createExpression(staticFilter, filterSpec); + + let filterFunc = null; + if (compiledStaticFilter.result === 'error') { + throw new Error(compiledStaticFilter.value.map(err => `${err.key}: ${err.message}`).join(', ')); } else { - const needGeometry = geometryNeeded(filter); - return {filter: (globalProperties: GlobalProperties, feature: Feature, canonical?: CanonicalTileID) => compiled.value.evaluate(globalProperties, feature, {}, canonical), - needGeometry}; + filterFunc = (globalProperties: GlobalProperties, feature: Feature, canonical?: CanonicalTileID) => compiledStaticFilter.value.evaluate(globalProperties, feature, {}, canonical); + } + + // If the static component is not equal to the entire filter then we have a dynamic component + // Compile the dynamic component separately + let dynamicFilterFunc = null; + let needFeature = null; + if (staticFilter !== filterExp) { + const compiledDynamicFilter = createExpression(filterExp, filterSpec); + + if (compiledDynamicFilter.result === 'error') { + throw new Error(compiledDynamicFilter.value.map(err => `${err.key}: ${err.message}`).join(', ')); + } else { + dynamicFilterFunc = (globalProperties: GlobalProperties, feature: Feature, canonical?: CanonicalTileID, featureTileCoord?: Point, featureDistanceData?: FeatureDistanceData) => compiledDynamicFilter.value.evaluate(globalProperties, feature, {}, canonical, undefined, undefined, featureTileCoord, featureDistanceData); + needFeature = !isFeatureConstant(compiledDynamicFilter.value.expression); + } + } + + filterFunc = ((filterFunc: any): FilterExpression); + const needGeometry = geometryNeeded(staticFilter); + + return { + filter: filterFunc, + dynamicFilter: dynamicFilterFunc ? dynamicFilterFunc : undefined, + needGeometry, + needFeature: !!needFeature + }; +} + +function extractStaticFilter(filter: any): any { + if (!isDynamicFilter(filter)) { + return filter; + } + + // Shallow copy so we can replace expressions in-place + let result = filter.slice(); + + // 1. Union branches + unionDynamicBranches(result); + + // 2. Collapse dynamic conditions to `true` + result = collapseDynamicBooleanExpressions(result); + + return result; +} + +function collapseDynamicBooleanExpressions(expression: any): any { + if (!Array.isArray(expression)) { + return expression; + } + + const collapsed = collapsedExpression(expression); + if (collapsed === true) { + return collapsed; + } else { + return collapsed.map((subExpression) => collapseDynamicBooleanExpressions(subExpression)); + } +} + +/** + * Traverses the expression and replaces all instances of branching on a + * `dynamic` conditional (such as `['pitch']` or `['distance-from-center']`) + * into an `any` expression. + * This ensures that all possible outcomes of a `dynamic` branch are considered + * when evaluating the expression upfront during filtering. + * + * @param {Array} filter the filter expression mutated in-place. + */ +function unionDynamicBranches(filter: any) { + let isBranchingDynamically = false; + const branches = []; + + if (filter[0] === 'case') { + for (let i = 1; i < filter.length - 1; i += 2) { + isBranchingDynamically = isBranchingDynamically || isDynamicFilter(filter[i]); + branches.push(filter[i + 1]); + } + + branches.push(filter[filter.length - 1]); + } else if (filter[0] === 'match') { + isBranchingDynamically = isBranchingDynamically || isDynamicFilter(filter[1]); + + for (let i = 2; i < filter.length - 1; i += 2) { + branches.push(filter[i + 1]); + } + branches.push(filter[filter.length - 1]); + } else if (filter[0] === 'step') { + isBranchingDynamically = isBranchingDynamically || isDynamicFilter(filter[1]); + + for (let i = 1; i < filter.length - 1; i += 2) { + branches.push(filter[i + 1]); + } + } + + if (isBranchingDynamically) { + filter.length = 0; + filter.push('any', ...branches); + } + + // traverse and recurse into children + for (let i = 1; i < filter.length; i++) { + unionDynamicBranches(filter[i]); + } +} + +function isDynamicFilter(filter: any): boolean { + // Base Cases + if (!Array.isArray(filter)) { + return false; + } + if (isRootExpressionDynamic(filter[0])) { + return true; + } + + for (let i = 1; i < filter.length; i++) { + const child = filter[i]; + if (isDynamicFilter(child)) { + return true; + } + } + + return false; +} + +function isRootExpressionDynamic(expression: string): boolean { + return expression === 'pitch' || + expression === 'distance-from-center'; +} + +const dynamicConditionExpressions = new Set([ + 'in', + '==', + '!=', + '>', + '>=', + '<', + '<=', + 'to-boolean' +]); + +function collapsedExpression(expression: any): any { + if (dynamicConditionExpressions.has(expression[0])) { + + for (let i = 1; i < expression.length; i++) { + const param = expression[i]; + if (isDynamicFilter(param)) { + return true; + } + } } + return expression; } // Comparison function to sort numbers and strings diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index 9cfe5deaf88..e25044a9bd5 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -94,6 +94,15 @@ "delay": 0 } }, + "projection": { + "type": "projection", + "doc": "The projection the map should be rendered in. Suported projections are Albers, Equal Earth, Equirectangular (WGS84), Globe, Lambert conformal conic, Mercator, Natural Earth, and Winkel Tripel. Terrain and fog are not supported for projections other than mercator.", + "example": { + "name": "albers", + "center": [-154, 50], + "parallels": [55, 65] + } + }, "layers": { "required": true, "type": "array", @@ -652,7 +661,7 @@ }, "filter": { "type": "filter", - "doc": "A expression specifying conditions on source features. Only features that match the filter are displayed. Zoom expressions in filters are only evaluated at integer zoom levels. The `feature-state` expression is not supported in filter expressions." + "doc": "An expression specifying conditions on source features. Only features that match the filter are displayed. Zoom expressions in filters are only evaluated at integer zoom levels. The `[\"feature-state\", ...]` expression is not supported in filter expressions. The `[\"pitch\"]` and `[\"distance-from-center\"]` expressions are supported only for filter expressions on the symbol layer." }, "layout": { "type": "layout", @@ -2500,6 +2509,72 @@ "value": "*", "doc": "A filter selects specific features from a layer." }, + "filter_symbol": { + "type": "boolean", + "doc": "Expression which determines whether or not to display a symbol. Symbols support dynamic filtering, meaning this expression can use the `[\"pitch\"]` and `[\"distance-from-center\"]` expressions to reference the current state of the view.", + "default": false, + "transition": false, + "property-type": "data-driven", + "expression": { + "interpolated": false, + "parameters": ["zoom", "feature", "pitch", "distance-from-center"] + } + }, + "filter_fill": { + "type": "boolean", + "doc": "Expression which determines whether or not to display a polygon. Fill layer does NOT support dynamic filtering, meaning this expression can NOT use the `[\"pitch\"]` and `[\"distance-from-center\"]` expressions to reference the current state of the view.", + "default": false, + "transition": false, + "property-type": "data-driven", + "expression": { + "interpolated": false, + "parameters": ["zoom", "feature"] + } + }, + "filter_line": { + "type": "boolean", + "doc": "Expression which determines whether or not to display a Polygon or LineString. Line layer does NOT support dynamic filtering, meaning this expression can NOT use the `[\"pitch\"]` and `[\"distance-from-center\"]` expressions to reference the current state of the view.", + "default": false, + "transition": false, + "property-type": "data-driven", + "expression": { + "interpolated": false, + "parameters": ["zoom", "feature"] + } + }, + "filter_circle": { + "type": "boolean", + "doc": "Expression which determines whether or not to display a circle. Circle layer does NOT support dynamic filtering, meaning this expression can NOT use the `[\"pitch\"]` and `[\"distance-from-center\"]` expressions to reference the current state of the view.", + "default": false, + "transition": false, + "property-type": "data-driven", + "expression": { + "interpolated": false, + "parameters": ["zoom", "feature"] + } + }, + "filter_fill-extrusion": { + "type": "boolean", + "doc": "Expression which determines whether or not to display a Polygon. Fill-extrusion layer does NOT support dynamic filtering, meaning this expression can NOT use the `[\"pitch\"]` and `[\"distance-from-center\"]` expressions to reference the current state of the view.", + "default": false, + "transition": false, + "property-type": "data-driven", + "expression": { + "interpolated": false, + "parameters": ["zoom", "feature"] + } + }, + "filter_heatmap": { + "type": "boolean", + "doc": "Expression used to determine whether a point is being displayed or not. Heatmap layer does NOT support dynamic filtering, meaning this expression can NOT use the `[\"pitch\"]` and `[\"distance-from-center\"]` expressions to reference the current state of the view.", + "default": false, + "transition": false, + "property-type": "data-driven", + "expression": { + "interpolated": false, + "parameters": ["zoom", "feature"] + } + }, "filter_operator": { "type": "enum", "values": { @@ -3091,7 +3166,7 @@ } }, "length": { - "doc": "Gets the length of an array or string.", + "doc": "Returns the length of an array or string.", "group": "Lookup", "sdk-support": { "basic functionality": { @@ -3103,7 +3178,7 @@ } }, "properties": { - "doc": "Gets the feature properties object. Note that in some cases, it may be more efficient to use [\"get\", \"property_name\"] directly.", + "doc": "Returns the feature properties object. Note that in some cases, it may be more efficient to use `[\"get\", \"property_name\"]` directly.", "group": "Feature data", "sdk-support": { "basic functionality": { @@ -3124,7 +3199,7 @@ } }, "geometry-type": { - "doc": "Gets the feature's geometry type: `Point`, `MultiPoint`, `LineString`, `MultiLineString`, `Polygon`, `MultiPolygon`. `Multi*` feature types are only returned in GeoJSON sources. When working with vector tile sources, use the singular forms.", + "doc": "Returns the feature's geometry type: `Point`, `MultiPoint`, `LineString`, `MultiLineString`, `Polygon`, `MultiPolygon`. `Multi*` feature types are only returned in GeoJSON sources. When working with vector tile sources, use the singular forms.", "group": "Feature data", "sdk-support": { "basic functionality": { @@ -3136,7 +3211,7 @@ } }, "id": { - "doc": "Gets the feature's id, if it has one.", + "doc": "Returns the feature's id, if it has one.", "group": "Feature data", "sdk-support": { "basic functionality": { @@ -3148,8 +3223,8 @@ } }, "zoom": { - "doc": "Gets the current zoom level. Note that in style layout and paint properties, [\"zoom\"] may only appear as the input to a top-level \"step\" or \"interpolate\" expression.", - "group": "Zoom", + "doc": "Returns the current zoom level. Note that in style layout and paint properties, [\"zoom\"] may only appear as the input to a top-level \"step\" or \"interpolate\" expression.", + "group": "Camera", "sdk-support": { "basic functionality": { "js": "0.41.0", @@ -3159,8 +3234,26 @@ } } }, + "pitch": { + "doc": "Returns the current pitch in degrees. `[\"pitch\"]` may only be used in the `filter` expression for a `symbol` layer.", + "group": "Camera", + "sdk-support": { + "basic functionality": { + "js": "2.6.0" + } + } + }, + "distance-from-center": { + "doc": "Returns the distance of a `symbol` instance from the center of the map. The distance is measured in pixels divided by the height of the map container. It measures 0 at the center, decreases towards the camera and increase away from the camera. For example, if the height of the map is 1000px, a value of -1 means 1000px away from the center towards the camera, and a value of 1 means a distance of 1000px away from the camera from the center. `[\"distance-from-center\"]` may only be used in the `filter` expression for a `symbol` layer.", + "group": "Camera", + "sdk-support": { + "basic functionality": { + "js": "2.6.0" + } + } + }, "heatmap-density": { - "doc": "Gets the kernel density estimation of a pixel in a heatmap layer, which is a relative measure of how many data points are crowded around a particular pixel. Can only be used in the `heatmap-color` property.", + "doc": "Returns the kernel density estimation of a pixel in a heatmap layer, which is a relative measure of how many data points are crowded around a particular pixel. Can only be used in the `heatmap-color` property.", "group": "Heatmap", "sdk-support": { "basic functionality": { @@ -3172,7 +3265,7 @@ } }, "line-progress": { - "doc": "Gets the progress along a gradient line. Can only be used in the `line-gradient` property.", + "doc": "Returns the progress along a gradient line. Can only be used in the `line-gradient` property.", "group": "Feature data", "sdk-support": { "basic functionality": { @@ -3184,7 +3277,7 @@ } }, "sky-radial-progress": { - "doc": "Gets the distance of a point on the sky from the sun position. Returns 0 at sun position and 1 when the distance reaches `sky-gradient-radius`. Can only be used in the `sky-gradient` property.", + "doc": "Returns the distance of a point on the sky from the sun position. Returns 0 at sun position and 1 when the distance reaches `sky-gradient-radius`. Can only be used in the `sky-gradient` property.", "group": "sky", "sdk-support": { "basic functionality": { @@ -3195,7 +3288,7 @@ } }, "accumulated": { - "doc": "Gets the value of a cluster property accumulated so far. Can only be used in the `clusterProperties` option of a clustered GeoJSON source.", + "doc": "Returns the value of a cluster property accumulated so far. Can only be used in the `clusterProperties` option of a clustered GeoJSON source.", "group": "Feature data", "sdk-support": { "basic functionality": { @@ -3874,6 +3967,92 @@ } } }, + "projection": { + "name": { + "type": "enum", + "values": { + "albers": { + "doc": "An Albers equal-area projection centered on the continental United States. You can configure the projection for a different region by setting `center` and `parallels` properties. You may want to set max bounds to constrain the map to the relevant region." + }, + "equalEarth": { + "doc": "An Equal Earth projection." + }, + "equirectangular": { + "doc": "An Equirectangular projection. This projection is very similar to the Plate Carrée projection." + }, + "lambertConformalConic": { + "doc": "A Lambert conformal conic projection. You can configure the projection for a region by setting `center` and `parallels` properties. You may want to set max bounds to constrain the map to the relevant region." + }, + "mercator": { + "doc": "The Mercator projection is the default projection." + }, + "naturalEarth": { + "doc": "A Natural Earth projection." + }, + "winkelTripel": { + "doc": "A Winkel Tripel projection." + } + }, + "default": "mercator", + "doc": "The name of the projection to be used for rendering the map.", + "required": true, + "sdk-support": { + "basic functionality": { + "js": "2.6.0" + } + } + }, + "center": { + "type": "array", + "length": 2, + "value": "number", + "property-type": "data-constant", + "transition": false, + "doc": "The reference longitude and latitude of the projection. `center` takes the form of [lng, lat]. This property is only configurable for conic projections (Albers and Lambert Conformal Conic). All other projections are centered on [0, 0].", + "example": [ + -96, + 37.5 + ], + "requires": [ + { + "name": [ + "albers", + "lambertConformalConic" + ] + } + ], + "sdk-support": { + "basic functionality": { + "js": "2.6.0" + } + } + }, + "parallels": { + "type": "array", + "length": 2, + "value": "number", + "property-type": "data-constant", + "transition": false, + "doc": "The standard parallels of the projection, denoting the desired latitude range with minimal distortion. `parallels` takes the form of [lat0, lat1]. This property is only configurable for conic projections (Albers and Lambert Conformal Conic).", + "example": [ + 29.5, + 45.5 + ], + "requires": [ + { + "name": [ + "albers", + "lambertConformalConic" + ] + } + ], + "sdk-support": { + "basic functionality": { + "js": "2.6.0" + } + } + } + }, "terrain" : { "source": { "type": "string", diff --git a/src/style-spec/style-spec.js b/src/style-spec/style-spec.js index 1d1dddafa9e..6837d8401ba 100644 --- a/src/style-spec/style-spec.js +++ b/src/style-spec/style-spec.js @@ -1,7 +1,7 @@ // @flow type ExpressionType = 'data-driven' | 'cross-faded' | 'cross-faded-data-driven' | 'color-ramp' | 'data-constant' | 'constant'; -type ExpressionParameters = Array<'zoom' | 'feature' | 'feature-state' | 'heatmap-density' | 'line-progress' | 'sky-radial-progress'>; +type ExpressionParameters = Array<'zoom' | 'feature' | 'feature-state' | 'heatmap-density' | 'line-progress' | 'sky-radial-progress' | 'pitch' | 'distance-from-center'>; type ExpressionSpecification = { interpolated: boolean, diff --git a/src/style-spec/types.js b/src/style-spec/types.js index 55118fdc198..892c0abcad8 100644 --- a/src/style-spec/types.js +++ b/src/style-spec/types.js @@ -72,6 +72,7 @@ export type StyleSpecification = {| "sprite"?: string, "glyphs"?: string, "transition"?: TransitionSpecification, + "projection"?: ProjectionSpecification, "layers": Array |} @@ -93,6 +94,12 @@ export type FogSpecification = {| "horizon-blend"?: PropertyValueSpecification |} +export type ProjectionSpecification = {| + "name": "albers" | "equalEarth" | "equirectangular" | "lambertConformalConic" | "mercator" | "naturalEarth" | "winkelTripel", + "center"?: [number, number], + "parallels"?: [number, number] +|} + export type VectorSourceSpecification = { "type": "vector", "url"?: string, diff --git a/src/style-spec/validate/validate.js b/src/style-spec/validate/validate.js index c810a7a6476..6aad0833572 100644 --- a/src/style-spec/validate/validate.js +++ b/src/style-spec/validate/validate.js @@ -22,6 +22,7 @@ import validateFog from './validate_fog.js'; import validateString from './validate_string.js'; import validateFormatted from './validate_formatted.js'; import validateImage from './validate_image.js'; +import validateProjection from './validate_projection.js'; const VALIDATORS = { '*'() { @@ -43,7 +44,8 @@ const VALIDATORS = { 'fog': validateFog, 'string': validateString, 'formatted': validateFormatted, - 'resolvedImage': validateImage + 'resolvedImage': validateImage, + 'projection': validateProjection }; // Main recursive validation function. Tracks: diff --git a/src/style-spec/validate/validate_expression.js b/src/style-spec/validate/validate_expression.js index 5fc24a3f94c..a321bf12cd7 100644 --- a/src/style-spec/validate/validate_expression.js +++ b/src/style-spec/validate/validate_expression.js @@ -5,6 +5,9 @@ import ValidationError from '../error/validation_error.js'; import {createExpression, createPropertyExpression} from '../expression/index.js'; import {deepUnbundle} from '../util/unbundle_jsonlint.js'; import {isStateConstant, isGlobalPropertyConstant, isFeatureConstant} from '../expression/is_constant.js'; +import CompoundExpression from '../expression/compound_expression.js'; + +import type {Expression} from '../expression/expression.js'; export default function validateExpression(options: any): Array { const expression = (options.expressionContext === 'property' ? createPropertyExpression : createExpression)(deepUnbundle(options.value), options.valueSpec); @@ -26,8 +29,8 @@ export default function validateExpression(options: any): Array return [new ValidationError(options.key, options.value, '"feature-state" data expressions are not supported with layout properties.')]; } - if (options.expressionContext === 'filter' && !isStateConstant(expressionObj)) { - return [new ValidationError(options.key, options.value, '"feature-state" data expressions are not supported with filters.')]; + if (options.expressionContext === 'filter') { + return disallowedFilterParameters(expressionObj, options); } if (options.expressionContext && options.expressionContext.indexOf('cluster') === 0) { @@ -41,3 +44,31 @@ export default function validateExpression(options: any): Array return []; } + +export function disallowedFilterParameters(e: Expression, options: any): Array { + const disallowedParameters = new Set([ + 'zoom', + 'feature-state', + 'pitch', + 'distance-from-center' + ]); + for (const param of options.valueSpec.expression.parameters) { + disallowedParameters.delete(param); + } + + if (disallowedParameters.size === 0) { + return []; + } + const errors = []; + + if (e instanceof CompoundExpression) { + if (disallowedParameters.has(e.name)) { + return [new ValidationError(options.key, options.value, `["${e.name}"] expression is not supported in a filter for a ${options.object.type} layer with id: ${options.object.id}`)]; + } + } + e.eachChild((arg) => { + errors.push(...disallowedFilterParameters(arg, options)); + }); + + return errors; +} diff --git a/src/style-spec/validate/validate_filter.js b/src/style-spec/validate/validate_filter.js index 3e03f1f984c..1b10b087eff 100644 --- a/src/style-spec/validate/validate_filter.js +++ b/src/style-spec/validate/validate_filter.js @@ -9,9 +9,11 @@ import {isExpressionFilter} from '../feature_filter/index.js'; export default function validateFilter(options) { if (isExpressionFilter(deepUnbundle(options.value))) { + const layerType = deepUnbundle(options.layerType); return validateExpression(extend({}, options, { expressionContext: 'filter', - valueSpec: {value: 'boolean'} + // We default to a layerType of `fill` because that points to a non-dynamic filter definition within the style-spec. + valueSpec: options.styleSpec[`filter_${layerType || 'fill'}`] })); } else { return validateNonExpressionFilter(options); diff --git a/src/style-spec/validate/validate_layer.js b/src/style-spec/validate/validate_layer.js index 32a3494be3d..931b2262982 100644 --- a/src/style-spec/validate/validate_layer.js +++ b/src/style-spec/validate/validate_layer.js @@ -98,7 +98,9 @@ export default function validateLayer(options) { objectKey: 'type' }); }, - filter: validateFilter, + filter(options) { + return validateFilter(extend({layerType: type}, options)); + }, layout(options) { return validateObject({ layer, diff --git a/src/style-spec/validate/validate_projection.js b/src/style-spec/validate/validate_projection.js new file mode 100644 index 00000000000..dba6d53b329 --- /dev/null +++ b/src/style-spec/validate/validate_projection.js @@ -0,0 +1,30 @@ +import ValidationError from '../error/validation_error.js'; +import getType from '../util/get_type.js'; +import validate from './validate.js'; + +export default function validateProjection(options) { + const projection = options.value; + const styleSpec = options.styleSpec; + const projectionSpec = styleSpec.projection; + const style = options.style; + + let errors = []; + + const rootType = getType(projection); + + if (rootType === 'object') { + for (const key in projection) { + errors = errors.concat(validate({ + key, + value: projection[key], + valueSpec: projectionSpec[key], + style, + styleSpec + })); + } + } else if (rootType !== 'string') { + errors = errors.concat([new ValidationError('projection', projection, `object or string expected, ${rootType} found`)]); + } + + return errors; +} diff --git a/src/style/evaluation_parameters.js b/src/style/evaluation_parameters.js index 2ba7105106b..e64bbbaa54d 100644 --- a/src/style/evaluation_parameters.js +++ b/src/style/evaluation_parameters.js @@ -14,6 +14,7 @@ export type CrossfadeParameters = { class EvaluationParameters { zoom: number; + pitch: number; now: number; fadeDuration: number; zoomHistory: ZoomHistory; @@ -28,11 +29,13 @@ class EvaluationParameters { this.fadeDuration = options.fadeDuration; this.zoomHistory = options.zoomHistory; this.transition = options.transition; + this.pitch = options.pitch; } else { this.now = 0; this.fadeDuration = 0; this.zoomHistory = new ZoomHistory(); this.transition = {}; + this.pitch = 0; } } diff --git a/src/style/fog.js b/src/style/fog.js index 5cafafa1dc9..1fdd1747e63 100644 --- a/src/style/fog.js +++ b/src/style/fog.js @@ -34,11 +34,16 @@ class Fog extends Evented { _transitioning: Transitioning; properties: PossiblyEvaluated; + // Alternate projections do not yet support fog. + // Disable fog rendering until they do. + _disabledForProjections: boolean; + constructor(fogOptions?: FogSpecification) { super(); this._transitionable = new Transitionable(fogProperties); this.set(fogOptions); this._transitioning = this._transitionable.untransitioned(); + this._disabledForProjections = false; } get state(): FogState { @@ -69,6 +74,7 @@ class Fog extends Evented { } getOpacity(pitch: number): number { + if (this._disabledForProjections) return 0; const fogColor = (this.properties && this.properties.get('color')) || 1.0; const pitchFactor = smoothstep(FOG_PITCH_START, FOG_PITCH_END, pitch); return pitchFactor * fogColor.a; diff --git a/src/style/query_geometry.js b/src/style/query_geometry.js index 7cd1547a7c1..646dadf73da 100644 --- a/src/style/query_geometry.js +++ b/src/style/query_geometry.js @@ -12,6 +12,7 @@ import {vec3} from 'gl-matrix'; import {Ray} from '../util/primitives.js'; import MercatorCoordinate from '../geo/mercator_coordinate.js'; import type {OverscaledTileID} from '../source/tile_id.js'; +import {getTilePoint, getTileVec3} from '../geo/projection/tile_transform.js'; /** * A data-class that represents a screenspace query from `Map#queryRenderedFeatures`. @@ -179,15 +180,16 @@ export class QueryGeometry { // outside the query volume even if it looks like it overlaps visually, a 1px bias value overcomes that. const bias = 1; const padding = tile.queryPadding + bias; + const wrap = tile.tileID.wrap; const geometryForTileCheck = use3D ? - this._bufferedCameraMercator(padding, transform).map((p) => tile.tileID.getTilePoint(p)) : - this._bufferedScreenMercator(padding, transform).map((p) => tile.tileID.getTilePoint(p)); - const tilespaceVec3s = this.screenGeometryMercator.map((p) => tile.tileID.getTileVec3(p)); + this._bufferedCameraMercator(padding, transform).map((p) => getTilePoint(tile.tileTransform, p, wrap)) : + this._bufferedScreenMercator(padding, transform).map((p) => getTilePoint(tile.tileTransform, p, wrap)); + const tilespaceVec3s = this.screenGeometryMercator.map((p) => getTileVec3(tile.tileTransform, p, wrap)); const tilespaceGeometry = tilespaceVec3s.map((v) => new Point(v[0], v[1])); const cameraMercator = transform.getFreeCameraOptions().position || new MercatorCoordinate(0, 0, 0); - const tilespaceCameraPosition = tile.tileID.getTileVec3(cameraMercator); + const tilespaceCameraPosition = getTileVec3(tile.tileTransform, cameraMercator, wrap); const tilespaceRays = tilespaceVec3s.map((tileVec) => { const dir = vec3.sub(tileVec, tileVec, tilespaceCameraPosition); vec3.normalize(dir, dir); diff --git a/src/style/style.js b/src/style/style.js index 0a46525c26c..f5a23c3e604 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -66,7 +66,8 @@ import type { LightSpecification, SourceSpecification, TerrainSpecification, - FogSpecification + FogSpecification, + ProjectionSpecification } from '../style-spec/types.js'; import type {CustomLayerInterface} from './style_layer/custom_style_layer.js'; import type {Validator} from './validate_style.js'; @@ -86,7 +87,8 @@ const supportedDiffOperations = pick(diffOperations, [ 'setTransition', 'setGeoJSONSourceData', 'setTerrain', - 'setFog' + 'setFog', + 'setProjection' // 'setGlyphs', // 'setSprite', ]); @@ -95,7 +97,8 @@ const ignoredDiffOperations = pick(diffOperations, [ 'setCenter', 'setZoom', 'setBearing', - 'setPitch' + 'setPitch', + 'setProjection' ]); const empty = emptyStyle(); @@ -322,6 +325,11 @@ class Style extends Evented { this._serializedLayers[layer.id] = layer.serialize(); this._updateLayerCount(layer, true); } + + if (this.stylesheet.projection && this.map.transform._unmodifiedProjection) { + this.setProjection(this.stylesheet.projection); + } + this.dispatcher.broadcast('setLayers', this._serializeLayers(this._order)); this.light = new Light(this.stylesheet.light); @@ -337,6 +345,21 @@ class Style extends Evented { this.fire(new Event('style.load')); } + setProjection(projection?: ?ProjectionSpecification) { + this.map.painter.clearBackgroundTiles(); + for (const id in this._sourceCaches) { + this._sourceCaches[id].clearTiles(); + } + + this.map.transform.setProjection(projection); + this.dispatcher.broadcast('setProjection', this.map.transform.projectionOptions); + + const fog = this.fog; + if (fog) fog._disabledForProjections = Boolean(projection && projection.name !== 'mercator'); + + this.map._update(true); + } + _loadSprite(url: string) { this._spriteRequest = loadSprite(url, this.map._requestManager, (err, images) => { this._spriteRequest = null; @@ -652,10 +675,9 @@ class Style extends Evented { this.fire(new Event('data', {dataType: 'style'})); } - listImages() { + listImages(): Array { this._checkLoaded(); - - return this.imageManager.listImages(); + return this._availableImages.slice(); } addSource(id: string, source: SourceSpecification, options: StyleSetterOptions = {}) { @@ -999,7 +1021,7 @@ class Style extends Evented { return; } - if (this._validate(validateStyle.filter, `layers.${layer.id}.filter`, filter, null, options)) { + if (this._validate(validateStyle.filter, `layers.${layer.id}.filter`, filter, {layerType: layer.type}, options)) { return; } @@ -1178,6 +1200,7 @@ class Style extends Evented { sprite: this.stylesheet.sprite, glyphs: this.stylesheet.glyphs, transition: this.stylesheet.transition, + projection: this.stylesheet.projection, sources, layers: this._serializeLayers(this._order) }, (value) => { return value !== undefined; }); @@ -1194,6 +1217,8 @@ class Style extends Evented { sourceCache.pause(); } this._changed = true; + layer.invalidateCompiledFilter(); + } _flattenAndSortRenderedFeatures(sourceResults: Array) { @@ -1763,6 +1788,10 @@ class Style extends Evented { hasCircleLayers(): boolean { return this._numCircleLayers > 0; } + + clearWorkerCaches() { + this.dispatcher.broadcast('clearCaches'); + } } Style.getSourceType = getSourceType; diff --git a/src/style/style_layer.js b/src/style/style_layer.js index d236073bd0d..a45661efd92 100644 --- a/src/style/style_layer.js +++ b/src/style/style_layer.js @@ -13,6 +13,7 @@ import {Evented} from '../util/evented.js'; import {Layout, Transitionable, Transitioning, Properties, PossiblyEvaluated, PossiblyEvaluatedPropertyValue} from './properties.js'; import {supportsPropertyExpression} from '../style-spec/util/properties.js'; import ProgramConfiguration from '../data/program_configuration.js'; +import featureFilter from '../style-spec/feature_filter/index.js'; import type {FeatureState} from '../style-spec/expression/index.js'; import type {Bucket} from '../data/bucket.js'; @@ -53,6 +54,7 @@ class StyleLayer extends Evented { +paint: mixed; _featureFilter: FeatureFilter; + _filterCompiled: boolean; +queryRadius: (bucket: Bucket) => number; +queryIntersectsFeature: (queryGeometry: TilespaceQueryGeometry, @@ -73,7 +75,8 @@ class StyleLayer extends Evented { this.id = layer.id; this.type = layer.type; - this._featureFilter = {filter: () => true, needGeometry: false}; + this._featureFilter = {filter: () => true, needGeometry: false, needFeature: false}; + this._filterCompiled = false; if (layer.type === 'custom') return; @@ -296,6 +299,25 @@ class StyleLayer extends Evented { } return false; } + + compileFilter() { + if (!this._filterCompiled) { + this._featureFilter = featureFilter(this.filter); + this._filterCompiled = true; + } + } + + invalidateCompiledFilter() { + this._filterCompiled = false; + } + + dynamicFilter() { + return this._featureFilter.dynamicFilter; + } + + dynamicFilterNeedsFeature() { + return this._featureFilter.needFeature; + } } export default StyleLayer; diff --git a/src/style/style_layer_index.js b/src/style/style_layer_index.js index bd32e3e3d47..86feb123486 100644 --- a/src/style/style_layer_index.js +++ b/src/style/style_layer_index.js @@ -4,7 +4,6 @@ import StyleLayer from './style_layer.js'; import createStyleLayer from './create_style_layer.js'; import {values} from '../util/util.js'; -import featureFilter from '../style-spec/feature_filter/index.js'; import groupByLayout from '../style-spec/group_by_layout.js'; import type {TypedStyleLayer} from './style_layer/typed_style_layer.js'; @@ -38,7 +37,7 @@ class StyleLayerIndex { this._layerConfigs[layerConfig.id] = layerConfig; const layer = this._layers[layerConfig.id] = createStyleLayer(layerConfig); - layer._featureFilter = featureFilter(layer.filter); + layer.compileFilter(); if (this.keyCache[layerConfig.id]) delete this.keyCache[layerConfig.id]; } diff --git a/src/symbol/placement.js b/src/symbol/placement.js index 154cc5150e5..b1a4d70b7b8 100644 --- a/src/symbol/placement.js +++ b/src/symbol/placement.js @@ -9,7 +9,6 @@ import {getAnchorJustification, evaluateVariableOffset} from './symbol_layout.js import {getAnchorAlignment, WritingMode} from './shaping.js'; import {mat4} from 'gl-matrix'; import assert from 'assert'; -import pixelsToTileUnits from '../source/pixels_to_tile_units.js'; import Point from '@mapbox/point-geometry'; import type Transform from '../geo/transform.js'; import type StyleLayer from '../style/style_layer.js'; @@ -40,9 +39,12 @@ class OpacityState { class JointOpacityState { text: OpacityState; icon: OpacityState; - constructor(prevState: ?JointOpacityState, increment: number, placedText: boolean, placedIcon: boolean, skipFade: ?boolean) { + clipped: boolean; + constructor(prevState: ?JointOpacityState, increment: number, placedText: boolean, placedIcon: boolean, skipFade: ?boolean, clipped: boolean = false) { this.text = new OpacityState(prevState ? prevState.text : null, increment, placedText, skipFade); this.icon = new OpacityState(prevState ? prevState.icon : null, increment, placedIcon, skipFade); + + this.clipped = clipped; } isHidden() { return this.text.isHidden() && this.icon.isHidden(); @@ -57,10 +59,13 @@ class JointPlacement { // and if a subsequent viewport change brings them into view, they'll be fully // visible right away. skipFade: boolean; - constructor(text: boolean, icon: boolean, skipFade: boolean) { + + clipped: boolean + constructor(text: boolean, icon: boolean, skipFade: boolean, clipped: boolean = false) { this.text = text; this.icon = icon; this.skipFade = skipFade; + this.clipped = clipped; } } @@ -224,21 +229,27 @@ export class Placement { getBucketParts(results: Array, styleLayer: StyleLayer, tile: Tile, sortAcrossTiles: boolean) { const symbolBucket = ((tile.getBucket(styleLayer): any): SymbolBucket); const bucketFeatureIndex = tile.latestFeatureIndex; + if (!symbolBucket || !bucketFeatureIndex || styleLayer.id !== symbolBucket.layerIds[0]) return; - const collisionBoxArray = tile.collisionBoxArray; - const layout = symbolBucket.layers[0].layout; + const collisionBoxArray = tile.collisionBoxArray; const scale = Math.pow(2, this.transform.zoom - tile.tileID.overscaledZ); const textPixelRatio = tile.tileSize / EXTENT; + const unwrappedTileID = tile.tileID.toUnwrapped(); - const posMatrix = this.transform.calculateProjMatrix(tile.tileID.toUnwrapped()); + const posMatrix = this.transform.calculateProjMatrix(unwrappedTileID); const pitchWithMap = layout.get('text-pitch-alignment') === 'map'; const rotateWithMap = layout.get('text-rotation-alignment') === 'map'; - const pixelsToTiles = pixelsToTileUnits(tile, 1, this.transform.zoom); + + styleLayer.compileFilter(); + + const dynamicFilter = styleLayer.dynamicFilter(); + const dynamicFilterNeedsFeature = styleLayer.dynamicFilterNeedsFeature(); + const pixelsToTiles = this.transform.calculatePixelsToTileUnitsMatrix(tile); const textLabelPlaneMatrix = projection.getLabelPlaneMatrix(posMatrix, pitchWithMap, @@ -259,6 +270,18 @@ export class Placement { labelToScreenMatrix = mat4.multiply([], this.transform.labelPlaneMatrix, glMatrix); } + let clippingData = null; + assert(!!tile.latestFeatureIndex); + if (!!dynamicFilter && tile.latestFeatureIndex) { + + clippingData = { + unwrappedTileID, + dynamicFilter, + dynamicFilterNeedsFeature, + featureIndex: tile.latestFeatureIndex + }; + } + // As long as this placement lives, we have to hold onto this bucket's // matching FeatureIndex/data for querying purposes this.retainedQueryData[symbolBucket.bucketInstanceId] = new RetainedQueryData( @@ -275,6 +298,7 @@ export class Placement { posMatrix, textLabelPlaneMatrix, labelToScreenMatrix, + clippingData, scale, textPixelRatio, holdingForFade: tile.holdingForFade(), @@ -357,6 +381,7 @@ export class Placement { posMatrix, textLabelPlaneMatrix, labelToScreenMatrix, + clippingData, textPixelRatio, holdingForFade, collisionBoxArray, @@ -400,6 +425,37 @@ export class Placement { } const placeSymbol = (symbolInstance: SymbolInstance, symbolIndex: number, collisionArrays: CollisionArrays) => { + if (clippingData) { + // Setup globals + const globals = { + zoom: this.transform.zoom, + pitch: this.transform.pitch, + }; + + // Deserialize feature only if necessary + let feature = null; + if (clippingData.dynamicFilterNeedsFeature) { + const featureIndex = clippingData.featureIndex; + const retainedQueryData = this.retainedQueryData[bucket.bucketInstanceId]; + feature = featureIndex.loadFeature({ + featureIndex: symbolInstance.featureIndex, + bucketIndex: retainedQueryData.bucketIndex, + sourceLayerIndex: retainedQueryData.sourceLayerIndex, + layoutVertexArrayOffset: 0 + }); + } + const canonicalTileId = this.retainedQueryData[bucket.bucketInstanceId].tileID.canonical; + + const filterFunc = clippingData.dynamicFilter; + const shouldClip = !filterFunc(globals, feature, canonicalTileId, new Point(symbolInstance.tileAnchorX, symbolInstance.tileAnchorY), this.transform.calculateDistanceTileData(clippingData.unwrappedTileID)); + + if (shouldClip) { + this.placements[symbolInstance.crossTileID] = new JointPlacement(false, false, false, true); + seenCrossTileIDs[symbolInstance.crossTileID] = true; + return; + } + } + if (seenCrossTileIDs[symbolInstance.crossTileID]) return; if (holdingForFade) { // Mark all symbols from this tile as "not placed", but don't add to seenCrossTileIDs, because we don't @@ -801,12 +857,12 @@ export class Placement { const jointPlacement = this.placements[crossTileID]; const prevOpacity = prevOpacities[crossTileID]; if (prevOpacity) { - this.opacities[crossTileID] = new JointOpacityState(prevOpacity, increment, jointPlacement.text, jointPlacement.icon); + this.opacities[crossTileID] = new JointOpacityState(prevOpacity, increment, jointPlacement.text, jointPlacement.icon, null, jointPlacement.clipped); placementChanged = placementChanged || jointPlacement.text !== prevOpacity.text.placed || jointPlacement.icon !== prevOpacity.icon.placed; } else { - this.opacities[crossTileID] = new JointOpacityState(null, increment, jointPlacement.text, jointPlacement.icon, jointPlacement.skipFade); + this.opacities[crossTileID] = new JointOpacityState(null, increment, jointPlacement.text, jointPlacement.icon, jointPlacement.skipFade, jointPlacement.clipped); placementChanged = placementChanged || jointPlacement.text || jointPlacement.icon; } } @@ -862,6 +918,7 @@ export class Placement { if (bucket.hasTextCollisionBoxData()) bucket.textCollisionBox.collisionVertexArray.clear(); const layout = bucket.layers[0].layout; + const hasClipping = !!bucket.layers[0].dynamicFilter(); const duplicateOpacityState = new JointOpacityState(null, 0, false, false, true); const textAllowOverlap = layout.get('text-allow-overlap'); const iconAllowOverlap = layout.get('icon-allow-overlap'); @@ -978,8 +1035,8 @@ export class Placement { const collisionArrays = bucket.collisionArrays[s]; if (collisionArrays) { let shift = new Point(0, 0); + let used = true; if (collisionArrays.textBox || collisionArrays.verticalTextBox) { - let used = true; if (variablePlacement) { const variableOffset = this.variableOffsets[crossTileID]; if (variableOffset) { @@ -1003,6 +1060,10 @@ export class Placement { } } + if (hasClipping) { + used = !opacityState.clipped; + } + if (collisionArrays.textBox) { updateCollisionVertices(bucket.textCollisionBox.collisionVertexArray, opacityState.text.placed, !used || horizontalHidden, shift.x, shift.y); } @@ -1011,7 +1072,7 @@ export class Placement { } } - const verticalIconUsed = Boolean(!verticalHidden && collisionArrays.verticalIconBox); + const verticalIconUsed = used && Boolean(!verticalHidden && collisionArrays.verticalIconBox); if (collisionArrays.iconBox) { updateCollisionVertices(bucket.iconCollisionBox.collisionVertexArray, opacityState.icon.placed, verticalIconUsed, diff --git a/src/symbol/projection.js b/src/symbol/projection.js index 7ae83849571..a61dde4422c 100644 --- a/src/symbol/projection.js +++ b/src/symbol/projection.js @@ -2,7 +2,7 @@ import Point from '@mapbox/point-geometry'; -import {mat4, vec3, vec4} from 'gl-matrix'; +import {mat2, mat4, vec3, vec4} from 'gl-matrix'; import * as symbolSize from './symbol_size.js'; import {addDynamicAttributes} from '../data/bucket/symbol_bucket.js'; @@ -78,10 +78,14 @@ function getLabelPlaneMatrix(posMatrix: mat4, pitchWithMap: boolean, rotateWithMap: boolean, transform: Transform, - pixelsToTileUnits: number) { + pixelsToTileUnits: Float32Array) { const m = mat4.create(); if (pitchWithMap) { - mat4.scale(m, m, [1 / pixelsToTileUnits, 1 / pixelsToTileUnits, 1]); + const s = mat2.invert([], pixelsToTileUnits); + m[0] = s[0]; + m[1] = s[1]; + m[4] = s[2]; + m[5] = s[3]; if (!rotateWithMap) { mat4.rotateZ(m, m, transform.angle); } @@ -98,10 +102,15 @@ function getGlCoordMatrix(posMatrix: mat4, pitchWithMap: boolean, rotateWithMap: boolean, transform: Transform, - pixelsToTileUnits: number) { + pixelsToTileUnits: Float32Array) { if (pitchWithMap) { const m = mat4.clone(posMatrix); - mat4.scale(m, m, [pixelsToTileUnits, pixelsToTileUnits, 1]); + const s = mat4.identity([]); + s[0] = pixelsToTileUnits[0]; + s[1] = pixelsToTileUnits[1]; + s[4] = pixelsToTileUnits[2]; + s[5] = pixelsToTileUnits[3]; + mat4.multiply(m, m, s); if (!rotateWithMap) { mat4.rotateZ(m, m, -transform.angle); } diff --git a/src/symbol/symbol_layout.js b/src/symbol/symbol_layout.js index 850d5676ab6..b888b6739e7 100644 --- a/src/symbol/symbol_layout.js +++ b/src/symbol/symbol_layout.js @@ -153,6 +153,7 @@ export function performSymbolLayout(bucket: SymbolBucket, imageMap: {[_: string]: StyleImage}, imagePositions: {[_: string]: ImagePosition}, showCollisionBoxes: boolean, + availableImages: Array, canonical: CanonicalTileID, tileZoom: number) { bucket.createArrays(); @@ -315,7 +316,7 @@ export function performSymbolLayout(bucket: SymbolBucket, bucket.iconsInText = shapedText ? shapedText.iconsInText : false; } if (shapedText || shapedIcon) { - addFeature(bucket, feature, shapedTextOrientations, shapedIcon, imageMap, sizes, layoutTextSize, layoutIconSize, textOffset, isSDFIcon, canonical); + addFeature(bucket, feature, shapedTextOrientations, shapedIcon, imageMap, sizes, layoutTextSize, layoutIconSize, textOffset, isSDFIcon, availableImages, canonical); } } @@ -355,7 +356,9 @@ function addFeature(bucket: SymbolBucket, layoutTextSize: number, layoutIconSize: number, textOffset: [number, number], - isSDFIcon: boolean, canonical: CanonicalTileID) { + isSDFIcon: boolean, + availableImages: Array, + 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 @@ -407,7 +410,7 @@ function addFeature(bucket: SymbolBucket, bucket.collisionBoxArray, feature.index, feature.sourceLayerIndex, bucket.index, textPadding, textAlongLine, textOffset, iconBoxScale, iconPadding, iconAlongLine, iconOffset, - feature, sizes, isSDFIcon, canonical); + feature, sizes, isSDFIcon, availableImages, canonical); }; if (symbolPlacement === 'line') { @@ -486,6 +489,7 @@ function addTextVertices(bucket: SymbolBucket, placedTextSymbolIndices: {[_: string]: number}, placedIconIndex: number, sizes: Sizes, + availableImages: Array, canonical: CanonicalTileID) { const glyphQuads = getGlyphQuads(anchor, shapedText, textOffset, layer, textAlongLine, feature, imageMap, bucket.allowVerticalPlacement); @@ -523,6 +527,7 @@ function addTextVertices(bucket: SymbolBucket, lineArray.lineStartIndex, lineArray.lineLength, placedIconIndex, + availableImages, canonical); // The placedSymbolArray is used at render time in drawTileSymbols @@ -643,6 +648,7 @@ function addSymbol(bucket: SymbolBucket, feature: SymbolFeature, sizes: Sizes, isSDFIcon: boolean, + availableImages: Array, canonical: CanonicalTileID) { const lineArray = bucket.addToLineVertexArray(anchor, line); @@ -729,7 +735,9 @@ function addSymbol(bucket: SymbolBucket, lineArray.lineStartIndex, lineArray.lineLength, // The icon itself does not have an associated symbol since the text isnt placed yet - -1, canonical); + -1, + availableImages, + canonical); placedIconSymbolIndex = bucket.icon.placedSymbolArray.length - 1; @@ -749,7 +757,9 @@ function addSymbol(bucket: SymbolBucket, lineArray.lineStartIndex, lineArray.lineLength, // The icon itself does not have an associated symbol since the text isnt placed yet - -1, canonical); + -1, + availableImages, + canonical); verticalPlacedIconSymbolIndex = bucket.icon.placedSymbolArray.length - 1; } @@ -775,7 +785,7 @@ function addSymbol(bucket: SymbolBucket, bucket, projectedAnchor, anchor, shaping, imageMap, layer, textAlongLine, feature, textOffset, lineArray, shapedTextOrientations.vertical ? WritingMode.horizontal : WritingMode.horizontalOnly, singleLine ? (Object.keys(shapedTextOrientations.horizontal): any) : [justification], - placedTextSymbolIndices, placedIconSymbolIndex, sizes, canonical); + placedTextSymbolIndices, placedIconSymbolIndex, sizes, availableImages, canonical); if (singleLine) { break; @@ -785,7 +795,7 @@ function addSymbol(bucket: SymbolBucket, if (shapedTextOrientations.vertical) { numVerticalGlyphVertices += addTextVertices( bucket, projectedAnchor, anchor, shapedTextOrientations.vertical, imageMap, layer, textAlongLine, feature, - textOffset, lineArray, WritingMode.vertical, ['vertical'], placedTextSymbolIndices, verticalPlacedIconSymbolIndex, sizes, canonical); + textOffset, lineArray, WritingMode.vertical, ['vertical'], placedTextSymbolIndices, verticalPlacedIconSymbolIndex, sizes, availableImages, canonical); } // Check if runtime collision circles should be used for any of the collision features. diff --git a/src/terrain/terrain.js b/src/terrain/terrain.js index 068eb88d0d0..12a50539a0c 100644 --- a/src/terrain/terrain.js +++ b/src/terrain/terrain.js @@ -4,7 +4,7 @@ import Point from '@mapbox/point-geometry'; import SourceCache from '../source/source_cache.js'; import {OverscaledTileID} from '../source/tile_id.js'; import Tile from '../source/tile.js'; -import rasterBoundsAttributes from '../data/raster_bounds_attributes.js'; +import boundsAttributes from '../data/bounds_attributes.js'; import {RasterBoundsArray, TriangleIndexArray, LineIndexArray} from '../data/array_types.js'; import SegmentVector from '../data/segment.js'; import Texture from '../render/texture.js'; @@ -222,7 +222,7 @@ export class Terrain extends Elevation { // edge vertices from neighboring tiles evaluate to the same 3D point. const [triangleGridArray, triangleGridIndices, skirtIndicesOffset] = createGrid(GRID_DIM + 1); const context = painter.context; - this.gridBuffer = context.createVertexBuffer(triangleGridArray, rasterBoundsAttributes.members); + this.gridBuffer = context.createVertexBuffer(triangleGridArray, boundsAttributes.members); this.gridIndexBuffer = context.createIndexBuffer(triangleGridIndices); this.gridSegments = SegmentVector.simpleSegment(0, 0, triangleGridArray.length, triangleGridIndices.length); this.gridNoSkirtSegments = SegmentVector.simpleSegment(0, 0, triangleGridArray.length, skirtIndicesOffset); diff --git a/src/ui/camera.js b/src/ui/camera.js index fe3728c77fd..363fd7a4262 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -172,7 +172,7 @@ class Camera extends Evented { * // Return a LngLat object such as {lng: 0, lat: 0}. * const center = map.getCenter(); * // Access longitude and latitude values directly. - * const {longitude, latitude} = map.getCenter(); + * const {lng, lat} = map.getCenter(); * @see [Tutorial: Use Mapbox GL JS in a React app](https://docs.mapbox.com/help/tutorials/use-mapbox-gl-js-with-react/#store-the-new-coordinates) */ getCenter(): LngLat { return new LngLat(this.transform.center.lng, this.transform.center.lat); } @@ -353,7 +353,9 @@ class Camera extends Evented { * const bearing = map.getBearing(); * @see [Example: Navigate the map with game-like controls](https://www.mapbox.com/mapbox-gl-js/example/game-controls/) */ - getBearing(): number { return this.transform.bearing; } + getBearing(): number { + return this.transform.bearing; + } /** * Sets the map's bearing (rotation). The bearing is the compass direction that is "up"; for example, a bearing @@ -1564,7 +1566,7 @@ class Camera extends Evented { // interpolating between the two endpoints will cross it. _normalizeCenter(center: LngLat) { const tr = this.transform; - if (!tr.renderWorldCopies || tr.lngRange) return; + if (!tr.renderWorldCopies || tr.maxBounds) return; const delta = center.lng - tr.center.lng; center.lng += diff --git a/src/ui/control/attribution_control.js b/src/ui/control/attribution_control.js index deee0c535ae..ffa989d0b0f 100644 --- a/src/ui/control/attribution_control.js +++ b/src/ui/control/attribution_control.js @@ -57,6 +57,7 @@ class AttributionControl { this._map = map; this._container = DOM.create('div', 'mapboxgl-ctrl mapboxgl-ctrl-attrib'); this._compactButton = DOM.create('button', 'mapboxgl-ctrl-attrib-button', this._container); + DOM.create('span', `mapboxgl-ctrl-icon`, this._compactButton).setAttribute('aria-hidden', true); this._compactButton.type = 'button'; this._compactButton.addEventListener('click', this._toggleAttribution); this._setElementTitle(this._compactButton, 'ToggleAttribution'); @@ -96,17 +97,17 @@ class AttributionControl { _setElementTitle(element: HTMLElement, title: string) { const str = this._map._getUIString(`AttributionControl.${title}`); - element.title = str; element.setAttribute('aria-label', str); + if (element.firstElementChild) element.firstElementChild.setAttribute('title', str); } _toggleAttribution() { if (this._container.classList.contains('mapboxgl-compact-show')) { this._container.classList.remove('mapboxgl-compact-show'); - this._compactButton.setAttribute('aria-pressed', 'false'); + this._compactButton.setAttribute('aria-expanded', 'false'); } else { this._container.classList.add('mapboxgl-compact-show'); - this._compactButton.setAttribute('aria-pressed', 'true'); + this._compactButton.setAttribute('aria-expanded', 'true'); } } diff --git a/src/ui/control/fullscreen_control.js b/src/ui/control/fullscreen_control.js index c7be9eaf0c9..9ce26342468 100644 --- a/src/ui/control/fullscreen_control.js +++ b/src/ui/control/fullscreen_control.js @@ -90,7 +90,7 @@ class FullscreenControl { _updateTitle() { const title = this._getTitle(); this._fullscreenButton.setAttribute("aria-label", title); - this._fullscreenButton.title = title; + if (this._fullscreenButton.firstElementChild) this._fullscreenButton.firstElementChild.setAttribute('title', title); } _getTitle() { diff --git a/src/ui/control/geolocate_control.js b/src/ui/control/geolocate_control.js index 0cefc4795cd..6d5164f67e8 100644 --- a/src/ui/control/geolocate_control.js +++ b/src/ui/control/geolocate_control.js @@ -379,8 +379,8 @@ class GeolocateControl extends Evented { this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-background-error'); this._geolocateButton.disabled = true; const title = this._map._getUIString('GeolocateControl.LocationNotAvailable'); - this._geolocateButton.title = title; this._geolocateButton.setAttribute('aria-label', title); + if (this._geolocateButton.firstElementChild) this._geolocateButton.firstElementChild.setAttribute('title', title); if (this._geolocationWatchID !== undefined) { this._clearWatch(); @@ -414,18 +414,19 @@ class GeolocateControl extends Evented { this._container.addEventListener('contextmenu', (e: MouseEvent) => e.preventDefault()); this._geolocateButton = DOM.create('button', `mapboxgl-ctrl-geolocate`, this._container); DOM.create('span', `mapboxgl-ctrl-icon`, this._geolocateButton).setAttribute('aria-hidden', true); + this._geolocateButton.type = 'button'; if (supported === false) { warnOnce('Geolocation support is not available so the GeolocateControl will be disabled.'); const title = this._map._getUIString('GeolocateControl.LocationNotAvailable'); this._geolocateButton.disabled = true; - this._geolocateButton.title = title; this._geolocateButton.setAttribute('aria-label', title); + if (this._geolocateButton.firstElementChild) this._geolocateButton.firstElementChild.setAttribute('title', title); } else { const title = this._map._getUIString('GeolocateControl.FindMyLocation'); - this._geolocateButton.title = title; this._geolocateButton.setAttribute('aria-label', title); + if (this._geolocateButton.firstElementChild) this._geolocateButton.firstElementChild.setAttribute('title', title); } if (this.options.trackUserLocation) { diff --git a/src/ui/control/navigation_control.js b/src/ui/control/navigation_control.js index 49c6f00a077..faf046d8b93 100644 --- a/src/ui/control/navigation_control.js +++ b/src/ui/control/navigation_control.js @@ -149,8 +149,8 @@ class NavigationControl { _setButtonTitle(button: HTMLButtonElement, title: string) { const str = this._map._getUIString(`NavigationControl.${title}`); - button.title = str; button.setAttribute('aria-label', str); + if (button.firstElementChild) button.firstElementChild.setAttribute('title', str); } } diff --git a/src/ui/default_locale.js b/src/ui/default_locale.js index 520109bf1a0..bc7d47c600b 100644 --- a/src/ui/default_locale.js +++ b/src/ui/default_locale.js @@ -17,7 +17,8 @@ const defaultLocale = { 'ScaleControl.Miles': 'mi', 'ScaleControl.NauticalMiles': 'nm', 'ScrollZoomBlocker.CtrlMessage': 'Use ctrl + scroll to zoom the map', - 'ScrollZoomBlocker.CmdMessage': 'Use ⌘ + scroll to zoom the map' + 'ScrollZoomBlocker.CmdMessage': 'Use ⌘ + scroll to zoom the map', + 'TouchPanBlocker.Message': 'Use two fingers to move the map' }; export default defaultLocale; diff --git a/src/ui/events.js b/src/ui/events.js index 50a378c7674..21a0f17d7a6 100644 --- a/src/ui/events.js +++ b/src/ui/events.js @@ -74,9 +74,10 @@ export class MapMouseEvent extends Event { lngLat: LngLat; /** - * If a `layerId` was specified when adding the event listener with {@link Map#on}, `features` will be an array of - * [GeoJSON](http://geojson.org/) [Feature objects](https://tools.ietf.org/html/rfc7946#section-3.2). - * The array will contain all features from that layer that are rendered at the event's point. + * If a single `layerId`(as a single string) or multiple `layerIds` (as an array of strings) were specified when adding the event listener with {@link Map#on}, + * `features` will be an array of [GeoJSON](http://geojson.org/) [Feature objects](https://tools.ietf.org/html/rfc7946#section-3.2). + * The array will contain all features from that layer that are rendered at the event's point, + * in the order that they are rendered with the topmost feature being at the start of the array. * The `features` are identical to those returned by {@link Map#queryRenderedFeatures}. * * If no `layerId` was specified when adding the event listener, `features` will be `undefined`. @@ -89,6 +90,12 @@ export class MapMouseEvent extends Event { * }); * * @example + * // logging features for two layers (with `e.features`) + * map.on('click', ['layer1', 'layer2'], (e) => { + * console.log(`There are ${e.features.length} features at point ${e.point}`); + * }); + * + * @example * // logging all features for all layers (without `e.features`) * map.on('click', (e) => { * const features = map.queryRenderedFeatures(e.point); diff --git a/src/ui/free_camera.js b/src/ui/free_camera.js index 925f209720c..d4d4c02b4aa 100644 --- a/src/ui/free_camera.js +++ b/src/ui/free_camera.js @@ -306,6 +306,7 @@ class FreeCamera { vec3.scale(invPosition, invPosition, -worldSize); mat4.fromQuat(matrix, invOrientation); + mat4.translate(matrix, matrix, invPosition); // Pre-multiply y (2nd row) diff --git a/src/ui/handler/box_zoom.js b/src/ui/handler/box_zoom.js index 96c6dacac87..e76e3e117e1 100644 --- a/src/ui/handler/box_zoom.js +++ b/src/ui/handler/box_zoom.js @@ -153,6 +153,10 @@ class BoxZoomHandler { } } + blur() { + this.reset(); + } + reset() { this._active = false; diff --git a/src/ui/handler/click_zoom.js b/src/ui/handler/click_zoom.js index 19362ede8c0..44d29f26976 100644 --- a/src/ui/handler/click_zoom.js +++ b/src/ui/handler/click_zoom.js @@ -16,6 +16,10 @@ export default class ClickZoomHandler { this._active = false; } + blur() { + this.reset(); + } + dblclick(e: MouseEvent, point: Point) { e.preventDefault(); return { diff --git a/src/ui/handler/keyboard.js b/src/ui/handler/keyboard.js index eb04f773aae..b5bc103ff74 100644 --- a/src/ui/handler/keyboard.js +++ b/src/ui/handler/keyboard.js @@ -45,6 +45,10 @@ class KeyboardHandler { this._rotationDisabled = false; } + blur() { + this.reset(); + } + reset() { this._active = false; } diff --git a/src/ui/handler/mouse.js b/src/ui/handler/mouse.js index d1a94551476..059e024cea6 100644 --- a/src/ui/handler/mouse.js +++ b/src/ui/handler/mouse.js @@ -31,6 +31,10 @@ class MouseHandler { this._clickTolerance = options.clickTolerance || 1; } + blur() { + this.reset(); + } + reset() { this._active = false; this._moved = false; diff --git a/src/ui/handler/scroll_zoom.js b/src/ui/handler/scroll_zoom.js index 74084468297..7a815207089 100644 --- a/src/ui/handler/scroll_zoom.js +++ b/src/ui/handler/scroll_zoom.js @@ -143,7 +143,7 @@ class ScrollZoomHandler { if (this.isEnabled()) return; this._enabled = true; this._aroundCenter = options && options.around === 'center'; - if (this._map._gestureHandling) this._addScrollZoomBlocker(); + if (this._map._cooperativeGestures) this._addScrollZoomBlocker(); } /** @@ -155,13 +155,16 @@ class ScrollZoomHandler { disable() { if (!this.isEnabled()) return; this._enabled = false; - if (this._map._gestureHandling) this._alertContainer.remove(); + if (this._map._cooperativeGestures) { + clearTimeout(this._alertTimer); + this._alertContainer.remove(); + } } wheel(e: WheelEvent) { if (!this.isEnabled()) return; - if (this._map._gestureHandling) { + if (this._map._cooperativeGestures) { if (!e.ctrlKey && !e.metaKey && !this.isZooming() && !this._isFullscreen()) { this._showBlockerAlert(); return; @@ -371,6 +374,10 @@ class ScrollZoomHandler { return easing; } + blur() { + this.reset(); + } + reset() { this._active = false; } @@ -391,7 +398,7 @@ class ScrollZoomHandler { } _isFullscreen() { - return window.document.fullscreenElement !== null; + return !!window.document.fullscreenElement; } _showBlockerAlert() { diff --git a/src/ui/handler/touch_pan.js b/src/ui/handler/touch_pan.js index c435bb246e2..59b478001e1 100644 --- a/src/ui/handler/touch_pan.js +++ b/src/ui/handler/touch_pan.js @@ -1,21 +1,31 @@ // @flow import Point from '@mapbox/point-geometry'; +import type Map from '../map.js'; import {indexTouches} from './handler_util.js'; +import {bindAll} from '../../util/util.js'; +import DOM from '../../util/dom.js'; export default class TouchPanHandler { + _map: Map; + _el: HTMLElement; _enabled: boolean; _active: boolean; _touches: { [string | number]: Point }; _minTouches: number; _clickTolerance: number; _sum: Point; + _alertContainer: HTMLElement; + _alertTimer: TimeoutID; - constructor(options: { clickTolerance: number }) { + constructor(map: Map, options: { clickTolerance: number }) { + this._map = map; + this._el = map.getCanvasContainer(); this._minTouches = 1; this._clickTolerance = options.clickTolerance || 1; this.reset(); + bindAll(['_addTouchPanBlocker', '_showTouchPanBlockerAlert'], this); } reset() { @@ -30,7 +40,21 @@ export default class TouchPanHandler { touchmove(e: TouchEvent, points: Array, mapTouches: Array) { if (!this._active || mapTouches.length < this._minTouches) return; + + // if cooperative gesture handling is set to true, require two fingers to touch pan + if (this._map._cooperativeGestures && !this._map.isMoving()) { + if (mapTouches.length === 1) { + this._showTouchPanBlockerAlert(); + return; + } else if (this._alertContainer.style.visibility !== 'hidden') { + // immediately hide alert if it is visible when two fingers are used to pan. + this._alertContainer.style.visibility = 'hidden'; + clearTimeout(this._alertTimer); + } + } + e.preventDefault(); + return this._calculateTransform(e, points, mapTouches); } @@ -84,10 +108,20 @@ export default class TouchPanHandler { enable() { this._enabled = true; + if (this._map._cooperativeGestures) { + this._addTouchPanBlocker(); + // override touch-action css property to enable scrolling page over map + this._el.classList.add('mapboxgl-touch-pan-blocker-override', 'mapboxgl-scrollable-page'); + } } disable() { this._enabled = false; + if (this._map._cooperativeGestures) { + clearTimeout(this._alertTimer); + this._alertContainer.remove(); + this._el.classList.remove('mapboxgl-touch-pan-blocker-override', 'mapboxgl-scrollable-page'); + } this.reset(); } @@ -98,4 +132,28 @@ export default class TouchPanHandler { isActive() { return this._active; } + + _addTouchPanBlocker() { + if (this._map && !this._alertContainer) { + this._alertContainer = DOM.create('div', 'mapboxgl-touch-pan-blocker', this._map._container); + + this._alertContainer.textContent = this._map._getUIString('TouchPanBlocker.Message'); + + // dynamically set the font size of the touch pan blocker alert message + this._alertContainer.style.fontSize = `${Math.max(10, Math.min(24, Math.floor(this._el.clientWidth * 0.05)))}px`; + } + } + + _showTouchPanBlockerAlert() { + if (this._alertContainer.style.visibility === 'hidden') this._alertContainer.style.visibility = 'visible'; + + this._alertContainer.classList.add('mapboxgl-touch-pan-blocker-show'); + + clearTimeout(this._alertTimer); + + this._alertTimer = setTimeout(() => { + this._alertContainer.classList.remove('mapboxgl-touch-pan-blocker-show'); + }, 500); + } + } diff --git a/src/ui/handler/touch_zoom_rotate.js b/src/ui/handler/touch_zoom_rotate.js index 199fd6cec89..628ed3695aa 100644 --- a/src/ui/handler/touch_zoom_rotate.js +++ b/src/ui/handler/touch_zoom_rotate.js @@ -2,6 +2,7 @@ import Point from '@mapbox/point-geometry'; import DOM from '../../util/dom.js'; +import type Map from '../map.js'; class TwoTouchHandler { @@ -205,6 +206,12 @@ export class TouchPitchHandler extends TwoTouchHandler { _valid: boolean | void; _firstMove: number; _lastPoints: [Point, Point]; + _map: Map; + + constructor(map: Map) { + super(); + this._map = map; + } reset() { super.reset(); @@ -220,13 +227,17 @@ export class TouchPitchHandler extends TwoTouchHandler { this._valid = false; } + } _move(points: [Point, Point], center: Point, e: TouchEvent) { const vectorA = points[0].sub(this._lastPoints[0]); const vectorB = points[1].sub(this._lastPoints[1]); + if (this._map._cooperativeGestures && e.touches.length < 3) return; + this._valid = this.gestureBeginsVertically(vectorA, vectorB, e.timeStamp); + if (!this._valid) return; this._lastPoints = points; diff --git a/src/ui/handler_manager.js b/src/ui/handler_manager.js index b9b4e3a12ca..238f2078445 100644 --- a/src/ui/handler_manager.js +++ b/src/ui/handler_manager.js @@ -246,7 +246,7 @@ class HandlerManager { const tapDragZoom = new TapDragZoomHandler(); this._add('tapDragZoom', tapDragZoom); - const touchPitch = map.touchPitch = new TouchPitchHandler(); + const touchPitch = map.touchPitch = new TouchPitchHandler(map); this._add('touchPitch', touchPitch); const mouseRotate = new MouseRotateHandler(options); @@ -256,7 +256,7 @@ class HandlerManager { this._add('mousePitch', mousePitch, ['mouseRotate']); const mousePan = new MousePanHandler(options); - const touchPan = new TouchPanHandler(options); + const touchPan = new TouchPanHandler(map, options); map.dragPan = new DragPanHandler(el, mousePan, touchPan); this._add('mousePan', mousePan); this._add('touchPan', touchPan, ['touchZoom', 'touchRotate']); @@ -309,6 +309,7 @@ class HandlerManager { isZooming() { return !!this._eventsInProgress.zoom || this._map.scrollZoom.isZooming(); } + isRotating() { return !!this._eventsInProgress.rotate; } @@ -344,11 +345,6 @@ class HandlerManager { handleEvent(e: InputEvent | RenderFrameEvent, eventName?: string) { - if (e.type === 'blur') { - this.stop(true); - return; - } - this._updatingCamera = true; assert(e.timeStamp !== undefined); diff --git a/src/ui/map.js b/src/ui/map.js index 8b4113df246..d67ec4e8f45 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -62,7 +62,8 @@ import type { LightSpecification, TerrainSpecification, FogSpecification, - SourceSpecification + SourceSpecification, + ProjectionSpecification } from '../style-spec/types.js'; import type {ElevationQueryOptions} from '../terrain/elevation.js'; @@ -106,7 +107,7 @@ type MapOptions = { doubleClickZoom?: boolean, touchZoomRotate?: boolean, touchPitch?: boolean, - gestureHandling?: boolean, + cooperativeGestures?: boolean, trackResize?: boolean, center?: LngLatLike, zoom?: number, @@ -118,7 +119,8 @@ type MapOptions = { transformRequest?: RequestTransformFunction, accessToken: string, testMode: ?boolean, - locale?: Object + locale?: Object, + projection?: ProjectionSpecification | string }; const defaultMinZoom = -2; @@ -149,7 +151,7 @@ const defaultOptions = { doubleClickZoom: true, touchZoomRotate: true, touchPitch: true, - gestureHandling: false, + cooperativeGestures: false, bearingSnap: 7, clickTolerance: 3, @@ -236,7 +238,7 @@ const defaultOptions = { * @param {boolean} [options.doubleClickZoom=true] If `true`, the "double click to zoom" interaction is enabled (see {@link DoubleClickZoomHandler}). * @param {boolean | Object} [options.touchZoomRotate=true] If `true`, the "pinch to rotate and zoom" interaction is enabled. An `Object` value is passed as options to {@link TouchZoomRotateHandler#enable}. * @param {boolean | Object} [options.touchPitch=true] If `true`, the "drag to pitch" interaction is enabled. An `Object` value is passed as options to {@link TouchPitchHandler}. - * @param {boolean} [options.gestureHandling=false] If `true`, scroll zoom will require pressing the ctrl or ⌘ key while scrolling to zoom map. + * @param {boolean} [options.cooperativeGestures] If `true`, scroll zoom will require pressing the ctrl or ⌘ key while scrolling to zoom map, and touch pan will require using two fingers while panning to move the map. Touch pitch will require three fingers to activate if enabled. * @param {boolean} [options.trackResize=true] If `true`, the map will automatically resize when the browser window resizes. * @param {LngLatLike} [options.center=[0, 0]] The inital geographical centerpoint of the map. If `center` is not specified in the constructor options, Mapbox GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `[0, 0]` Note: Mapbox GL uses longitude, latitude coordinate order (as opposed to latitude, longitude) to match GeoJSON. * @param {number} [options.zoom=0] The initial zoom level of the map. If `zoom` is not specified in the constructor options, Mapbox GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `0`. @@ -267,6 +269,8 @@ const defaultOptions = { * @param {Object} [options.locale=null] A patch to apply to the default localization table for UI strings such as control tooltips. The `locale` object maps namespaced UI string IDs to translated strings in the target language; * see `src/ui/default_locale.js` for an example with all supported string IDs. The object may specify all UI strings (thereby adding support for a new translation) or only a subset of strings (thereby patching the default translation table). * @param {boolean} [options.testMode=false] Silences errors and warnings generated due to an invalid accessToken, useful when using the library to write unit tests. + * @param {ProjectionSpecification} [options.projection='mercator'] The projection the map should be rendered in. Available projections are Albers ('albers'), Equal Earth ('equalEarth'), Equirectangular/Plate Carrée/WGS84 ('equirectangular'), Lambert ('lambertConformalConic'), Mercator ('mercator'), Natural Earth ('naturalEarth'), and Winkel Tripel ('winkelTripel'). + * Conical projections such as Albers and Lambert have configurable `center` and `parallels` properties that allow developers to define the region in which the projection has minimal distortion; see the example for how to configure these properties. * @example * const map = new mapboxgl.Map({ * container: 'map', // container ID @@ -345,7 +349,7 @@ class Map extends Camera { _removed: boolean; _speedIndexTiming: boolean; _clickTolerance: number; - _gestureHandling: boolean; + _cooperativeGestures: boolean; _silenceAuthErrors: boolean; _averageElevationLastSampledAt: number; _averageElevation: EasedVariable; @@ -446,7 +450,7 @@ class Map extends Camera { this._mapId = uniqueId(); this._locale = extend({}, defaultLocale, options.locale); this._clickTolerance = options.clickTolerance; - this._gestureHandling = options.gestureHandling; + this._cooperativeGestures = options.cooperativeGestures; this._averageElevationLastSampledAt = -Infinity; this._averageElevation = new EasedVariable(0); @@ -497,6 +501,17 @@ class Map extends Camera { this.handlers = new HandlerManager(this, options); + this._localFontFamily = options.localFontFamily; + this._localIdeographFontFamily = options.localIdeographFontFamily; + + if (options.style) { + this.setStyle(options.style, {localFontFamily: this._localFontFamily, localIdeographFontFamily: this._localIdeographFontFamily}); + } + + if (options.projection) { + this.setProjection(options.projection); + } + const hashName = (typeof options.hash === 'string' && options.hash) || undefined; this._hash = options.hash && (new Hash(hashName)).addTo(this); // don't set position from options if set through hash @@ -516,11 +531,6 @@ class Map extends Camera { this.resize(); - this._localFontFamily = options.localFontFamily; - this._localIdeographFontFamily = options.localIdeographFontFamily; - - if (options.style) this.setStyle(options.style, {localFontFamily: this._localFontFamily, localIdeographFontFamily: this._localIdeographFontFamily}); - if (options.attributionControl) this.addControl(new AttributionControl({customAttribution: options.customAttribution})); @@ -696,9 +706,10 @@ class Map extends Camera { * if (mapDiv.style.visibility === true) map.resize(); */ resize(eventData?: Object) { - const dimensions = this._containerDimensions(); - const width = dimensions[0]; - const height = dimensions[1]; + const [width, height] = this._containerDimensions(); + + // do nothing if container remained the same size + if (width === this.transform.width && height === this.transform.height) return this; this._resizeCanvas(width, height); @@ -707,7 +718,6 @@ class Map extends Camera { const fireMoving = !this._moving; if (fireMoving) { - this.stop(); this.fire(new Event('movestart', eventData)) .fire(new Event('move', eventData)); } @@ -741,7 +751,7 @@ class Map extends Camera { * const maxBounds = map.getMaxBounds(); */ getMaxBounds(): LngLatBounds | null { - return this.transform.getMaxBounds(); + return this.transform.getMaxBounds() || null; } /** @@ -982,6 +992,38 @@ class Map extends Camera { /** @section {Point conversion} */ + /** + * Returns a {@link ProjectionSpecification} object that defines the current map projection. + * + * @returns {ProjectionSpecification} The {@link ProjectionSpecification} defining the current map projection. + * @example + * const projection = map.getProjection(); + */ + getProjection() { + return this.transform.getProjection(); + } + + /** + * Sets the map's projection. If called with `null` or `undefined`, the map will reset to Mercator. + * + * @param {ProjectionSpecification | string | null | undefined} projection The projection that the map should be rendered in. + * This can be a {@link ProjectionSpecification} object or a string of the projection's name. + * @example + * map.setProjection('albers'); + * map.setProjection({ + * name: 'albers', + * center: [35, 55], + * parallels: [20, 60] + * }); + */ + setProjection(projection?: ?ProjectionSpecification | string) { + this._lazyInitEmptyStyle(); + if (typeof projection === 'string') { + projection = (({name: projection}: any): ProjectionSpecification); + } + this.style.setProjection(projection); + } + /** * Returns a {@link Point} representing pixel coordinates, relative to the map's `container`, * that correspond to the specified geographical location. @@ -1053,11 +1095,12 @@ class Map extends Camera { return this._rotating || this.handlers && this.handlers.isRotating(); } - _createDelegatedListener(type: MapEvent, layerId: any, listener: any) { + _createDelegatedListener(type: MapEvent, layers: Array, listener: any) { if (type === 'mouseenter' || type === 'mouseover') { let mousein = false; const mousemove = (e) => { - const features = this.getLayer(layerId) ? this.queryRenderedFeatures(e.point, {layers: [layerId]}) : []; + const filteredLayers = layers.filter(layerId => this.getLayer(layerId)); + const features = filteredLayers.length ? this.queryRenderedFeatures(e.point, {layers: filteredLayers}) : []; if (!features.length) { mousein = false; } else if (!mousein) { @@ -1068,11 +1111,13 @@ class Map extends Camera { const mouseout = () => { mousein = false; }; - return {layer: layerId, listener, delegates: {mousemove, mouseout}}; + + return {layers: new Set(layers), listener, delegates: {mousemove, mouseout}}; } else if (type === 'mouseleave' || type === 'mouseout') { let mousein = false; const mousemove = (e) => { - const features = this.getLayer(layerId) ? this.queryRenderedFeatures(e.point, {layers: [layerId]}) : []; + const filteredLayers = layers.filter(layerId => this.getLayer(layerId)); + const features = filteredLayers.length ? this.queryRenderedFeatures(e.point, {layers: filteredLayers}) : []; if (features.length) { mousein = true; } else if (mousein) { @@ -1086,10 +1131,12 @@ class Map extends Camera { listener.call(this, new MapMouseEvent(type, this, e.originalEvent)); } }; - return {layer: layerId, listener, delegates: {mousemove, mouseout}}; + + return {layers: new Set(layers), listener, delegates: {mousemove, mouseout}}; } else { const delegate = (e) => { - const features = this.getLayer(layerId) ? this.queryRenderedFeatures(e.point, {layers: [layerId]}) : []; + const filteredLayers = layers.filter(layerId => this.getLayer(layerId)); + const features = filteredLayers.length ? this.queryRenderedFeatures(e.point, {layers: filteredLayers}) : []; if (features.length) { // Here we need to mutate the original event, so that preventDefault works as expected. e.features = features; @@ -1097,7 +1144,8 @@ class Map extends Camera { delete e.features; } }; - return {layer: layerId, listener, delegates: {[type]: delegate}}; + + return {layers: new Set(layers), listener, delegates: {[type]: delegate}}; } } @@ -1162,12 +1210,12 @@ class Map extends Camera { * | [`sourcedataloading`](#map.event:sourcedataloading) | | * | [`styleimagemissing`](#map.event:styleimagemissing) | | * - * @param {string} layerId (optional) The ID of a style layer. If you provide a `layerId`, - * the listener will be triggered only if its location is within a visible feature in this layer, + * @param {string | Array} layerIds (optional) The ID(s) of a style layer(s). If you provide a `layerId`, + * the listener will be triggered only if its location is within a visible feature in these layers, * and the event will have a `features` property containing an array of the matching features. - * If you do not provide a `layerId`, the listener will be triggered by a corresponding event + * If you do not provide `layerIds`, the listener will be triggered by a corresponding event * happening anywhere on the map, and the event will not have a `features` property. - * Note that many event types are not compatible with the optional `layerId` parameter. + * Note that many event types are not compatible with the optional `layerIds` parameter. * @param {Function} listener The function to be called when the event is fired. * @returns {Map} Returns itself to allow for method chaining. * @example @@ -1200,18 +1248,30 @@ class Map extends Camera { * .setHTML(`Country name: ${e.features[0].properties.name}`) * .addTo(map); * }); + * @example + * // Set an event listener that will fire + * // when a feature on the countries or background layers of the map is clicked. + * map.on('click', ['countries', 'background'], (e) => { + * new mapboxgl.Popup() + * .setLngLat(e.lngLat) + * .setHTML(`Country name: ${e.features[0].properties.name}`) + * .addTo(map); + * }); * @see [Example: Add 3D terrain to a map](https://docs.mapbox.com/mapbox-gl-js/example/add-terrain/) * @see [Example: Center the map on a clicked symbol](https://docs.mapbox.com/mapbox-gl-js/example/center-on-symbol/) * @see [Example: Create a draggable marker](https://docs.mapbox.com/mapbox-gl-js/example/drag-a-point/) * @see [Example: Create a hover effect](https://docs.mapbox.com/mapbox-gl-js/example/hover-styles/) * @see [Example: Display popup on click](https://docs.mapbox.com/mapbox-gl-js/example/popup-on-click/) */ - on(type: MapEvent, layerId: any, listener: any) { + on(type: MapEvent, layerIds: any, listener: any) { if (listener === undefined) { - return super.on(type, layerId); + return super.on(type, layerIds); } - const delegatedListener = this._createDelegatedListener(type, layerId, listener); + if (!Array.isArray(layerIds)) { + layerIds = [layerIds]; + } + const delegatedListener = this._createDelegatedListener(type, layerIds, listener); this._delegatedListeners = this._delegatedListeners || {}; this._delegatedListeners[type] = this._delegatedListeners[type] || []; @@ -1234,12 +1294,12 @@ class Map extends Camera { * a visible portion of the specified layer from outside that layer or outside the map canvas. `mouseleave` * and `mouseout` events are triggered when the cursor leaves a visible portion of the specified layer, or leaves * the map canvas. - * @param {string} layerId (optional) The ID of a style layer. If you provide a `layerId`, - * the listener will be triggered only if its location is within a visible feature in this layer, + * @param {string | Array} layerIds (optional) The ID(s) of a style layer(s). If you provide `layerIds`, + * the listener will be triggered only if its location is within a visible feature in these layers, * and the event will have a `features` property containing an array of the matching features. - * If you do not provide a `layerId`, the listener will be triggered by a corresponding event + * If you do not provide `layerIds`, the listener will be triggered by a corresponding event * happening anywhere on the map, and the event will not have a `features` property. - * Note that many event types are not compatible with the optional `layerId` parameter. + * Note that many event types are not compatible with the optional `layerIds` parameter. * @param {Function} listener The function to be called when the event is fired. * @returns {Map} Returns itself to allow for method chaining. * @example @@ -1253,17 +1313,26 @@ class Map extends Camera { * map.once('touchstart', 'my-point-layer', (e) => { * console.log(`The first map touch on the point layer was at: ${e.lnglat}`); * }); + * @example + * // Log the coordinates of a user's first map touch + * // on specific layers. + * map.once('touchstart', ['my-point-layer', 'my-point-layer-2'], (e) => { + * console.log(`The first map touch on the point layer was at: ${e.lnglat}`); + * }); * @see [Example: Create a draggable point](https://docs.mapbox.com/mapbox-gl-js/example/drag-a-point/) * @see [Example: Animate the camera around a point with 3D terrain](https://docs.mapbox.com/mapbox-gl-js/example/free-camera-point/) * @see [Example: Play map locations as a slideshow](https://docs.mapbox.com/mapbox-gl-js/example/playback-locations/) */ - once(type: MapEvent, layerId: any, listener: any) { + once(type: MapEvent, layerIds: any, listener: any) { if (listener === undefined) { - return super.once(type, layerId); + return super.once(type, layerIds); } - const delegatedListener = this._createDelegatedListener(type, layerId, listener); + if (!Array.isArray(layerIds)) { + layerIds = [layerIds]; + } + const delegatedListener = this._createDelegatedListener(type, layerIds, listener); for (const event in delegatedListener.delegates) { this.once((event: any), delegatedListener.delegates[event]); @@ -1277,7 +1346,7 @@ class Map extends Camera { * optionally limited to layer-specific events. * * @param {string} type The event type previously used to install the listener. - * @param {string} layerId (optional) The layer ID previously used to install the listener. + * @param {string | Array} layerIds (optional) The layer ID(s) previously used to install the listener. * @param {Function} listener The function previously installed as a listener. * @returns {Map} Returns itself to allow for method chaining. * @example @@ -1297,16 +1366,28 @@ class Map extends Camera { * }); * @see [Example: Create a draggable point](https://docs.mapbox.com/mapbox-gl-js/example/drag-a-point/) */ - off(type: MapEvent, layerId: any, listener: any) { + off(type: MapEvent, layerIds: any, listener: any) { if (listener === undefined) { - return super.off(type, layerId); + return super.off(type, layerIds); } - const removeDelegatedListener = (delegatedListeners) => { - const listeners = delegatedListeners[type]; + layerIds = new Set(Array.isArray(layerIds) ? layerIds : [layerIds]); + const areLayerArraysEqual = (hash1, hash2) => { + if (hash1.size !== hash2.size) { + return false; // at-least 1 arr has duplicate value(s) + } + + // comparing values + for (const value of hash1) { + if (!hash2.has(value)) return false; + } + return true; + }; + + const removeDelegatedListeners = (listeners: Array) => { for (let i = 0; i < listeners.length; i++) { const delegatedListener = listeners[i]; - if (delegatedListener.layer === layerId && delegatedListener.listener === listener) { + if (delegatedListener.listener === listener && areLayerArraysEqual(delegatedListener.layers, layerIds)) { for (const event in delegatedListener.delegates) { this.off((event: any), delegatedListener.delegates[event]); } @@ -1316,8 +1397,9 @@ class Map extends Camera { } }; - if (this._delegatedListeners && this._delegatedListeners[type]) { - removeDelegatedListener(this._delegatedListeners); + const delegatedListeners = this._delegatedListeners ? this._delegatedListeners[type] : undefined; + if (delegatedListeners) { + removeDelegatedListeners(delegatedListeners); } return this; @@ -2761,12 +2843,14 @@ class Map extends Camera { this._styleDirty = false; const zoom = this.transform.zoom; + const pitch = this.transform.pitch; const now = browser.now(); this.style.zoomHistory.update(zoom, now); const parameters = new EvaluationParameters(zoom, { now, fadeDuration, + pitch, zoomHistory: this.style.zoomHistory, transition: this.style.getTransition() }); @@ -2802,20 +2886,22 @@ class Map extends Camera { this._placementDirty = this.style && this.style._updatePlacement(this.painter.transform, this.showCollisionBoxes, fadeDuration, this._crossSourceCollisions); // Actually draw - this.painter.render(this.style, { - showTileBoundaries: this.showTileBoundaries, - showTerrainWireframe: this.showTerrainWireframe, - showOverdrawInspector: this._showOverdrawInspector, - showQueryGeometry: !!this._showQueryGeometry, - rotating: this.isRotating(), - zooming: this.isZooming(), - moving: this.isMoving(), - fadeDuration, - isInitialLoad: this._isInitialLoad, - showPadding: this.showPadding, - gpuTiming: !!this.listens('gpu-timing-layer'), - speedIndexTiming: this.speedIndexTiming, - }); + if (this.style) { + this.painter.render(this.style, { + showTileBoundaries: this.showTileBoundaries, + showTerrainWireframe: this.showTerrainWireframe, + showOverdrawInspector: this._showOverdrawInspector, + showQueryGeometry: !!this._showQueryGeometry, + rotating: this.isRotating(), + zooming: this.isZooming(), + moving: this.isMoving(), + fadeDuration, + isInitialLoad: this._isInitialLoad, + showPadding: this.showPadding, + gpuTiming: !!this.listens('gpu-timing-layer'), + speedIndexTiming: this.speedIndexTiming, + }); + } this.fire(new Event('render')); @@ -3076,6 +3162,9 @@ class Map extends Camera { } this._renderTaskQueue.clear(); this._domRenderTaskQueue.clear(); + if (this.style) { + this.style.clearWorkerCaches(); + } this.painter.destroy(); this.handlers.destroy(); delete this.handlers; diff --git a/src/ui/marker.js b/src/ui/marker.js index c4e234e57f4..16053ee0758 100644 --- a/src/ui/marker.js +++ b/src/ui/marker.js @@ -369,6 +369,7 @@ export default class Marker extends Evented { if (this._popup) { this._popup.remove(); this._popup = null; + this._element.removeAttribute('role'); this._element.removeEventListener('keypress', this._onKeyPress); if (!this._originalTabIndex) { @@ -395,11 +396,13 @@ export default class Marker extends Evented { this._popup = popup; if (this._lngLat) this._popup.setLngLat(this._lngLat); + this._element.setAttribute('role', 'button'); this._originalTabIndex = this._element.getAttribute('tabindex'); if (!this._originalTabIndex) { this._element.setAttribute('tabindex', '0'); } this._element.addEventListener('keypress', this._onKeyPress); + this._element.setAttribute('aria-expanded', 'false'); } return this; @@ -456,10 +459,15 @@ export default class Marker extends Evented { */ togglePopup() { const popup = this._popup; - - if (!popup) return this; - else if (popup.isOpen()) popup.remove(); - else popup.addTo(this._map); + if (!popup) { + return this; + } else if (popup.isOpen()) { + popup.remove(); + this._element.setAttribute('aria-expanded', 'false'); + } else { + popup.addTo(this._map); + this._element.setAttribute('aria-expanded', 'true'); + } return this; } diff --git a/test/expression.test.js b/test/expression.test.js index 2c4155d43f6..db741f604e2 100644 --- a/test/expression.test.js +++ b/test/expression.test.js @@ -6,12 +6,16 @@ import {toString} from '../src/style-spec/expression/types.js'; import ignores from './ignores.json'; import {CanonicalTileID} from '../src/source/tile_id.js'; import MercatorCoordinate from '../src/geo/mercator_coordinate.js'; +import tileTransform, {getTilePoint} from '../src/geo/projection/tile_transform.js'; +import {getProjection} from '../src/geo/projection/index.js'; import {fileURLToPath} from 'url'; const __filename = fileURLToPath(import.meta.url); +const projection = getProjection({name: 'mercator'}); function getPoint(coord, canonical) { - const p = canonical.getTilePoint(MercatorCoordinate.fromLngLat({lng: coord[0], lat: coord[1]}, 0)); + const tileTr = tileTransform(canonical, projection); + const p = getTilePoint(tileTr, MercatorCoordinate.fromLngLat({lng: coord[0], lat: coord[1]}, 0)); p.x = Math.round(p.x); p.y = Math.round(p.y); return p; diff --git a/test/ignores.json b/test/ignores.json index 6cdca5868ba..ecfaa189d62 100644 --- a/test/ignores.json +++ b/test/ignores.json @@ -32,5 +32,8 @@ "render-tests/within/paint-line": "https://github.com/mapbox/mapbox-gl-js/issues/7023", "render-tests/distance/layout-text-size": "skip - distance expression is not implemented", "render-tests/skybox/atmosphere-padding": "skip - https://github.com/mapbox/mapbox-gl-js/issues/10314", - "render-tests/terrain/symbol-draping/style.json": "skip - https://github.com/mapbox/mapbox-gl-js/issues/10365" + "render-tests/terrain/symbol-draping/style.json": "skip - https://github.com/mapbox/mapbox-gl-js/issues/10365", + "render-tests/fill-extrusion-terrain/flat-roof-over-border-of-different-zoom-zoomin": "skip - https://github.com/mapbox/mapbox-gl-js/issues/11041", + "render-tests/fill-extrusion-terrain/flat-roof-over-border-of-different-zoom": "skip - https://github.com/mapbox/mapbox-gl-js/issues/11041", + "render-tests/text-variable-anchor/pitched": "skip - non-deterministic symbol placement on tile boundaries" } diff --git a/test/integration/data/distance-lines.geojson b/test/integration/data/distance-lines.geojson new file mode 100644 index 00000000000..b2b5c7e647f --- /dev/null +++ b/test/integration/data/distance-lines.geojson @@ -0,0 +1,488 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "distance": "-2.50" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -120.50430589094042, + 37.919470015974326 + ], + [ + -120.30344797631889, + 37.919470015974326 + ], + [ + -120.10259006169738, + 37.919470015974326 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.25" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -120.50430589094042, + 37.93927382972113 + ], + [ + -120.30344797631889, + 37.93927382972113 + ], + [ + -120.10259006169738, + 37.93927382972113 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.00" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -120.50430589094042, + 37.9590723086595 + ], + [ + -120.30344797631889, + 37.9590723086595 + ], + [ + -120.10259006169738, + 37.9590723086595 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.75" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -120.50430589094042, + 37.97886545186313 + ], + [ + -120.30344797631889, + 37.97886545186313 + ], + [ + -120.10259006169738, + 37.97886545186313 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.50" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -120.50430589094042, + 37.99865325840861 + ], + [ + -120.30344797631889, + 37.99865325840861 + ], + [ + -120.10259006169738, + 37.99865325840861 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.25" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -120.50430589094042, + 38.01843572737516 + ], + [ + -120.30344797631889, + 38.01843572737516 + ], + [ + -120.10259006169738, + 38.01843572737516 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.00" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -120.50430589094042, + 38.038212857845025 + ], + [ + -120.30344797631889, + 38.038212857845025 + ], + [ + -120.10259006169738, + 38.038212857845025 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.75" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -120.50430589094042, + 38.057984648903016 + ], + [ + -120.30344797631889, + 38.057984648903016 + ], + [ + -120.10259006169738, + 38.057984648903016 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.50" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -120.50430589094042, + 38.07775109963683 + ], + [ + -120.30344797631889, + 38.07775109963683 + ], + [ + -120.10259006169738, + 38.07775109963683 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.25" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -120.50430589094042, + 38.09751220913702 + ], + [ + -120.30344797631889, + 38.09751220913702 + ], + [ + -120.10259006169738, + 38.09751220913702 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.00" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -120.50430589094042, + 38.11726797649678 + ], + [ + -120.30344797631889, + 38.11726797649678 + ], + [ + -120.10259006169738, + 38.11726797649678 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.25" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -120.50430589094042, + 38.137018400812224 + ], + [ + -120.30344797631889, + 38.137018400812224 + ], + [ + -120.10259006169738, + 38.137018400812224 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.50" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -120.50430589094042, + 38.156763481182196 + ], + [ + -120.30344797631889, + 38.156763481182196 + ], + [ + -120.10259006169738, + 38.156763481182196 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.75" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -120.50430589094042, + 38.176503216708255 + ], + [ + -120.30344797631889, + 38.176503216708255 + ], + [ + -120.10259006169738, + 38.176503216708255 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.00" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -120.50430589094042, + 38.19623760649492 + ], + [ + -120.30344797631889, + 38.19623760649492 + ], + [ + -120.10259006169738, + 38.19623760649492 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.25" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -120.50430589094042, + 38.21596664964929 + ], + [ + -120.30344797631889, + 38.21596664964929 + ], + [ + -120.10259006169738, + 38.21596664964929 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.50" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -120.50430589094042, + 38.23569034528143 + ], + [ + -120.30344797631889, + 38.23569034528143 + ], + [ + -120.10259006169738, + 38.23569034528143 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.75" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -120.50430589094042, + 38.25540869250392 + ], + [ + -120.30344797631889, + 38.25540869250392 + ], + [ + -120.10259006169738, + 38.25540869250392 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.00" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -120.50430589094042, + 38.27512169043243 + ], + [ + -120.30344797631889, + 38.27512169043243 + ], + [ + -120.10259006169738, + 38.27512169043243 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.25" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -120.50430589094042, + 38.29482933818517 + ], + [ + -120.30344797631889, + 38.29482933818517 + ], + [ + -120.10259006169738, + 38.29482933818517 + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.50" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -120.50430589094042, + 38.314531634883195 + ], + [ + -120.30344797631889, + 38.314531634883195 + ], + [ + -120.10259006169738, + 38.314531634883195 + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/test/integration/data/distance-points.geojson b/test/integration/data/distance-points.geojson new file mode 100644 index 00000000000..ddeb33d8610 --- /dev/null +++ b/test/integration/data/distance-points.geojson @@ -0,0 +1,4646 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "distance": "-2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.50430589094042, + 37.919470015974326 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.47919865161273, + 37.919470015974326 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.45409141228504, + 37.919470015974326 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.42898417295734, + 37.919470015974326 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.40387693362966, + 37.919470015974326 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.37876969430197, + 37.919470015974326 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.35366245497428, + 37.919470015974326 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.32855521564659, + 37.919470015974326 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.30344797631889, + 37.919470015974326 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.27834073699121, + 37.919470015974326 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.25323349766353, + 37.919470015974326 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.22812625833583, + 37.919470015974326 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.20301901900814, + 37.919470015974326 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.17791177968047, + 37.919470015974326 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.15280454035278, + 37.919470015974326 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.12769730102508, + 37.919470015974326 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.10259006169738, + 37.919470015974326 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.50430589094042, + 37.93927382972113 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.47919865161273, + 37.93927382972113 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.45409141228504, + 37.93927382972113 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.42898417295734, + 37.93927382972113 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.40387693362966, + 37.93927382972113 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.37876969430197, + 37.93927382972113 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.35366245497428, + 37.93927382972113 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.32855521564659, + 37.93927382972113 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.30344797631889, + 37.93927382972113 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.27834073699121, + 37.93927382972113 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.25323349766353, + 37.93927382972113 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.22812625833583, + 37.93927382972113 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.20301901900814, + 37.93927382972113 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.17791177968047, + 37.93927382972113 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.15280454035278, + 37.93927382972113 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.12769730102508, + 37.93927382972113 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.10259006169738, + 37.93927382972113 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.50430589094042, + 37.9590723086595 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.47919865161273, + 37.9590723086595 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.45409141228504, + 37.9590723086595 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.42898417295734, + 37.9590723086595 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.40387693362966, + 37.9590723086595 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.37876969430197, + 37.9590723086595 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.35366245497428, + 37.9590723086595 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.32855521564659, + 37.9590723086595 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.30344797631889, + 37.9590723086595 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.27834073699121, + 37.9590723086595 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.25323349766353, + 37.9590723086595 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.22812625833583, + 37.9590723086595 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.20301901900814, + 37.9590723086595 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.17791177968047, + 37.9590723086595 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.15280454035278, + 37.9590723086595 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.12769730102508, + 37.9590723086595 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.10259006169738, + 37.9590723086595 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.50430589094042, + 37.97886545186313 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.47919865161273, + 37.97886545186313 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.45409141228504, + 37.97886545186313 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.42898417295734, + 37.97886545186313 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.40387693362966, + 37.97886545186313 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.37876969430197, + 37.97886545186313 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.35366245497428, + 37.97886545186313 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.32855521564659, + 37.97886545186313 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.30344797631889, + 37.97886545186313 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.27834073699121, + 37.97886545186313 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.25323349766353, + 37.97886545186313 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.22812625833583, + 37.97886545186313 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.20301901900814, + 37.97886545186313 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.17791177968047, + 37.97886545186313 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.15280454035278, + 37.97886545186313 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.12769730102508, + 37.97886545186313 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.10259006169738, + 37.97886545186313 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.50430589094042, + 37.99865325840861 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.47919865161273, + 37.99865325840861 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.45409141228504, + 37.99865325840861 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.42898417295734, + 37.99865325840861 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.40387693362966, + 37.99865325840861 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.37876969430197, + 37.99865325840861 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.35366245497428, + 37.99865325840861 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.32855521564659, + 37.99865325840861 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.30344797631889, + 37.99865325840861 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.27834073699121, + 37.99865325840861 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.25323349766353, + 37.99865325840861 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.22812625833583, + 37.99865325840861 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.20301901900814, + 37.99865325840861 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.17791177968047, + 37.99865325840861 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.15280454035278, + 37.99865325840861 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.12769730102508, + 37.99865325840861 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.10259006169738, + 37.99865325840861 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.50430589094042, + 38.01843572737516 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.47919865161273, + 38.01843572737516 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.45409141228504, + 38.01843572737516 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.42898417295734, + 38.01843572737516 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.40387693362966, + 38.01843572737516 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.37876969430197, + 38.01843572737516 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.35366245497428, + 38.01843572737516 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.32855521564659, + 38.01843572737516 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.30344797631889, + 38.01843572737516 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.27834073699121, + 38.01843572737516 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.25323349766353, + 38.01843572737516 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.22812625833583, + 38.01843572737516 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.20301901900814, + 38.01843572737516 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.17791177968047, + 38.01843572737516 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.15280454035278, + 38.01843572737516 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.12769730102508, + 38.01843572737516 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.10259006169738, + 38.01843572737516 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.50430589094042, + 38.038212857845025 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.47919865161273, + 38.038212857845025 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.45409141228504, + 38.038212857845025 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.42898417295734, + 38.038212857845025 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.40387693362966, + 38.038212857845025 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.37876969430197, + 38.038212857845025 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.35366245497428, + 38.038212857845025 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.32855521564659, + 38.038212857845025 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.30344797631889, + 38.038212857845025 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.27834073699121, + 38.038212857845025 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.25323349766353, + 38.038212857845025 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.22812625833583, + 38.038212857845025 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.20301901900814, + 38.038212857845025 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.17791177968047, + 38.038212857845025 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.15280454035278, + 38.038212857845025 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.12769730102508, + 38.038212857845025 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.10259006169738, + 38.038212857845025 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.50430589094042, + 38.057984648903016 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.47919865161273, + 38.057984648903016 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.45409141228504, + 38.057984648903016 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.42898417295734, + 38.057984648903016 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.40387693362966, + 38.057984648903016 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.37876969430197, + 38.057984648903016 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.35366245497428, + 38.057984648903016 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.32855521564659, + 38.057984648903016 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.30344797631889, + 38.057984648903016 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.27834073699121, + 38.057984648903016 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.25323349766353, + 38.057984648903016 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.22812625833583, + 38.057984648903016 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.20301901900814, + 38.057984648903016 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.17791177968047, + 38.057984648903016 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.15280454035278, + 38.057984648903016 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.12769730102508, + 38.057984648903016 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.10259006169738, + 38.057984648903016 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.50430589094042, + 38.07775109963683 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.47919865161273, + 38.07775109963683 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.45409141228504, + 38.07775109963683 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.42898417295734, + 38.07775109963683 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.40387693362966, + 38.07775109963683 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.37876969430197, + 38.07775109963683 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.35366245497428, + 38.07775109963683 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.32855521564659, + 38.07775109963683 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.30344797631889, + 38.07775109963683 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.27834073699121, + 38.07775109963683 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.25323349766353, + 38.07775109963683 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.22812625833583, + 38.07775109963683 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.20301901900814, + 38.07775109963683 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.17791177968047, + 38.07775109963683 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.15280454035278, + 38.07775109963683 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.12769730102508, + 38.07775109963683 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.10259006169738, + 38.07775109963683 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.50430589094042, + 38.09751220913702 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.47919865161273, + 38.09751220913702 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.45409141228504, + 38.09751220913702 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.42898417295734, + 38.09751220913702 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.40387693362966, + 38.09751220913702 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.37876969430197, + 38.09751220913702 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.35366245497428, + 38.09751220913702 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.32855521564659, + 38.09751220913702 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.30344797631889, + 38.09751220913702 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.27834073699121, + 38.09751220913702 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.25323349766353, + 38.09751220913702 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.22812625833583, + 38.09751220913702 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.20301901900814, + 38.09751220913702 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.17791177968047, + 38.09751220913702 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.15280454035278, + 38.09751220913702 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.12769730102508, + 38.09751220913702 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "-0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.10259006169738, + 38.09751220913702 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.50430589094042, + 38.11726797649678 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.47919865161273, + 38.11726797649678 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.45409141228504, + 38.11726797649678 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.42898417295734, + 38.11726797649678 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.40387693362966, + 38.11726797649678 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.37876969430197, + 38.11726797649678 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.35366245497428, + 38.11726797649678 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.32855521564659, + 38.11726797649678 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.30344797631889, + 38.11726797649678 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.27834073699121, + 38.11726797649678 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.25323349766353, + 38.11726797649678 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.22812625833583, + 38.11726797649678 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.20301901900814, + 38.11726797649678 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.17791177968047, + 38.11726797649678 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.15280454035278, + 38.11726797649678 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.12769730102508, + 38.11726797649678 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.10259006169738, + 38.11726797649678 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.50430589094042, + 38.137018400812224 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.47919865161273, + 38.137018400812224 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.45409141228504, + 38.137018400812224 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.42898417295734, + 38.137018400812224 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.40387693362966, + 38.137018400812224 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.37876969430197, + 38.137018400812224 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.35366245497428, + 38.137018400812224 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.32855521564659, + 38.137018400812224 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.30344797631889, + 38.137018400812224 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.27834073699121, + 38.137018400812224 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.25323349766353, + 38.137018400812224 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.22812625833583, + 38.137018400812224 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.20301901900814, + 38.137018400812224 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.17791177968047, + 38.137018400812224 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.15280454035278, + 38.137018400812224 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.12769730102508, + 38.137018400812224 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.10259006169738, + 38.137018400812224 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.50430589094042, + 38.156763481182196 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.47919865161273, + 38.156763481182196 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.45409141228504, + 38.156763481182196 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.42898417295734, + 38.156763481182196 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.40387693362966, + 38.156763481182196 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.37876969430197, + 38.156763481182196 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.35366245497428, + 38.156763481182196 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.32855521564659, + 38.156763481182196 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.30344797631889, + 38.156763481182196 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.27834073699121, + 38.156763481182196 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.25323349766353, + 38.156763481182196 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.22812625833583, + 38.156763481182196 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.20301901900814, + 38.156763481182196 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.17791177968047, + 38.156763481182196 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.15280454035278, + 38.156763481182196 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.12769730102508, + 38.156763481182196 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.10259006169738, + 38.156763481182196 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.50430589094042, + 38.176503216708255 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.47919865161273, + 38.176503216708255 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.45409141228504, + 38.176503216708255 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.42898417295734, + 38.176503216708255 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.40387693362966, + 38.176503216708255 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.37876969430197, + 38.176503216708255 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.35366245497428, + 38.176503216708255 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.32855521564659, + 38.176503216708255 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.30344797631889, + 38.176503216708255 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.27834073699121, + 38.176503216708255 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.25323349766353, + 38.176503216708255 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.22812625833583, + 38.176503216708255 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.20301901900814, + 38.176503216708255 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.17791177968047, + 38.176503216708255 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.15280454035278, + 38.176503216708255 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.12769730102508, + 38.176503216708255 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "0.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.10259006169738, + 38.176503216708255 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.50430589094042, + 38.19623760649492 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.47919865161273, + 38.19623760649492 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.45409141228504, + 38.19623760649492 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.42898417295734, + 38.19623760649492 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.40387693362966, + 38.19623760649492 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.37876969430197, + 38.19623760649492 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.35366245497428, + 38.19623760649492 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.32855521564659, + 38.19623760649492 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.30344797631889, + 38.19623760649492 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.27834073699121, + 38.19623760649492 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.25323349766353, + 38.19623760649492 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.22812625833583, + 38.19623760649492 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.20301901900814, + 38.19623760649492 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.17791177968047, + 38.19623760649492 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.15280454035278, + 38.19623760649492 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.12769730102508, + 38.19623760649492 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.10259006169738, + 38.19623760649492 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.50430589094042, + 38.21596664964929 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.47919865161273, + 38.21596664964929 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.45409141228504, + 38.21596664964929 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.42898417295734, + 38.21596664964929 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.40387693362966, + 38.21596664964929 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.37876969430197, + 38.21596664964929 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.35366245497428, + 38.21596664964929 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.32855521564659, + 38.21596664964929 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.30344797631889, + 38.21596664964929 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.27834073699121, + 38.21596664964929 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.25323349766353, + 38.21596664964929 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.22812625833583, + 38.21596664964929 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.20301901900814, + 38.21596664964929 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.17791177968047, + 38.21596664964929 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.15280454035278, + 38.21596664964929 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.12769730102508, + 38.21596664964929 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.10259006169738, + 38.21596664964929 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.50430589094042, + 38.23569034528143 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.47919865161273, + 38.23569034528143 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.45409141228504, + 38.23569034528143 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.42898417295734, + 38.23569034528143 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.40387693362966, + 38.23569034528143 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.37876969430197, + 38.23569034528143 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.35366245497428, + 38.23569034528143 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.32855521564659, + 38.23569034528143 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.30344797631889, + 38.23569034528143 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.27834073699121, + 38.23569034528143 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.25323349766353, + 38.23569034528143 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.22812625833583, + 38.23569034528143 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.20301901900814, + 38.23569034528143 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.17791177968047, + 38.23569034528143 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.15280454035278, + 38.23569034528143 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.12769730102508, + 38.23569034528143 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.10259006169738, + 38.23569034528143 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.50430589094042, + 38.25540869250392 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.47919865161273, + 38.25540869250392 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.45409141228504, + 38.25540869250392 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.42898417295734, + 38.25540869250392 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.40387693362966, + 38.25540869250392 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.37876969430197, + 38.25540869250392 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.35366245497428, + 38.25540869250392 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.32855521564659, + 38.25540869250392 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.30344797631889, + 38.25540869250392 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.27834073699121, + 38.25540869250392 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.25323349766353, + 38.25540869250392 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.22812625833583, + 38.25540869250392 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.20301901900814, + 38.25540869250392 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.17791177968047, + 38.25540869250392 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.15280454035278, + 38.25540869250392 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.12769730102508, + 38.25540869250392 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "1.75" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.10259006169738, + 38.25540869250392 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.50430589094042, + 38.27512169043243 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.47919865161273, + 38.27512169043243 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.45409141228504, + 38.27512169043243 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.42898417295734, + 38.27512169043243 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.40387693362966, + 38.27512169043243 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.37876969430197, + 38.27512169043243 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.35366245497428, + 38.27512169043243 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.32855521564659, + 38.27512169043243 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.30344797631889, + 38.27512169043243 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.27834073699121, + 38.27512169043243 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.25323349766353, + 38.27512169043243 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.22812625833583, + 38.27512169043243 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.20301901900814, + 38.27512169043243 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.17791177968047, + 38.27512169043243 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.15280454035278, + 38.27512169043243 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.12769730102508, + 38.27512169043243 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.00" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.10259006169738, + 38.27512169043243 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.50430589094042, + 38.29482933818517 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.47919865161273, + 38.29482933818517 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.45409141228504, + 38.29482933818517 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.42898417295734, + 38.29482933818517 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.40387693362966, + 38.29482933818517 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.37876969430197, + 38.29482933818517 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.35366245497428, + 38.29482933818517 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.32855521564659, + 38.29482933818517 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.30344797631889, + 38.29482933818517 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.27834073699121, + 38.29482933818517 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.25323349766353, + 38.29482933818517 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.22812625833583, + 38.29482933818517 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.20301901900814, + 38.29482933818517 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.17791177968047, + 38.29482933818517 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.15280454035278, + 38.29482933818517 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.12769730102508, + 38.29482933818517 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.25" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.10259006169738, + 38.29482933818517 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.50430589094042, + 38.314531634883195 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.47919865161273, + 38.314531634883195 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.45409141228504, + 38.314531634883195 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.42898417295734, + 38.314531634883195 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.40387693362966, + 38.314531634883195 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.37876969430197, + 38.314531634883195 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.35366245497428, + 38.314531634883195 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.32855521564659, + 38.314531634883195 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.30344797631889, + 38.314531634883195 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.27834073699121, + 38.314531634883195 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.25323349766353, + 38.314531634883195 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.22812625833583, + 38.314531634883195 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.20301901900814, + 38.314531634883195 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.17791177968047, + 38.314531634883195 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.15280454035278, + 38.314531634883195 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.12769730102508, + 38.314531634883195 + ] + } + }, + { + "type": "Feature", + "properties": { + "distance": "2.50" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -120.10259006169738, + 38.314531634883195 + ] + } + } + ] +} \ No newline at end of file diff --git a/test/integration/lib/query.js b/test/integration/lib/query.js index 29ca4ed9a44..6e8fc86a2ca 100644 --- a/test/integration/lib/query.js +++ b/test/integration/lib/query.js @@ -149,7 +149,7 @@ async function runTest(t) { ]; } - browserWriteFile.postMessage(fileInfo); + if (!process.env.CI) browserWriteFile.postMessage(fileInfo); } catch (e) { t.error(e); diff --git a/test/integration/lib/render.js b/test/integration/lib/render.js index 93902815d3b..ed2e590e7e0 100644 --- a/test/integration/lib/render.js +++ b/test/integration/lib/render.js @@ -74,6 +74,7 @@ function ensureTeardown(t) { delete map.painter.context.gl; map = null; } + mapboxgl.clearStorage(); expectedCtx.clearRect(0, 0, expectedCanvas.width, expectedCanvas.height); diffCtx.clearRect(0, 0, diffCanvas.width, diffCanvas.height); @@ -144,6 +145,7 @@ async function runTest(t) { fadeDuration: options.fadeDuration || 0, optimizeForTerrain: options.optimizeForTerrain || false, localIdeographFontFamily: options.localIdeographFontFamily || false, + projection: options.projection, crossSourceCollisions: typeof options.crossSourceCollisions === "undefined" ? true : options.crossSourceCollisions, transformRequest: (url, resourceType) => { // some tests have the port hardcoded to 2900 @@ -308,7 +310,7 @@ async function runTest(t) { updateHTML(testMetaData); } - browserWriteFile.postMessage(fileInfo); + if (!process.env.CI) browserWriteFile.postMessage(fileInfo); } catch (e) { t.error(e); diff --git a/test/integration/render-tests/background-visibility/none/expected.png b/test/integration/render-tests/background-visibility/none/expected.png index 724d17cd7d3..aad72f146fb 100644 Binary files a/test/integration/render-tests/background-visibility/none/expected.png and b/test/integration/render-tests/background-visibility/none/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance-data-driven/early-dynamic-high-pitch/expected.png b/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance-data-driven/early-dynamic-high-pitch/expected.png new file mode 100644 index 00000000000..f0c42704f04 Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance-data-driven/early-dynamic-high-pitch/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance-data-driven/early-dynamic-high-pitch/style.json b/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance-data-driven/early-dynamic-high-pitch/style.json new file mode 100644 index 00000000000..6bba706be14 --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance-data-driven/early-dynamic-high-pitch/style.json @@ -0,0 +1,67 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 75, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-lines.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "rings-lines", + "type": "line", + "source": "rings", + "layout": {}, + "paint": { + "line-width": ["abs",["*", 10, ["to-number", ["get", "distance"]]]] + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": ["case", + ["<", ["pitch"], 60], ["in", ["get", "distance"], ["literal", ["0.25", "-0.75", "1.50"]]], + ["all", [">=", ["pitch"], 60], [">", ["distance-from-center"], 0]], ["in", ["get", "distance"], ["literal", ["1.00", "2.00"]]], + false + ], + "layout": { + "symbol-placement": "line", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance-data-driven/early-dynamic-low-pitch/expected.png b/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance-data-driven/early-dynamic-low-pitch/expected.png new file mode 100644 index 00000000000..3c35f0edbce Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance-data-driven/early-dynamic-low-pitch/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance-data-driven/early-dynamic-low-pitch/style.json b/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance-data-driven/early-dynamic-low-pitch/style.json new file mode 100644 index 00000000000..da6d72d3a80 --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance-data-driven/early-dynamic-low-pitch/style.json @@ -0,0 +1,67 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 45, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-lines.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "rings-lines", + "type": "line", + "source": "rings", + "layout": {}, + "paint": { + "line-width": ["abs",["*", 10, ["to-number", ["get", "distance"]]]] + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": ["case", + ["<", ["pitch"], 60], ["in", ["get", "distance"], ["literal", ["0.25", "0.75", "1.50"]]], + ["all", [">=", ["pitch"], 60], ["<", ["distance-from-center"], 0.5]], ["in", ["get", "distance"], ["literal", ["1.00", "2.00"]]], + false + ], + "layout": { + "symbol-placement": "line", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance-data-driven/late-dynamic/expected.png b/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance-data-driven/late-dynamic/expected.png new file mode 100644 index 00000000000..1b5b5689fd8 Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance-data-driven/late-dynamic/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance-data-driven/late-dynamic/style.json b/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance-data-driven/late-dynamic/style.json new file mode 100644 index 00000000000..d2e832e11d6 --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance-data-driven/late-dynamic/style.json @@ -0,0 +1,66 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 73.5, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-lines.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "rings-lines", + "type": "line", + "source": "rings", + "layout": {}, + "paint": { + "line-width": ["abs",["*", 10, ["to-number", ["get", "distance"]]]] + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": ["all", + ["in", ["get", "distance"], ["literal", ["1.00", "2.00"]]], + [">", ["distance-from-center"], 0.5] + ], + "layout": { + "symbol-placement": "line", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance/high-pitch-far-hidden/expected.png b/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance/high-pitch-far-hidden/expected.png new file mode 100644 index 00000000000..3e5b5517c25 Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance/high-pitch-far-hidden/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance/high-pitch-far-hidden/style.json b/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance/high-pitch-far-hidden/style.json new file mode 100644 index 00000000000..ce2df982d3d --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance/high-pitch-far-hidden/style.json @@ -0,0 +1,67 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 73.5, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-lines.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "rings-lines", + "type": "line", + "source": "rings", + "layout": {}, + "paint": { + "line-width": ["abs",["*", 10, ["to-number", ["get", "distance"]]]] + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": ["case", + ["<", ["pitch"], 60], true, + ["all", [">=", ["pitch"], 60], ["<", ["distance-from-center"], 0.5]], true, + false + ], + "layout": { + "symbol-placement": "line", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance/low-pitch-far-visible/expected.png b/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance/low-pitch-far-visible/expected.png new file mode 100644 index 00000000000..c2bfa4dea07 Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance/low-pitch-far-visible/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance/low-pitch-far-visible/style.json b/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance/low-pitch-far-visible/style.json new file mode 100644 index 00000000000..fd53adce3a1 --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/line/combined-pitch-distance/low-pitch-far-visible/style.json @@ -0,0 +1,67 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 55, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-lines.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "rings-lines", + "type": "line", + "source": "rings", + "layout": {}, + "paint": { + "line-width": ["abs",["*", 10, ["to-number", ["get", "distance"]]]] + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": ["case", + ["<", ["pitch"], 60], true, + ["all", [">=", ["pitch"], 60], ["<", ["distance-from-center"], 0.5]], true, + false + ], + "layout": { + "symbol-placement": "line", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/distance-far-cull/expected.png b/test/integration/render-tests/dynamic-filter/symbols/line/distance-far-cull/expected.png new file mode 100644 index 00000000000..793ff74e571 Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/line/distance-far-cull/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/distance-far-cull/style.json b/test/integration/render-tests/dynamic-filter/symbols/line/distance-far-cull/style.json new file mode 100644 index 00000000000..31c8877aee0 --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/line/distance-far-cull/style.json @@ -0,0 +1,63 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 73.5, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-lines.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "rings-lines", + "type": "line", + "source": "rings", + "layout": {}, + "paint": { + "line-width": ["abs",["*", 10, ["to-number", ["get", "distance"]]]] + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": ["<", ["distance-from-center"], 1.1], + "layout": { + "symbol-placement": "line", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/distance-near-and-far-cull/expected.png b/test/integration/render-tests/dynamic-filter/symbols/line/distance-near-and-far-cull/expected.png new file mode 100644 index 00000000000..15016b85505 Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/line/distance-near-and-far-cull/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/distance-near-and-far-cull/style.json b/test/integration/render-tests/dynamic-filter/symbols/line/distance-near-and-far-cull/style.json new file mode 100644 index 00000000000..f18d9b2f8ee --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/line/distance-near-and-far-cull/style.json @@ -0,0 +1,63 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 73.5, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-lines.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "rings-lines", + "type": "line", + "source": "rings", + "layout": {}, + "paint": { + "line-width": ["abs",["*", 10, ["to-number", ["get", "distance"]]]] + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": ["all", ["<", ["distance-from-center"], 1.5], [">", ["distance-from-center"], -0.6]], + "layout": { + "symbol-placement": "line", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/distance-near-cull/expected.png b/test/integration/render-tests/dynamic-filter/symbols/line/distance-near-cull/expected.png new file mode 100644 index 00000000000..04d7112bf4f Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/line/distance-near-cull/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/distance-near-cull/style.json b/test/integration/render-tests/dynamic-filter/symbols/line/distance-near-cull/style.json new file mode 100644 index 00000000000..33bc7bc6ea7 --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/line/distance-near-cull/style.json @@ -0,0 +1,63 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 73.5, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-lines.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "rings-lines", + "type": "line", + "source": "rings", + "layout": {}, + "paint": { + "line-width": ["abs",["*", 10, ["to-number", ["get", "distance"]]]] + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": [">", ["distance-from-center"], 1.1], + "layout": { + "symbol-placement": "line", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/distance-nofilter/expected.png b/test/integration/render-tests/dynamic-filter/symbols/line/distance-nofilter/expected.png new file mode 100644 index 00000000000..7b64e604095 Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/line/distance-nofilter/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/distance-nofilter/style.json b/test/integration/render-tests/dynamic-filter/symbols/line/distance-nofilter/style.json new file mode 100644 index 00000000000..6a7b9a8c537 --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/line/distance-nofilter/style.json @@ -0,0 +1,62 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 73.5, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-lines.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "rings-lines", + "type": "line", + "source": "rings", + "layout": {}, + "paint": { + "line-width": ["abs",["*", 10, ["to-number", ["get", "distance"]]]] + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "layout": { + "symbol-placement": "line", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/pitch-high-cull/expected.png b/test/integration/render-tests/dynamic-filter/symbols/line/pitch-high-cull/expected.png new file mode 100644 index 00000000000..e3cd6001d95 Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/line/pitch-high-cull/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/pitch-high-cull/style.json b/test/integration/render-tests/dynamic-filter/symbols/line/pitch-high-cull/style.json new file mode 100644 index 00000000000..1a287fbcfa8 --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/line/pitch-high-cull/style.json @@ -0,0 +1,63 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 73.5, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-lines.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "rings-lines", + "type": "line", + "source": "rings", + "layout": {}, + "paint": { + "line-width": ["abs",["*", 10, ["to-number", ["get", "distance"]]]] + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": ["<", ["pitch"], 60], + "layout": { + "symbol-placement": "line", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/pitch-high-show/expected.png b/test/integration/render-tests/dynamic-filter/symbols/line/pitch-high-show/expected.png new file mode 100644 index 00000000000..2f5aca3589a Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/line/pitch-high-show/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/pitch-high-show/style.json b/test/integration/render-tests/dynamic-filter/symbols/line/pitch-high-show/style.json new file mode 100644 index 00000000000..95bf64feb05 --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/line/pitch-high-show/style.json @@ -0,0 +1,63 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 75, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-lines.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "rings-lines", + "type": "line", + "source": "rings", + "layout": {}, + "paint": { + "line-width": ["abs",["*", 10, ["to-number", ["get", "distance"]]]] + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": [">", ["pitch"], 60], + "layout": { + "symbol-placement": "line", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/pitch-low-cull/expected.png b/test/integration/render-tests/dynamic-filter/symbols/line/pitch-low-cull/expected.png new file mode 100644 index 00000000000..0ad4d586a3d Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/line/pitch-low-cull/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/pitch-low-cull/style.json b/test/integration/render-tests/dynamic-filter/symbols/line/pitch-low-cull/style.json new file mode 100644 index 00000000000..9124618ac7c --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/line/pitch-low-cull/style.json @@ -0,0 +1,63 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 45, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-lines.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "rings-lines", + "type": "line", + "source": "rings", + "layout": {}, + "paint": { + "line-width": ["abs",["*", 10, ["to-number", ["get", "distance"]]]] + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": [">", ["pitch"], 60], + "layout": { + "symbol-placement": "line", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/pitch-low-show/expected.png b/test/integration/render-tests/dynamic-filter/symbols/line/pitch-low-show/expected.png new file mode 100644 index 00000000000..24a6453b52d Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/line/pitch-low-show/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/line/pitch-low-show/style.json b/test/integration/render-tests/dynamic-filter/symbols/line/pitch-low-show/style.json new file mode 100644 index 00000000000..3cb0846e8e1 --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/line/pitch-low-show/style.json @@ -0,0 +1,63 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 45, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-lines.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "rings-lines", + "type": "line", + "source": "rings", + "layout": {}, + "paint": { + "line-width": ["abs",["*", 10, ["to-number", ["get", "distance"]]]] + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": ["<", ["pitch"], 60], + "layout": { + "symbol-placement": "line", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance-data-driven/early-dynamic-high-pitch/expected.png b/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance-data-driven/early-dynamic-high-pitch/expected.png new file mode 100644 index 00000000000..70b84eba2b4 Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance-data-driven/early-dynamic-high-pitch/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance-data-driven/early-dynamic-high-pitch/style.json b/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance-data-driven/early-dynamic-high-pitch/style.json new file mode 100644 index 00000000000..99fe745a2e8 --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance-data-driven/early-dynamic-high-pitch/style.json @@ -0,0 +1,68 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 75, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-points.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "type": "circle", + "id": "rings-layer", + "source": "rings", + "paint": { + "circle-radius": 5, + "circle-color": "blue", + "circle-pitch-scale": "viewport" + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": ["case", + ["<", ["pitch"], 60], ["in", ["get", "distance"], ["literal", ["0.25", "0.75", "1.50"]]], + ["all", [">=", ["pitch"], 60], [">", ["distance-from-center"], 0]], ["in", ["get", "distance"], ["literal", ["1.00", "2.00"]]], + false + ], + "layout": { + "symbol-placement": "point", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance-data-driven/early-dynamic-low-pitch/expected.png b/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance-data-driven/early-dynamic-low-pitch/expected.png new file mode 100644 index 00000000000..ff4ac04f8c5 Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance-data-driven/early-dynamic-low-pitch/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance-data-driven/early-dynamic-low-pitch/style.json b/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance-data-driven/early-dynamic-low-pitch/style.json new file mode 100644 index 00000000000..35d27a015ea --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance-data-driven/early-dynamic-low-pitch/style.json @@ -0,0 +1,68 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 45, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-points.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "type": "circle", + "id": "rings-layer", + "source": "rings", + "paint": { + "circle-radius": 5, + "circle-color": "blue", + "circle-pitch-scale": "viewport" + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": ["case", + ["<", ["pitch"], 60], ["in", ["get", "distance"], ["literal", ["0.25", "0.75", "1.50"]]], + ["all", [">=", ["pitch"], 60], ["<", ["distance-from-center"], 0.5]], ["in", ["get", "distance"], ["literal", ["1.00", "2.00"]]], + false + ], + "layout": { + "symbol-placement": "point", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance-data-driven/late-dynamic/expected.png b/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance-data-driven/late-dynamic/expected.png new file mode 100644 index 00000000000..6eb4f64287c Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance-data-driven/late-dynamic/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance-data-driven/late-dynamic/style.json b/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance-data-driven/late-dynamic/style.json new file mode 100644 index 00000000000..f4e4b6d9998 --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance-data-driven/late-dynamic/style.json @@ -0,0 +1,67 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 73.5, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-points.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "type": "circle", + "id": "rings-layer", + "source": "rings", + "paint": { + "circle-radius": 5, + "circle-color": "blue", + "circle-pitch-scale": "viewport" + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": ["all", + ["in", ["get", "distance"], ["literal", ["-0.25", "1.00", "2.00"]]], + [">", ["distance-from-center"], 0.5] + ], + "layout": { + "symbol-placement": "point", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance/high-pitch-far-hidden/expected.png b/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance/high-pitch-far-hidden/expected.png new file mode 100644 index 00000000000..cb05529b6b2 Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance/high-pitch-far-hidden/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance/high-pitch-far-hidden/style.json b/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance/high-pitch-far-hidden/style.json new file mode 100644 index 00000000000..d1f34b01ab7 --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance/high-pitch-far-hidden/style.json @@ -0,0 +1,68 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 73.5, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-points.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "type": "circle", + "id": "rings-layer", + "source": "rings", + "paint": { + "circle-radius": 5, + "circle-color": "blue", + "circle-pitch-scale": "viewport" + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": ["case", + ["<", ["pitch"], 60], true, + ["all", [">=", ["pitch"], 60], ["<", ["distance-from-center"], 0.5]], true, + false + ], + "layout": { + "symbol-placement": "point", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance/low-pitch-far-visible/expected.png b/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance/low-pitch-far-visible/expected.png new file mode 100644 index 00000000000..461957e3e8c Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance/low-pitch-far-visible/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance/low-pitch-far-visible/style.json b/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance/low-pitch-far-visible/style.json new file mode 100644 index 00000000000..42a5a8f1de3 --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/point/combined-pitch-distance/low-pitch-far-visible/style.json @@ -0,0 +1,68 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 55, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-points.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "type": "circle", + "id": "rings-layer", + "source": "rings", + "paint": { + "circle-radius": 5, + "circle-color": "blue", + "circle-pitch-scale": "viewport" + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": ["case", + ["<", ["pitch"], 60], true, + ["all", [">=", ["pitch"], 60], ["<", ["distance-from-center"], 0.5]], true, + false + ], + "layout": { + "symbol-placement": "point", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/distance-far-cull/expected.png b/test/integration/render-tests/dynamic-filter/symbols/point/distance-far-cull/expected.png new file mode 100644 index 00000000000..6d0cf720a68 Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/point/distance-far-cull/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/distance-far-cull/style.json b/test/integration/render-tests/dynamic-filter/symbols/point/distance-far-cull/style.json new file mode 100644 index 00000000000..8b41822e30c --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/point/distance-far-cull/style.json @@ -0,0 +1,64 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 73.5, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-points.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "type": "circle", + "id": "rings-layer", + "source": "rings", + "paint": { + "circle-radius": 5, + "circle-color": "blue", + "circle-pitch-scale": "viewport" + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": ["<", ["distance-from-center"], 1.1], + "layout": { + "symbol-placement": "point", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/distance-near-and-far-cull/expected.png b/test/integration/render-tests/dynamic-filter/symbols/point/distance-near-and-far-cull/expected.png new file mode 100644 index 00000000000..c5262a25710 Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/point/distance-near-and-far-cull/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/distance-near-and-far-cull/style.json b/test/integration/render-tests/dynamic-filter/symbols/point/distance-near-and-far-cull/style.json new file mode 100644 index 00000000000..52dbe81644c --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/point/distance-near-and-far-cull/style.json @@ -0,0 +1,64 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 73.5, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-points.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "type": "circle", + "id": "rings-layer", + "source": "rings", + "paint": { + "circle-radius": 5, + "circle-color": "blue", + "circle-pitch-scale": "viewport" + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": ["all", ["<", ["distance-from-center"], 1.5], [">", ["distance-from-center"], -0.6]], + "layout": { + "symbol-placement": "point", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/distance-near-cull/expected.png b/test/integration/render-tests/dynamic-filter/symbols/point/distance-near-cull/expected.png new file mode 100644 index 00000000000..f7e85383c29 Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/point/distance-near-cull/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/distance-near-cull/style.json b/test/integration/render-tests/dynamic-filter/symbols/point/distance-near-cull/style.json new file mode 100644 index 00000000000..2e3b811999f --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/point/distance-near-cull/style.json @@ -0,0 +1,64 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 73.5, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-points.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "type": "circle", + "id": "rings-layer", + "source": "rings", + "paint": { + "circle-radius": 5, + "circle-color": "blue", + "circle-pitch-scale": "viewport" + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": [">", ["distance-from-center"], 1.1], + "layout": { + "symbol-placement": "point", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/distance-nofilter/expected.png b/test/integration/render-tests/dynamic-filter/symbols/point/distance-nofilter/expected.png new file mode 100644 index 00000000000..df2e3884baa Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/point/distance-nofilter/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/distance-nofilter/style.json b/test/integration/render-tests/dynamic-filter/symbols/point/distance-nofilter/style.json new file mode 100644 index 00000000000..22fbb072400 --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/point/distance-nofilter/style.json @@ -0,0 +1,63 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 73.5, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-points.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "type": "circle", + "id": "rings-layer", + "source": "rings", + "paint": { + "circle-radius": 5, + "circle-color": "blue", + "circle-pitch-scale": "viewport" + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "layout": { + "symbol-placement": "point", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/pitch-high-cull/expected.png b/test/integration/render-tests/dynamic-filter/symbols/point/pitch-high-cull/expected.png new file mode 100644 index 00000000000..eac3da291a0 Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/point/pitch-high-cull/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/pitch-high-cull/style.json b/test/integration/render-tests/dynamic-filter/symbols/point/pitch-high-cull/style.json new file mode 100644 index 00000000000..2d385d319bd --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/point/pitch-high-cull/style.json @@ -0,0 +1,64 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 73.5, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-points.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "type": "circle", + "id": "rings-layer", + "source": "rings", + "paint": { + "circle-radius": 5, + "circle-color": "blue", + "circle-pitch-scale": "viewport" + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": ["<", ["pitch"], 60], + "layout": { + "symbol-placement": "point", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/pitch-high-show/expected.png b/test/integration/render-tests/dynamic-filter/symbols/point/pitch-high-show/expected.png new file mode 100644 index 00000000000..53409c2a0fd Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/point/pitch-high-show/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/pitch-high-show/style.json b/test/integration/render-tests/dynamic-filter/symbols/point/pitch-high-show/style.json new file mode 100644 index 00000000000..6203b9c0942 --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/point/pitch-high-show/style.json @@ -0,0 +1,63 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 75, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-points.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "type": "circle", + "id": "rings-layer", + "source": "rings", + "paint": { + "circle-radius": 5, + "circle-color": "blue", + "circle-pitch-scale": "viewport" + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": [">", ["pitch"], 60], + "layout": { + "symbol-placement": "point", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/pitch-low-cull/expected.png b/test/integration/render-tests/dynamic-filter/symbols/point/pitch-low-cull/expected.png new file mode 100644 index 00000000000..bef3b6e9297 Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/point/pitch-low-cull/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/pitch-low-cull/style.json b/test/integration/render-tests/dynamic-filter/symbols/point/pitch-low-cull/style.json new file mode 100644 index 00000000000..b96e3936b61 --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/point/pitch-low-cull/style.json @@ -0,0 +1,64 @@ +{ + "version": 8, + "metadata": { + "test": { + "collisionDebug": true, + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 45, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-points.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "type": "circle", + "id": "rings-layer", + "source": "rings", + "paint": { + "circle-radius": 5, + "circle-color": "blue", + "circle-pitch-scale": "viewport" + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": [">", ["pitch"], 60], + "layout": { + "symbol-placement": "point", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/pitch-low-show/expected.png b/test/integration/render-tests/dynamic-filter/symbols/point/pitch-low-show/expected.png new file mode 100644 index 00000000000..3bac5115228 Binary files /dev/null and b/test/integration/render-tests/dynamic-filter/symbols/point/pitch-low-show/expected.png differ diff --git a/test/integration/render-tests/dynamic-filter/symbols/point/pitch-low-show/style.json b/test/integration/render-tests/dynamic-filter/symbols/point/pitch-low-show/style.json new file mode 100644 index 00000000000..f9f31edb19c --- /dev/null +++ b/test/integration/render-tests/dynamic-filter/symbols/point/pitch-low-show/style.json @@ -0,0 +1,63 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 264, + "width": 400, + "operations": [["wait"]] + } + }, + "center": [-120.30344797631889, 38.11726797649675], + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "zoom": 10.852, + "pitch": 45, + "sources": { + "rings": { + "type": "geojson", + "data": "local://data/distance-points.geojson" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "type": "circle", + "id": "rings-layer", + "source": "rings", + "paint": { + "circle-radius": 5, + "circle-color": "blue", + "circle-pitch-scale": "viewport" + } + }, + { + "type": "symbol", + "id": "rings-labels", + "source": "rings", + "filter": ["<", ["pitch"], 60], + "layout": { + "symbol-placement": "point", + "text-size": 10, + "symbol-spacing": 50, + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-field": ["get", "distance"], + "text-pitch-alignment": "viewport", + "text-allow-overlap": true + }, + "paint": { + "text-color": "red", + "text-halo-color": "white", + "text-halo-width": 2 + } + } + ] + } diff --git a/test/integration/render-tests/fill-extrusion-terrain/flat-roof-over-border-of-different-zoom-zoomin/expected.png b/test/integration/render-tests/fill-extrusion-terrain/flat-roof-over-border-of-different-zoom-zoomin/expected.png new file mode 100644 index 00000000000..71186ed4f7a Binary files /dev/null and b/test/integration/render-tests/fill-extrusion-terrain/flat-roof-over-border-of-different-zoom-zoomin/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-terrain/flat-roof-over-border-of-different-zoom-zoomin/style.json b/test/integration/render-tests/fill-extrusion-terrain/flat-roof-over-border-of-different-zoom-zoomin/style.json new file mode 100644 index 00000000000..47bbb6b0208 --- /dev/null +++ b/test/integration/render-tests/fill-extrusion-terrain/flat-roof-over-border-of-different-zoom-zoomin/style.json @@ -0,0 +1,149 @@ +{ + "version": 8, + "metadata": { + "description": "Tests various cases of flatRoofsUpdate().", + "test": { + "height": 512, + "width": 512, + "operations": [ + ["wait"], + [ + "setZoom", + 16 + ], + ["wait"] + ] + } + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/const/0-0-0.terrain.512.png" + ], + "maxzoom": 14, + "tileSize": 512 + }, + "mapbox": { + "type": "vector", + "maxzoom": 16, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + } + }, + "terrain": { + "source": "rgbterrain" + }, + "pitch": 50, + "bearing": 0, + "zoom": 15.5, + "center": [ + -122.448383, + 37.743011 + ], + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "blue" + } + }, + { + "id": "road", + "type": "line", + "source": "mapbox", + "source-layer": "road", + "paint": { + "line-color": "lightgreen", + "line-width": 10 + } + }, + { + "id": "building", + "source": "mapbox", + "paint": { + "fill-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 15.0, + 0.0, + 16.0, + 1.0 + ], + "fill-outline-color": [ + "rgba", + 205.00001525878907, + 202.00001525878907, + 198.00001525878907, + 1.0 + ], + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 15.0, + [ + "rgba", + 223.00001525878907, + 220.00001525878907, + 215.00001525878907, + 1.0 + ], + 16.0, + [ + "rgba", + 220.00001525878907, + 217.00001525878907, + 214.00001525878907, + 1.0 + ] + ] + }, + "filter": [ + "all", + [ + "!=", + [ + "get", + "type" + ], + "building:part" + ], + [ + "==", + [ + "get", + "underground" + ], + "false" + ] + ], + "source-layer": "building", + "type": "fill", + "minzoom": 15.0 + }, + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "mapbox", + "source-layer": "building", + "filter": ["==", "extrude", "true"], + "paint": { + "fill-extrusion-color": "gray", + "fill-extrusion-opacity": 0.7, + "fill-extrusion-height": ["get", "height"] + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/fill-extrusion-terrain/flat-roof-over-border-of-different-zoom/expected.png b/test/integration/render-tests/fill-extrusion-terrain/flat-roof-over-border-of-different-zoom/expected.png new file mode 100644 index 00000000000..71186ed4f7a Binary files /dev/null and b/test/integration/render-tests/fill-extrusion-terrain/flat-roof-over-border-of-different-zoom/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-terrain/flat-roof-over-border-of-different-zoom/style.json b/test/integration/render-tests/fill-extrusion-terrain/flat-roof-over-border-of-different-zoom/style.json new file mode 100644 index 00000000000..8471fe91944 --- /dev/null +++ b/test/integration/render-tests/fill-extrusion-terrain/flat-roof-over-border-of-different-zoom/style.json @@ -0,0 +1,144 @@ +{ + "version": 8, + "metadata": { + "description": "Tests various cases of flatRoofsUpdate().", + "test": { + "height": 512, + "width": 512, + "operations": [ + ["wait"] + ] + } + }, + "sources": { + "rgbterrain": { + "type": "raster-dem", + "tiles": [ + "local://tiles/const/0-0-0.terrain.512.png" + ], + "maxzoom": 14, + "tileSize": 512 + }, + "mapbox": { + "type": "vector", + "maxzoom": 16, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + } + }, + "terrain": { + "source": "rgbterrain" + }, + "pitch": 50, + "bearing": 0, + "zoom": 16, + "center": [ + -122.448383, + 37.743011 + ], + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "blue" + } + }, + { + "id": "road", + "type": "line", + "source": "mapbox", + "source-layer": "road", + "paint": { + "line-color": "lightgreen", + "line-width": 10 + } + }, + { + "id": "building", + "source": "mapbox", + "paint": { + "fill-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 15.0, + 0.0, + 16.0, + 1.0 + ], + "fill-outline-color": [ + "rgba", + 205.00001525878907, + 202.00001525878907, + 198.00001525878907, + 1.0 + ], + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 15.0, + [ + "rgba", + 223.00001525878907, + 220.00001525878907, + 215.00001525878907, + 1.0 + ], + 16.0, + [ + "rgba", + 220.00001525878907, + 217.00001525878907, + 214.00001525878907, + 1.0 + ] + ] + }, + "filter": [ + "all", + [ + "!=", + [ + "get", + "type" + ], + "building:part" + ], + [ + "==", + [ + "get", + "underground" + ], + "false" + ] + ], + "source-layer": "building", + "type": "fill", + "minzoom": 15.0 + }, + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "mapbox", + "source-layer": "building", + "filter": ["==", "extrude", "true"], + "paint": { + "fill-extrusion-color": "gray", + "fill-extrusion-opacity": 0.7, + "fill-extrusion-height": ["get", "height"] + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/image-fallback-nested/circle/expected.png b/test/integration/render-tests/image-fallback-nested/circle/expected.png new file mode 100644 index 00000000000..743399c2c7c Binary files /dev/null and b/test/integration/render-tests/image-fallback-nested/circle/expected.png differ diff --git a/test/integration/render-tests/image-fallback-nested/circle/style.json b/test/integration/render-tests/image-fallback-nested/circle/style.json new file mode 100644 index 00000000000..7a309997db5 --- /dev/null +++ b/test/integration/render-tests/image-fallback-nested/circle/style.json @@ -0,0 +1,83 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 128, + "width": 128 + } + }, + "center": [ 0, 0 ], + "zoom": 0, + "sprite": "local://sprites/sprite", + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { "icon": "fav-bicycle-18"}, + "geometry": { + "type": "Point", + "coordinates": [ + 0, + 16 + ] + } + }, + { + "type": "Feature", + "properties": { "icon": "missing-icon"}, + "geometry": { + "type": "Point", + "coordinates": [ + 0, + -16 + ] + } + } + ] + } + } + }, + "layers": [ + { + "id": "text", + "type": "circle", + "source": "geojson", + "paint": { + "circle-color": [ "case", + ["==", "missing", + ["to-string", ["coalesce", + ["image", ["get", "icon"]], + "missing" + ]] + ], + "red", + "blue" + ], + "circle-radius": [ "case", + ["==", "missing", + ["to-string", ["coalesce", + ["image", ["get", "icon"]], + "missing" + ]] + ], + 8, + 16 + ], + "circle-opacity": [ "case", + ["==", "missing", + ["to-string", ["coalesce", + ["image", ["get", "icon"]], + "missing" + ]] + ], + 1, + 0.2 + ] + } + } + ] +} diff --git a/test/integration/render-tests/image-fallback-nested/feature-state-inside/expected.png b/test/integration/render-tests/image-fallback-nested/feature-state-inside/expected.png new file mode 100644 index 00000000000..820b2ef40ae Binary files /dev/null and b/test/integration/render-tests/image-fallback-nested/feature-state-inside/expected.png differ diff --git a/test/integration/render-tests/image-fallback-nested/feature-state-inside/style.json b/test/integration/render-tests/image-fallback-nested/feature-state-inside/style.json new file mode 100644 index 00000000000..00daf72dc34 --- /dev/null +++ b/test/integration/render-tests/image-fallback-nested/feature-state-inside/style.json @@ -0,0 +1,134 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64, + "operations": [ + [ + "setFeatureState", + { + "source": "geojson", + "id": "0" + }, + { + "hover": true + } + ], + [ + "wait" + ], + [ + "setFeatureState", + { + "source": "geojson", + "id": "2" + }, + { + "hover": true + } + ], + [ + "wait" + ] + ] + } + }, + "zoom": 2, + "sprite": "local://sprites/sprite", + "sources": { + "geojson": { + "type": "geojson", + "data": + { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": 0, + "properties": { "icon": "fav-bicycle-18"}, + "geometry": { + "type": "Point", + "coordinates": [ + -2, + 2 + ] + } + }, + { + "type": "Feature", + "id": 1, + "properties": { "icon": "fav-bicycle-18"}, + "geometry": { + "type": "Point", + "coordinates": [ + 2, + 2 + ] + } + }, + { + "type": "Feature", + "id": 2, + "properties": { "icon": "missing-icon"}, + "geometry": { + "type": "Point", + "coordinates": [ + 2, + -2 + ] + } + }, + { + "type": "Feature", + "id": 3, + "properties": { "icon": "missing-icon"}, + "geometry": { + "type": "Point", + "coordinates": [ + -2, + -2 + ] + } + } + ] + } + } + }, + "layers": [ + { + "id": "circle", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 5, + "circle-color": + [ "case", + ["boolean", [ "feature-state", "hover"], false ], + [ "case", + ["==", "missing", + ["to-string", ["coalesce", + ["image", ["get", "icon"]], + "missing" + ] + ] + ], + "yellow", + "green" + ], + [ "case", + ["==", "missing", + ["to-string", ["coalesce", + ["image", ["get", "icon"]], + "missing" + ] + ] + ], + "red", + "blue" + ] + ] + } + } + ] +} diff --git a/test/integration/render-tests/image-fallback-nested/feature-state-outside/expected.png b/test/integration/render-tests/image-fallback-nested/feature-state-outside/expected.png new file mode 100644 index 00000000000..820b2ef40ae Binary files /dev/null and b/test/integration/render-tests/image-fallback-nested/feature-state-outside/expected.png differ diff --git a/test/integration/render-tests/image-fallback-nested/feature-state-outside/style.json b/test/integration/render-tests/image-fallback-nested/feature-state-outside/style.json new file mode 100644 index 00000000000..98f7b57aef5 --- /dev/null +++ b/test/integration/render-tests/image-fallback-nested/feature-state-outside/style.json @@ -0,0 +1,127 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64, + "operations": [ + [ + "setFeatureState", + { + "source": "geojson", + "id": "0" + }, + { + "hover": true + } + ], + [ + "wait" + ], + [ + "setFeatureState", + { + "source": "geojson", + "id": "2" + }, + { + "hover": true + } + ], + [ + "wait" + ] + ] + } + }, + "zoom": 2, + "sprite": "local://sprites/sprite", + "sources": { + "geojson": { + "type": "geojson", + "data": + { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": 0, + "properties": { "icon": "fav-bicycle-18"}, + "geometry": { + "type": "Point", + "coordinates": [ + -2, + 2 + ] + } + }, + { + "type": "Feature", + "id": 1, + "properties": { "icon": "fav-bicycle-18"}, + "geometry": { + "type": "Point", + "coordinates": [ + 2, + 2 + ] + } + }, + { + "type": "Feature", + "id": 2, + "properties": { "icon": "missing-icon"}, + "geometry": { + "type": "Point", + "coordinates": [ + 2, + -2 + ] + } + }, + { + "type": "Feature", + "id": 3, + "properties": { "icon": "missing-icon"}, + "geometry": { + "type": "Point", + "coordinates": [ + -2, + -2 + ] + } + } + ] + } + } + }, + "layers": [ + { + "id": "circle", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 5, + "circle-color": [ "case", + ["==", "missing", + ["to-string", ["coalesce", + ["image", ["get", "icon"]], + "missing" + ] + ] + ], + [ "case", + ["boolean", [ "feature-state", "hover"], false ], + "yellow", + "red" + ], + [ "case", + ["boolean", [ "feature-state", "hover"], false ], + "green", + "blue" + ] + ] + } + } + ] +} diff --git a/test/integration/render-tests/image-fallback-nested/icon/expected.png b/test/integration/render-tests/image-fallback-nested/icon/expected.png new file mode 100644 index 00000000000..5232d5f8cfb Binary files /dev/null and b/test/integration/render-tests/image-fallback-nested/icon/expected.png differ diff --git a/test/integration/render-tests/image-fallback-nested/icon/style.json b/test/integration/render-tests/image-fallback-nested/icon/style.json new file mode 100644 index 00000000000..6b3d01c0bbf --- /dev/null +++ b/test/integration/render-tests/image-fallback-nested/icon/style.json @@ -0,0 +1,112 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 128, + "width": 128 + } + }, + "center": [ 0, 0 ], + "zoom": 0, + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { "icon": "fav-bicycle-18"}, + "geometry": { + "type": "Point", + "coordinates": [ + 0, + 16 + ] + } + }, + { + "type": "Feature", + "properties": { "icon": "missing-icon"}, + "geometry": { + "type": "Point", + "coordinates": [ + 0, + -16 + ] + } + } + ] + } + } + }, + "layers": [ + { + "id": "text", + "type": "symbol", + "source": "geojson", + "layout": { + "text-allow-overlap": true, + "icon-allow-overlap": true, + "icon-image": "dot.sdf", + "text-field": ["to-string", + ["coalesce", + ["image", ["get", "icon"]], + "no icon found" + ] + ], + + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-offset": [0, 0.6], + "text-anchor": "top" + }, + "paint": { + "icon-color": [ "case", + ["==", "missing", + ["to-string", ["coalesce", + ["image", ["get", "icon"]], + "missing" + ]] + ], + "red", + "blue" + ], + "text-color": [ "case", + ["==", "missing", + ["to-string", ["coalesce", + ["image", ["get", "icon"]], + "missing" + ]] + ], + "red", + "blue" + ], + "icon-opacity": [ "case", + ["==", "missing", + ["to-string", ["coalesce", + ["image", ["get", "icon"]], + "missing" + ]] + ], + 1, + 0.2 + ], + "text-opacity": [ "case", + ["==", "missing", + ["to-string", ["coalesce", + ["image", ["get", "icon"]], + "missing" + ]] + ], + 1, + 0.2 + ] + } + } + ] +} diff --git a/test/integration/render-tests/image-fallback-nested/line/expected.png b/test/integration/render-tests/image-fallback-nested/line/expected.png new file mode 100644 index 00000000000..4c9c0110c24 Binary files /dev/null and b/test/integration/render-tests/image-fallback-nested/line/expected.png differ diff --git a/test/integration/render-tests/image-fallback-nested/line/style.json b/test/integration/render-tests/image-fallback-nested/line/style.json new file mode 100644 index 00000000000..b6fc6dace13 --- /dev/null +++ b/test/integration/render-tests/image-fallback-nested/line/style.json @@ -0,0 +1,88 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 128, + "width": 128 + } + }, + "center": [ 0, 0 ], + "zoom": 0, + "sprite": "local://sprites/sprite", + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { "icon": "fav-bicycle-18"}, + "geometry": { + "type": "LineString", + "coordinates": [ + [ -16, 16], + [16, 16] + ] + } + }, + { + "type": "Feature", + "properties": { "icon": "missing-icon"}, + "geometry": { + "type": "LineString", + "coordinates": [ + [ -16, -16], + [16, -16] + ] + } + } + ] + } + } + }, + "layers": [ + { + "id": "text", + "type": "line", + "source": "geojson", + "layout": { + }, + "paint": { + "line-color": + [ "case", + ["==", "missing", + ["to-string", ["coalesce", + ["image", ["get", "icon"]], + "missing" + ]] + ], + "red", + "blue" + ], + "line-width": + [ "case", + ["==", "missing", + ["to-string", ["coalesce", + ["image", ["get", "icon"]], + "missing" + ]] + ], + 10, + 20 + ], + "line-opacity": + [ "case", + ["==", "missing", + ["to-string", ["coalesce", + ["image", ["get", "icon"]], + "missing" + ]] + ], + 1, + 0.2 + ] + } + } + ] +} diff --git a/test/integration/render-tests/image-fallback-nested/text/expected.png b/test/integration/render-tests/image-fallback-nested/text/expected.png new file mode 100644 index 00000000000..ad51ebf12fa Binary files /dev/null and b/test/integration/render-tests/image-fallback-nested/text/expected.png differ diff --git a/test/integration/render-tests/image-fallback-nested/text/style.json b/test/integration/render-tests/image-fallback-nested/text/style.json new file mode 100644 index 00000000000..f5ee0d116ad --- /dev/null +++ b/test/integration/render-tests/image-fallback-nested/text/style.json @@ -0,0 +1,95 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 128, + "width": 128 + } + }, + "center": [ 0, 0 ], + "zoom": 0, + "sprite": "local://sprites/sprite", + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { "icon": "fav-bicycle-18"}, + "geometry": { + "type": "Point", + "coordinates": [ + 0, + 16 + ] + } + }, + { + "type": "Feature", + "properties": { "icon": "missing-icon"}, + "geometry": { + "type": "Point", + "coordinates": [ + 0, + -16 + ] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "text", + "type": "symbol", + "source": "geojson", + "layout": { + "text-allow-overlap": true, + "icon-allow-overlap": true, + "icon-image": [ + "coalesce", + ["image", ["get", "icon"]], + ["image", "park"] + ], + "text-field": ["to-string", + ["coalesce", + ["image", ["get", "icon"]], + "no icon found" + ] + ], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-offset": [0, 0.6], + "text-anchor": "top" + }, + "paint": { + "text-color": [ "case", + ["==", "missing", + ["to-string", ["coalesce", + ["image", ["get", "icon"]], + "missing" + ]] + ], + "red", + "blue" + ], + "text-opacity": [ "case", + ["==", "missing", + ["to-string", ["coalesce", + ["image", ["get", "icon"]], + "missing" + ]] + ], + 1, + 0.2 + ] + } + } + ] +} diff --git a/test/integration/render-tests/image-fallback-nested/zoom/expected.png b/test/integration/render-tests/image-fallback-nested/zoom/expected.png new file mode 100644 index 00000000000..64bc1adfda7 Binary files /dev/null and b/test/integration/render-tests/image-fallback-nested/zoom/expected.png differ diff --git a/test/integration/render-tests/image-fallback-nested/zoom/style.json b/test/integration/render-tests/image-fallback-nested/zoom/style.json new file mode 100644 index 00000000000..1ffe42adaff --- /dev/null +++ b/test/integration/render-tests/image-fallback-nested/zoom/style.json @@ -0,0 +1,70 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64 + } + }, + "zoom": 2, + "sprite": "local://sprites/sprite", + "sources": { + "geojson": { + "type": "geojson", + "data": + { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { "icon": "fav-bicycle-18"}, + "geometry": { + "type": "Point", + "coordinates": [ + -2, + 0 + ] + } + }, + { + "type": "Feature", + "properties": { "icon": "missing-icon"}, + "geometry": { + "type": "Point", + "coordinates": [ + 2, + 0 + ] + } + } + ] + } + } + }, + "layers": [ + { + "id": "circle", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 5, + "circle-color": [ + "step", + ["zoom"], + "black", + 1, + [ "case", + ["==", "missing", + ["to-string", ["coalesce", + ["image", ["get", "icon"]], + "missing" + ]] + ], + "red", + "blue" + ] + ] + } + } + ] +} diff --git a/test/integration/render-tests/map-projections/albers-configured/expected.png b/test/integration/render-tests/map-projections/albers-configured/expected.png new file mode 100644 index 00000000000..703a596ad69 Binary files /dev/null and b/test/integration/render-tests/map-projections/albers-configured/expected.png differ diff --git a/test/integration/render-tests/map-projections/albers-configured/style.json b/test/integration/render-tests/map-projections/albers-configured/style.json new file mode 100644 index 00000000000..d1b27020c61 --- /dev/null +++ b/test/integration/render-tests/map-projections/albers-configured/style.json @@ -0,0 +1,18 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 512, + "height": 512, + "operations": [ + ["setStyle", "local://mapbox-gl-styles/styles/basic-v9.json"], + ["wait"], + ["setProjection", {"name": "albers", "center": [-154, 63]}], + ["wait"] + ] + } + }, + "center": [-122.414, 37.776], + "sources": {}, + "layers": [] + } \ No newline at end of file diff --git a/test/integration/render-tests/map-projections/albers/expected.png b/test/integration/render-tests/map-projections/albers/expected.png new file mode 100644 index 00000000000..36806ecea25 Binary files /dev/null and b/test/integration/render-tests/map-projections/albers/expected.png differ diff --git a/test/integration/render-tests/map-projections/albers/style.json b/test/integration/render-tests/map-projections/albers/style.json new file mode 100644 index 00000000000..8c1843bec2a --- /dev/null +++ b/test/integration/render-tests/map-projections/albers/style.json @@ -0,0 +1,18 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 512, + "height": 512, + "operations": [ + ["setStyle", "local://mapbox-gl-styles/styles/basic-v9.json"], + ["wait"], + ["setProjection", "albers"], + ["wait"] + ] + } + }, + "center": [-122.414, 37.776], + "sources": {}, + "layers": [] + } \ No newline at end of file diff --git a/test/integration/render-tests/map-projections/equal-earth/expected.png b/test/integration/render-tests/map-projections/equal-earth/expected.png new file mode 100644 index 00000000000..b49409a3b5c Binary files /dev/null and b/test/integration/render-tests/map-projections/equal-earth/expected.png differ diff --git a/test/integration/render-tests/map-projections/equal-earth/style.json b/test/integration/render-tests/map-projections/equal-earth/style.json new file mode 100644 index 00000000000..0111dad26a4 --- /dev/null +++ b/test/integration/render-tests/map-projections/equal-earth/style.json @@ -0,0 +1,17 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 512, + "height": 512, + "operations": [ + ["setStyle", "local://mapbox-gl-styles/styles/basic-v9.json"], + ["wait"], + ["setProjection", "equalEarth"], + ["wait"] + ] + } + }, + "sources": {}, + "layers": [] + } \ No newline at end of file diff --git a/test/integration/render-tests/map-projections/equirectangular/expected.png b/test/integration/render-tests/map-projections/equirectangular/expected.png new file mode 100644 index 00000000000..4f475bc89fa Binary files /dev/null and b/test/integration/render-tests/map-projections/equirectangular/expected.png differ diff --git a/test/integration/render-tests/map-projections/equirectangular/style.json b/test/integration/render-tests/map-projections/equirectangular/style.json new file mode 100644 index 00000000000..06855d57996 --- /dev/null +++ b/test/integration/render-tests/map-projections/equirectangular/style.json @@ -0,0 +1,17 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 512, + "height": 512, + "operations": [ + ["setStyle", "local://mapbox-gl-styles/styles/basic-v9.json"], + ["wait"], + ["setProjection", "equirectangular"], + ["wait"] + ] + } + }, + "sources": {}, + "layers": [] + } \ No newline at end of file diff --git a/test/integration/render-tests/map-projections/lambert/expected.png b/test/integration/render-tests/map-projections/lambert/expected.png new file mode 100644 index 00000000000..1b13608ea4d Binary files /dev/null and b/test/integration/render-tests/map-projections/lambert/expected.png differ diff --git a/test/integration/render-tests/map-projections/lambert/style.json b/test/integration/render-tests/map-projections/lambert/style.json new file mode 100644 index 00000000000..c23857d3618 --- /dev/null +++ b/test/integration/render-tests/map-projections/lambert/style.json @@ -0,0 +1,18 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 512, + "height": 512, + "operations": [ + ["setStyle", "local://mapbox-gl-styles/styles/basic-v9.json"], + ["wait"], + ["setProjection", "lambertConformalConic"], + ["wait"] + ] + } + }, + "center": [-122.414, 37.776], + "sources": {}, + "layers": [] + } \ No newline at end of file diff --git a/test/integration/render-tests/map-projections/natural-earth/expected.png b/test/integration/render-tests/map-projections/natural-earth/expected.png new file mode 100644 index 00000000000..9bd964b3966 Binary files /dev/null and b/test/integration/render-tests/map-projections/natural-earth/expected.png differ diff --git a/test/integration/render-tests/map-projections/natural-earth/style.json b/test/integration/render-tests/map-projections/natural-earth/style.json new file mode 100644 index 00000000000..0e1eb24811f --- /dev/null +++ b/test/integration/render-tests/map-projections/natural-earth/style.json @@ -0,0 +1,17 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 512, + "height": 512, + "operations": [ + ["setStyle", "local://mapbox-gl-styles/styles/basic-v9.json"], + ["wait"], + ["setProjection", "naturalEarth"], + ["wait"] + ] + } + }, + "sources": {}, + "layers": [] + } \ No newline at end of file diff --git a/test/integration/render-tests/map-projections/winkel-tripel/expected.png b/test/integration/render-tests/map-projections/winkel-tripel/expected.png new file mode 100644 index 00000000000..e63c90ea905 Binary files /dev/null and b/test/integration/render-tests/map-projections/winkel-tripel/expected.png differ diff --git a/test/integration/render-tests/map-projections/winkel-tripel/style.json b/test/integration/render-tests/map-projections/winkel-tripel/style.json new file mode 100644 index 00000000000..35ea92485eb --- /dev/null +++ b/test/integration/render-tests/map-projections/winkel-tripel/style.json @@ -0,0 +1,17 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 512, + "height": 512, + "operations": [ + ["setStyle", "local://mapbox-gl-styles/styles/basic-v9.json"], + ["wait"], + ["setProjection", "winkelTripel"], + ["wait"] + ] + } + }, + "sources": {}, + "layers": [] + } \ No newline at end of file diff --git a/test/integration/tiles/15-5238-12668.mvt b/test/integration/tiles/15-5238-12668.mvt new file mode 100644 index 00000000000..3fa905c6109 Binary files /dev/null and b/test/integration/tiles/15-5238-12668.mvt differ diff --git a/test/integration/tiles/16-10476-25337mvt b/test/integration/tiles/16-10476-25337mvt new file mode 100644 index 00000000000..f8d24870229 Binary files /dev/null and b/test/integration/tiles/16-10476-25337mvt differ diff --git a/test/integration/tiles/16-10476-25338.mvt b/test/integration/tiles/16-10476-25338.mvt new file mode 100644 index 00000000000..e70a8589b9f Binary files /dev/null and b/test/integration/tiles/16-10476-25338.mvt differ diff --git a/test/integration/tiles/16-10477-25337.mvt b/test/integration/tiles/16-10477-25337.mvt new file mode 100644 index 00000000000..0db7336b0c8 Binary files /dev/null and b/test/integration/tiles/16-10477-25337.mvt differ diff --git a/test/integration/tiles/16-10477-25338.mvt b/test/integration/tiles/16-10477-25338.mvt new file mode 100644 index 00000000000..d88404f14ab Binary files /dev/null and b/test/integration/tiles/16-10477-25338.mvt differ diff --git a/test/release/scroll_zoom_blocker.html b/test/release/scroll_zoom_blocker.html new file mode 120000 index 00000000000..9d1fff0af99 --- /dev/null +++ b/test/release/scroll_zoom_blocker.html @@ -0,0 +1 @@ +../../debug/scroll_zoom_blocker.html \ No newline at end of file diff --git a/test/unit/data/symbol_bucket.test.js b/test/unit/data/symbol_bucket.test.js index a73cfffcf63..aabb639d4bf 100644 --- a/test/unit/data/symbol_bucket.test.js +++ b/test/unit/data/symbol_bucket.test.js @@ -13,6 +13,7 @@ import Tile from '../../../src/source/tile.js'; import CrossTileSymbolIndex from '../../../src/symbol/cross_tile_symbol_index.js'; import FeatureIndex from '../../../src/data/feature_index.js'; import {createSymbolBucket} from '../../util/create_symbol_layer.js'; +import {getProjection} from '../../../src/geo/projection/index.js'; import {fileURLToPath} from 'url'; const __dirname = fileURLToPath(new URL('.', import.meta.url)); @@ -47,11 +48,12 @@ test('SymbolBucket', (t) => { const placement = new Placement(transform, 0, true); const tileID = new OverscaledTileID(0, 0, 0, 0, 0); const crossTileSymbolIndex = new CrossTileSymbolIndex(); + const painter = {transform: {projection: getProjection({name: 'mercator'})}}; // add feature from bucket A bucketA.populate([{feature}], options); performSymbolLayout(bucketA, stacks, glyphPositions); - const tileA = new Tile(tileID, 512); + const tileA = new Tile(tileID, 512, 0, painter); tileA.latestFeatureIndex = new FeatureIndex(tileID); tileA.buckets = {test: bucketA}; tileA.collisionBoxArray = collisionBoxArray; @@ -59,7 +61,7 @@ test('SymbolBucket', (t) => { // add same feature from bucket B bucketB.populate([{feature}], options); performSymbolLayout(bucketB, stacks, glyphPositions); - const tileB = new Tile(tileID, 512); + const tileB = new Tile(tileID, 512, 0, painter); tileB.buckets = {test: bucketB}; tileB.collisionBoxArray = collisionBoxArray; diff --git a/test/unit/geo/transform.test.js b/test/unit/geo/transform.test.js index ba2928e45db..93cd7d8dd09 100644 --- a/test/unit/geo/transform.test.js +++ b/test/unit/geo/transform.test.js @@ -5,7 +5,7 @@ import LngLat from '../../../src/geo/lng_lat.js'; import {OverscaledTileID, CanonicalTileID} from '../../../src/source/tile_id.js'; import {fixedNum, fixedLngLat, fixedCoord, fixedPoint, fixedVec3, fixedVec4} from '../../util/fixed.js'; import {FreeCameraOptions} from '../../../src/ui/free_camera.js'; -import MercatorCoordinate, {mercatorZfromAltitude} from '../../../src/geo/mercator_coordinate.js'; +import MercatorCoordinate, {mercatorZfromAltitude, MAX_MERCATOR_LATITUDE} from '../../../src/geo/mercator_coordinate.js'; import {vec3, quat} from 'gl-matrix'; import LngLatBounds from '../../../src/geo/lng_lat_bounds.js'; import {degToRad} from '../../../src/util/util.js'; @@ -16,7 +16,6 @@ test('transform', (t) => { const transform = new Transform(); transform.resize(500, 500); t.equal(transform.unmodified, true); - t.equal(transform.maxValidLatitude, 85.051129); t.equal(transform.tileSize, 512, 'tileSize'); t.equal(transform.worldSize, 512, 'worldSize'); t.equal(transform.width, 500, 'width'); @@ -95,15 +94,12 @@ test('transform', (t) => { t.end(); }); - t.test('lngRange & latRange constrain zoom and center', (t) => { + t.test('maxBounds constrain zoom and center', (t) => { const transform = new Transform(); transform.center = new LngLat(0, 0); transform.zoom = 10; transform.resize(500, 500); - - transform.lngRange = [-5, 5]; - transform.latRange = [-5, 5]; - + transform.setMaxBounds(LngLatBounds.convert([-5, -5, 5, 5])); transform.zoom = 0; t.equal(transform.zoom, 5.135709286104402); @@ -122,8 +118,7 @@ test('transform', (t) => { const transform = new Transform(); transform.zoom = 6; transform.resize(500, 500); - transform.lngRange = [160, 190]; - transform.latRange = [-55, -23]; + transform.setMaxBounds(LngLatBounds.convert([160, -55, 190, -23])); transform.center = new LngLat(-170, -40); @@ -137,8 +132,7 @@ test('transform', (t) => { const transform = new Transform(); transform.zoom = 6; transform.resize(500, 500); - transform.lngRange = [-190, -160]; - transform.latRange = [-55, -23]; + transform.setMaxBounds(LngLatBounds.convert([-190, -55, -160, -23])); transform.center = new LngLat(170, -40); @@ -152,8 +146,7 @@ test('transform', (t) => { const transform = new Transform(); transform.zoom = 6; transform.resize(500, 500); - transform.lngRange = [0, 360]; - transform.latRange = [-90, 90]; + transform.setMaxBounds(LngLatBounds.convert([0, -90, 360, 90])); transform.center = new LngLat(-155, 0); @@ -166,8 +159,7 @@ test('transform', (t) => { const transform = new Transform(); transform.zoom = 6; transform.resize(500, 500); - transform.lngRange = [-360, 0]; - transform.latRange = [-90, 90]; + transform.setMaxBounds(LngLatBounds.convert([-360, -90, 0, 90])); transform.center = new LngLat(160, 0); t.same(transform.center.lng.toFixed(10), -200); @@ -223,8 +215,8 @@ test('transform', (t) => { t.end(); }); - t.test('_minZoomForBounds respects latRange and lngRange', (t) => { - t.test('it returns 0 when latRange and lngRange are undefined', (t) => { + t.test('_minZoomForBounds respects maxBounds', (t) => { + t.test('it returns 0 when lngRange is undefined', (t) => { const transform = new Transform(); transform.center = new LngLat(0, 0); transform.zoom = 10; @@ -239,8 +231,7 @@ test('transform', (t) => { transform.center = new LngLat(0, 0); transform.zoom = 10; transform.resize(500, 500); - transform.lngRange = [-5, 5]; - transform.latRange = [-5, 5]; + transform.setMaxBounds(LngLatBounds.convert([-5, -5, 5, 5])); const preComputedMinZoom = transform._minZoomForBounds(); transform.zoom = 0; @@ -349,7 +340,7 @@ test('transform', (t) => { const bounds = transform.getBounds(); // Bounds stops at the edge of the map - t.same(bounds.getNorth().toFixed(6), transform.maxValidLatitude); + t.same(bounds.getNorth().toFixed(6), MAX_MERCATOR_LATITUDE); // Top corners of bounds line up with side of view t.same(transform.locationPoint(bounds.getNorthWest()).x.toFixed(10), 0); t.same(transform.locationPoint(bounds.getNorthEast()).x.toFixed(10), transform.width); @@ -369,7 +360,7 @@ test('transform', (t) => { const bounds = transform.getBounds(); // Bounds stops at the edge of the map - t.same(bounds.getSouth().toFixed(6), -transform.maxValidLatitude); + t.same(bounds.getSouth().toFixed(6), -MAX_MERCATOR_LATITUDE); // Top corners of bounds line up with side of view t.same(transform.locationPoint(bounds.getSouthEast()).x.toFixed(10), 0); t.same(transform.locationPoint(bounds.getSouthWest()).x.toFixed(10), transform.width); @@ -1067,8 +1058,8 @@ test('transform', (t) => { t.test('clamps latitude', (t) => { const transform = new Transform(); - t.deepEqual(transform.project(new LngLat(0, -90)), transform.project(new LngLat(0, -transform.maxValidLatitude))); - t.deepEqual(transform.project(new LngLat(0, 90)), transform.project(new LngLat(0, transform.maxValidLatitude))); + t.deepEqual(transform.project(new LngLat(0, -90)), transform.project(new LngLat(0, -MAX_MERCATOR_LATITUDE))); + t.deepEqual(transform.project(new LngLat(0, 90)), transform.project(new LngLat(0, MAX_MERCATOR_LATITUDE))); t.end(); }); @@ -1359,7 +1350,7 @@ test('transform', (t) => { t.test('clamp to bounds', (t) => { const transform = new Transform(); transform.resize(100, 100); - transform.setMaxBounds(new LngLatBounds(new LngLat(-180, -transform.maxValidLatitude), new LngLat(180, transform.maxValidLatitude))); + transform.setMaxBounds(new LngLatBounds(new LngLat(-180, -MAX_MERCATOR_LATITUDE), new LngLat(180, MAX_MERCATOR_LATITUDE))); transform.zoom = 8.56; const options = new FreeCameraOptions(); @@ -1490,7 +1481,7 @@ test('transform', (t) => { }); t.test('_translateCameraConstrained', (t) => { - t.test('it clamps at zoom 0 when lngRange and latRange are not defined', (t) => { + t.test('it clamps at zoom 0 when maxBounds are not defined', (t) => { const transform = new Transform(); transform.center = new LngLat(0, 0); transform.zoom = 10; @@ -1525,8 +1516,7 @@ test('transform', (t) => { transform.center = new LngLat(0, 0); transform.zoom = 20; transform.resize(500, 500); - transform.lngRange = [-5, 5]; - transform.latRange = [-5, 5]; + transform.setMaxBounds(LngLatBounds.convert([-5, -5, 5, 5])); //record constrained zoom transform.zoom = 0; diff --git a/test/unit/source/geojson_worker_source.test.js b/test/unit/source/geojson_worker_source.test.js index e7b4a466a90..6bf3076b691 100644 --- a/test/unit/source/geojson_worker_source.test.js +++ b/test/unit/source/geojson_worker_source.test.js @@ -3,6 +3,7 @@ import GeoJSONWorkerSource from '../../../src/source/geojson_worker_source.js'; import StyleLayerIndex from '../../../src/style/style_layer_index.js'; import {OverscaledTileID} from '../../../src/source/tile_id.js'; import perf from '../../../src/util/performance.js'; +import {getProjection} from '../../../src/geo/projection/index.js'; const actor = {send: () => {}}; @@ -34,7 +35,8 @@ test('reloadTile', (t) => { source: 'sourceId', uid: 0, tileID: new OverscaledTileID(0, 0, 0, 0, 0), - maxZoom: 10 + maxZoom: 10, + projection: getProjection({name: 'mercator'}) }; function addData(callback) { diff --git a/test/unit/source/source_cache.test.js b/test/unit/source/source_cache.test.js index f97a55b40a4..52b2749fbf1 100644 --- a/test/unit/source/source_cache.test.js +++ b/test/unit/source/source_cache.test.js @@ -68,7 +68,8 @@ function createSourceCache(options, used) { type: 'mock-source-type' }, spec), /* dispatcher */ {}, eventedParent)); sc.used = typeof used === 'boolean' ? used : true; - sc.transform = {tileZoom: 0}; + sc.transform = new Transform(); + sc.map = {painter: {transform: sc.transform}}; return {sourceCache: sc, eventedParent}; } @@ -334,7 +335,10 @@ test('SourceCache#removeTile', (t) => { callback(); } }); - sourceCache.map = {painter: {crossTileSymbolIndex: "", tileExtentVAO: {}}}; + sourceCache.map = {painter: {transform: new Transform(), crossTileSymbolIndex: "", tileExtentVAO: {}, context: { + createIndexBuffer: () => {}, + createVertexBuffer: () => {} + }}}; sourceCache._addTile(tileID); diff --git a/test/unit/source/vector_tile_worker_source.test.js b/test/unit/source/vector_tile_worker_source.test.js index 6aa005f14ff..9b90edea628 100644 --- a/test/unit/source/vector_tile_worker_source.test.js +++ b/test/unit/source/vector_tile_worker_source.test.js @@ -6,6 +6,7 @@ import {test} from '../../util/test.js'; import VectorTileWorkerSource from '../../../src/source/vector_tile_worker_source.js'; import StyleLayerIndex from '../../../src/style/style_layer_index.js'; import perf from '../../../src/util/performance.js'; +import {getProjection} from '../../../src/geo/projection/index.js'; import {fileURLToPath} from 'url'; const __dirname = fileURLToPath(new URL('.', import.meta.url)); @@ -19,6 +20,7 @@ test('VectorTileWorkerSource#abortTile aborts pending request', (t) => { source: 'source', uid: 0, tileID: {overscaledZ: 0, wrap: 0, canonical: {x: 0, y: 0, z: 0, w: 0}}, + projection: getProjection({name: 'mercator'}), request: {url: 'http://localhost:2900/abort'} }, (err, res) => { t.false(err); @@ -46,7 +48,8 @@ test('VectorTileWorkerSource#abortTile aborts pending async request', (t) => { source.loadTile({ uid: 0, - tileID: {overscaledZ: 0, wrap: 0, canonical: {x: 0, y: 0, z: 0, w: 0}} + tileID: {overscaledZ: 0, wrap: 0, canonical: {x: 0, y: 0, z: 0, w: 0}}, + projection: getProjection({name: 'mercator'}) }, (err, res) => { t.false(err); t.false(res); @@ -246,6 +249,7 @@ test('VectorTileWorkerSource provides resource timing information', (t) => { source: 'source', uid: 0, tileID: {overscaledZ: 0, wrap: 0, canonical: {x: 0, y: 0, z: 0, w: 0}}, + projection: getProjection({name: 'mercator'}), request: {url: 'http://localhost:2900/faketile.pbf', collectResourceTiming: true} }, (err, res) => { t.false(err); diff --git a/test/unit/source/worker.test.js b/test/unit/source/worker.test.js index 927e4ae0773..3fcaba05458 100644 --- a/test/unit/source/worker.test.js +++ b/test/unit/source/worker.test.js @@ -10,6 +10,7 @@ test('load tile', (t) => { t.test('calls callback on error', (t) => { window.useFakeXMLHttpRequest(); const worker = new Worker(_self); + worker.setProjection(0, {name: 'mercator'}); worker.loadTile(0, { type: 'vector', source: 'source', diff --git a/test/unit/source/worker_tile.test.js b/test/unit/source/worker_tile.test.js index 04375c7826e..9d3b1751daa 100644 --- a/test/unit/source/worker_tile.test.js +++ b/test/unit/source/worker_tile.test.js @@ -3,6 +3,7 @@ import WorkerTile from '../../../src/source/worker_tile.js'; import Wrapper from '../../../src/source/geojson_wrapper.js'; import {OverscaledTileID} from '../../../src/source/tile_id.js'; import StyleLayerIndex from '../../../src/style/style_layer_index.js'; +import {getProjection} from '../../../src/geo/projection/index.js'; function createWorkerTile() { return new WorkerTile({ @@ -12,7 +13,8 @@ function createWorkerTile() { tileSize: 512, source: 'source', tileID: new OverscaledTileID(1, 0, 1, 1, 1), - overscaling: 1 + overscaling: 1, + projection: getProjection({name: 'mercator'}) }); } diff --git a/test/unit/style-spec/feature_filter.test.js b/test/unit/style-spec/feature_filter.test.js index fd8b3f9e72e..fc4189fdb06 100644 --- a/test/unit/style-spec/feature_filter.test.js +++ b/test/unit/style-spec/feature_filter.test.js @@ -1,5 +1,5 @@ import {test} from '../../util/test.js'; -import {default as createFilter, isExpressionFilter} from '../../../src/style-spec/feature_filter/index.js'; +import {default as createFilter, isExpressionFilter, isDynamicFilter, extractStaticFilter} from '../../../src/style-spec/feature_filter/index.js'; import convertFilter from '../../../src/style-spec/feature_filter/convert.js'; import Point from '@mapbox/point-geometry'; @@ -90,6 +90,938 @@ test('filter', t => { t.end(); }); + t.test('dynamic filters', (t) => { + + const DYNAMIC_FILTERS = [ + ["case", + ["<", ["pitch"], 60], true, + ["all", [">=", ["pitch"], 60], ["<", ["distance-from-center"], 2]], true, + false + ], + ["case", + ["<", ["pitch"], 60], ["<", ["get", "filter_rank"], 2], + [">", ["get", "filter_rank"], 4], + ], + ["all", ["<", ["get", "filter_rank"], 2 ], [ "<", ["pitch"], 60]], + ["any", ["<", ["get", "filter_rank"], 2 ], [ "<", ["pitch"], 60]], + ["<", ["pitch"], 60], + ["all", + [ + "<=", + ["get", "filterrank"], + 3 + ], + [ + "match", + ["get", "class"], + "settlement", + [ + "match", + ["get", "worldview"], + ["all", "US"], + true, + false + ], + "disputed_settlement", + [ + "all", + [ + "==", + ["get", "disputed"], + "true" + ], + [ + "match", + ["get", "worldview"], + ["all", "US"], + true, + false + ], + ["all", [">=", ["pitch"], 60], ["<", ["distance-from-center"], 2]] + ], + false + ], + [ + "step", + ["zoom"], + false, + 8, + [ + "<", + ["get", "symbolrank"], + 11 + ], + 10, + [ + "<", + ["get", "symbolrank"], + 12 + ], + 11, + [ + "<", + ["get", "symbolrank"], + 13 + ], + 12, + [ + "<", + ["get", "symbolrank"], + 15 + ], + 13, + [ + ">=", + ["get", "symbolrank"], + 11 + ], + 14, + [ + ">=", + ["get", "symbolrank"], + 13 + ] + ] + ] + ]; + + const STATIC_FILTERS = [ + ["match", + ["get", "class"], + "country", + [ + "match", + ["get", "worldview"], + ["all", "US"], + true, + false + ], + "disputed_country", + [ + "all", + [ + "==", + ["get", "disputed"], + "true" + ], + [ + "match", + ["get", "worldview"], + ["all", "US"], + true, + false + ] + ], + false + ], + ["all", + [ + "<=", + ["get", "filterrank"], + 3 + ], + [ + "match", + ["get", "class"], + "settlement", + [ + "match", + ["get", "worldview"], + ["all", "US"], + true, + false + ], + "disputed_settlement", + [ + "all", + [ + "==", + ["get", "disputed"], + "true" + ], + [ + "match", + ["get", "worldview"], + ["all", "US"], + true, + false + ] + ], + false + ], + [ + "step", + ["zoom"], + false, + 8, + [ + "<", + ["get", "symbolrank"], + 11 + ], + 10, + [ + "<", + ["get", "symbolrank"], + 12 + ], + 11, + [ + "<", + ["get", "symbolrank"], + 13 + ], + 12, + [ + "<", + ["get", "symbolrank"], + 15 + ], + 13, + [ + ">=", + ["get", "symbolrank"], + 11 + ], + 14, + [ + ">=", + ["get", "symbolrank"], + 13 + ] + ] + ], + ["all", + [ + "match", + ["get", "class"], + "settlement_subdivision", + [ + "match", + ["get", "worldview"], + ["all", "US"], + true, + false + ], + "disputed_settlement_subdivision", + [ + "all", + [ + "==", + ["get", "disputed"], + "true" + ], + [ + "match", + ["get", "worldview"], + ["all", "US"], + true, + false + ] + ], + false + ], + [ + "<=", + ["get", "filterrank"], + 4 + ] + ], + ["<=", + ["get", "filterrank"], + [ + "+", + [ + "step", + ["zoom"], + 0, + 16, + 1, + 17, + 2 + ], + 3 + ] + ], + ["<=", ["get", "test_param"], null] + ]; + + t.test('isDynamicFilter', (t) => { + t.test('true', (t) => { + for (const filter of DYNAMIC_FILTERS) { + t.ok(isDynamicFilter(filter), `Filter ${JSON.stringify(filter, null, 2)} should be classified as dynamic.`); + } + t.end(); + }); + + t.test('false', (t) => { + for (const filter of STATIC_FILTERS) { + t.notOk(isDynamicFilter(filter), `Filter ${JSON.stringify(filter, null, 2)} should be classified as static.`); + } + t.end(); + }); + + t.end(); + }); + + t.test('extractStaticFilter', (t) => { + t.test('it lets static filters pass through', (t) => { + for (const filter of STATIC_FILTERS) { + t.equal(extractStaticFilter(filter), filter); + } + t.end(); + }); + + t.test('it collapses dynamic case expressions to any expressions', (t) => { + const testCases = [ + { + dynamic: ["case", + ["<", ["pitch"], 60], true, + ["all", [">=", ["pitch"], 60], ["<", ["distance-from-center"], 2]], true, + false + ], + static: ["any", true, true, false] + }, + { + dynamic: ["case", + ["<", ["pitch"], 60], ["<", ["get", "filter_rank"], 2], + [">", ["get", "filter_rank"], 4], + ], + static: ["any", ["<", ["get", "filter_rank"], 2], [">", ["get", "filter_rank"], 4]] + }, + { + dynamic: ["case", + ["<", ["pitch"], 60], ["<", ["get", "filter_rank"], 2], + ["all", [">=", ["pitch"], 60], ["<", ["distance-from-center"], 2]], [">", ["get", "filter_rank"], 4], + false + ], + static: ["any", ["<", ["get", "filter_rank"], 2], [">", ["get", "filter_rank"], 4], false] + }, + { + dynamic: ["case", + ["<", ["pitch"], 60], ["<", ["get", "filter_rank"], 2], + ["all", [">=", ["pitch"], 60], ["<", ["distance-from-center"], 2]], [">", ["get", "filter_rank"], 4], + ["any", ["==", ["get", "filter_rank"], 2], ["==", ["get", "filter_rank"], 3]] + ], + static: ["any", + ["<", ["get", "filter_rank"], 2], + [">", ["get", "filter_rank"], 4], + ["any", ["==", ["get", "filter_rank"], 2], ["==", ["get", "filter_rank"], 3]] + ] + }, + { + dynamic: ["all", + [ + "match", + ["get", "class"], + "settlement_subdivision", + [ + "match", + ["get", "worldview"], + ["all", "US"], + true, + false + ], + "disputed_settlement_subdivision", + [ + "all", + [ + "==", + ["get", "disputed"], + "true" + ], + [ + "case", + ["<", ["pitch"], 60], ["==", ["get", "worldview"], "US"], + ["all", [">=", ["pitch"], 60], ["<", ["distance-from-center"], 2]], ["==", ["get", "worldview"], "IND"], + ["==", ["get", "worldview"], "INTL"] + ] + ], + false + ], + [ + "<=", + ["get", "filterrank"], + 4 + ] + ], + static: ["all", + [ + "match", + ["get", "class"], + "settlement_subdivision", + [ + "match", + ["get", "worldview"], + ["all", "US"], + true, + false + ], + "disputed_settlement_subdivision", + [ + "all", + [ + "==", + ["get", "disputed"], + "true" + ], + [ + "any", + ["==", ["get", "worldview"], "US"], + ["==", ["get", "worldview"], "IND"], + ["==", ["get", "worldview"], "INTL"] + ] + ], + false + ], + [ + "<=", + ["get", "filterrank"], + 4 + ] + ] + }, + { + dynamic: ["all", + [ + "match", + ["get", "class"], + "settlement_subdivision", + [ + "match", + ["get", "worldview"], + ["all", "US"], + true, + false + ], + "disputed_settlement_subdivision", + [ + "all", + [ + "==", + ["get", "disputed"], + "true" + ], + [ + "case", + ["<", ["pitch"], 60], ["==", ["get", "worldview"], "US"], + ["all", [">=", ["pitch"], 60], ["<", ["distance-from-center"], 2]], ["==", ["get", "worldview"], "IND"], + ["==", ["get", "worldview"], "INTL"] + ] + ], + false + ], + [ + "case", + ["<", ["pitch"], 60], ["<", ["get", "filterrank"], 4], + [">=", ["get", "filterrank"], 5] + ] + ], + static: ["all", + [ + "match", + ["get", "class"], + "settlement_subdivision", + [ + "match", + ["get", "worldview"], + ["all", "US"], + true, + false + ], + "disputed_settlement_subdivision", + [ + "all", + [ + "==", + ["get", "disputed"], + "true" + ], + [ + "any", + ["==", ["get", "worldview"], "US"], + ["==", ["get", "worldview"], "IND"], + ["==", ["get", "worldview"], "INTL"] + ] + ], + false + ], + [ + "any", + ["<", ["get", "filterrank"], 4], + [">=", ["get", "filterrank"], 5] + ] + ] + } + ]; + + for (const testCase of testCases) { + t.deepEqual(extractStaticFilter(testCase.dynamic), testCase.static); + } + + t.end(); + }); + + t.test('it collapses dynamic match expressions to any expressions', (t) => { + const testCases = [ + { + dynamic: ["match", + ["pitch"], + [10, 20, 30], [ "<", ["get", "filterrank"], 2], + [70, 80], [ ">", ["get", "filterrank"], 5], + ["all", [ ">", ["get", "filterrank"], 2], [ "<", ["get", "filterrank"], 5]] + ], + static: ["any", + [ "<", ["get", "filterrank"], 2], + [ ">", ["get", "filterrank"], 5], + ["all", [ ">", ["get", "filterrank"], 2], [ "<", ["get", "filterrank"], 5]] + ] + }, + { + dynamic: ["all", + [ + "match", + ["get", "class"], + "settlement_subdivision", + [ + "match", + ["get", "worldview"], + ["all", "US"], + true, + false + ], + "disputed_settlement_subdivision", + [ + "all", + [ + "match", + ["distance-from-center"], + [1, 2], ["==", ["get", "worldview"], "US"], + [4, 5], ["==", ["get", "worldview"], "IND"], + ["==", ["get", "worldview"], "INTL"] + ], + [ + "case", + ["<", ["pitch"], 60], ["==", ["get", "worldview"], "US"], + ["all", [">=", ["pitch"], 60], ["<", ["distance-from-center"], 2]], ["==", ["get", "worldview"], "IND"], + ["==", ["get", "worldview"], "INTL"] + ] + ], + false + ], + [ + "<=", + ["get", "filterrank"], + 4 + ] + ], + static: ["all", + [ + "match", + ["get", "class"], + "settlement_subdivision", + [ + "match", + ["get", "worldview"], + ["all", "US"], + true, + false + ], + "disputed_settlement_subdivision", + [ + "all", + [ + "any", + ["==", ["get", "worldview"], "US"], + ["==", ["get", "worldview"], "IND"], + ["==", ["get", "worldview"], "INTL"] + ], + [ + "any", + ["==", ["get", "worldview"], "US"], + ["==", ["get", "worldview"], "IND"], + ["==", ["get", "worldview"], "INTL"] + ] + ], + false + ], + [ + "<=", + ["get", "filterrank"], + 4 + ] + ] + } + ]; + + for (const testCase of testCases) { + t.deepEqual(extractStaticFilter(testCase.dynamic), testCase.static); + } + + t.end(); + }); + + t.test('it collapses dynamic step expressions to any expressions', (t) => { + const testCases = [ + { + dynamic: [ + "all", + [ + "<=", + ["get", "filterrank"], + 3 + ], + [ + "match", + ["get", "class"], + "settlement", + [ + "match", + ["get", "worldview"], + ["all", "US"], + true, + false + ], + "disputed_settlement", + [ + "all", + [ + "==", + ["get", "disputed"], + "true" + ], + [ + "match", + ["get", "worldview"], + ["all", "US"], + true, + false + ] + ], + false + ], + [ + "step", + ["pitch"], + true, + 10, + [ + ">=", + ["get", "symbolrank"], + 10 + ], + 20, + [ + ">=", + ["get", "symbolrank"], + 20 + ], + 30, + [ + ">=", + ["get", "symbolrank"], + 30 + ], + 40, + [ + ">=", + ["get", "symbolrank"], + 40 + ], + 50, + [ + ">=", + ["get", "symbolrank"], + 50 + ], + 60, + [ + ">=", + ["get", "symbolrank"], + 60 + ] + ] + ], + static: [ + "all", + [ + "<=", + ["get", "filterrank"], + 3 + ], + [ + "match", + ["get", "class"], + "settlement", + [ + "match", + ["get", "worldview"], + ["all", "US"], + true, + false + ], + "disputed_settlement", + [ + "all", + [ + "==", + ["get", "disputed"], + "true" + ], + [ + "match", + ["get", "worldview"], + ["all", "US"], + true, + false + ] + ], + false + ], + [ + "any", + true, + [ + ">=", + ["get", "symbolrank"], + 10 + ], + [ + ">=", + ["get", "symbolrank"], + 20 + ], + [ + ">=", + ["get", "symbolrank"], + 30 + ], + [ + ">=", + ["get", "symbolrank"], + 40 + ], + [ + ">=", + ["get", "symbolrank"], + 50 + ], + [ + ">=", + ["get", "symbolrank"], + 60 + ] + ] + ] + } + ]; + + for (const testCase of testCases) { + t.deepEqual(extractStaticFilter(testCase.dynamic), testCase.static); + } + + t.end(); + + }); + + t.test('it collapses dynamic conditionals to true', (t) => { + const testCases = [ + { + dynamic: ["<", ["pitch"], 60], + static: true + }, + { + dynamic: ["all", ["<", ["pitch"], 60], ["<", ["distance-from-center"], 4]], + static: ["all", true, true] + }, + { + dynamic: ["all", ["<", ["+", ["*", ["pitch"], 2], 5], 60], ["<", ["+", ["distance-from-center"], 1], 4]], + static: ["all", true, true] + }, + { + dynamic: [ + "all", + [ + "<=", + ["get", "filterrank"], + 3 + ], + [ + "match", + ["get", "class"], + "settlement", + [ + "match", + ["get", "worldview"], + ["all", "US"], + true, + false + ], + "disputed_settlement", + [ + "all", + [ + "==", + ["get", "disputed"], + "true" + ], + [ + "match", + ["get", "worldview"], + ["all", "US"], + true, + false + ] + ], + false + ], + [ + "step", + ["zoom"], + true, + 8, + [ + ">=", + ["get", "symbolrank"], + 11 + ], + 10, + [ + ">=", + ["get", "symbolrank"], + 12 + ], + 11, + [ + ">=", + ["get", "symbolrank"], + 13 + ], + 12, + [ + ">=", + ["get", "symbolrank"], + 15 + ], + 13, + [ + ">=", + ["get", "symbolrank"], + 11 + ], + 14, + [ + ">=", + ["get", "symbolrank"], + 13 + ] + ], + [ + "<=", + ["pitch"], + 60 + ], + [ + "<=", + ["distance-from-center"], + 2 + ] + ], + static: [ + "all", + [ + "<=", + ["get", "filterrank"], + 3 + ], + [ + "match", + ["get", "class"], + "settlement", + [ + "match", + ["get", "worldview"], + ["all", "US"], + true, + false + ], + "disputed_settlement", + [ + "all", + [ + "==", + ["get", "disputed"], + "true" + ], + [ + "match", + ["get", "worldview"], + ["all", "US"], + true, + false + ] + ], + false + ], + [ + "step", + ["zoom"], + true, + 8, + [ + ">=", + ["get", "symbolrank"], + 11 + ], + 10, + [ + ">=", + ["get", "symbolrank"], + 12 + ], + 11, + [ + ">=", + ["get", "symbolrank"], + 13 + ], + 12, + [ + ">=", + ["get", "symbolrank"], + 15 + ], + 13, + [ + ">=", + ["get", "symbolrank"], + 11 + ], + 14, + [ + ">=", + ["get", "symbolrank"], + 13 + ] + ], + true, + true + ] + } + ]; + for (const testCase of testCases) { + t.deepEqual(extractStaticFilter(testCase.dynamic), testCase.static); + } + + t.end(); + }); + + t.end(); + }); + + t.end(); + }); + legacyFilterTests(t, createFilter); t.end(); diff --git a/test/unit/style-spec/fixture/bad-dasharray.output.json b/test/unit/style-spec/fixture/bad-dasharray.output.json index b7b7dcf0077..9b9529d9417 100644 --- a/test/unit/style-spec/fixture/bad-dasharray.output.json +++ b/test/unit/style-spec/fixture/bad-dasharray.output.json @@ -1,10 +1,10 @@ [ { - "line": 16, - "message": "layers[0].paint.line-dasharray[1]: -2 is less than the minimum value 0" + "message": "layers[0].paint.line-dasharray[1]: -2 is less than the minimum value 0", + "line": 16 }, { - "line": 16, - "message": "layers[0].paint.line-dasharray[2]: -1 is less than the minimum value 0" + "message": "layers[0].paint.line-dasharray[2]: -1 is less than the minimum value 0", + "line": 16 } ] \ No newline at end of file diff --git a/test/unit/style-spec/fixture/bad-sky.output.json b/test/unit/style-spec/fixture/bad-sky.output.json index f6a5e63fff2..4e3ca7c81f0 100644 --- a/test/unit/style-spec/fixture/bad-sky.output.json +++ b/test/unit/style-spec/fixture/bad-sky.output.json @@ -1,18 +1,18 @@ [ { - "line": 23, - "message": "layers[1].paint.sky-atmosphere-sun[0]: -1 is less than the minimum value 0" + "message": "layers[1].paint.sky-atmosphere-sun[0]: -1 is less than the minimum value 0", + "line": 23 }, { - "line": 23, - "message": "layers[1].paint.sky-atmosphere-sun[1]: 181 is greater than the maximum value 180" + "message": "layers[1].paint.sky-atmosphere-sun[1]: 181 is greater than the maximum value 180", + "line": 23 }, { - "line": 39, - "message": "layers[3].paint.sky-gradient-center[0]: 361 is greater than the maximum value 360" + "message": "layers[3].paint.sky-gradient-center[0]: 361 is greater than the maximum value 360", + "line": 39 }, { - "line": 39, - "message": "layers[3].paint.sky-gradient-center[1]: -1 is less than the minimum value 0" + "message": "layers[3].paint.sky-gradient-center[1]: -1 is less than the minimum value 0", + "line": 39 } ] \ No newline at end of file diff --git a/test/unit/style-spec/fixture/filters-dynamic-distance.input.json b/test/unit/style-spec/fixture/filters-dynamic-distance.input.json new file mode 100644 index 00000000000..5e8844ad950 --- /dev/null +++ b/test/unit/style-spec/fixture/filters-dynamic-distance.input.json @@ -0,0 +1,53 @@ +{ + "version": 8, + "sources": { + "source": { + "type": "vector", + "url": "mapbox://mapbox.mapbox-streets-v5" + } + }, + "layers": [ + { + "id": "symbol-supported", + "type": "symbol", + "source": "source", + "source-layer": "source-layer", + "filter": ["any", ["<", ["get", "filter_rank"], 2 ], [ "<", ["distance-from-center"], 60]] + }, + { + "id": "fill-not-supported", + "type": "fill", + "source": "source", + "source-layer": "source-layer", + "filter": ["any", ["<", ["get", "filter_rank"], 2 ], [ "<", ["distance-from-center"], 60]] + }, + { + "id": "line-not-supported", + "type": "line", + "source": "source", + "source-layer": "source-layer", + "filter": ["any", ["<", ["get", "filter_rank"], 2 ], [ "<", ["distance-from-center"], 60]] + }, + { + "id": "circle-not-supported", + "type": "circle", + "source": "source", + "source-layer": "source-layer", + "filter": ["any", ["<", ["get", "filter_rank"], 2 ], [ "<", ["distance-from-center"], 60]] + }, + { + "id": "fill-extrusion-not-supported", + "type": "fill-extrusion", + "source": "source", + "source-layer": "source-layer", + "filter": ["any", ["<", ["get", "filter_rank"], 2 ], [ "<", ["distance-from-center"], 60]] + }, + { + "id": "heatmap-not-supported", + "type": "heatmap", + "source": "source", + "source-layer": "source-layer", + "filter": ["any", ["<", ["get", "filter_rank"], 2 ], [ "<", ["distance-from-center"], 60]] + } + ] +} diff --git a/test/unit/style-spec/fixture/filters-dynamic-distance.output-api-supported.json b/test/unit/style-spec/fixture/filters-dynamic-distance.output-api-supported.json new file mode 100644 index 00000000000..f449526e74f --- /dev/null +++ b/test/unit/style-spec/fixture/filters-dynamic-distance.output-api-supported.json @@ -0,0 +1,22 @@ +[ + { + "message": "layers[1].filter: [\"distance-from-center\"] expression is not supported in a filter for a fill layer with id: fill-not-supported", + "line": 22 + }, + { + "message": "layers[2].filter: [\"distance-from-center\"] expression is not supported in a filter for a line layer with id: line-not-supported", + "line": 29 + }, + { + "message": "layers[3].filter: [\"distance-from-center\"] expression is not supported in a filter for a circle layer with id: circle-not-supported", + "line": 36 + }, + { + "message": "layers[4].filter: [\"distance-from-center\"] expression is not supported in a filter for a fill-extrusion layer with id: fill-extrusion-not-supported", + "line": 43 + }, + { + "message": "layers[5].filter: [\"distance-from-center\"] expression is not supported in a filter for a heatmap layer with id: heatmap-not-supported", + "line": 50 + } +] \ No newline at end of file diff --git a/test/unit/style-spec/fixture/filters-dynamic-distance.output.json b/test/unit/style-spec/fixture/filters-dynamic-distance.output.json new file mode 100644 index 00000000000..f449526e74f --- /dev/null +++ b/test/unit/style-spec/fixture/filters-dynamic-distance.output.json @@ -0,0 +1,22 @@ +[ + { + "message": "layers[1].filter: [\"distance-from-center\"] expression is not supported in a filter for a fill layer with id: fill-not-supported", + "line": 22 + }, + { + "message": "layers[2].filter: [\"distance-from-center\"] expression is not supported in a filter for a line layer with id: line-not-supported", + "line": 29 + }, + { + "message": "layers[3].filter: [\"distance-from-center\"] expression is not supported in a filter for a circle layer with id: circle-not-supported", + "line": 36 + }, + { + "message": "layers[4].filter: [\"distance-from-center\"] expression is not supported in a filter for a fill-extrusion layer with id: fill-extrusion-not-supported", + "line": 43 + }, + { + "message": "layers[5].filter: [\"distance-from-center\"] expression is not supported in a filter for a heatmap layer with id: heatmap-not-supported", + "line": 50 + } +] \ No newline at end of file diff --git a/test/unit/style-spec/fixture/filters-dynamic-pitch.input.json b/test/unit/style-spec/fixture/filters-dynamic-pitch.input.json new file mode 100644 index 00000000000..026a57cc6d1 --- /dev/null +++ b/test/unit/style-spec/fixture/filters-dynamic-pitch.input.json @@ -0,0 +1,53 @@ +{ + "version": 8, + "sources": { + "source": { + "type": "vector", + "url": "mapbox://mapbox.mapbox-streets-v5" + } + }, + "layers": [ + { + "id": "symbol-supported", + "type": "symbol", + "source": "source", + "source-layer": "source-layer", + "filter": ["any", ["<", ["get", "filter_rank"], 2 ], [ "<", ["pitch"], 60]] + }, + { + "id": "fill-not-supported", + "type": "fill", + "source": "source", + "source-layer": "source-layer", + "filter": ["any", ["<", ["get", "filter_rank"], 2 ], [ "<", ["pitch"], 60]] + }, + { + "id": "line-not-supported", + "type": "line", + "source": "source", + "source-layer": "source-layer", + "filter": ["any", ["<", ["get", "filter_rank"], 2 ], [ "<", ["pitch"], 60]] + }, + { + "id": "circle-not-supported", + "type": "circle", + "source": "source", + "source-layer": "source-layer", + "filter": ["any", ["<", ["get", "filter_rank"], 2 ], [ "<", ["pitch"], 60]] + }, + { + "id": "fill-extrusion-not-supported", + "type": "fill-extrusion", + "source": "source", + "source-layer": "source-layer", + "filter": ["any", ["<", ["get", "filter_rank"], 2 ], [ "<", ["pitch"], 60]] + }, + { + "id": "heatmap-not-supported", + "type": "heatmap", + "source": "source", + "source-layer": "source-layer", + "filter": ["any", ["<", ["get", "filter_rank"], 2 ], [ "<", ["pitch"], 60]] + } + ] +} diff --git a/test/unit/style-spec/fixture/filters-dynamic-pitch.output-api-supported.json b/test/unit/style-spec/fixture/filters-dynamic-pitch.output-api-supported.json new file mode 100644 index 00000000000..23ee46fb204 --- /dev/null +++ b/test/unit/style-spec/fixture/filters-dynamic-pitch.output-api-supported.json @@ -0,0 +1,22 @@ +[ + { + "message": "layers[1].filter: [\"pitch\"] expression is not supported in a filter for a fill layer with id: fill-not-supported", + "line": 22 + }, + { + "message": "layers[2].filter: [\"pitch\"] expression is not supported in a filter for a line layer with id: line-not-supported", + "line": 29 + }, + { + "message": "layers[3].filter: [\"pitch\"] expression is not supported in a filter for a circle layer with id: circle-not-supported", + "line": 36 + }, + { + "message": "layers[4].filter: [\"pitch\"] expression is not supported in a filter for a fill-extrusion layer with id: fill-extrusion-not-supported", + "line": 43 + }, + { + "message": "layers[5].filter: [\"pitch\"] expression is not supported in a filter for a heatmap layer with id: heatmap-not-supported", + "line": 50 + } +] \ No newline at end of file diff --git a/test/unit/style-spec/fixture/filters-dynamic-pitch.output.json b/test/unit/style-spec/fixture/filters-dynamic-pitch.output.json new file mode 100644 index 00000000000..23ee46fb204 --- /dev/null +++ b/test/unit/style-spec/fixture/filters-dynamic-pitch.output.json @@ -0,0 +1,22 @@ +[ + { + "message": "layers[1].filter: [\"pitch\"] expression is not supported in a filter for a fill layer with id: fill-not-supported", + "line": 22 + }, + { + "message": "layers[2].filter: [\"pitch\"] expression is not supported in a filter for a line layer with id: line-not-supported", + "line": 29 + }, + { + "message": "layers[3].filter: [\"pitch\"] expression is not supported in a filter for a circle layer with id: circle-not-supported", + "line": 36 + }, + { + "message": "layers[4].filter: [\"pitch\"] expression is not supported in a filter for a fill-extrusion layer with id: fill-extrusion-not-supported", + "line": 43 + }, + { + "message": "layers[5].filter: [\"pitch\"] expression is not supported in a filter for a heatmap layer with id: heatmap-not-supported", + "line": 50 + } +] \ No newline at end of file diff --git a/test/unit/style-spec/fixture/filters.output-api-supported.json b/test/unit/style-spec/fixture/filters.output-api-supported.json index c47ac48984e..4b32d93c15d 100644 --- a/test/unit/style-spec/fixture/filters.output-api-supported.json +++ b/test/unit/style-spec/fixture/filters.output-api-supported.json @@ -56,7 +56,7 @@ "line": 152 }, { - "message": "layers[15].filter: \"feature-state\" data expressions are not supported with filters.", + "message": "layers[15].filter: [\"feature-state\"] expression is not supported in a filter for a line layer with id: filter expressions with feature-state", "line": 159 } ] \ No newline at end of file diff --git a/test/unit/style-spec/fixture/filters.output.json b/test/unit/style-spec/fixture/filters.output.json index c47ac48984e..4b32d93c15d 100644 --- a/test/unit/style-spec/fixture/filters.output.json +++ b/test/unit/style-spec/fixture/filters.output.json @@ -56,7 +56,7 @@ "line": 152 }, { - "message": "layers[15].filter: \"feature-state\" data expressions are not supported with filters.", + "message": "layers[15].filter: [\"feature-state\"] expression is not supported in a filter for a line layer with id: filter expressions with feature-state", "line": 159 } ] \ No newline at end of file diff --git a/test/unit/style-spec/fixture/fog-invalid-input.output.json b/test/unit/style-spec/fixture/fog-invalid-input.output.json index 59542a3f2d2..514516df2b7 100644 --- a/test/unit/style-spec/fixture/fog-invalid-input.output.json +++ b/test/unit/style-spec/fixture/fog-invalid-input.output.json @@ -1,14 +1,14 @@ [ { - "line": 11, - "message": "range[0]: -100 is less than the minimum value -20" + "message": "range[0]: -100 is less than the minimum value -20", + "line": 11 }, { - "line": 11, - "message": "range[1]: -80 is less than the minimum value -20" + "message": "range[1]: -80 is less than the minimum value -20", + "line": 11 }, { - "line": 12, - "message": "horizon-blend: -4 is less than the minimum value 0" + "message": "horizon-blend: -4 is less than the minimum value 0", + "line": 12 } -] +] \ No newline at end of file diff --git a/test/unit/style-spec/fixture/numbers.output.json b/test/unit/style-spec/fixture/numbers.output.json index 8fadd34d7ee..04c23b820c5 100644 --- a/test/unit/style-spec/fixture/numbers.output.json +++ b/test/unit/style-spec/fixture/numbers.output.json @@ -1,21 +1,21 @@ [ - { - "message": "layers[2].paint.circle-radius: -1 is less than the minimum value 0", - "line": 42 - }, - { - "message": "layers[3].paint.circle-radius: number expected, null found" - }, - { - "message": "layers[4].paint.circle-radius: missing required property \"stops\"", - "line": 58 - }, - { - "message": "layers[5].paint.circle-radius: number expected, array found", - "line": 66 - }, - { - "message": "layers[6].paint.circle-radius: number expected, boolean found", - "line": 74 - } -] + { + "message": "layers[2].paint.circle-radius: -1 is less than the minimum value 0", + "line": 42 + }, + { + "message": "layers[3].paint.circle-radius: number expected, null found" + }, + { + "message": "layers[4].paint.circle-radius: missing required property \"stops\"", + "line": 58 + }, + { + "message": "layers[5].paint.circle-radius: number expected, array found", + "line": 66 + }, + { + "message": "layers[6].paint.circle-radius: number expected, boolean found", + "line": 74 + } +] \ No newline at end of file diff --git a/test/unit/style-spec/spec.test.js b/test/unit/style-spec/spec.test.js index 0d3e12c3b38..4df03298ad3 100644 --- a/test/unit/style-spec/spec.test.js +++ b/test/unit/style-spec/spec.test.js @@ -163,7 +163,16 @@ function validSchema(k, t, obj, ref, version, kind) { t.ok(ref['property-type'][obj['property-type']], `${k}.expression: property-type: ${obj['property-type']}`); t.equal('boolean', typeof expression.interpolated, `${k}.expression.interpolated.required (boolean)`); t.equal(true, Array.isArray(expression.parameters), `${k}.expression.parameters array`); - if (obj['property-type'] !== 'color-ramp') t.equal(true, expression.parameters.every(k => k === 'zoom' || k === 'feature' || k === 'feature-state')); + if (obj['property-type'] !== 'color-ramp') { + t.equal(true, expression.parameters.every(k => { + return k === 'zoom' || + k === 'feature' || + k === 'feature-state' || + k === 'pitch' || + k === 'distance-from-center'; + }) + ); + } } // schema key required checks diff --git a/test/unit/terrain/terrain.test.js b/test/unit/terrain/terrain.test.js index d246283f05a..40dacb0b274 100644 --- a/test/unit/terrain/terrain.test.js +++ b/test/unit/terrain/terrain.test.js @@ -3,7 +3,7 @@ import {extend} from '../../../src/util/util.js'; import {createMap} from '../../util/index.js'; import DEMData from '../../../src/data/dem_data.js'; import {RGBAImage} from '../../../src/util/image.js'; -import MercatorCoordinate from '../../../src/geo/mercator_coordinate.js'; +import MercatorCoordinate, {MAX_MERCATOR_LATITUDE} from '../../../src/geo/mercator_coordinate.js'; import window from '../../../src/util/window.js'; import {OverscaledTileID} from '../../../src/source/tile_id.js'; import styleSpec from '../../../src/style-spec/reference/latest.js'; @@ -369,6 +369,9 @@ test('Elevation', (t) => { }; const map = createMap(t, { style: extend(createStyle(), { + projection: { + name: 'mercator' + }, sources: { trace: { type: 'geojson', @@ -1511,7 +1514,7 @@ test('terrain getBounds', (t) => { map.once('render', () => { t.ok(map.transform.elevation); const bounds = map.getBounds(); - t.same(bounds.getNorth().toFixed(6), map.transform.maxValidLatitude); + t.same(bounds.getNorth().toFixed(6), MAX_MERCATOR_LATITUDE); t.same( toFixed(bounds.toArray()), toFixed([[ -23.3484820899, 77.6464759596 ], [ 23.3484820899, 85.0511287798 ]]) @@ -1521,7 +1524,7 @@ test('terrain getBounds', (t) => { map.setCenter({lng: 0, lat: -90}); const sBounds = map.getBounds(); - t.same(sBounds.getSouth().toFixed(6), -map.transform.maxValidLatitude); + t.same(sBounds.getSouth().toFixed(6), -MAX_MERCATOR_LATITUDE); t.same( toFixed(sBounds.toArray()), toFixed([[ -23.3484820899, -85.0511287798 ], [ 23.3484820899, -77.6464759596]]) diff --git a/test/unit/ui/control/attribution.test.js b/test/unit/ui/control/attribution.test.js index 76891d6129c..4aa9d0364cf 100644 --- a/test/unit/ui/control/attribution.test.js +++ b/test/unit/ui/control/attribution.test.js @@ -37,6 +37,7 @@ test('AttributionControl appears in the position specified by the position optio test('AttributionControl appears in compact mode if compact option is used', (t) => { const map = createMap(t); + Object.defineProperty(map.getContainer(), 'getBoundingClientRect', {value: () => ({height: 200, width: 700})}); Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 700, configurable: true}); let attributionControl = new AttributionControl({ @@ -49,6 +50,7 @@ test('AttributionControl appears in compact mode if compact option is used', (t) t.equal(container.querySelectorAll('.mapboxgl-ctrl-attrib.mapboxgl-compact').length, 1); map.removeControl(attributionControl); + Object.defineProperty(map.getContainer(), 'getBoundingClientRect', {value: () => ({height: 200, width: 600})}); Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 600, configurable: true}); attributionControl = new AttributionControl({ compact: false @@ -61,6 +63,7 @@ test('AttributionControl appears in compact mode if compact option is used', (t) test('AttributionControl appears in compact mode if container is less then 640 pixel wide', (t) => { const map = createMap(t); + Object.defineProperty(map.getContainer(), 'getBoundingClientRect', {value: () => ({height: 200, width: 700})}); Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 700, configurable: true}); map.addControl(new AttributionControl()); @@ -68,6 +71,7 @@ test('AttributionControl appears in compact mode if container is less then 640 p t.equal(container.querySelectorAll('.mapboxgl-ctrl-attrib:not(.mapboxgl-compact)').length, 1); + Object.defineProperty(map.getContainer(), 'getBoundingClientRect', {value: () => ({height: 200, width: 600})}); Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 600, configurable: true}); map.resize(); diff --git a/test/unit/ui/control/logo.test.js b/test/unit/ui/control/logo.test.js index ea0c77f7825..a3491f55170 100644 --- a/test/unit/ui/control/logo.test.js +++ b/test/unit/ui/control/logo.test.js @@ -104,12 +104,16 @@ test('LogoControl appears in compact mode if container is less then 250 pixel wi const map = createMap(t); const container = map.getContainer(); + Object.defineProperty(map.getContainer(), 'getBoundingClientRect', {value: () => ({height: 200, width: 255})}); Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 255, configurable: true}); map.resize(); + t.equal(container.querySelectorAll('.mapboxgl-ctrl-logo:not(.mapboxgl-compact)').length, 1); + Object.defineProperty(map.getContainer(), 'getBoundingClientRect', {value: () => ({height: 200, width: 245})}); Object.defineProperty(map.getCanvasContainer(), 'offsetWidth', {value: 245, configurable: true}); map.resize(); + t.equal(container.querySelectorAll('.mapboxgl-ctrl-logo.mapboxgl-compact').length, 1); t.end(); diff --git a/test/unit/ui/handler/drag_pan.test.js b/test/unit/ui/handler/drag_pan.test.js index 1a476a32a47..98ea491a2ca 100644 --- a/test/unit/ui/handler/drag_pan.test.js +++ b/test/unit/ui/handler/drag_pan.test.js @@ -151,13 +151,15 @@ test('DragPanHandler ends a mouse-triggered drag if the window blurs', (t) => { map._renderTaskQueue.run(); simulate.blur(window); + map._renderTaskQueue.run(); + t.equal(dragend.callCount, 1); map.remove(); t.end(); }); -test('DragPanHandler ends a touch-triggered drag if the window blurs', (t) => { +test('DragPanHandler does not end a touch-triggered drag if the window blurs', (t) => { const map = createMap(t); const target = map.getCanvas(); @@ -171,7 +173,40 @@ test('DragPanHandler ends a touch-triggered drag if the window blurs', (t) => { map._renderTaskQueue.run(); simulate.blur(window); + map._renderTaskQueue.run(); + + t.equal(dragend.callCount, 0); + + map.remove(); + t.end(); +}); + +test('DragPanHandler does not end a touch-triggered drag if the window resizes', (t) => { + const map = createMap(t); + const target = map.getCanvas(); + + const dragend = t.spy(); + map.on('dragend', dragend); + + const drag = t.spy(); + map.on('drag', drag); + + simulate.touchstart(map.getCanvas(), {touches: [{target, clientX: 0, clientY: 0}]}); + map._renderTaskQueue.run(); + + simulate.touchmove(map.getCanvas(), {touches: [{target, clientX: 10, clientY: 10}]}); + map._renderTaskQueue.run(); + + map.resize(); + + simulate.touchmove(map.getCanvas(), {touches: [{target, clientX: 20, clientY: 10}]}); + map._renderTaskQueue.run(); + + simulate.touchend(map.getCanvas()); + map._renderTaskQueue.run(); + t.equal(dragend.callCount, 1); + t.equal(drag.callCount, 2); map.remove(); t.end(); diff --git a/test/unit/ui/handler/drag_rotate.test.js b/test/unit/ui/handler/drag_rotate.test.js index 4fe68a46fd2..f12c0eba8bb 100644 --- a/test/unit/ui/handler/drag_rotate.test.js +++ b/test/unit/ui/handler/drag_rotate.test.js @@ -492,6 +492,8 @@ test('DragRotateHandler ends rotation if the window blurs (#3389)', (t) => { t.equal(rotate.callCount, 1); simulate.blur(window); + map._renderTaskQueue.run(); + t.equal(rotateend.callCount, 1); map.remove(); diff --git a/test/unit/ui/handler/scroll_zoom.test.js b/test/unit/ui/handler/scroll_zoom.test.js index 3fabc22e563..bad0a6afa2b 100644 --- a/test/unit/ui/handler/scroll_zoom.test.js +++ b/test/unit/ui/handler/scroll_zoom.test.js @@ -24,12 +24,12 @@ function createMap(t) { }); } -function createMapWithGestureHandling(t) { +function createMapWithCooperativeGestures(t) { t.stub(Map.prototype, '_detectMissingCSS'); t.stub(Map.prototype, '_authenticate'); return new Map({ container: DOM.create('div', '', window.document.body), - gestureHandling: true + cooperativeGestures: true }); } @@ -376,15 +376,15 @@ test('ScrollZoomHandler', (t) => { t.end(); }); -test('When gestureHandling option is set to true, a .mapboxgl-scroll-zoom-blocker element is added to map', (t) => { - const map = createMapWithGestureHandling(t); +test('When cooperativeGestures option is set to true, a .mapboxgl-scroll-zoom-blocker element is added to map', (t) => { + const map = createMapWithCooperativeGestures(t); t.equal(map.getContainer().querySelectorAll('.mapboxgl-scroll-zoom-blocker').length, 1); t.end(); }); -test('When gestureHandling option is set to true, scroll zoom is prevented when the ctrl key or meta key is not pressed during wheel event', (t) => { - const map = createMapWithGestureHandling(t); +test('When cooperativeGestures option is set to true, scroll zoom is prevented when the ctrl key or meta key is not pressed during wheel event', (t) => { + const map = createMapWithCooperativeGestures(t); const zoomSpy = t.spy(); map.on('zoom', zoomSpy); @@ -395,8 +395,8 @@ test('When gestureHandling option is set to true, scroll zoom is prevented when t.end(); }); -test('When gestureHandling option is set to true, scroll zoom is activated when ctrl key is pressed during wheel event', (t) => { - const map = createMapWithGestureHandling(t); +test('When cooperativeGestures option is set to true, scroll zoom is activated when ctrl key is pressed during wheel event', (t) => { + const map = createMapWithCooperativeGestures(t); const zoomSpy = t.spy(); map.on('zoom', zoomSpy); @@ -409,8 +409,8 @@ test('When gestureHandling option is set to true, scroll zoom is activated when t.end(); }); -test('When gestureHandling option is set to true, scroll zoom is activated when meta key is pressed during wheel event', (t) => { - const map = createMapWithGestureHandling(t); +test('When cooperativeGestures option is set to true, scroll zoom is activated when meta key is pressed during wheel event', (t) => { + const map = createMapWithCooperativeGestures(t); const zoomSpy = t.spy(); map.on('zoom', zoomSpy); @@ -424,7 +424,7 @@ test('When gestureHandling option is set to true, scroll zoom is activated when }); test('Disabling scrollZoom removes scroll zoom blocker container', (t) => { - const map = createMapWithGestureHandling(t); + const map = createMapWithCooperativeGestures(t); map.scrollZoom.disable(); diff --git a/test/unit/ui/handler/touch_pan.test.js b/test/unit/ui/handler/touch_pan.test.js new file mode 100644 index 00000000000..cd579261fac --- /dev/null +++ b/test/unit/ui/handler/touch_pan.test.js @@ -0,0 +1,64 @@ +import {test} from '../../../util/test.js'; +import window from '../../../../src/util/window.js'; +import Map from '../../../../src/ui/map.js'; +import DOM from '../../../../src/util/dom.js'; +import simulate from '../../../util/simulate_interaction.js'; + +function createMapWithCooperativeGestures(t) { + t.stub(Map.prototype, '_detectMissingCSS'); + t.stub(Map.prototype, '_authenticate'); + return new Map({ + container: DOM.create('div', '', window.document.body), + cooperativeGestures: true + }); +} + +test('If cooperativeGestures option is set to true, a `.mapboxgl-touch-pan-blocker` element is added to map', (t) => { + const map = createMapWithCooperativeGestures(t); + + t.equal(map.getContainer().querySelectorAll('.mapboxgl-touch-pan-blocker').length, 1); + t.end(); +}); + +test('If cooperativeGestures option is set to true, touch pan is prevented when one finger is used to pan', (t) => { + const map = createMapWithCooperativeGestures(t); + const target = map.getCanvas(); + + const moveSpy = t.spy(); + map.on('move', moveSpy); + + simulate.touchstart(map.getCanvas(), {touches: [{target, identifier: 1, clientX: 0, clientY: -50}]}); + map._renderTaskQueue.run(); + + simulate.touchmove(map.getCanvas(), {touches: [{target, identifier: 1, clientX: 0, clientY: -40}]}); + map._renderTaskQueue.run(); + + t.equal(moveSpy.callCount, 0); + t.end(); +}); + +test('If cooperativeGestures option is set to true, touch pan is triggered when two fingers are used to pan', (t) => { + const map = createMapWithCooperativeGestures(t); + const target = map.getCanvas(); + + const moveSpy = t.spy(); + map.on('move', moveSpy); + + simulate.touchstart(map.getCanvas(), {touches: [{target, identifier: 1, clientX: 0, clientY: -40}, {target, identifier: 2, clientX: 0, clientY: -30}]}); + map._renderTaskQueue.run(); + + simulate.touchmove(map.getCanvas(), {touches: [{target, identifier: 1, clientX: 0, clientY: -50}, {target, identifier: 2, clientX: 0, clientY: -40}]}); + map._renderTaskQueue.run(); + + t.equal(moveSpy.callCount, 1); + t.end(); +}); + +test('Disabling touch pan removes the `.mapboxgl-touch-pan-blocker` element', (t) => { + const map = createMapWithCooperativeGestures(t); + + map.handlers._handlersById.touchPan.disable(); + + t.equal(map.getContainer().querySelectorAll('.mapboxgl-touch-pan-blocker').length, 0); + t.end(); +}); diff --git a/test/unit/ui/map.test.js b/test/unit/ui/map.test.js index 484fc3a4f60..e38a29b03fb 100755 --- a/test/unit/ui/map.test.js +++ b/test/unit/ui/map.test.js @@ -4,6 +4,7 @@ import window from '../../../src/util/window.js'; import Map from '../../../src/ui/map.js'; import {createMap} from '../../util/index.js'; import LngLat from '../../../src/geo/lng_lat.js'; +import LngLatBounds from '../../../src/geo/lng_lat_bounds.js'; import Tile from '../../../src/source/tile.js'; import {OverscaledTileID} from '../../../src/source/tile_id.js'; import {Event, ErrorEvent} from '../../../src/util/evented.js'; @@ -11,6 +12,7 @@ import simulate from '../../util/simulate_interaction.js'; import {fixedLngLat, fixedNum} from '../../util/fixed.js'; import Fog from '../../../src/style/fog.js'; import Color from '../../../src/style-spec/util/color.js'; +import {MAX_MERCATOR_LATITUDE} from '../../../src/geo/mercator_coordinate.js'; function createStyleSource() { return { @@ -234,10 +236,10 @@ test('Map', (t) => { t.stub(Map.prototype, '_detectMissingCSS'); t.stub(Map.prototype, '_authenticate'); const map = new Map({container: window.document.createElement('div'), testMode: true}); - map.transform.lngRange = [-120, 140]; - map.transform.latRange = [-60, 80]; + + map.transform.setMaxBounds(LngLatBounds.convert([-120, -60, 140, 80])); map.transform.resize(600, 400); - t.equal(map.transform.zoom, 0.6983039737971012, 'map transform is constrained'); + t.ok(map.transform.zoom, 0.698303973797101, 'map transform is constrained'); t.ok(map.transform.unmodified, 'map transform is not modified'); map.setStyle(createStyle()); map.on('style.load', () => { @@ -764,10 +766,42 @@ test('Map', (t) => { t.end(); }); + t.test('does nothing if container size is the same', (t) => { + const map = createMap(t); + + t.spy(map.transform, 'resize'); + t.spy(map.painter, 'resize'); + + map.resize(); + + t.notOk(map.transform.resize.called); + t.notOk(map.painter.resize.called); + + t.end(); + }); + + t.test('does not call stop on resize', (t) => { + const map = createMap(t); + + Object.defineProperty(map.getContainer(), 'getBoundingClientRect', + {value: () => ({height: 250, width: 250})}); + + t.spy(map, 'stop'); + + map.resize(); + + t.notOk(map.stop.called); + + t.end(); + }); + t.test('fires movestart, move, resize, and moveend events', (t) => { const map = createMap(t), events = []; + Object.defineProperty(map.getContainer(), 'getBoundingClientRect', + {value: () => ({height: 250, width: 250})}); + ['movestart', 'move', 'resize', 'moveend'].forEach((event) => { map.on(event, (e) => { events.push(e.type); @@ -873,7 +907,7 @@ test('Map', (t) => { const map = createMap(t, {zoom: 2, center: [0, 90], pitch: 80, skipCSSStub: true}); const bounds = map.getBounds(); - t.same(bounds.getNorth().toFixed(6), map.transform.maxValidLatitude); + t.same(bounds.getNorth().toFixed(6), MAX_MERCATOR_LATITUDE); t.same( toFixed(bounds.toArray()), toFixed([[ -23.3484820899, 77.6464759596 ], [ 23.3484820899, 85.0511287798 ]]) @@ -883,7 +917,7 @@ test('Map', (t) => { map.setCenter({lng: 0, lat: -90}); const sBounds = map.getBounds(); - t.same(sBounds.getSouth().toFixed(6), -map.transform.maxValidLatitude); + t.same(sBounds.getSouth().toFixed(6), -MAX_MERCATOR_LATITUDE); t.same( toFixed(sBounds.toArray()), toFixed([[ -23.3484820899, -85.0511287798 ], [ 23.3484820899, -77.6464759596]]) @@ -1278,6 +1312,119 @@ test('Map', (t) => { t.end(); }); + t.test('#getProjection', (t) => { + t.test('map defaults to Mercator', (t) => { + const map = createMap(t); + t.deepEqual(map.getProjection(), {name: 'mercator', center: [0, 0]}); + t.end(); + }); + + t.test('respects projection options object', (t) => { + const options = { + name: 'albers', + center: [12, 34], + parallels: [10, 42] + }; + const map = createMap(t, {projection: options}); + t.deepEqual(map.getProjection(), options); + t.end(); + }); + + t.test('respects projection options string', (t) => { + const map = createMap(t, {projection: 'albers'}); + t.deepEqual(map.getProjection(), { + name: 'albers', + center: [-96, 37.5], + parallels: [29.5, 45.5] + }); + t.end(); + }); + + t.test('composites user and default projection options', (t) => { + const options = { + name: 'albers', + center: [12, 34] + }; + const map = createMap(t, {projection: options}); + t.deepEqual(map.getProjection(), { + name: 'albers', + center: [12, 34], + parallels: [29.5, 45.5] + }); + t.end(); + }); + + t.test('does not composite user and default projection options for non-conical projections', (t) => { + const options = { + name: 'naturalEarth', + center: [12, 34] + }; + const map = createMap(t, {projection: options}); + t.deepEqual(map.getProjection(), { + name: 'naturalEarth', + center: [0, 0] + }); + t.end(); + }); + t.end(); + }); + + t.test('#setProjection', (t) => { + t.test('sets projection by string', (t) => { + const map = createMap(t); + map.setProjection('albers'); + t.deepEqual(map.getProjection(), { + name: 'albers', + center: [-96, 37.5], + parallels: [29.5, 45.5] + }); + t.end(); + }); + + t.test('throws error if invalid projection name is supplied', (t) => { + const map = createMap(t); + map.on('error', ({error}) => { + t.match(error.message, /Invalid projection name: fakeProj/); + t.end(); + }); + t.end(); + }); + + t.test('sets projection by options object', (t) => { + const options = { + name: 'albers', + center: [12, 34], + parallels: [10, 42] + }; + const map = createMap(t); + map.setProjection(options); + t.deepEqual(map.getProjection(), options); + t.end(); + }); + + t.test('sets projection by options object with just name', (t) => { + const map = createMap(t); + map.setProjection({name: 'albers'}); + t.deepEqual(map.getProjection(), { + name: 'albers', + center: [-96, 37.5], + parallels: [29.5, 45.5] + }); + t.end(); + }); + + t.test('setProjection with no argument defaults to Mercator', (t) => { + const map = createMap(t); + map.setProjection({name: 'albers'}); + t.equal(map.transform._unmodifiedProjection, false); + map.setProjection(); + t.deepEqual(map.getProjection(), {name: 'mercator', center: [0, 0]}); + t.equal(map.transform._unmodifiedProjection, true); + t.end(); + }); + t.end(); + }); + t.test('#remove', (t) => { const map = createMap(t); t.equal(map.getContainer().childNodes.length, 3); diff --git a/test/unit/ui/map_events.test.js b/test/unit/ui/map_events.test.js index e8d48a3ff8b..200676b4f7a 100644 --- a/test/unit/ui/map_events.test.js +++ b/test/unit/ui/map_events.test.js @@ -657,3 +657,473 @@ test("Map#on click should fire preclick before click", (t) => { map.remove(); t.end(); }); + +test('Map#on adds a listener for an event on multiple layers which do not exist', (t) => { + const map = createMap(t); + const features = [{}]; + + t.stub(map, 'getLayer').returns(undefined); + t.stub(map, 'queryRenderedFeatures').callsFake((point, options) => { + t.deepEqual(options, {layers: []}); + return features; + }); + + const spy = t.spy(); + + map.on('click', ['layer1', 'layer2'], spy); + simulate.click(map.getCanvas()); + + t.ok(spy.notCalled); + t.end(); +}); + +test('Map#on adds a listener for an event on multiple layers which some do not exist', (t) => { + const map = createMap(t); + const features = [{}]; + + const getLayerCB = t.stub(map, 'getLayer'); + getLayerCB.onCall(0).returns(undefined); + getLayerCB.onCall(1).returns({}); + getLayerCB.returns({}); + + t.stub(map, 'queryRenderedFeatures').callsFake((point, options) => { + t.deepEqual(options, {layers: ['background']}); + return features; + }); + + const spy = t.spy(function (e) { + t.equal(this, map); + t.equal(e.type, 'click'); + t.equal(e.features, features); + }); + + map.on('click', ['layer', 'background'], spy); + simulate.click(map.getCanvas()); + + t.ok(spy.calledOnce); + t.end(); +}); + +test('Map#on distinguishes distinct event types - multiple layers', (t) => { + const map = createMap(t); + + t.stub(map, 'getLayer').returns({}); + t.stub(map, 'queryRenderedFeatures').callsFake((point, options) => { + t.deepEqual(options, {layers: ['layer1', 'layer2']}); + return [{}]; + }); + + const spyDown = t.spy((e) => { + t.equal(e.type, 'mousedown'); + }); + + const spyUp = t.spy((e) => { + t.equal(e.type, 'mouseup'); + }); + + map.on('mousedown', ['layer1', 'layer2'], spyDown); + map.on('mouseup', ['layer1', 'layer2'], spyUp); + simulate.click(map.getCanvas()); + + t.ok(spyDown.calledOnce); + t.ok(spyUp.calledOnce); + t.end(); +}); + +test('Map#on distinguishes distinct multiple layers', (t) => { + const map = createMap(t); + const featuresA = [{}]; + const featuresB = [{}]; + + t.stub(map, 'getLayer').returns({}); + t.stub(map, 'queryRenderedFeatures').callsFake((point, options) => { + return options.layers[0] === 'A' ? featuresA : featuresB; + }); + + const spyA = t.spy((e) => { + t.equal(e.features, featuresA); + }); + + const spyB = t.spy((e) => { + t.equal(e.features, featuresB); + }); + + map.on('click', ['A', 'A2'], spyA); + map.on('click', ['B', 'B2'], spyB); + simulate.click(map.getCanvas()); + + t.ok(spyA.calledOnce); + t.ok(spyB.calledOnce); + t.end(); +}); + +test('Map#off removes a delegated event listener - multiple layers', (t) => { + const map = createMap(t); + + t.stub(map, 'getLayer').returns({}); + t.stub(map, 'queryRenderedFeatures').returns([{}]); + + const spy = t.spy(); + + map.on('click', ['layer1', 'layer2'], spy); + map.off('click', ['layer2', 'layer1'], spy); + simulate.click(map.getCanvas()); + + t.ok(spy.notCalled); + t.end(); +}); + +test('Map#off distinguishes distinct event types - multiple layers', (t) => { + const map = createMap(t); + + t.stub(map, 'getLayer').returns({}); + t.stub(map, 'queryRenderedFeatures').returns([{}]); + + const spy = t.spy((e) => { + t.equal(e.type, 'mousedown'); + }); + + map.on('mousedown', ['layer1', 'layer2'], spy); + map.on('mouseup', ['layer1', 'layer2'], spy); + map.off('mouseup', ['layer1', 'layer2'], spy); + simulate.click(map.getCanvas()); + + t.ok(spy.calledOnce); + t.end(); +}); + +test('Map#off distinguishes distinct layers - multiple layers', (t) => { + const map = createMap(t); + const featuresA = [{}]; + + t.stub(map, 'getLayer').returns({}); + t.stub(map, 'queryRenderedFeatures').callsFake((point, options) => { + t.deepEqual(options, {layers: ['A', 'B']}); + return featuresA; + }); + + const spy = t.spy((e) => { + t.equal(e.features, featuresA); + }); + + map.on('click', ['A', 'B'], spy); + map.on('click', ['C', 'D'], spy); + map.off('click', ['C', 'D'], spy); + simulate.click(map.getCanvas()); + + t.ok(spy.calledOnce); + t.end(); +}); + +test('Map#off distinguishes distinct listeners - multiple layers', (t) => { + const map = createMap(t); + + t.stub(map, 'getLayer').returns({}); + t.stub(map, 'queryRenderedFeatures').returns([{}]); + + const spyA = t.spy(); + const spyB = t.spy(); + + map.on('click', ['layer1', 'layer2'], spyA); + map.on('click', ['layer1', 'layer2'], spyB); + map.off('click', ['layer1', 'layer2'], spyB); + simulate.click(map.getCanvas()); + + t.ok(spyA.calledOnce); + t.ok(spyB.notCalled); + t.end(); +}); + +['mouseenter', 'mouseover'].forEach((event) => { + test(`Map#on ${event} does not fire if the specified layer does not exist - multiple layers`, (t) => { + const map = createMap(t); + + t.stub(map, 'getLayer').returns(null); + + const spy = t.spy(); + + map.on(event, ['layer1', 'layer2'], spy); + simulate.mousemove(map.getCanvas()); + simulate.mousemove(map.getCanvas()); + + t.ok(spy.notCalled); + t.end(); + }); + + test(`Map#on ${event} fires when entering the specified layer - multiple layers`, (t) => { + const map = createMap(t); + const features = [{}]; + + t.stub(map, 'getLayer').returns({}); + t.stub(map, 'queryRenderedFeatures').callsFake((point, options) => { + t.deepEqual(options, {layers: ['layer1', 'layer2']}); + return features; + }); + + const spy = t.spy(function (e) { + t.equal(this, map); + t.equal(e.type, event); + t.equal(e.target, map); + t.equal(e.features, features); + }); + + map.on(event, ['layer1', 'layer2'], spy); + simulate.mousemove(map.getCanvas()); + + t.ok(spy.calledOnce); + t.end(); + }); + + test(`Map#on ${event} does not fire on mousemove within the specified layer - multiple layers`, (t) => { + const map = createMap(t); + + t.stub(map, 'getLayer').returns({}); + t.stub(map, 'queryRenderedFeatures').returns([{}]); + + const spy = t.spy(); + + map.on(event, ['layer1', 'layer2'], spy); + simulate.mousemove(map.getCanvas()); + simulate.mousemove(map.getCanvas()); + + t.ok(spy.calledOnce); + t.end(); + }); + + test(`Map#on ${event} fires when reentering the specified layer - multiple layers`, (t) => { + const map = createMap(t); + + t.stub(map, 'getLayer').returns({}); + t.stub(map, 'queryRenderedFeatures') + .onFirstCall().returns([{}]) + .onSecondCall().returns([]) + .onThirdCall().returns([{}]); + + const spy = t.spy(); + + map.on(event, ['layer1', 'layer2'], spy); + simulate.mousemove(map.getCanvas()); + simulate.mousemove(map.getCanvas()); + simulate.mousemove(map.getCanvas()); + + t.ok(spy.calledTwice); + t.end(); + }); + + test(`Map#on ${event} fires when reentering the specified layer after leaving the canvas - multiple layers`, (t) => { + const map = createMap(t); + + t.stub(map, 'getLayer').returns({}); + t.stub(map, 'queryRenderedFeatures').returns([{}]); + + const spy = t.spy(); + + map.on(event, ['layer1', 'layer2'], spy); + simulate.mousemove(map.getCanvas()); + simulate.mouseout(map.getCanvas()); + simulate.mousemove(map.getCanvas()); + + t.ok(spy.calledTwice); + t.end(); + }); + + test(`Map#on ${event} distinguishes distinct layers - multiple layers`, (t) => { + const map = createMap(t); + const featuresA = [{}]; + const featuresB = [{}]; + + t.stub(map, 'getLayer').returns({}); + t.stub(map, 'queryRenderedFeatures').callsFake((point, options) => { + return options.layers[0] === 'A' ? featuresA : featuresB; + }); + + const spyA = t.spy((e) => { + t.equal(e.features, featuresA); + }); + + const spyB = t.spy((e) => { + t.equal(e.features, featuresB); + }); + + map.on(event, ['A', 'A2'], spyA); + map.on(event, ['B', 'B2'], spyB); + + simulate.mousemove(map.getCanvas()); + simulate.mousemove(map.getCanvas()); + + t.ok(spyA.calledOnce); + t.ok(spyB.calledOnce); + t.end(); + }); + + test(`Map#on ${event} distinguishes distinct listeners - multiple layers`, (t) => { + const map = createMap(t); + + t.stub(map, 'getLayer').returns({}); + t.stub(map, 'queryRenderedFeatures').returns([{}]); + + const spyA = t.spy(); + const spyB = t.spy(); + + map.on(event, ['layer1', 'layer2'], spyA); + map.on(event, ['layer1', 'layer2'], spyB); + simulate.mousemove(map.getCanvas()); + + t.ok(spyA.calledOnce); + t.ok(spyB.calledOnce); + t.end(); + }); + + test(`Map#off ${event} removes a delegated event listener - multiple layers`, (t) => { + const map = createMap(t); + + t.stub(map, 'getLayer').returns({}); + t.stub(map, 'queryRenderedFeatures').returns([{}]); + + const spy = t.spy(); + + map.on(event, ['layer1', 'layer2'], spy); + map.off(event, ['layer1', 'layer2'], spy); + simulate.mousemove(map.getCanvas()); + + t.ok(spy.notCalled); + t.end(); + }); + + test(`Map#off ${event} distinguishes distinct layers - multiple layers`, (t) => { + const map = createMap(t); + const featuresA = [{}]; + + t.stub(map, 'getLayer').returns({}); + t.stub(map, 'queryRenderedFeatures').callsFake((point, options) => { + t.deepEqual(options, {layers: ['A', 'A2']}); + return featuresA; + }); + + const spy = t.spy((e) => { + t.equal(e.features, featuresA); + }); + + map.on(event, ['A', 'A2'], spy); + map.on(event, ['B', 'B2'], spy); + map.off(event, ['B', 'B2'], spy); + simulate.mousemove(map.getCanvas()); + + t.ok(spy.calledOnce); + t.end(); + }); + + test(`Map#off ${event} distinguishes distinct listeners - multiple layers`, (t) => { + const map = createMap(t); + + t.stub(map, 'getLayer').returns({}); + t.stub(map, 'queryRenderedFeatures').returns([{}]); + + const spyA = t.spy(); + const spyB = t.spy(); + + map.on(event, ['layer1', 'layer2'], spyA); + map.on(event, ['layer1', 'layer2'], spyB); + map.off(event, ['layer1', 'layer2'], spyB); + simulate.mousemove(map.getCanvas()); + + t.ok(spyA.calledOnce); + t.ok(spyB.notCalled); + t.end(); + }); +}); + +['mouseleave', 'mouseout'].forEach((event) => { + test(`Map#on ${event} does not fire if the specified layer does not exist - multiple layers`, (t) => { + const map = createMap(t); + + t.stub(map, 'getLayer').returns(null); + + const spy = t.spy(); + + map.on(event, ['layer1', 'layer2'], spy); + simulate.mousemove(map.getCanvas()); + simulate.mousemove(map.getCanvas()); + + t.ok(spy.notCalled); + t.end(); + }); + + test(`Map#on ${event} does not fire on mousemove when entering or within the specified layer - multiple layers`, (t) => { + const map = createMap(t); + + t.stub(map, 'getLayer').returns({}); + t.stub(map, 'queryRenderedFeatures').returns([{}]); + + const spy = t.spy(); + + map.on(event, ['layer1', 'layer2'], spy); + simulate.mousemove(map.getCanvas()); + simulate.mousemove(map.getCanvas()); + + t.ok(spy.notCalled); + t.end(); + }); + + test(`Map#on ${event} fires when exiting the specified layer - multiple layers`, (t) => { + const map = createMap(t); + + t.stub(map, 'getLayer').returns({}); + t.stub(map, 'queryRenderedFeatures') + .onFirstCall().returns([{}]) + .onSecondCall().returns([]); + + const spy = t.spy(function (e) { + t.equal(this, map); + t.equal(e.type, event); + t.equal(e.features, undefined); + }); + + map.on(event, ['layer1', 'layer2'], spy); + simulate.mousemove(map.getCanvas()); + simulate.mousemove(map.getCanvas()); + + t.ok(spy.calledOnce); + t.end(); + }); + + test(`Map#on ${event} fires when exiting the canvas - multiple layers`, (t) => { + const map = createMap(t); + + t.stub(map, 'getLayer').returns({}); + t.stub(map, 'queryRenderedFeatures').returns([{}]); + + const spy = t.spy(function (e) { + t.equal(this, map); + t.equal(e.type, event); + t.equal(e.features, undefined); + }); + + map.on(event, ['layer1', 'layer2'], spy); + simulate.mousemove(map.getCanvas()); + simulate.mouseout(map.getCanvas()); + + t.ok(spy.calledOnce); + t.end(); + }); + + test(`Map#off ${event} removes a delegated event listener - multiple layers`, (t) => { + const map = createMap(t); + + t.stub(map, 'getLayer').returns({}); + t.stub(map, 'queryRenderedFeatures') + .onFirstCall().returns([{}]) + .onSecondCall().returns([]); + + const spy = t.spy(); + + map.on(event, ['layer1', 'layer2'], spy); + map.off(event, ['layer1', 'layer2'], spy); + simulate.mousemove(map.getCanvas()); + simulate.mousemove(map.getCanvas()); + simulate.mouseout(map.getCanvas()); + + t.ok(spy.notCalled); + t.end(); + }); +}); diff --git a/test/unit/ui/marker.test.js b/test/unit/ui/marker.test.js index aa19db9a91f..688e2a73260 100644 --- a/test/unit/ui/marker.test.js +++ b/test/unit/ui/marker.test.js @@ -135,6 +135,7 @@ test('Marker#togglePopup opens a popup that was closed', (t) => { .togglePopup(); t.ok(marker.getPopup().isOpen()); + t.equal(marker.getElement().getAttribute('aria-expanded'), 'true'); map.remove(); t.end(); @@ -150,6 +151,7 @@ test('Marker#togglePopup closes a popup that was open', (t) => { .togglePopup(); t.ok(!marker.getPopup().isOpen()); + t.equal(marker.getElement().getAttribute('aria-expanded'), 'false'); map.remove(); t.end(); @@ -830,7 +832,7 @@ test('Drag above horizon clamps', (t) => { }); test('Drag below / behind camera', (t) => { - const map = createMap(t); + const map = createMap(t, {zoom: 3}); map.setPitch(85); const marker = new Marker({draggable: true}) .setLngLat(map.unproject([map.transform.width / 2, map.transform.height - 20])) diff --git a/yarn.lock b/yarn.lock index c26b12b8e79..6b09fba267a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1111,12 +1111,12 @@ dependencies: "@mapbox/geojsonhint" "^2.2.0" -"@mapbox/geojson-rewind@^0.5.0": - version "0.5.0" - resolved "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.0.tgz" - integrity sha512-73l/qJQgj/T/zO1JXVfuVvvKDgikD/7D/rHAD28S9BG1OTstgmftrmqfCx4U+zQAmtsB6HcDA3a7ymdnJZAQgg== +"@mapbox/geojson-rewind@^0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@mapbox/geojson-rewind/-/geojson-rewind-0.5.1.tgz#adbe16dc683eb40e90934c51a5e28c7bbf44f4e1" + integrity sha512-eL7fMmfTBKjrb+VFHXCGv9Ot0zc3C0U+CwXo1IrP+EPwDczLoXv34Tgq3y+2mPSFNVUXgU42ILWJTC7145KPTA== dependencies: - concat-stream "~2.0.0" + get-stream "^6.0.1" minimist "^1.2.5" "@mapbox/geojson-types@^1.0.2": @@ -2857,7 +2857,7 @@ concat-stream@^1.6.0, concat-stream@^1.6.1, concat-stream@~1.6.0: readable-stream "^2.2.2" typedarray "^0.0.6" -concat-stream@^2.0.0, concat-stream@~2.0.0: +concat-stream@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz" integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== @@ -4196,10 +4196,10 @@ duplexify@^3.6.0: readable-stream "^2.0.0" stream-shift "^1.0.0" -earcut@^2.2.2: - version "2.2.2" - resolved "https://registry.npmjs.org/earcut/-/earcut-2.2.2.tgz" - integrity sha512-eZoZPPJcUHnfRZ0PjLvx2qBordSiO8ofC3vt+qACLM95u+4DovnbYNpQtJh0DNsWj8RnxrQytD4WA8gj5cRIaQ== +earcut@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.2.3.tgz#d44ced2ff5a18859568e327dd9c7d46b16f55cf4" + integrity sha512-iRDI1QeCQIhMCZk48DRDMVgQSSBDmbzzNhnxIo+pwx3swkfjMh6vh0nWLq1NdvGHLKH6wIrAM3vQWeTj6qeoug== ecc-jsbn@~0.1.1: version "0.1.2" @@ -5294,6 +5294,11 @@ get-stream@^5.1.0: dependencies: pump "^3.0.0" +get-stream@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz" @@ -11184,10 +11189,10 @@ sugarss@^2.0.0: dependencies: postcss "^7.0.2" -supercluster@^7.1.3: - version "7.1.3" - resolved "https://registry.npmjs.org/supercluster/-/supercluster-7.1.3.tgz" - integrity sha512-7+bR4FbF5SYsmkHfDp61QiwCKtwNDyPsddk9TzfsDA5DQr5Goii5CVD2SXjglweFCxjrzVZf945ahqYfUIk8UA== +supercluster@^7.1.4: + version "7.1.4" + resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-7.1.4.tgz#6762aabfd985d3390b49f13b815567d5116a828a" + integrity sha512-GhKkRM1jMR6WUwGPw05fs66pOFWhf59lXq+Q3J3SxPvhNcmgOtLRV6aVQPMRsmXdpaeFJGivt+t7QXUPL3ff4g== dependencies: kdbush "^3.0.0" @@ -12315,14 +12320,14 @@ vm-browserify@^1.0.0: resolved "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== -vt-pbf@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.1.tgz" - integrity sha512-pHjWdrIoxurpmTcbfBWXaPwSmtPAHS105253P1qyEfSTV2HJddqjM+kIHquaT/L6lVJIk9ltTGc0IxR/G47hYA== +vt-pbf@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/vt-pbf/-/vt-pbf-3.1.3.tgz#68fd150756465e2edae1cc5c048e063916dcfaac" + integrity sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA== dependencies: "@mapbox/point-geometry" "0.1.0" "@mapbox/vector-tile" "^1.3.1" - pbf "^3.0.5" + pbf "^3.2.1" vue-template-compiler@^2.5.16: version "2.6.12"