From 107a5256bebb4d06c6fb073618632d865e48e5b9 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 19 Oct 2021 17:24:23 -0400 Subject: [PATCH] add support for non-mercator projections (#11124) * rough projection support * projections stencil clipping and refactor (#410) * Enable stencil clipping for line and fill layers * Use buffers from tile * Refactor tile bounds buffers * Rename things to not exclusively be RasterBounds * Create projections directory * More refactoring * Combine matrix calculations * Cleanup * Refactor projections to new folder * Begin debug work * Tile boundaries are working * Refactor indexbuffer and segmentvector to per painter * merge projectx and projecty functions * nits Co-authored-by: Ansis Brammanis * Projections fix location issues (#414) * Add debug page * Add projection option * Wire up projection code with worker * Rename projections folder to projection * Refactor and update free camera * Add Projection type and fix center calculations * Fix bug with transform._center * Make Winkel projection noop for now * Update demo HTML and CSS * temp remove undistortion Co-authored-by: Ansis Brammanis * [projections] Adaptive geometry resampling for alternative projections (#10753) * implement adaptive resampling of reprojected geometry * address feedback * Refactor projections code to get all tests passing (#10732) * [projections] Simplify and optimize tile transform code (#10780) * simplify projections tile transform * skip resampling for mercator * [projections] Fix performance regression in draw_background (#10747) * separate tiles for background layers * additional lint & flow fixes * try fixing tests * try fixing render tests Co-authored-by: Ansis Brammanis * Pin chrome to version 91 (#10887) (#10896) * Pin to chrome version 91 * Pin chrome version for test-browser Co-authored-by: Arindam Bose * Refactor raw projections, handle projection options (#10913) * refactor raw projections, handle projection options * add unprojection to winkel tripel * fix flow * Pin chrome to version 91 (#10887) * Pin to chrome version 91 * Pin chrome version for test-browser * fix lint * remove to superfluous sin calls Co-authored-by: Arindam Bose * Fix bearing for non-mercator projections (#10781) * Use adaptive resampling with MARTINI & Earcut for non-Mercator tiles (#10980) * use adaptive resampling and earcut for non-Mercator tile bounds * fix unit test * use adaptive MARTINI mesh for non-Mercator raster tiles * Clamp unproject to valid geo range in alternate projections (#10992) * clamp unproject to mercator bounds in all projections * fix marker test * avoid wrapping center for non-Mercator projections * extend alt projections clamping to full lat range * correct zoom, bearing and shear for projections (#10976) * fix zoom, bearing and skew for projections * refactor adjustments * lint * add comments * Fix circle and heatmap on alternate projections (#11074) * fix circle & heatmap on alternate projections (blunder) * fix unit test * fix pitch, line-width and other properties for projections (#11080) and: - fix fill-extrusions - remove global projection variable to allow multiple maps on one page - avoid recalculating tileTransform * Add Equal Earth, Natural Earth and Lambert Conformal Conic projections (#11091) * Fix constraining logic for alternate projections (#11092) * adaptive bbox for projections, refactor resampling * better precision for adaptive bounds * remove leftover * fix zoom/shear adjustments near poles * optimize tile transform * fix lint * attempt to fix tests * simplify, clarify and consolidate constraining logic * minor renames in transform * safer clamping for zoom adjustments * Projections public API (#11002) Co-authored-by: Ansis Brammanis * fix conflicts * fix seams around alternate-projected tiles (#11119) * fix unit tests * remove alaska * Basic support for custom maxBounds in alternate projections (#11121) * rudimentary support for custom maxBounds in alternate projections * fix flow * fix image and video sources in alternate projections (#11123) * clean up debug pages * remove uncessary deg <--> rad conversions * fix filename casing * fix queryRenderedFeatures for alternate projections (#11125) * Projections fixups (#11127) * disable terrain and fog for alternate projections (#11126) * Lazily instantiate projected tile debug buffers and release projected buffers when tiles are unloaded (#11128) * enable lod tile loading for projections (#11129) * enable lod tile loading for projections to significantly reduce the number of tiles at low zoom levels * use Math.hypot(...) Co-authored-by: Vladimir Agafonkin * add comments Co-authored-by: Vladimir Agafonkin * allow map.setProjection(null) * add limitations * avoid recreating tile buffer Co-authored-by: Karim Naaji * fix assertion error * fix requires * center projections vertically Center projections vertically in 0 to 1 range. This shouldn't matter but there is some constraining behavior that is currently affected by this. * Fix tile buffer destroyed but not reset (#11134) * mention settin bounds in projection docs Co-authored-by: Ryan Hamley Co-authored-by: Vladimir Agafonkin Co-authored-by: Arindam Bose Co-authored-by: Karim Naaji --- LICENSE.txt | 4 +- build/generate-flow-typed-style-spec.js | 4 + build/generate-struct-arrays.js | 9 +- debug/projections.html | 189 +++++++++ src/data/array_types.js | 40 +- ...nds_attributes.js => bounds_attributes.js} | 0 src/data/bucket.js | 3 +- src/data/bucket/circle_bucket.js | 5 +- src/data/bucket/fill_bucket.js | 5 +- src/data/bucket/fill_extrusion_bucket.js | 5 +- src/data/bucket/line_bucket.js | 5 +- src/data/bucket/symbol_bucket.js | 5 +- src/data/feature_index.js | 4 +- src/data/load_geometry.js | 74 +++- src/geo/mercator_coordinate.js | 8 +- src/geo/projection/adjustments.js | 147 +++++++ src/geo/projection/albers.js | 44 ++ src/geo/projection/equal_earth.js | 56 +++ src/geo/projection/equirectangular.js | 18 + src/geo/projection/index.js | 36 ++ src/geo/projection/lambert.js | 70 +++ src/geo/projection/mercator.js | 18 + src/geo/projection/natural_earth.js | 51 +++ src/geo/projection/resample.js | 49 +++ src/geo/projection/tile_transform.js | 99 +++++ src/geo/projection/winkel_tripel.js | 64 +++ src/geo/transform.js | 398 ++++++++++++------ src/render/draw_background.js | 17 +- src/render/draw_debug.js | 8 +- src/render/draw_hillshade.js | 12 +- src/render/draw_raster.js | 6 +- src/render/draw_symbol.js | 7 +- src/render/painter.js | 81 ++-- src/render/program/circle_program.js | 18 +- src/render/program/line_program.js | 15 +- src/render/uniform_binding.js | 19 + src/shaders/circle.vertex.glsl | 2 +- src/shaders/line.vertex.glsl | 6 +- src/shaders/line_pattern.vertex.glsl | 6 +- src/source/canvas_source.js | 8 +- src/source/image_source.js | 23 +- src/source/pixels_to_tile_units.js | 10 + src/source/source_cache.js | 7 +- src/source/tile.js | 121 +++++- src/source/tile_id.js | 26 -- src/source/tile_mesh.js | 162 +++++++ src/source/video_source.js | 8 +- src/source/worker.js | 15 +- src/source/worker_source.js | 4 +- src/source/worker_tile.js | 16 +- src/style-spec/diff.js | 9 +- src/style-spec/reference/v8.json | 95 +++++ src/style-spec/types.js | 7 + src/style-spec/validate/validate.js | 4 +- .../validate/validate_projection.js | 30 ++ src/style/fog.js | 6 + src/style/query_geometry.js | 10 +- src/style/style.js | 30 +- src/symbol/placement.js | 3 +- src/symbol/projection.js | 19 +- src/terrain/terrain.js | 4 +- src/ui/camera.js | 6 +- src/ui/free_camera.js | 1 + src/ui/map.js | 88 +++- test/expression.test.js | 6 +- test/integration/lib/render.js | 1 + .../albers-configured/expected.png | Bin 0 -> 25356 bytes .../albers-configured/style.json | 18 + .../map-projections/albers/expected.png | Bin 0 -> 34447 bytes .../map-projections/albers/style.json | 18 + .../map-projections/equal-earth/expected.png | Bin 0 -> 36510 bytes .../map-projections/equal-earth/style.json | 17 + .../equirectangular/expected.png | Bin 0 -> 33937 bytes .../equirectangular/style.json | 17 + .../map-projections/lambert/expected.png | Bin 0 -> 32116 bytes .../map-projections/lambert/style.json | 18 + .../natural-earth/expected.png | Bin 0 -> 36504 bytes .../map-projections/natural-earth/style.json | 17 + .../winkel-tripel/expected.png | Bin 0 -> 38955 bytes .../map-projections/winkel-tripel/style.json | 17 + test/unit/data/symbol_bucket.test.js | 6 +- test/unit/geo/transform.test.js | 44 +- .../unit/source/geojson_worker_source.test.js | 4 +- test/unit/source/source_cache.test.js | 8 +- .../source/vector_tile_worker_source.test.js | 6 +- test/unit/source/worker.test.js | 1 + test/unit/source/worker_tile.test.js | 4 +- test/unit/terrain/terrain.test.js | 9 +- test/unit/ui/map.test.js | 125 +++++- test/unit/ui/marker.test.js | 2 +- 90 files changed, 2256 insertions(+), 401 deletions(-) create mode 100644 debug/projections.html rename src/data/{raster_bounds_attributes.js => bounds_attributes.js} (100%) create mode 100644 src/geo/projection/adjustments.js create mode 100644 src/geo/projection/albers.js create mode 100644 src/geo/projection/equal_earth.js create mode 100644 src/geo/projection/equirectangular.js create mode 100644 src/geo/projection/index.js create mode 100644 src/geo/projection/lambert.js create mode 100644 src/geo/projection/mercator.js create mode 100644 src/geo/projection/natural_earth.js create mode 100644 src/geo/projection/resample.js create mode 100644 src/geo/projection/tile_transform.js create mode 100644 src/geo/projection/winkel_tripel.js create mode 100644 src/source/tile_mesh.js create mode 100644 src/style-spec/validate/validate_projection.js create mode 100644 test/integration/render-tests/map-projections/albers-configured/expected.png create mode 100644 test/integration/render-tests/map-projections/albers-configured/style.json create mode 100644 test/integration/render-tests/map-projections/albers/expected.png create mode 100644 test/integration/render-tests/map-projections/albers/style.json create mode 100644 test/integration/render-tests/map-projections/equal-earth/expected.png create mode 100644 test/integration/render-tests/map-projections/equal-earth/style.json create mode 100644 test/integration/render-tests/map-projections/equirectangular/expected.png create mode 100644 test/integration/render-tests/map-projections/equirectangular/style.json create mode 100644 test/integration/render-tests/map-projections/lambert/expected.png create mode 100644 test/integration/render-tests/map-projections/lambert/style.json create mode 100644 test/integration/render-tests/map-projections/natural-earth/expected.png create mode 100644 test/integration/render-tests/map-projections/natural-earth/style.json create mode 100644 test/integration/render-tests/map-projections/winkel-tripel/expected.png create mode 100644 test/integration/render-tests/map-projections/winkel-tripel/style.json 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/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/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/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 b286d445c07..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,7 +79,7 @@ export interface Bucket { +layers: Array; +stateDependentLayers: Array; +stateDependentLayerIds: Array; - populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID): void; + populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID, tileTransform: TileTransform): void; update(states: FeatureStates, vtLayer: VectorTileLayer, availableImages: Array, imagePositions: {[_: string]: ImagePosition}): void; isEmpty(): boolean; diff --git a/src/data/bucket/circle_bucket.js b/src/data/bucket/circle_bucket.js index 6afe789757a..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 }; diff --git a/src/data/bucket/fill_bucket.js b/src/data/bucket/fill_bucket.js index 2c2e6da9b62..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 }; diff --git a/src/data/bucket/fill_extrusion_bucket.js b/src/data/bucket/fill_extrusion_bucket.js index 16505a40d6c..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: {} diff --git a/src/data/bucket/line_bucket.js b/src/data/bucket/line_bucket.js index f63758a9eae..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 }; diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index 50cc769f276..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) { diff --git a/src/data/feature_index.js b/src/data/feature_index.js index 5a291b703b5..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, @@ -153,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); 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/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 d8f30068af8..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,15 @@ 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; @@ -32,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; @@ -87,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; @@ -107,6 +122,7 @@ class Transform { _constraining: boolean; _projMatrixCache: {[_: number]: Float32Array}; _alignedProjMatrixCache: {[_: number]: Float32Array}; + _pixelsToTileUnitsCache: {[_: number]: Float32Array}; _fogTileMatrixCache: {[_: number]: Float32Array}; _distanceTileDataCache: {[_: number]: FeatureDistanceData}; _camera: FreeCamera; @@ -115,7 +131,6 @@ class Transform { 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; @@ -124,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; @@ -153,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; @@ -169,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; } @@ -196,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; @@ -224,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; @@ -261,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; @@ -331,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 @@ -414,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); @@ -648,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); @@ -667,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, @@ -758,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; @@ -802,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; } @@ -811,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); @@ -857,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; @@ -882,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; } @@ -902,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. @@ -920,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(); } } @@ -984,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); } /** @@ -1227,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; } /** @@ -1239,27 +1342,49 @@ 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; @@ -1334,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(); } @@ -1377,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(); @@ -1397,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); @@ -1422,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; + x2 = (x + shift + this.worldSize) % this.worldSize - shift; - const w2 = size.x / 2; - if (x - w2 < minX) x = minX + w2; - if (x + w2 > maxX) x = maxX - w2; - - 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 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))); } - let minLngZoom = DEFAULT_MIN_ZOOM; - if (this.lngRange) { - const lngRange = this.lngRange; - minLngZoom = minZoomForDim(this.width, [mercatorXfromLng(lngRange[0]), mercatorXfromLng(lngRange[1])]); - } - - 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. @@ -1583,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]); @@ -1645,6 +1771,7 @@ class Transform { this._projMatrixCache = {}; this._alignedProjMatrixCache = {}; + this._pixelsToTileUnitsCache = {}; } _calcFogMatrices() { @@ -1703,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. */ @@ -1744,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(); @@ -1773,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/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/tile.js b/src/source/tile.js index e72b38db13d..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,10 @@ 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 = @@ -47,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 @@ -80,6 +107,8 @@ class Tile { actor: ?Actor; vtLayers: {[_: string]: VectorTileLayer}; isSymbolTile: ?boolean; + isRaster: ?boolean; + tileTransform: TileTransform; neighboringTiles: ?Object; dem: ?DEMData; @@ -102,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; @@ -119,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 @@ -127,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) { @@ -255,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(); @@ -335,7 +395,8 @@ class Tile { tileResult, pixelPosMatrix, transform, - params + params, + tileTransform: this.tileTransform }, layers, serializedLayers, sourceFeatureState); } @@ -514,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 f78f6bd70c4..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,6 +40,8 @@ export default class Worker { workerSourceTypes: {[_: string]: Class }; workerSources: {[_: string]: {[_: string]: {[_: string]: WorkerSource } } }; demWorkerSources: {[_: string]: {[_: string]: RasterDEMTileWorkerSource } }; + projections: {[_: string]: Projection }; + defaultProjection: Projection; isSpriteLoaded: {[_: string]: boolean }; referrer: ?string; terrain: ?boolean; @@ -51,6 +55,9 @@ export default class Worker { this.availableImages = {}; this.isSpriteLoaded = {}; + this.projections = {}; + this.defaultProjection = getProjection({name: 'mercator'}); + this.workerSourceTypes = { vector: VectorTileWorkerSource, geojson: GeoJSONWorkerSource @@ -124,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(); @@ -137,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); } @@ -148,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); } 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 948fa6b68bf..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,6 +158,7 @@ class WorkerTile { index: featureIndex.bucketLayerIDs.length, layers: family, zoom: this.zoom, + canonical: this.canonical, pixelRatio: this.pixelRatio, overscaling: this.overscaling, collisionBoxArray: this.collisionBoxArray, @@ -156,7 +168,7 @@ class WorkerTile { 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)); } } 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/reference/v8.json b/src/style-spec/reference/v8.json index 2b036408c14..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", @@ -3958,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/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_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/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 ca5a4cb4e10..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; @@ -1177,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; }); diff --git a/src/symbol/placement.js b/src/symbol/placement.js index b6cdc645ee8..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'; @@ -250,7 +249,7 @@ export class Placement { const dynamicFilter = styleLayer.dynamicFilter(); const dynamicFilterNeedsFeature = styleLayer.dynamicFilterNeedsFeature(); - const pixelsToTiles = pixelsToTileUnits(tile, 1, this.transform.zoom); + const pixelsToTiles = this.transform.calculatePixelsToTileUnitsMatrix(tile); const textLabelPlaneMatrix = projection.getLabelPlaneMatrix(posMatrix, pitchWithMap, 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/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 f00dda5ef32..363fd7a4262 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -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/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/map.js b/src/ui/map.js index 2f7bf56cbee..7d6eec2e83b 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'; @@ -118,7 +119,8 @@ type MapOptions = { transformRequest?: RequestTransformFunction, accessToken: string, testMode: ?boolean, - locale?: Object + locale?: Object, + projection?: ProjectionSpecification | string }; const defaultMinZoom = -2; @@ -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 @@ -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})); @@ -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. @@ -2844,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')); 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/integration/lib/render.js b/test/integration/lib/render.js index 623fed8abf7..ed2e590e7e0 100644 --- a/test/integration/lib/render.js +++ b/test/integration/lib/render.js @@ -145,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 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 0000000000000000000000000000000000000000..703a596ad697cb61eec273a0222d022ec6655b16 GIT binary patch literal 25356 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⪼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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1b13608ea4d86f6dec65a182317108980509f576 GIT binary patch literal 32116 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%0D*XXH+O4mt8YFY z%zGbQRE(Ax)@8(SX=goD@ml=OBVrxEzn?GQm)y}dr)5^MeX)ncru)TrUBl;rDW)gSP|gSnZy?(IdgwhP((Tu{36vorF6 z$ao32=T)`RJf+knmXg5t5^cLfzI|+fLi{~HCP8yBP*#6oYZ@)3brX=w`D}^~fE9DJ z9x{qlr$E(!;`Hn|oGJ#^Yns_-6V8*g=X>+o_c5P|&W_ej?I|dzJYt^Z^n9tS(~ZW& z(oSwG2UB%DSN~Wr0)0cCM|(SdVevhAch_SqSpK)bbolX7Ot8j>hykIQZh)-sFE-xm_Oj zacS^xM>jYAC2CrFIu##Eppn_BxG3L`Pf92p^}?UiGAx-Fa~Aa;*k2KJ2Ugeq^5oc; zeOe^f@ekPTPj`}tg7DS)L|M&+k9HT*RBJ%f=;$~n7Jn}O;yRuA>JZPhzOT}FNo|Xs z9zLV<0nweymAKDVBHL1JEx*s$$iZ=4%J^I9 zh9}9Ze+o|{`+@?bLx;80k;+N$2?d`LDgcl=!Sl^&v)wF+Cxe(9W-*-)5jTe|%~zHW z=DcCDHxl|S#E$a?SLS_>3hejG8x$-jJ_WPzYsg?^WMw6b?C!5v^iVGx)`SnbcqGFU; zu$0Dyo8EzC$PAwY+U!@A`DP!@{!EeenLl92?*>tcHgPM6%(PGASN^a(;j28A3=KvE zgRocIcOy?N3&9n1$~NGYHbnP|Y(Jp7oaZQK425_I>L(tU*m8aL3OLAA)B(dX!WXJ4 zB_Ui5nD2eAqPC9p9JJ-kpqFbb!FmyNKQAiu_UJoXFqo`%SG?}>$Dn(qd7r1|`p32V zJOJ_IXVC3I=*?PP+nDdoZIjoLueZ{Xz&`0~mXorP1yp1dZ(8D%M0KPb+2o`Lw~!{$ z7!FolL=7suuhb%k%yl{jT-KwHWcoR$^z}lYYJ*BB(=-Nqvfb93+-{n{>#o=)rsyr- zdy_(YyK0S_>5k`-vL~lKWS2LW$4Bqx%BYH9%WJGy48Os6#cCd*3k1qs3W4jYia$sK z%^{pqXFh6YD0sgB^SL@_NNgSWTN>%!6)5?!mwKuOSlgswS_(zOYxI2l>Kr7lliIUP zgQ?)Hx7R;t!^_)Tj<@%=K6S{)zCpRF_=r_Ot2%?N-WS;Bb1Y|-{2f>nNVv%W4c!J^ z5CbB3NTHm5=VW*S=hj8h*$+)wjyM@u!Or^SG5V6(=yQ5fMJ zF;;rn&Pl(AEK7OQzzxCpf~!9c;z9%l)G%7slapfYONX1A8;`gEQ@E^b=`9{B^1(?q zk+S6C4~Qckk0gEk@Xk+*UowdyU(`NLjBv_-q|zKe54s!4UO8U~+85+@SaTVgu@$*W!vDwNx6Hmn|UL0nIG5`(J@262<~75=YE#h~F>MuXC0Pl zJ`2e8khjGI#$75J2P#c`(JvZIEO5z4NmuqVMOax)^=%8T?vKehi?YD~21sJzkQ4gocYSy}PlwtXsGbY+z;I54yQtSRF0UJUW@;U5!{@eQD-&_Xc=H zNl@{s&Km>5FtD$<8Z65o?}np^l%t&dFIZ*4QBxD&#jf-4im;_qQjWcIK3?lPYOc`^ zSE<2*NUGffgPXf1gz!(H9+^nPaHPDe>eSy$BedYpsA#ipY( zhPB63-=|^v!W<6}52(MMYjBglyu-uBW+w4-d>NynqRMI-XoN-!&1vL8^l0WeGh$Yf zdi++`dxC{yXbIw3(zP|qfPl=$l~4SRkoNO1${&?$00-ax{9D8jc<&$?%iWP=KefU0 z)<)meXpI>s6ODl-;s~yttwPq#Vh6&qgM$N%w)#$~)wN%Youi5lR|n&QYj4v!=LGpZ zkZ*;&HwFu{{$&|QeO)XxEP-XFO;1zWu2mhcCTCynN`ll8HV$rgR*T=*DRdYq9Yq)Q zC1?j%rtP)2mE}%{N@kT!(yC5{IvzC2G3>ql2$^%rf|-VevA6*TqBJZn! zn@J`sQf$JxIT?1Y7w#sL=XL^n{y9mpgmE zutvlw3)`YYMuC<2H0zHb4`zfecqzufWBB(jfhT$034H@)^o8iwTH^YOB^tzNtX}ri zZQC4VI53N_*n;x+z9@#snYlZtgirBtT>mA3^?40sC4C8&my;XaCm0$ifb*|Dz_sb} zh29iJ^IHoAlT03QAcmFLb0GEg?jhK(dGWV?_%riB1ONE-lKnlw z77+ghh3DcSY2=S8D=Uw^yOL0TUl^?ilr1#J$Nx1a`GIE_IEe>G1*R@ruk(VAc0~5> zD>(O6NZ-es;`?S?Gx;TL(a1$4X=`hk=;S)!s?eeEqm=W4rN>9lXh6{vf)DDp%-~vS zAvxDWzZva=mfpSQ`1DTkM~u;Oz0Q?A$zpS77^7(UXcr4rgs-4t2NJO98}HtAb02Vh zrU>#=8p{kj`v-_9B}8ZqDiXXs7(&wARrI|v){LRt`6@vP#sB^;V5;JE3W4U5-6mQ;OZ zjXp{iRqd@e!oWlQ0+KUbR#ucyarkzs#Xeho#v#8X#@R#8&b7&+9<$^Tn4Pf2 zaDZxVOO)W$vRhXt>4wIRnn?Q=F$Y9Omia|=g#_TuZ;j9;>)9$M7Zj<8hN((qW@S-( zMDCMrFEqYml{{F_9^(VT0AfF$FKoJ{h`ZbCB+~XR%&0~uC`|Vzu+28$?xvv;6Ye{~ zF$!#avJeIN(+qlU0WsDYrfcmuQgZpfrsz=hp1?xePv-BjlK0>$p*(Fw8BTu1Db&zL zYDg3ngbY5hnx1r>XM3|*ZEISXrh9nuispR({<2G))l0(iAoLY=AetZ4?|`KqFECX< zt};2PG~@@U6F2&FzM(F)(lb7;V{2>s>Mf=_CJHFXD+c8paS6y7?C_?hrlf99%dR1~ z&ZM=UC(T|)SVE5Kwl5T(cMnoBiTnZYk<0?dS#FM{Y>KovR0`DFqsY;ngSoQBu_`YE z?jyySyAN1lqTS+m=Rrx1@KWQZ6_6RKB(F^jSC?(*olBo6Tb-)1>BhmYMGXvx!f5CM zRl&}ykY`&j%KTH43b+C^n$11@y~^4R_CFP%AY!flKJP*d=YIk?qCT87CuD~6_M9J& zo20u%iFh&Mx?)~fIxpDHqwU{H+j;TzoZYZ^kieK>=#nu_-u2%}$;jA5088qyOj!R~ zvGRnwepn~Ok)e-k2M@oNTnddIuaN{3XiU_9D9Nm&*Z&B8=|Njx6pj--rocAFf9Wp> z70&akKW_KHdwUld8DV+8H>Y9pijM1r(F!O^wOWy`Xj`6-qFX-G8^$`WEM-<>ZEM!0 zfS^T2tSm?3bw8qk0w2`67Ha-Dc4?#$32!gU)z&N9e_2CX&-w+s$;q)T7cu{=CJMMW*1G;BWkZhwJvOma`G6R zwf8ov(6-HvQxbYo{^!q`Lw|#b0!jR!)0-$yvu`d(qbzp7P0rDS=&wdrLVrix?`b-} zqek+ixxjsmtkrHz=Znlb5Y_XDS+w3p>wfTWv*`gT^63w!;8~8ffii7cYQ}ZRw+3Fq z23UULwAHi*bfA31$wY-dEJPh1r&)Z+**g$dzVY+-dr)2YG$|X@9~V+#(e#WFY%MMtT)7bdWH82xjdB~fm?Y=$5&=LM1cGf4=8I~dsH=%>U$~?=y5d} z?KHWKOad&{PCvg;{Y{u(P+~m9KYBX|rM39dLMuTaw$Si8l336i^#Bv@TyyX+vkXSF z+fQSc!+5-iG-!OsDpvP_z2(F^?w1Ft#298-1VnAc)$U_!Z=A2DK z$yrWYBTRK^T?Yw$220-#;;1?JQnB~;b>|K^Tkg-d#8x+}Ue)u!M4`xxBFW7{u(aeBzQB#u3x2?_0-)KehyLs0eP$%;$t{ z%U7BXw$C$X>XOXaXOUQdi=ltQH6dlV_f3^+s4qKTMQnR7X;g|j7Cze+NlZE=`(ESQ z`+X8rfqYcBBXRkO{HRzx#@W>y^udU+Om6hlf_cu=?9Ak0j`x=+>V?8Ih3;)pZ{GRc z*JWlNZ7us`uxsK0zH=d&wiRmqW~Dl4 z@nbzumy~Rera_lJVd+(xEKI-nL%>u_4r4N=!s#RV)=6jO8M;|1 z0V=u9i0G;7CZ9s)AJ~dm?%GqzdS!hC~P}ENqj8kuV)76 z>FDHs1tK_zfuEn2-9Y|GNLj+>8)B;cN&T|PabY?k`GuUEoTZD4i%@*5XomAs{GiLj z`-J7LkMHAC>WWhj-k+>bs9^$uVHaQ;3QuEz#Vi>Zrt~`NX&th9clafY8jYUXSP!4+N{U0qr08288 z)EzwpKl$k=iuSZ1Uu(Hq=Y>*U#x7y-5ZAZmP;kHvN{dEQ)w7Kr%F(eVq`y4HE#FOB z$Ns4%IZz^@bia|tD^)r@RFR+j8^w%8yMEPjPwGLF(Mb`+onFf!&CHaE`2|X46D^GOZ21mKj+nF1}r~`ExZIH zx+gSHXA$&>3C+#TR!H1_Xd?kM8J-eCM^cPSQ_F4^5o(~Wqgf+mf2K`*5b+>gLEZTE zC*f%I*v9Afs^+oyTdB)M!}lrpi?!ofF9M6{wIW#7u8s}6gdITz)t6tw;I`R^!1hws zVzwuAGwzB*ZJO_Q^e|1_gq{mB4;UC)%{6)yRw0Ndt5-&7zMs3)%v9SZzyas|)_|;! zt!dnr@J|%wg%T_Ip^g}jB$?;I<0-43?-~Odc`cX?p;a5DHg#t#AVRbGhtQq^**7Ce zcdW1LgXjjkYaM5dx|BI2tW%)HD+4}8%Zr$3ly$mJKqi4i+=VbV?KcJn*Np@7O!?|9 zi6+?d77@?A(L!gPN-W?afK>|o4+e<~{RbDOi?I>-s#eVJX&)MHzCBR)snE*1l&3d2 z(68?a^x}p8fy7(|gN#$*>Ojy{!p+u1+nEaA<25Qox7Sr~D)-0IXqEh?+Fh=106wNT z%>!$$13gYy{-mps+lY3~eQ|a@M`GCJdfrbZ6XP!VHIhipr6t~JmyS$+o!cbp<~FI`I>)}rU^cg5rh+yhSfw>FiU4I!47=DG zI2}hcrCzT{>yJSlM(+0?E=|$HB&eWBMd;gqi+~xe8`Cq>me;XVMa4Ct?$}eKSz-aK z>HJPwEV?b-EdNa)1x9+FMin^u5$NU1@`X!10=MWr9{tIoKlgpKr z??;2t@epB*#^lN{{7REjLGo>x% zSPVaNN_4ZW{)++!0QKLV(fGCvA&aIzZQk&#~2r+;kf?d?^tk&i4RllUAsTmM$N zKU+bgx7Xq~bZKOl)nDz&2&;Juej&k|dbzvmBG#{6(!SCna5iw~Q%nB+O>YvYEp-PgZ;rv2IH=q{t&mR{gBpVwa`_maixvGEH z?HRYe^gVK$4eV<4pDOt^@`rE~3fmU8d*8o_&NTQ9CDq?$_?@Sg3@3`9T5v~4M-6T5 z)kjo=o0kG$wNEAQWb2oh!^^$UzPkrV^PHa_a}9cah!P%N8F=eERb!v>!uLXb+<7ub z?Bh3gPfw+n9yQaJ-#F2QZm#+$r^+aP;CB{8*-cJNh=_NWu%kEC%;X4c=mqm$sva1(KTgENA$GT-ENy;f1yfXCnz*l?=938OyC@md& z^=Dwf%W>GgZwyrNRm(7e0KLc6{2RFTuL$80umoD{wpat6roW6~lRMmm9Uu7`Ft;8> zeBmkOTlj+i!kkgrS^6lICE!27*ReqF`@dWMHaS1j=2of~=@dgtTCsysFA7qIfxymKTCb*h#vL5HBqTOlSszBRlFK_`Cl zL0nqP{p8%IlEm=q^Zg!B2Q)?^nTTS)2TJUY_I3@s&;#-D7lViEecWi%!_Lk;S8=1* zo*su~7rqS5Uc*2Qu`1gK&yBkVmveyFdjn%q07b^SBf{)u&(^6A-+4`yzx}o|+n^~1Rv8o|vD1;M%z<0~hl+^8-?Z+6 zZ2S+UwXMm~*6TTXCVLYZS*Zr!Y;C=Xlob27@H1W<`W@ zgYMfRr9ITMZCo}3na4&!%}{in>wg+r&;O(mVIND~uEBE>%kl0gOe+4V*=;V8F*sg# z_{cc~68)a`Dn2)Rc{D+ZL09WRCg#qtbu;ED&sxWY(#oz>P(sgB|AWecw9TdYIDChl zFEKi(=m`>okx+bpU32qn)w!VLvKVDZNQ)W6uOy&&$KKvv^DE$t)XOrvn-RU4u>X|Y z7XwKz^g2DAfk>;=q~YreWR>rsvuh4}GXV4Rw~TOW)D15I z_DtIXFTYx#e$ETf%UWph(+#}rF)byhF2#Jx^mO!3e`&IuRxOulTlX2L9(ggsZ?3>K z2Qg0hv(ba5VQTzDdf$X?CPw(;7Z*Eohd}=CTV&w*hw{G27Y=-8_|cbj12O&6ze$kB zq0cc)0)l_v!6(Y>N_Svj+v+2ZH=w zG9l|GO599c4;y>-Pv%0<}bF}g6pG&eWr zdh-jTt#IoYRM{5XAUC4?gb&420VFn|aWf|82B&?Q}hq)C@_ z&d>q^7D^+Ebb}x*p-4+3A&qp`P3^iJPyvQtqvxdsduzJs`mQojn1|+HE-S){jB%n zpt1Sg-^m6nX)WmtPbPfXX-wh%N%IGs=hp1=yK54!wWCLlw`ApJP4o|JS-9R-40!@f zN}ip#MfUn5rb&-K>x1qLFi4c^9*4H#&0r@|oGWnSN$52$UHmWt7!RERv@1*i$rii` zZ%|x+bV$*@6&)SjGKslgQRQ%Y)%~6q$BLb$3=U79NOUUE%}LEP_RA^8{s}7h3FHBa zRhP9h8-F|uCHF#Dt1P)LTw_G=BOR;QI-PM*p?`yl?&OT4lm`dQX zD`By6v$ztW9DtieU<%IdC#oFe^*v7KfUXcD#}x-CVa zdYbal(){b^l-%p6TMwRVv${I@7o?c2${?})t(!AluxEigoOw{Jjj@FfD^(t{nD6)w zH#l?RO$X2Q40v(B;VK^SgiI6~bTP_7XYI+8wF~LZ~Z$@{5J(Kmyl%lG1PE#a< zkEa6V`bf4gcla@Jz(Lw6K077tqt>^kP-qAvGe$m@aL>T3phhK~uflQMI9h&j;5y)f zW|y8HQ_3WNkr47X!9bmZ16EnW%9=dXs)+lCqM(Rm{xrdxH*q|$2AlB5XD)T;>ETm; zC6n>)d)0flzCUNU9EA9eqcY_w-f4}X6KoTeNK zA>Gdfh6qAvJpNxIxQv=xP`=CdUMNQ;nQH$&UWV9q{|+WtEbFr~Crr)p5p6JxeHw9D z!rktx7lWN-GXJ}pSG{S49fx=a_yj-+JqGDX)%SrE#1BL;rj!C?PNIF%Gv*9t>_S)A zH;npZh;grTh+xl<6%zH_*gdIloV&Bxx-wN&>DTWyoWh3a2i5-w+)N)q5@LJ)9-b={ zBq0XrBUbiFVB`S~l@3#C{q5t|y8OM%DFhI9CW7DbqvmHhI)lb?gN<-z3PJ~szi zqUwg!nzr}P4p}oKanQi8k!-#ls<8T^EM+f`Jk_(Y-gG~F=oG7{eIE1iXn+4FS(z|? z%Q4huLD2-+pua38@Xy)UN?yOfoJP@8C08bTKyMf7S^$ZP2b^giPnC#|;rQm?Kd~?N zcXzXWw^|vAH6bfP^yMo9lomr)6}{1E=zA{)%-}4{rO%#4JG_x8dMG4s^%T+a?HY3e zdnG>pdO$YggiM26KcbQT0*z!g^7F=iOP0*zkaJ*dCypTAG~JElyh}-P zN}wB%oNjUJm-Jh-PcCs;XYEZAy_;#j+Y2jN*3MavZDiYT>isP<*3&Or{Pi zu0Z@A^^>W>e*d46Co5$br<`T_1>%ZgZ^Qnh3l z-y6S^lM+GcLUDZqeUeus-wp;Cm*^R3D`B$Lvxc68lS<*|Nyksg4fE_TO*6i!&%E5d zuz6%D9H++l9NsW?5PsL+lk2`(T_;WBRXG&*b)HuVzm0Jr669Ci_?O=*9OoLgDfieD z9Cu5fYEv>x)v&W5(nT8sCur{U5Cv1)9U#|&XKy2!$$b#Pv7)b4jD{FQT};GQMoNE( z{fgbaRkKjrDO1*Q&#hXDT#MhgZ9p{B=GrpGit4Ttd;*!#=sms|;T)g_`DX3rW`U zbl2``!sB;WxE>s(l52D;M@GJ&DF_mmJojcaM)EeD^)L2IUPu&P`{PDIN=4HqJP|Yq zkH30D!>SbB(7*)e>$wocoE=(=-)Z`D`WX>7UhS@8Z4|#=0sLHZ`8=Q<6YBuCL||=) z3RK#01WP-wZ8?9!p?RAR^=hQ}iSq)p;o%v__n`PLI!1W2UlB2BBMGyN8;GWgcxX#U zjVqrWeW|+Hs0lE9d!JVaS;~kuw6F~i=&vTF;IO!rm-8o*!mR%Q43po2mQ>m7aD(vQ z$m2Xms|{g|^BTm>^BM$@&H*QGvdn>L68jmzuknAWQy6zz_{3(at6hIS?|~j_=lbucc~>~h@}cm<8R>vilWgGKjOPxLi_Qod9AABGMbjfSc897pE4{{7 z!3>!>g56}%I~JT3QscQrku(AOsM?$^?z5NoFbV4Rq zfXzsAZjRffpOzE?UmxQLh~J39z6V!UiTkW2eO!eLGlWZHi+&g^`|S`m@J+E~T zWDwr{`NieoaO}vWd(4Pv)SIKyfD%J*Dc{(V-j>#IC+pG$-Gi93qX7M3(|dKNpGK6R@X<~2$0DMl zL?!(iLuu>L3^#A27F0a`y059PT7<+bBdU7>_ktA3gW0ZZ(WKQ1 zk_1`Mge$+Ed@kZB0FuF7xBnT^Tdc!%$d=2AKB}+QP}L$+V)h;}b`GfYH@t zehl@nDNGEvn**PK0Ip0&HoRnMeO2G@Pya9j&r`xcLQ7K#`)P-hKqZ7QW&O$cy2&{A zH7M_c^kvEr#2_OMZ45nn7)18QPzRkfm`19=Jz*!HvPWLjb#!*R^ZJdUp0bxwyw1_t z!G3*iXTN}VN?+|MC_xOA!w?F?BF5){zWrQY&-A1o)x4aOW_erCAlX zp+)_)CBLoNpFHi$cHi+XW3i|4c59Zteu>`BhSx7k71{BH`S>t+u{XR(ZDV1Q)M(j@ z?Lijln&_m9Y95S+Q9-fI)}B_%AlyBB9kA)@EHeSJ&K zF=_JQ?J*sJgXh?SZmWj0)G1?*4{?uZW#)gw-wL)=#z>r61eLf1sxr8OtbVNn${=sJ&i^=hBW|wsl#E6 zjn%ANRe#$#-UtXvJ&-8RnqC4Bp-d^ADnVJwe1x%Qj+iABnW!5>r=;1<_ zmKtI9XNid#?WsU#P78izGemx4mua=ZaS9y6-R~O?GRVn@qyktYAaDu!mVdaZa0hF` zqY`t`!jwR;s7VC7NNbU*lvG4T{oCQ93$a67DFC`javpN#KAc;4-l{;Kc#&axP(|{YFN`D!sT?D}e!k{1Vbm1=Q>z+;cw<5-z+@$ z_1nZ;I*iZU08%=)rrD@u3Xb;iZ z*_m5L4FK9Hgy(Wfs9U(?ruD}zHmaYma%Frh)iv^HV!!h?J&M?#$RxY$-P+UFHcVJv z+=m}xpO#v^3}>HReE7=Tn8jN)hfE$w^htHMMl{@&8Pj2v@>w(fJ2riv~y+$Vm{B`HAG^ENXB%jj)@Ja=G~eBpJuK%L+zzI>kuW<=27in)h#>j z(a^(em3Ys6)wb_-*Oh353pGF%{5FMM6nLSOKnUqNU!ijV>lO9{!vG7~XwlA|Qs*7X zHPKO^m6jZR#BcZKN*Yrle3vSYQFgvCU_0?KR;<9>P#gL2dbqOZMi1Kn-}J>X|54u3 zCi9Fe|Bv{uWp(nQM8t$dxF9(>)kH<0O=V=^1BUaGx+ta6sSP&}0K$<+>)04qH+A(a zRQ^jh_Lk6BcJ#6ARShu<{2glTjcMML9{77PPUDFwDRj+9?3uI<1;--gX-MuAfXwF3 z?T-P+#8gtjbaGtq=AAq|fo9-d?~4bsa{~3>Ri1&p?*E#k669!%r)ccV{?&w+=!m2; zq9l0`Sa~tjOLMGyg9q)<7`eKjsoB;wq97)PxGd|$+|0?%!>4DBwY9N*42o5p1c6hm zCUjE;HgP{zUJAd#>*Ucx0(RJGVU_@*aK@VDBPEE_}jBdC#&dzr)kl_?5msfg=_!22O^wNdFkg5m%_mkRZA~e$SU$9OF z<9e7Ou6L$P9#rDo_qi;JcU+)&0+x9+ZhPp*6e+(K=dDB=Sj@9_wrZU@LnBUQfwT^w z;-uOnLj7$k)FSvlPyYqRI;HiR+7^8c!+v(DkkCuVey6ZN3s_Jd_f~l(g$|Z}Qxy&I zb>^r8aW>T&Ldfn^`IeoVfW4!w?(-lE4wMpZ5H>^xDg!)AsTv=kG`Z()=b;|g^a%q!I^o8dnT!`xJ*#{}Gif{HJ$P z0b%;eWa`-t*Wt0?K7ht-YRT@?!ciRAA`fzJb+&o{yHawb6AN%2+MDEBz zKG2Y~CQDOMt548vM9ZJZHLx}V7Ygk!C6IAhu^siG_^|phvOl2OL%otS?%g|ErgETg z8gb{GVTM$1u-^_-5IWw;chC(l2o)I4BF+0E?SFu-c3+Rq`I%=Gwsdyz`CCqYAfc3r z%FFkpvR4XnixjB}8~GNgA=Ct;C{&d|1k_CPslEVR_B5FWmX#m)*8cuC=xrwUoVmHV z?4wR~CNWj>fcndW;l$X5mk#B5c5X|C%RbYBH~Sg9^Ypo)<_JR(2hxI}LQ~yu9%HIi z2&m?)ty|ibiwQ`poH0%@foU~CRw{6p8iZ~3H_y&SP}FB;DiwXb|2lQp9L7<9Ud6Sg zIsWLeGCak&whwASE~hmt?~2!WJ-RUAP7O+NE3oOck<3$n z+7BDUn=^HV>Cj|kI4x*rntk%A7Gdra69T(G#SiuAi@pvG4%~8Ba%#eF9d1Wh@JWu3 z^XM&uiUOcB97XKKmahUQ%9%oFb#-eeRce+a-(&9Y;0vGz5I6UZ{i^he>KBL6&rxSf zHn!zsTj@(8(-i1g!JDSH6*z?d0ChT^X}fa>Aqtk+$(o~c#oz{+-oDS?nw#DJ$G znQ;)rOnUx}fc>R9T9}qomC|p08hOj0*Mko4Bu)UF(hI*b;-Wf84``(NZsi7l`!NI^ zT8|x0<6$woL5VcQ^tBrW#Zuc(VV&f}AM3w$k-L0=*9S<Z>ilJBUw6aX*!+8CcIzwwRWF?V=55@+G> zpppEWYQ2$9*&yUB%v2^eJ&^>p2ZE!(*nc@p3}VGRw5_MG)-*%S-pVRAba%PDDQ>Ce z0Jsfp_}>NYmCpryv>m=c>lME;QjmePLks@ksv0ocKJIg34&lPX3>vYzb9hS#8T}n|p*wf@G*hMY z{d%35qpG{jR=0OjRq#_2%pr6QRq?q|K`|^UDVE%@{liH5wi%246iL{+Rvo&o8YDVrz9L`W^#07i%v6l#15_QJ~~_rwAi1g8)|!$(YF4tFJP z_}(xu(DPJ0l{2tPz5r~o{!}bge9{C~>AgJA1Vc9}qnh1wv zpHhEUFO$ks-mYx;+?!MI6dJQLul2n)M@!2i9yf56C{@z;AeIrUcQvpZ9Fx8r z7Lu#M#b(h#oQ)~dlr%=ak|Lt>F_%wz?YB4Iv&jjc_O-lq+fAo~V&P1C!ac^{(nRm# z2A2Vv%VlNw2GF*Gw$8P!-Xik$leHwL)og$ogb|K}?o2Q=r?Aj`fK{GS-sO$o(f(5f z^VQ_ZMQPqlg=$@GkZPw39=_Dl(xPLk%P}x85Phx4?Ka^@Lk3ER`>J7`yb!a^d3APD zwZXreL~V{kDY|)nne`FR+Mnu7*#d6`UAaa&Bz(cIhA6N`ETU@c_+#4p5mBJwX7M-d zb^U!8)^RF!-K*1&?)BLM08z#dZ?I$WBKv8=FMk<#Y|LRfJ*~RtHzP|rBR6)ID(=;L z<3iPLt-uGijpwa&hSKA|YegTmK9CZRGr5Wip+ehPTO6)0{F-hdxgKmq@EsS&Cw|k4}XSZv7F<(GRb)*Y;g^sH04@;VN4)o zGhp1h`|<>h@V|d07vDAzt*HTu2aM7^wPMS_Ru|j|SY)<#*H)%J6tj!20Pm znXfzS?(XKTNRJkCob8=!k6+E3Th(%A=P`Cx`I&=i1}tM9Z^pILrj&wo7Va0P=~olJ z?xpLUJ)}X-2YH@qr0_X>^gUZkHa=<<|7!qHWezVkIaQSn=V}LMpk})iQkd?;ql_@2n}Q*l%-$^g)4^)R{wI{>-41;JG-kDv!1L? z28HilIP)_dGDg;KU^h#HfLg#GMvL}y7`kg+gU*e+Z3u>D`HE0E1wPkVZtR%@YC~p4Pa!IaFO%n*EORLURi)N9S_o0P4>B;Y(L^*>XF`00X3&@yb7TU=^_)}@n z(4CE#q2B)mQ1GSM7}f(lj#rHSK;!F`&i*JD`C=;7pL-`7i`ubYn~ZlVP|4tQpI7C;85^T z0Wg64e0BSSayPfOQz9lQVUtW#S+i=2FgS+vVyBXdMLyNW?Guh7%JlGTh@I$5@d~dv zn`PhGZNap)<682x#=R7tqoUbYn@Q0S)MPZaoG2ZxfICYuzo)gw-TL6UYu$#93{Eie z1?x3j@%LpLTTTIKbFV7&fItc^U(Lg#;__`*WhP-l$X+i`8E%`19h#51H+!xU-YI>* zJ@s^3T0fAcc{hf)cKw_^x0soluZHHu>kCVB5O4i3v|k&Z)Q)}IeSEF>SS;^Y+o?^- zbLrPF?MtS3`g*175JMioxo3HcAC5jW#b`0GvU=egEe0^B(P7q;R6o0GKB3%(zha>K9qsjk=K;* zL4iG)5^@OjO{UQSwjd9n<#6zoJ?)`#ZX_uq?B3bB%k%k#K}unSCuItR44>2EhW>WJ zmtQt8a!&3(4mnaxD$p&Diy>panX&7CT-o^c0^OOo-@xFY;@1^;3B5Htabg_h{5N~&xq_WO&}DJ zZd)nsH}N}X?e3Hiv%-fZY0`+FHXo}nv&!;Q*b`Yi{$(wo(3#Y@TEE@A4gi8Kz!w|j zK6k#em8dQgFE! z0{}n9)~Y|0EG+PzD4DFn?EJTYb9IfM_n~M*bD-g`> zs)fS5yq%S+CiI$wJ1@S7dV;*Wk;Cao4NXavqxNIM;AFCbvg5&ON^;vTcYz9TcrhhO z9vk>(f^=h8V<&f_>4_jk(NY)2kl~ykVPQ}wQZ_9l@#eggea(yz zeL$zNdT?|%;~a`f(l!4_9Mtx$wv4D-+0OTOA8&6xEES^r=X{{&-;Ea&wNGOV!~lb_ zJhQ9P^Y@T!n<^Vp-kBDHjdv!PXQ}~Y#7kf$xwbb-wV)ohAGfc>&LBuBUOu}Xe}WQ6 z6g<7$_)<2B3oxe!AQ%PntVVY*7ltjc#})9e2p;Q$|`Df`S?ZT%L!_ouYaeBGZYK5lD& zyCtnT%31H7oR?d8pU&#`21o?IS;{c%>TCtSu zsgNuQsH~ie2RZ@ZlkH1L16{=ZJ)6Pf4DhPEvMt#!uQ!~eX3RIItM;$UhgA11YnrR7 zZ2wGRy zKLC*Ue&mOXwQ$d)4$2FyJ4620M(Z@)#@mPRn$mw_mp;LLm>BWZX9V9$u2&XAla6-KAo$w>Ks>O(F&zUj{}v z!XoqbD8zuTNAlD)nP2X)^q?Rv%@`8Uq!YUv6cnU7`t<70bRr810dC3{2n?hc5cCBt zIw-)D)qI4iF@A)zFtD^B%o3gUb+N&+t>khnT7*g zBc@wt#7}L%%Mg9_y#Inp&@5eksF|VXEq0`4%n$bSr_>87?J>THh>^c-SFc>3qPDCg zVi?wa@i6qKi8o$q{Lif)N*Y6Rx+4Acxw*L=r_A$3X%hA5NdSJ)9GO7*Tv|rHV z)InwKlOHQjfAv0yqdq;2fB(LR*w4=LBXB{zP%&rCaxLWP1%>`>(bJ>xn@+Nh{HN>mq9Pb|F|*I%M#b9%aZPVrOSj*GDM#0VF6U zkz2mIn-F=tq#pvMrJ&AYFj0skN_-1@z+nr0)z(zlUZb>t2k8LvyCEMx{oZNCZ&u;V zqg9W`pJLk0VjfJ!XfJ%^t|P7KGnV!g=ahjS|nA`U1?C!ia`a35^as_48dllL! zS5YCySrGD2yefAo(}b26i&L2s6l4hqP94H^r8v#2c8jjP>-$z;Lm76ncPX)f{hqdt z5}FZbOzf)9^XtTCzh7DqU0VHTswvFxnlKD25OpOGBM!f$#+c)hW1c~Me9&~^w#!b| zZ3ziK0gJ`)0PE?Nn^nxmegfPi>?mEFA`1cdLdfq#m{FXU=lscbD!ssa*hZH!DKeOM zayBN^OKiA?<_uuUFUY(q+FcChq_BJ*hA75I+`)%`9n9bXf zm#UzIAP2&)l(=w2((y0BzK|y^0ass>l)cGoyYS{+vo3macHfSn16x~CV<+9ay>(-O z^iE<@Ki3Js37Vn5(&5qwJK|WMd|yxFoV%&x6Y2UTe=jGXbg2H8?JM!9Vtoq&(d8~N`BxJr_{62hgbbEO4GvIilAkSB` z4-jxVCvOiezV96!QG*7CZfnPf4XS$&{2Y7!48Iih$Ox=9#1rtcl5q4y!JCo^kBXCT zedyzx5FtB>zI-bkh$s16T?adUK{TS^k!3Vo>?m*pnRdyC^sz<==3~&=cYIZcESpd{ zee=@?1Nf#}1TwR73CK^{lNBSW!aHkJ)And#s?D4u3aWfNa~lDtcES@kT&BD!DVcu=0OfA( zOFKU6+11q&u{t*nh#OVYk$N;vc(DdvRv6Y$ra-Ad38(0Pa508u5xDnlPfF|FOpYXE zlqc7QguYYVTY9>&AM#Ef<-}*7`R-7;&5nz8+d#naYlZFJ=iddmo z;aIeu%0<{2f>9D)^D>D9V*E8-jYM40lInb#a#X>3`x6MViQmkamDIpc+yc)zCXLYH zhVaOGA2`(ADF;V-Ud-Bw>K;3};E&I)2xd0x!b82`c<#q|G87nF)%E;kzr-qtKJFn?SAlibkAjiI3 z(W#y+?%a6lu`&4#Bf@Y@FvsbPxI-rHcX6^>M6Ix5&hw!F=!iP#NF{x%aj(VOzo)x} z^)?O;8uUl_v8P%KZAa~K{-Y#(Ucf1Vr6OzyVKD~${PtogR4~lU6B8WvDU6Hiq~ec% zu77IG-pP7s##~^TajfWxPu|~=AOs4N_65tiXfTr7cp)x7r|(GP4hfXRy~&L5rOR?# za4a!4Ht8Qbu9*lp^XtAP)UqBQi$I0H#U36_{WxSsK*cr&q_3~yj7y=14G2K|y?X_3ia&j!%P!)zFDmLTW;K^b=YwZj+j}iB zXJhiA$+gySB2cLVsQ!;Px1#<~T{M@4a5rIas0smRBkLa>vGwpUoTq81pQy>U6TM&R zN`A^F#xlYdi-y6m2$8G$)|4==LTHVKv}${vd-Cp8-QouvUhOGs3qcT=b}BUF^zpk0 z(i7!G4ZWYOsHkvrbk&nXPkTe$!Phaysd6Z?7K1b#)~KH+CZCMxVBUY*xfVXi;?;nO?M(BiW=e5ic(< z9{)?o&200FoCRZ2HU}JRMrbI@u~`hYq2JK?5YmN&cI&^s0w zNyqb!55RG(b2zfv07!}*E z3<)0YWP5x2?COozCBZZZ&{zl#MuPRng2nWsw0CX~9|Hr5`sFn>dD{!!&-PhmBwljD zRlp;ZU6yq!hDqPQ@N;w5x~+=u|MHB8~7ZoK|o08_sIAF zkTm8o+v~wH{)H!YoxNiQf+7;W z7<`tjQDBGA`-E5?I8yxFIMbUwP)M1EyVZ&IfrSGjSH)X@K%dE<#009*?r|f%_t+2* zN-z+P`g}zYz;DoHxjJC(5jaPFx|rYFGB!c35Q|faO2nqK#ohzmY!>CGRZGYCD7KXY z#Iq!ka{47Vu%6n$-0qNr?E-=!bhv>XH!5bfAqeiQ~B0)BqY&NT;2?fH*jkl(-S5RL{6_EXWX?3@3tpEq)T|No~(4(K4)N1t9W zTl>)K{nrarcr{i4h8cWCRGkxZ{Mt5dzG7NK=J0=q8w38=@N~S(;Yeva?n{!Nh$KHo zo$B9^!!Zrdph?#afkAuUmR=XOz=T(v{|ymYLu(}tS-mXVp5LNVV=TC_Yu$^@d={EQ^3($s44>~qaW$|zw@cr0M>oPnT^M9?K%pr zHH*y5Q^e6^R_=pEAVveMXF=Uv0NAo74TwPm8fG^Vvn^V{{3jKlw%p x;}LXGWB>I;Iq>{q9-*@AyN{Ir+IL1BNC$bP5#{d=r;n1PJrZQ~Z1r^IDw?fkj7)MB<0E0rWG-imdn>em zix6ml5&mSk?@PEiNpcI}Q7|ryqzI-82Jo>3Bzw1}4qwhxdX$rBnWA z3f$v{OQx7iMvhI^pGa1zNk)8BpO~clLye7tmW_ppjfa$N@9+mcoM@B^Sh_~JC^_sW zQvA;(8Sop=9G+&RojrnZ481R{%bUb&?sPV8K!Suwa=m65c19WCp*DX?1EnOU=ZCqCTY(ed3UEfd9B;2(rC^~6 zPg4p1kM0Qf$Z5^Pl6h&noxv1fQ03RzDKM%rVJo4z-S!^hEaC)^_AoUzLt^;GLpGwr^4%^)?j=b|GVmdiwnYr zaf|(-R<}$I#~j1En~x5}gT@xIW0bK};)gF0gebuIcF=dVP<1L>4|L});*B~F^ZymN zE{YK0P|<`5^Ve1j7N+_RV$SF%B%OLQ1D+D468gz|6S*P-ap09!=b1Yobfzy79%D1i zcFK2_UTU?-JbOV#Kf4JN`BkEM8x*tj$?d`e4aN?@78THVd!3rl)Xkvd(Ae&!;)eB$ zXL#lhqOzHTS~)n=q2jd4r(rXKr5*4FAEMjS{In@ao^48i7{KxZ1V3RKmaIg3sVV_2 zcW3`$AsF zB(L^sV}Yriew8+A=_aA0Jrl{`7hE@gE+_b3Ib07|QxnK`#-j=FIhT#txZ^uXn77DK zJR7^|-;KGY#z6mV>lQNdPh$D{r^Bz$7HBPeO}MJLxqdtO73;;el{XQP^xsMCK03+A zr5Vk**(imPT9PD(dU;6x@TRhuuO>9MFV8o4O`gfthx(wsj8c6^>Vn(;eGX+l;UCX#Rz77P&zb9BzwuUXtqk z;WjrqWW@t)&5*WmL_!|pEQ#31@TvJJBS!5lYWZrlt(WN3CVd^fNjNKa_=04zT;~Mn z|9u*>=L+ejr~7JrW6biYy&t@O;nRZTA}W^bpZ>2D$Pyxr1?o2NdI|{4Zdm9I0S_K4 z7Id*&O&yv2Rn20Wp5dfy@T|hu_J3-nAy*>H&Bb3ImB4rXmIhU|vr#`lPs1fKIsHA% zDTV|)1jpjlE^Br`>rX-YSE57sF=vdxDLD_|EsP ztM_*QbqNf{O<~i5v|uYp5T1MZ6j%q(_R6DFEqe8{9Ez+;-^0s2{j4ISDv=+j$-B^V zYgq|!SI+mrdhs)+8+5B;nb>zZLoGtNGNU4^vdf7b%ypqq#Qzo)mF2rj&lG0)BHXS` zZTL$0*}o-37&yDvZs|o^bz3}+7{b99xbe855SxC|e>RY1t&iYK^!K(S$wYC}2Z8m% zn-ad|X$0rb9$r)2Z5)-1Jx5Yc!K>#S!ud~irU-!9kma(@MfjD_O00g;6VC;I{pY+@ zJ-Y?3^gd{pFZ~&`J2hqRt8SgE^%p|(`pqMnzP-k`lULLydz{S%IeQsPFZH4-iB-mf z6#p%sqb);=*w-zr=hK;Qnc`;$2l4E}(8T%5r}6MrWnw8S=90$&%2#6=PG4tfgfC5k z3a*hh)8rgqn&me|?zi;r$jLUla^R=uu2(TH4Jkxuc)-Kc)bN%n22?Y7SA5+K+$5jKQr0rk;g|_LL#Dsg=(YU_xCLoy7gZl z|9YIR#?Q}d>DCwr zfiuy1@{4HIP4Ptj?VzjafWroSe-^t6jrtFN;%$Z2$EB+31-`wf0uKm_#rRC2%!6D_ z@fuif7%H!=o$Bi0ChOo(t)ZbYGB6OVp{XgWs2GichBlTe9ZM9ZoXta;pPzqu!wtRf z;ycC^x^E>KssJAQ#m(SuQq$N;bnw#;91P__RxO*-lvkm^6J|bOi_Q8k3FS#9=F>Ui zvyJK|swv7i9yq|J1Gk6<>8576(BnKxR}lJcW4ZS3RS8vm@ z$L{Ac_Sp29B=(I-=FV77t7=*8{QOO^3wqQ{4BGN> z`VktsJ3Scb{O^2|r+uQ7IpZ-^omz-Hamey$jGy@eJ1NC$(U5p=o0wCc#E8LP6?f8S z#@zTq0eCC2*wSUvzvQVLK;oHN-l|Yl>y#dT-Jnx=#t)I-h z6M{ngF)ZFJ$E(+Etw9lp5NMX$8X6@T7!IoH$;hf2tz6vZCr)E3LRF!H`WAXioPsDFATcSE>>$^aDTfY ze8HKg*RC4b&w-rsGnndnwY6J3MBuVl9UL7c@;5sWmj~4AzVW4ydsKwur$hxcN+QzC zcDWZ>;>m5&q_`=M@g(FTBy|GM-wb^@WaDItQgKMH?OnO7;kwR4G|6m_W-vDqW)$F9 zojq9*e(-r-7GS>KD{b2t$|l?lJjK$NUIz z+aFrzJ(aC1=Wi6+k(!h0v}~Y%)^(f658SN+B{c#!Vy-R=1=z$3El^(@E3NemIlEvBH%7AqA78c)~-J8~U?xqD^P zSjQ!CxymUp50pE`$~4P;Qr9atJSPqg=gR9`_9qE_TY|(-V(FgqJ1o4ueMCsA$5LrR z@;CYkE+CO;O8l_3aLzturOtctAP3*>xEW}--l0oGOuW5VEeQ=pJ$HFp&eAA%$GZQ{ zZ!^$>D6=32aC}6y;S!<$$5##QXZy)}HZh+NZ|x)6wcCpG8uhfOEIj(Ry6ZA4&|B~& z(5hO$%PtTGpYaPvJ~MTZIk7+dX*mr;{8B~tdOkwh%i4=9Mw0DMV_iCc)9yXL5iEDJ zuYUDNG%>gN!F1vD{5+m^s^n6vo(P{y2_l<0xftWeTbxfx@tE__GkC4E8ILYpz+TwR zko8F}f&$epXHKMpnUP_q7bWO+!q%P(^Vv^}zBq(2@#uK1-NX6xSodM=3?giMUqPOU zdmKy7*c)Lav|sH@jSXWPO(#v6dyL~>(+8emsf97j!J&3SE0uUCLx#Mq3u1qY2C7s| zF#FQSJhX*y$o)XwxFW|ydRl$Z+ysuvB{xtrpy}^d5g`Ul4Jl?&?DntkD{Z3WD+u~t zO00KylJk3n=4d8~9yUgZWMgh*Ttd$1n5+EjQ~`ZRuepL)duF(^(rM5fVQ(t( z{CxJb>Auy}BTf+ekHYDRU0oB!EqBrAoTdeB~0WggqVQ7ug4(M$A zNfn{*M($5VpBvRUZbl6Y5z37wZl;qXI?p-YjEsco^FR#0a39ApMn2agZe{1+YWpD= zJ8|rW75@v!!X@-H3fi*C3Jv8ch=p1dJUKMx%|dv0~1vK>sX zvtd_Wotm6o2t@~vKh-$A`SJeE_=+^1>impa9^G@L-F2-ah-!r3{&C^(tr1L z6gUa9TlpvfOT>ivt?ZoMJ_iePbit#4`gx;MO(Mx{f=y{$ipk0&MC*$VNjY;bW;Btj z!oH)Us_xFcOHjxQLPHB57N^zK&kviz6>IL1q3E z-jybQq)FcK(CZu3GyI1SJDWl9b>E)cjlTC)_3AjJYE0Oo7M@eOF}kkTWA@CRDIs<( z2H*kMJy|gP|-1?1OeZR52$3}{wy3Q< zG$;7|j&*jv0NTFp#)`zft%U=`qSV`I_XXl#SE_mMTr;7J+1e3uBBQi`2aHz}F8Nil zLgyBvE-QyLKMo;H%LAyI_u#fydZ5y)l72<&k|H#N9$uu>1pKw*evY&!B0=^n)j*Xz z(Ya>usnqWY;7lnrK9FNq@;Sb&R72Rt$h?dkVh@%N7}S39N| zEnTqqJ{qXYz5hrWKnT_=OA2N#2dkR~kfbDl7KG!G@e7bCKx^c4A-Nh$qXW@`siBZ- z72rXGGiD;Za!&VNz4Q7@WLaKO%QWe|cI28bYycQtvpY=I96R=XZ*lSOS%@-zny$)^gH|h_4mHpVABSKuN(s6@<-LYpkvZfe}xr@4;%)0Dxm6L)+0b*TQrMB`hHsiS?fF|`*vz}tM68jfcK*Y6&01ask0gt zHI;((MobJEdsO$!P4(s6f)PEe@vJ26%#q83`@QH^jNnBZ)9HRnAePECl1NvuFgEo@ zl<+KOPWj$5546)|I8BVU`U@IkhAHPeEY(ClSupM1khIK=n#7@U*~9e^RuEcAo%m2l zb5B$d+&@3KB)*|%Il3uEGkj@L@q2x+RL8MeTW*}@gZPb#3i0xmGW_7J`XYpTrvOc{ zTMuVZXLP5`Ey4BoL40FD)(*1;7D}ct>S={Z<6jg0L02E*PR!mN zuZ^{J3<#vHg2y!xPRu7~yF5*vWzw~l+TSY(d+fD6_9WNC;~EVL|6sNkTFR>Nc(hY+ zZ6HYdDY|jG_WMPY#zMQt8Bsv1--{Q!RGleC9t&!fZhI4g>dj|0h*6GyhwCT}t9XAu z8y_DXJlaQ^^ox~%t-N1b2agzYTvwc}B~NT)Jb`ku`$tQI6(F2Spvq|-riRAz;eQ_;>@oy`qF43F3;CbL4!*gE(dvGF%N;vIRWJP*G7Y|80ube^6?YNJHOL z*?>k?F8N}m;xc7C3=)%xhvOPdO>KiL`PnsyIY z0&ot^5}8lB7SEfIG$D{~5<9{Ic1cu4}ES2iw)xL z+@8{ZNX|`qDgK=ur?c0&pL|J&&Qm!Qm%VwK+(TZkgm;gsySXSs_uEL%nT@&_Zrt3a z7Z|hYxZ)~ z{%|S4pOC;}W%915$e>tr(YDXM9BbZXJ8-}Q`(MtKy4CkyRPH|V=;<`rN-Ct~9?xq_3 zAmXb?%?t}txnxM@+f>ZHsgX^JaL$X&tPr)X-1S!*(Al@;2J0WCD`#F6STrztKsu8zg{I%FWN305 z9slNu_3S^P(AQw@C?)za1@(onaPSahIF4H|;5Y}qy&Ai?@7w+RrJA5(HFECA#X!1H zZIJkh-+cgU)w7D55`Irn&YU|1nWPx&9jqL{MhbHNbb!_lPS$lj1f%b>n|pdN{w^k# zX71q35|_g8hXKNc9wyA@|LpEU0(FXj+z=SMTy{A4ZrTN9ypcLWe znF6Y4wB**G&Hf@3=ggDc->K6#7x{KeZJd+#me+1u1BgK`(%$V29s8^Q93Cz-!-<|f0ovA z(ejcOh38JWI}R%1vLbONa0oUC9#+-ePjl5p%!3+H7(3jLWW*nOW5ZXQknx$)Py$Iz zK3)6xNw65_s&V!keQovWU?6amQY z6O3Qc`w%{|J zJRxK#%ywmhxb2s5!tdb&o=?~K(+R-b5iketCJbnkz2peYh8;BC?q^&DdZ$po=gm;- zUJw{2{#9SPhHM^bsK)fcd1^d_aZOFlKaPSB%PCiP_YXsyxd77( zN4oE-V5^WWH_zQNkc6+_#|8W#ES7yd{{mzPm)bAIO((nc*5m%{L#NLmUW^Wc<2~o& z!FxMPR!XJKX8W~-XkxCBSm9S1$o&H3=Huo0dCdmdV$-*kDCpbe%Uzx4Z@k3ckn=yJ zQ{KuSg_)$qKA^Vow?N*y!n3QZ%GhdQ(i-+BGVuSULY{6bJG`IfGq)fQYaZF#dBTs* z%{)q5OI7@YpcX~`w1!H_$GVLIimga4)G+e)m>HZau0R}-Ld4|!z3F~cCqCno-sT)#hwtC$6BE>bq=_L*il7d*3z15C>HxBa z?@0YgXd05uMt9TGTdA<;y-6HCqvXJBLGT)ZusmQP3|XgOQx*9roVRl6!y|m#=CZQS z7G>KxeOg~@-)oRpF<~vhlS=1D8ksBCo>=6~>W}~PaF`t4a7V7N#UbeggC|CEm1}4<#Ha%FkDG9y%e3=mM|4 zp!@e6vP2EV1{d4pZC|h^0)GhSB=WMtATTQ|U+L*t7}Mpvt~hjO&7tFk*j;U196*i_ z6Y`~FAN$~1RcPz3TB)!9{UsMo7bQh@nVz(G%dK!KqyC184||GHOx%0x`!G$JPw{m{ z$2*YK37oqs5q=fMjz_X`G3*4@r-dm44AgqD-mqRAS8-fJ;%dUdD(u5fau_1&5Q2se_k>=SjT8C`- zU%Pr_kZ>*MyxvRmKF(=N=@?5Ad?IEqU^#!wm#|-Jn~OWBi9Pqu5r!CYIjq~=;$Oh} z65%E<3a_8yoJsJYHfq!vxf~J{#uxW6qd7 zU}Ef~&y!*Mr!pCG(B=)~{RwmEVRZXLO~Cn&pT#mVc`#^S3^5HGPDJ@_y**#0k3F)J z*B3ePZO<|KT2V{kZAE1tn-Kgjaj5Y?qljm}S2`B`<5wBW$jdGM;Y1C%9@Blz&pb1J zJy7qPFMvV(2hJ< z2c)AHW~!9MXEBY+syo9>-OF;a6OKK^H&_}^GN6Fk1^oV0YbwbZ9!eju9-LSuo)NzP%PS9xx@MSBI)+!HnscudjGSq`#8(N!+U8|{{2flbl~+l z2egf%`@zTKPa(VCFp>XsKwppF9*zXV=q7`#BblJU)?gYGaZKV5Hmra3r^2zdn(6DC zTRgZ@472TA04v6sT~$cdqDMt)>Qir|F2~p=$(HlZ%cU4S3|u3iqn^rfSm&UUvQ*-L5oscwQq7io7(|bh*Ix)x z01mq@sX|0W-qB+JZb zMvr)vcsz;mnZ56FL3fK(!9l-EX=QmLz8i9(?J+jeB+8GPMUmrvYcJy)rqYshYh9!u z`NjPKx3xxEyw2QLw)k_<8u7wei%X3Z$5aOQC%c15p|Hn_(<&x2YckH6rH;VY`xrd7 z9N|O#K7At@%3Nb4f26l@d7~(BHcLjbtzC>@Fi|uvSuAr-@()eoLK{QAY5v32a|bdR zhT8FTc1s^$|7%Dqb7!>oaDR+W_3r@`tG}dUuj27t_NfJP}hYf z85_FYP-uzV0xv^?xMJMM6=N!KUmJ`gjN|1c{d)hGyR|NcNWSI5eADr`n^_HQ;$}dA zAA;XJR__!dXCgA-H;gWvj_Z0eL*ijm`mTQ#$FLxCj;w`x{j;fVgH3=R+%HG;T4io- zpG$O1C*?RY`;uG9NChs?)!1CYr?2P%#3>P!@~K3>+Stv{$+U>A(g}+vmPfVyH*Ss= zqSYj%fZK%>&Y2wjEtoX|ngwb@Cp%gUV@q4wT1fucuk78Is>&lKLL?zAdtV@9X=ZD4 zD4wiOXGW<_s0R(TQ~ML{ZP<(AIIVi+9c~qG*A}uplce2?Zy4MhQu)W6xZ(PSb-O)fWxzAjc zoJJ5`xF0l;pMM`9JAZ#FI>4SbP-35$ zpsEc05sL+_F`~DeG0aXiPx1E?NMZ|7k}7^Zhn}6S$GKJBNFlm#*HDM%w~2ukaCAQ# zz>2G>K9wS6uN1oE3sZtW3f(Nn!w`1*_To(NzDpglh!l~qCR3x}A0jQVJikm0dhG0I zO=5jBaY{#+A`8w=HLY#GC684t44I?w#%tT;F1$MH)iN;MLtn1)V;@V1i2g)@`jPSbM*jzAtG%hgB%V$V74!b;1?{5+?buLxa47%;T*nGI(Dib4^p z&**f?JKziFFH1o?nlOK6AbXYbhK%(JWrcOT)z^gumIA(X(7Ee_%0Pl2*u5Fc50}s4 z%^KYh+T_kNMGW;czC%%F_tFh^?d^(cjHRBO-ZG$Us0b3SbEKfx$NTjlfSVC zcR~eL!Hc>_T)d>6`>&+*e0V<*>`h@y7!c%1-W(V~hDb71i%T$NV_xmv5uS zj+Z0tITrha1gsbR?B|2=q8OhF{Pf_@@#jx>XLp$&=5gwDRl*Fry(szJ zPdTVv)Sg$PH{{nVc&?smdpdkB`phlQLq7;T|6(J%zyHf&)@oLL9(s`{B6gHn{lodX zU-*?Y(Jfl7`AX*B@b|xZsh1vw6u1D~*WRbbS>?CW#9Z2&eQ?WD^{BCiC`4#%AbSVFJwGv+ENu~DEQoyA`yxtI#t!$bvnL+yKlN6~P- zTz9X6f3tq##__VUQ6L*Z6N{Cz2`Gc4lKJaCWTMhz4t_(pw$m5RaOR;icelHr#?+_z zSU^rT(*62qo7g)2_Bv4^(jTuqWqv+LuG{`Sz+}NpSv@)y$$e)dAIG?ngeAJn+c3J- zztGLw5%unQ8~nyX;BzylJ;M~FaEi3WEYnHhore+4nwQk!^|FkkM`rJjErf(LAcW(2 zS@wjnzeoT(kD6gKYvTU=mjaINd7UVzz5_@d@lOym$z}?bVGF2Kwi3Kss zZWGTcq0Ff5LTIzQoethz7e|q^!lgvnHd@a>bZ+o%vT-By`L*ftktJ+ zm~vo9NJlW4xp)gn80veqIM-l0ZcQ;fONfn`Dmtw^) zZ*}<5pBo9@4*~?-sKiOJfd3n7sJdH5Eu@qKF>uKaXV9%b8)+=E8BVQl%?=%~DTFlE zfOVdC8fnuv&zfO%RVeG`R8htb90v0t1r}e2}^5N4;u?zO8SFK%N7vMK#=e z{~KET^XmQw2_|x#r!l8N)e+)Z!lsa|*(#JzRme4e>+ug~b##8&=X*2l0Pp?jr7eU$ zFb*7w`sdB$Z6%}PxGQOe(^L25wj}#6G6Ko~AO~elz}LN4c=96cU`hW@$-ke1*A}2Y z>_I|HYr~0~NdoDgy!W0;YFvLCE+`7MP#K*(?5xBl#Dd*V{T6z+LPtNF{HDz|P~DbpNsR}Q47|^7#*Sj5yB;=G zhs9k5y55S0q|~P~q*CBYeY|f^Ig$JoA(WJ{NM3G@jZ}hM+Fj2zw(O}3o>_aCBKDlW z!`mOU;v#>Ne&d88sn^u^$E=><*R9B?SfnRWG@@!$G>U(Rnj-q4>Lcy%GJ)0rMNW<@ zpNH`pV;a4$(vKDxzP6m-^~K7?LGVu1I)^I?U0(Vq1D`RhM%)I>H=tCo{&Jz$h(;e+ zi9iH>#xmE!tbpD1zWWKz@;MtH(Rl2?2YLJ)iHhdOd#pp3YkK|9$?1Fqpu&flKJ9TXg|F$Z3Q-tC}TU)edkIVPQ-KTMSVmq#n#gzNLZ+;%2SX6@czCIcUp z;EWrNvJ_&XN%F`?5zf`M zPkLenZ#}^X6-n=IQy}*mPjWdf8$Klo^?y0D)E#k3#te?eif2Fww5s&T(-s19`Zswq6`7r8up?D)RHs=()=-NxZkThR^~ zrk6=Lv{nIjJxUVW|Dx5(urTiZoAGmx>2>lE7NWci1qqYQv zWE@cBbm*qQ&F|^A(FZq!x^Bz*oa4{?hSMK}(bGfJ>PRdIzKj0XL|!=jBEK3^{LBK8 zAs+Ksnz*-&JFQJq3Vrx##(HNf9GpHR^GHaoE%U99YkHaFs}tM&i{oj6hW#pWQ{-ax zQJ1Se9ekUj=qI071XH!QT#-qhw`Pak!QZJvs8|~Gq~p2$aZJ2Vt)5p!3|6)oxB!_V zIO~AS>$Q_7;Wu5h&jJK#x)@(O2E7zDD0`d@apVQY^tXRTW5p%Iy|bO~sa_>l;T4(3 z+`IDOUw_bNvK7>XED;@E2(b!xPJ|fo2(!~p7zRZwlJsD*G^A!_QF#f;Z3oNE@)a8> z6`n7GUk=L&4M4sFC#5I7Dtvd|0zMzNDt_G>Jd2hMOGRN6pZ{%hJK7mbrr%pbBoh^b zY4d$*YE6A|wocpk^yu6|?RXUQeQfy1=Q?r~-$EmUzSH2q$U$Q?@NL}IhApXYcQx=f z*85b6UW9iW>#IGS>N`G!>PGxqXi;q1(KmD7bpTf18;eTYRDjeW=&0zwFD-s6e3{X2 zW;r%=5gc-c_{k@aC`rp2?)#Yh%r~Amq=j;8QDscy!==jOGymtvQt0BtJU(?VWv28M z3Rd>B56^ID+dcWyqS3&BAegi{Sv>81m1(zBC&~3~o#F0uE#vFIAoPvim!}`!L#NwQ zAkgwF^CKky;YAft!>}vI8R;nIq*LqDx)fwM z!F)gC;G>))z}oTPcr2V*hZ4oh0DIfj9&}J*ALh0G{&i4(p}+c(9boQw-<@Xl!iyY| zMF1oOX@E2zPThAeEtYmkE=JvxS)jr`=RKK!)*6A|LuCaiLa?46Al=iwnYW~v&U)Bs z@Jund^x7$$vVXRJB?)|*bjQ8!>Y#h430KX%%jGlUw(}r7^T|f?|1`Y616+di5c2CY z-mq~qrlF((%dctxmS}PYmr73bV!;C|Q9t*{ZcQ}yzeX!!f~xZkcZ|(1kVBxY^aqo7`E651uP%(n7r~P8{KoWKD;g0LBG)D$`&W)jY(Sf{cNVNP;tFPBCEJ$G@5lNP`!Nu3v~}K=IwM4F<=r+U=6@+_C*fe0F|L=v zwF$utAQ=@3Z4iH4fo$}+8VWmM%_5785_~2^6lhJ%9??;mWowKD3lpWnvVL5sGUOcV z26sAP+z#IJTl;i=;ifn$9F6?1U>)5c_>xoe6r*#>d6ASVI)?Y-NMYcl1So*}*#)iF zT`cSSd^yaGQ$86LMcDkJuEyUB2CTlki@8KsY`~luC*0Nhxw+c9b<9w)fKLkVYGd$X zOcp=6#~l>%n8;d+jUQx;o;WAssmmg&J7lN)5q9t14#(lnAM$u1-4B#e64pYzDsEkR z{zSFnztPRimic;e)e}kQ$<+jt4PZft5IqMHEMQzwYExB&-|`!NEMrPZ-iQ^uu4ts_ZT6CMlxO%pUuhD) zMP8mRdDk>yv>?;QwR4GL=yY`JAoZnPPi6CxM=q~e#t}L(N5H_uo77{r>`!L#|6mFK zu{H8CKr-&84oA|A`+g~i`+4u!UPxdbq?~n=8)5DTD_Kv?y_~*(0OoI}%=wc@3}DC(l!NUn0bs+86t z_atCppEyq21tJd^t#Cbo87G;ZK$$3bvoD=qZWVHdNjD=qfl<@S3Dwd6o9>u2C8UZ> z>`|CX+m$d&f4YpGWlvE2SmB zSJ|S)m|5&H^LTLD#f50-x5<~x$ind#0p?9{QldaZ)jUKc;>@_HWE{*Ua|*k)y21L@ zYG0}VAB07oK=uRdNN};2RUGf;HWo!?Ow9zXyX`?~+@XcnX)*`c0l9#bw=vkHn1RJ^;!748waB)|q5~ z*M;Ya#}0*|h6xrHonY3Y{@10@z5SVGH^bX2iqoHUZf=1Xx%bD-5h^4oBh+6+tQoiO z2{)!zk+QL%GZDFbcgJmnGj~Bt>kFjte1jIH@6H4@nrUOv6JQ`*KT9eSU62BnY17k_sLo)F# zDmtS}E+P^y8XPf zEM)kn0!e>yGXz3^KHh(!qN6LUv-nledCs1ZI_Hn{;~{FLXH|KFDjg35XX?}|_-A>V ztX!*_3zmXuBR5|N#ygZ_nkRXf#nwBIni89ICa(WF_1e~14RC!e%)K(a=i2^qo5IL4 zRzlNuDW-m%C!*D`&=w+HWaYC+N>4|xIB=K2?Vxb_dP+d!00aM(UnxrBXFt4(x?TqR z*Cn;f^^Y>6Pk)tTr>Lx^Y|n9oyjHo%6fVbT z5E3r=z5&U;3Dk%$-a4 ztaF%BsJPT5shQL{Bx5c>h^{mk4Cu~cn(wm<(eMo!dG!pb^~gstbrRW_apX^XsrRgp zYx*}#HsYWn%&gb7>OuHRy@;85!a0r8!*F}(Pn$sfAlKp5?}x_<27uQi^ExBu?lzvs zqmhe68_~+M4Qqg!?IG2!n1kGhj*r=c$R@JU75QJx&HAZDVX%$7rKP3b7`|E{7R;<`9B_+N%;`zMrEJ~8dr^A_SzrdT2P&8qkBN%a5 z6?Z0+7}QLyQg}akJpC$_SPkoP`k$0Q1~wZzGKq07%YN1C8k@`ZsSY z^@f&%@`fMz5Th1dKrUX%hD|)}zAvih3%2#*?590{CT_L4iVu`oA zH6T!uU0!UywM~5WD`W%?e7bo5od%em`_Y9?dW-Jdg}kM>FC2X{wq|4i{|xufaOVIr zgKpbwqCw0!uo#_>m>}!ee>DevQ;vutCHj4EHbr2{vZQ$URcafJLI;{cpV1;o4Y|)s&R>f-(ut!mp8?w+64!cF# z6nctDA_|lJ$^~CBk_2FG@!QrqpQ@0ZiSq6#PQ9HHhf*Jz8}dMX=%c++iqWyc(?b^Z zYwnEjLZ~b=at7)7^Ah{R`Gpw`sbWqGNk@rexO|U+zg}+TiTXv>I|_Z(oR|!+%MceD z^Tu*-JP#Ldn0J4^eF`YpqBDzU6D;g{??~d>l+;(5ECdJ_mT z!1|4IO0f!PDeS0mq~kVWnYX zcb9ZXmoUJ4yx-^f3D38ikHgG9`|PvNzOKElwbo{NC8OH3{1{3}j;8feXFSaL&1urs zi`U~MZ9P4Y$s1BZBo${=h!^fxqN@df%wZpraG_g_Y~CU>l%@Ax+32#padmV~0TeV^ z57|5uUX$L7xY0kRWezWV>FyDFkYX<62t9hlB>a9sSPkb32(JDoywy`aWGULm9EBC~ zFP6~D9RJ&NW)L*p6Ovt&4WF*g|Qa9pvy?)bW{r_9by;JaRZ5%W{`Hzg|i$tUja zE49w1$)+Lo;2kH1tGOT6!JNjGOyhTEcn^{aBG%|Pgfb}Dir<;$#&*ba5I?qPxwMYe z4YuIHkqC~iyzLia7Yie^BkduzsTf;E-DTJEZTqBqny`ZSbYiTpEs{QOZz@qKP|ZgN~E}F`nU#-uOBrm)q?fANn)tYFFp$XSTBqr)~{? zyNPT%wx+AfVCH;%tA?l_@~m_Fl?8z<-dEp0`3#XJ{6X&rpxpw`d(^iENCX((7aYo~ zF(8aSTJFlz#qT7`h2>7pW%jb4BnvrE!S`kG+sDs?p3(yJQI&sWW<-%rDf_%mNvYM5 zoOsDMPopWaAo7z!I~FysyDN_`ZSJ0UiY^p#hd3o<`RB5OPOOaur9$uO-{YtE9(-D} z{shBCw2&zyxLj!`OWd^FcTSv(O*b{o)-SRHd+G`pIxgH|YtnD}Ye(^SUpeEFD{yZ; z5*`{Qd;$f75!TVYP?YJ|cHlqV|I9uA`K*jWFP z-s6d<0bLE183{dnSM+64a5o_T-WbC0onast4zJXWSbKMV{3#N*4WE}!j4jTzqbDlF z*t9fxgTI`2M72Yx1eAQjt}I03<>l31y>P- zafcEKIQH=9@98;mab0O5{!PY7JY4)S`9oJ$@wV8XuSWto8|+N&FLR@F;^j2s?~I(R zNEKAw+28$gi&s<0{snNQH^&t`xYfnf-PY-GLVcakcsgSF*+H-Fc{<3VMe|uZf(T*s z8fqc){DoIE6N@MFQD}jEk=Vw^sIU*QF&XCqj2E5!*RoU5Bf^~Ru{nI_b(46#wU2Li zS^lxpKDmbHVSLJY9sDD#!Cm}KB>$yfm> z;?9VF4xoC`cq1h!ZvQbW+`sx5)WXTFY)C`q50Cp!RX%@@9PROuZ~K=%GbFYrJ9D95 zxn_FruwI`W9gBc^jIik#M%X?+1R;Onhu06d!4fUqxnaQ3D zZH<+HHky0{#DSf*g`|3AUAOP{P~VEMR%$jWF=$w6jdHvZW8XC2XZKZc=C&&{mntmE zZxjJp)dpaSgM~WnH^e#-PjLbX^vifGn|QlIL@R6$x8H8-V^jrpnLm^MN~m#}P1%Wo zi*ex)jEz12#PxBmAj>pIQtnF^3WI1TH&TQC$!#+ngv56n9T2m!a{lHV{#=c(_vj-m zHA);SZSOj>Mn7Z3)z1}G{lkYA3B!D!d%0p~?IbTA)yJVd`yv7{KAIow<$m(#)Sl0J9 z+4b1nukmvcjX)zsr%9qz^>F+?F-&H{Jh(ytr9ghN{)OSOMR8x z7Fts0eDAk=xXU-v_8d;HA6tbF#@&`-Muij7Q4&O2K5U76O2x1b5mfZMbQA3Ekj&nF zRckp8NL{Hn)|$w|0>{}HH&QQB= ze9Na(%5r5C=HJ{rVei>lII@;?wEHI|Gm~0-MY`Z!F;>0L5cN978-ufT{qWDShmsw+ z@6be_bh9uID$_j=daHL>aKKY+^HV-Oitl0FtC#dWJ-XO8qaSoP4SZJTSu=~AaV7H` z(U;=yMUxXnp6piE7mT8kk#feMwjKDNJcir%Qx#+Jw|>&DdRiYP9U1SNsSlrClHn8~ zWTIZD6+?8)_I35YIF>$|+rp8Ypm^t!`f~N&Gec(Db+A-3CPr!YcggP2QEn|3+`{AN z-hch5J9OP8dyp??;as#lqYVxG2^Wm~g;lTZc}b^rh`8fbnx$I0-9MMH~v#Y2aE&9dcTnH7Fycum0;#UCbTVTy+XvK2)fq-z|al7EYx z+!P;}&5CkxpgZ;U=OIb^OA#_S4W>c41vqeCrO1|!zwHfT@rhSrKhWIO%;qj;^Qoq} zER$ZERb}`+SKBz&=R!tPa1UZF=ZoUD`1pO-fADaxKI8O5nbx}GST|l#cLMtz{2mEN zB3=+sfO#RHzdqALO~f1x3VKYiYW*Jn0Es0Wl0`2p!+!QjC7zrr+2^HpX#wkpci}Bg z8P4BX^U;y65SM(1p@D+WwFsX8yp?9gl|L(DcMnF`uunlxiDt;SEYk%F{VsB*aCI0( zfPrSj6*BCkMV!eQs0N55UsYP<+v^(07nc$_`uWKFyw0+(^e>>zd}OD~o??Ho%Ky`` z@-|0by4has;09aV=&_iynD$C~xA(!U_2}w(Y~J>H3{`HDbWmm+SIN&Zvj#oZqo`f- zQn^VbI#Cx4{zpF`P@(S5YZa-OagCPu%HfeRaIh!>RqywAGcC}3b-pc>uiEL0{>atx z2&<`4o3N%m4r$VxlS&Aw9@xm_$?5TdwqBhtbKk_ZRjXfBef#Q&1*1X8xqc<@DFZ4; z&XNq-=6vJ}RzpT&jDy+^WI4Ab2!%dSGK)!1*1hf|fr{-%lY~l1OCR zNI0D3Ty6?uXf_vhau~YZas9mMyYfMzT(Vx-q52?d-<7`J(Yz1br`be(N0 zi5)-c$iPtDwN{gQ@)cX0$$vC%FqBI&Om#yNYZ64JB`+!JnR&vamLP!j&y4*1BF)S<3IRmnbbYzjP2(vKC$^oPz}iKH0566a&h2{;TZJo;MuWER_v!dep!_a%eK&8{QzG)@rV za}T#{1?W}MF|QD$d>zzC8^~OOy#UkmloF5HyQ|!Lrq;WqBStPB985E?bu!Y1gui~0 zK*KojGwWso=(K@S?jwS=3$5C%1sqCeN_(bmLJ!0i=Pp3NRx}*R#y$!#Ld5ttd%}Zh zJ!GiR0$O~OGYy;*pL~Zu)o99(Z#PI&zdacm{m}Ns?1ufqw`Zj}*>$>5Y+4u8>$Ytd z7tg{P|Lp8{SF}{o`^|o`XQ)2m`&>$JuNl#5{+98Ivr)F_bCN#7Di}9?&~2@AE|ud0 zK#V4_Y{UN8+_qBtDTFtS)hX<9*$EeWsllV?16lT0hq%&TOHt2H-e$F}?XAqmKUj&H zey*QQW<0K!itjh2I{R@XTWns011~O{rH!rI?})41Tb-%2WtSS=ZLQmtJ;Zt&7Cl1F zNX{7K;iynA@&_d+{mKj(IEDjuKa%nC%+MF5yaRMinDTRbX_iMX?yFFLC&}OmJ4Q{S zIVKdJtRIWN+ZjE7Y=m}sxb*s-UP*Nk@{5U9%>7rD;WLue?OgnDBt=5pjAr~$+f%p> zQ-NcZvn0*ai{}nSsvKr@E;*B$L#?yWoE6$X7WMPF{l1HMBaMq`iOXW&@oiG*SeOec zE#?lV>W=`R+C_n6JXhk>3w6g~%#X6p%lRQxNCs(ZUYJB+vP$yf?#Iecu8E)G{LaK}W_zYuMQQ@nXNS908${9* zhZ9h-vxQH{2PAghijfbbpZMjg_A~aG>HGQ^iRF17j zh%KR}({)4E`8HdCtpq}}#dXrs%?iqdq)Q=vwppMC(||V|ZtN5bbxsl;ltDf?n3*x_ z<^+K+!^OfcVywaDpLf=voedB?LHRf|zho@#-xfJ8$42IfhXj=Ct9zGK$ICe>V= zcIjt_&&)b!!el=lA4$w%_G!V|xGAb6$wTDB3B$wiDM&I1=A@IZF*(VmJM^Q#PDWsy z6k&nMy`FmA(jFP1Vu9Z+YF^D-s!N}*b8Hv54BYc8 z%>}%=WD0@H1l%_jwdOd15G+=s!lcEK)(}ny!@D!akL@v5_+9cK(*GALLohr z;RTeRs`Am|1uN=so6d&8DIuSCq+%xiQso82m`qFK^LfIr*EAf8KEroQ`N@}@ORRT% zT~I;&sDsxxZ{u|oWm68r)Jp3+ZJ^55ggEQQa&_n& zLY5*)WmO9_bYLzx{aPfvz1Z1;at^^}Sc#wikvG1#XS4vUxX?B`b~Q3Vjxo&%4&Mhjp<{5ec_QXtXw!sCYy7X znfW2>cyN_vINa>AVtGFX;Mz0_*vMM_BalVlfX42y+y>0!U~S3jPpEU^WW?67aDo<( zd5#uZE%SaBC^c7|bI67T%gawFzO;hTE~D6lxKdSLv0#A9UnOY0d?S0XA~Qj@UQG*E z{;-c}-|pyh6_n=b5J&VHDHoA~N%1HEcG~{*q}1~6)r|p!RFzP{8deaGhkmpA&_~_& zDrB_>=eLW`{Q2@KBGAi?4J7I5yyEfY`k8FJl|~D?;mU>jUDO+Ln7`2f0KgMJ&9JUh zIEJ94Jbx!xv^7REodH38s(d&B)wKBb?guPysHC(qYPQaGB{fGvCdXZs-uHSna2Gd` z!f$e>K(Pw_wECGqnTP?6`-5&j)@lCQ;osK#-CTF-q2gyncCw~f*!l_qL=eW(3I!O4dg8Z$52PqW1JQ1XuYR#XdZWFGFv;)O_;>9o(9EX!;`uaD{Bo z<6glWDt|O5tw=;_pZcS|UF_(?anC_C&&=a4Er40DJ<{{|QaBk)D+2ZKPvvQRec$Zc zU2fVd$ELewPB4S85D`@05#L4^BigU@i1H-b9|BYo?wR6lbjea)hxi&dMwqBQf>}&G zk+$-VwSNGQ;+e&!V4Pm_oI3UoTIe9K?yVXyE){&YruOW4oXqakCGG;`DW3LfFU@qd z9)i)(PS@6z*B6d%uE&U4QKvtT3LOS^KN5lq5iI7MMSU&zh$KGvq=+n%G@L>7%|xLl zGtZJiSTJojPUKhmf=5CA!(rlLlB@&1mgz73IR#c@cXOVJreKIUo*k`|kQDL~?`u$5 z=v}xw*5d=dU4mu$vn{}ON$fFfZ>BXDl9&Bx%;4AG@1{5Ax302Llf4URi6!3>_iDXP z+^I|q)V3qe?_z4Db;}LVVeB?OcY+Xk9=g8RT5-$e_dH*}N{72n>rLHX@B}1$vGlhO z!ECX1pR!J=3PcJ>W60duX(8W?bUuzlGK6xF44Bc5^)+q?5dT<2V|67xXpf6Ks;w;P zhgw(G7BZtgD>COr-$0GtR}M{hi-Mi^|P)wx+h^NlxJkVtvPA|a{!mrR2-ysWOWME&_4AzJI1tzZdJJgStp?jOESw^2M$dB&+(3~$BHUWuR~^-6ZZ%H5bB zeDr5L3`yQn!1`YZruvUA-@d_0g(ReH$7vRYzu22`1nZRQ2y-0o(#fP*j{1^!J@1_e zx*2dL_YuMR3dy^L@k4+_Zy+)bjbnxuf!@b(Y?I#tD@>eEB#Tw?^n0+ zaWOI2X7~9o#Z76!NS7c64%%)P%Xmw_kKZLJl>kXFeTaVjO7kT=zAd(c9Ov)o!q^+z zvC>NAe`TTy^@FOSH=+N_1<1*;u={*nT(0%;5-@0bVI8t^y*p7PP|`z2-K$q}Zt&8n zLy~7gl^_;lfB6#++Ycxqzino68%` z56+T#x|@M2p+EC#)C$#u7bJp`Ia-5*XGoU(xk`!OC)gIqZ?`m$rzL(&--+&IFvvCp zHm=$GdXrx=4cRIe+Gm_GqqVOAG2zyETzqbJcDDWA6+28IEeT(|W^i*h>SE3X9SP^Y zCj#v-p#swaSHGKmc{p0P**g#2y`Q?+__&zV zz2dhi`+9#j4$(CJDuQPn_SpUM6I;iwc_EM#H#sW`YB~JXmEa8WjksI}Ojh0!(I>1a z?2y!B-f=KV6Zs#nxXjNqWG;8L=sSgKY`r4zI5E5B3k?Wd)eMn2@QD{%gTT1rCpC?h z+?}(RiBU4B<6vy&rc^b|OtCA5K>PeOK;^$wMTcbfY53l8a2vTNll|#cKE;;o)p0Ur za4}WUT~u-}agT?)vAT@hlPH=$p0uwL6B$0^z)?qvj6QE$RIUz}g*+~AW}gKT2n$ma zKbWakq?(ZHxP#=^UCUB1w55a%{uN>YbIxJr6fL6X-4> zJaKdve`1q$%m|>LetZ4kq6kbFm`!@)T1e7Ne<)i4a*+m$T)gGp-_@|K9_bSdm&ujn zz+ZSl&dOfo`5Zkj+n@^WLFMOe*s@SN{pO@|6f?^*9oP#6cgo9$jq!v&&4Ze7(`P+O zx6qQ`n4G*nDOTegu!QqQy$2|7 zso14>2J5e}iU7xsp}nbb@fDeo#Y=Q~h7-)gcY(Ykp-NG9vO1yR%*=qjmyzH|DNm(w))_?6zZc4>U*obW|ATE=jP5v zD77*+w+|0dT<;K_SY{eW^`N{vskg^@`ZL#Q__3A$u>rH&pDtte7j%**7DL>Bo;*7B zrV9C^UXH6|C0o-w9j2}PM-*CpR%`%XUAaCIO>vj8!2PI*Wb;&9h#J@AaNgsC-_7p52VTFv#~WKs}`rQ_o%j|Hc=+pnvr-Sx;9Q zC|N8nvuCl)iGyTGf+#@Wy?EsE@_eK3yq|-l%SMf^Fy|KZtOp-13CTh1SF(hf@mAdU znU0mMLd}_*Gtt-_w zS^%q<_V{E02y;0}cbEj}B&dceht1Vb^4*&7d}+u0!_#tv(VIt4j^B3mr+iwITRmrA zdi}EL)Qy1+G4NW!VCru!U$m;OMplK|-%nA#iG@1SHfQItSUG{ABO z%I8Hg9Xf>1oZSbe%eLN8(#|JTj`5vjU(f$adI~ zdD2wb#p#=GN_(k(WgqKgfkEHqm1v1 zAV-+{dw}lZw-r&cDd@dF&Gk%<(jpMZ+@7Dq_KL@SBmX(9i}hLskw02SL|rL72i4Ps z61x|lL}Gt2OY~ozdD|Rh@L4c`CpE%{wZfWgQODARiV!HIZng0A>xPTn$v&C@7}q*t zZ`KPn$0sJ90Hsg98t}_Wzm;moBy0kX>B5d*0ro+pvR65&4mpzlbUR^B$LIKoE3CK} z+lUfV6UKB$RGcrT0q8ov&beWdn}A-@=$o&0PXX4i$hK+-yeJc!h^4x3e;`A8UBq{= zB_}86^t21t(diX`_Q}5@MN77dEA=Gz#FmS@@YH zAiLnq<@IPdB?q#3j-xzFpU=IE7}7Nj7z5{*hM;^s0#{J%34db7*c5eeO4&K=b$x$k zi+L!qbFgci3r)7NhQ3kN(T0S!2`Bdfv~g8+k1E|q28hdy51k64(%iW7^ZB+Xzjt3h z2d76b>G+NOvee!JQ(@|Q6;~0+o(!@7BxV3CAsd-XO+b}sdPg@Q@bo>C%U60odojI> z0-OE*j`Q|%Sv1W~60wqdBXJT|bLbSoZ!g0jNTHW4rQ`IQ0mVGX*Dt$HH($uiD#2h= zPuDPzrm$zxjb2^9O$n6D%CY2Ny(`73)akTOG&h(p? z_(08+&9$9&P++IaR>hFpGS&R2cR1Z+wxDYy7l+{vjcxr8;DVH}j4?&|&|YbZBi~Vh zrk4i6hil%P%Gz`?!fA;HQT7qnpzEioAmz^QmZmAawZ)WS>i%rIdU)%8ecWw zZl@pX?{`|om&8hI9wS3`L4v|+>aSmab$q7(wFp>-V!q`mQ@y)9FQ&ald5C%LCDTg# z%~qBF*x$|$$M6p`Nfm4DE%Va@Kk9Dl9BQ{l$li8vRI%-R6vJft1^RZpJx&!1Sn$i` z6{Us9!C1LQ$s@6+G9qTbx}w@cT{e%$w@Ip;NI^ynrhp|6;QAUU|HNVy_4lmsYrFsn zl7?&Qe2T+RLj!4uYa+vA>8A6>Xf^L5sObq&cTcV(k*wNsrAb1~cSfa6D{Nqtk4&G@ zTM-`*;BP~FBN2ZZJwLUJdmm<7<9ItGR;hWR*+p} zwv>m`^f%)te;z+RJ>Pxy9y4;XJ7#=w+y>U*cGu{JULOqQvdEa`CUFh?{+{os{(QD_ z&RdsMq5NL^DxV9lU=iTuZccHGRmi6`)a_1Wq2a<>7V>O(hj-;rK{`m@!q##gJ;zxy zt7q1$adb><$|jY!jDQ>9IP_F@b$x0zO7!jZ@wb(}2NnsaBJ!cI`WPTazA*RxEb$cK z=EwD%QNmvoH>z0Dnftbu_NbugIDu&E?yHA)>pG&pE90a7fnxaQSfQDHe=?A`fe8Hj z@!lN*3*KSv?-B49uq5fKTY0hZ=4u~v0DOUVc`)~k=*M&Larpb|;irzJlp@^J@BXqB z-hWpq&`6f)X#QL|zHtp$04{FaE_4?^Cb?hsztw24V%bJx3DCG1-p~00!vzJw{>kWA zEd%Z@&PS{Vt24CX8kf=EmodsP872jcqQ!X^;mPV0QWgR`R4VSZ13+_x8E4!V1E`3d zN$#X5pg*A10e z24R89OxBnn4v^M0&{UUyBQiSO`d+*mru_hraf~n8HAlN`|HSkY`crOn{<|Z+P+Ok6ArA1k~ z{AyX{p60HzUbb`^fe&a=qAg)oBS=T zw;Z`1+!xpJa9q%8F%2x&ROQy|V;i?QV9<7XIE00R9`u!R2EZT~AHu`JV}8RypCq_A zUEV&2g~$u`W8U190PE0m zk2kFEI(f_IVpoqw^iL9uz&21+G86);peBaB^X7e84%snlW?n4-K{rFgA#yzZ*kGuh zM;KmAws_p%tt2TnD#D|`QGa*Gegc&c@fSYIABobpX7B$$pNJ~geGQQC)CD|zc#TEb zNqWFZL~^C0t~(G%6U=%SS^t^H$Qv0lMx%G)6{IWrIxiW?I^Uy7{a&fM1k`xVX3-!ZfRmuHm1NI{K&2ST3f^Q+LOHh(R=#%u!sm zlgT5C5>{VV?zA~|;Q6kY(bRwjH{DYnsFE#kk^mal?=mRZNsjuk+9)8l^|HfV4 z?%Mw8G{>q@sFve9*Ku!4gvlBj^= z8*%F7Q6T|J|Kv@frg==#eM1Z*%DId88oGB1aki&DYNL~rf@s1)1H(B|uMslucH(t0 zf=r4PPcVjxC=uQ!UW@AuZtG!A3$*UdQ9T?t*1Y~l ztE8%1)k4zEmbZOquBVOifm#buv^xCOeb~2rU|;dS$~pRBWNHMtn`KzTLf*C)FDlv} zp0Qi#AJ88+81?u`S7mEGkQdq1!tBdHU&&y(<$;&ZGEf%n?0BA7+DN=!z~&B*BToPx z-*^movka;{HQjTvNC;$K#n1SB5OA~0Lb5HJ$MOeQS;Iyh#y%@<5#j}Yvv+nb+m2wv z)Cj}k12-7CeoJ)sfIV%t)jOS9!l`enkbGqv(>u>!2_mg}6t8+M;1baBr`Xt$7S8Ew zA#nn4V+UCjF7YBSE-(9g2CtR5kXM8Wcxyw7 z?KXU#5L_fX_)|8y%?190R)+}V@ZCL}Nr9IEJxG-AyGczyNvF-^+xE{CEs)(WwhjXs1&!PW?! z0{;a$t z81T=1m6_bv*GI*I`X3!^6&(UNE=02Qqw72B>J(;3#<+sIhtjSt#rh{kVRU#3Qg1o4 zOTvp3G(ASZe+L5L&V41UL<5-}LnKYyxHe7UFx0 z^&b8W0xZ&V>X{qi7|HBu-3ZdRGu$|{MOKTPVzb71 z0RxdOYX{TaA{_Mdl$V}ueKwpcG1IuJU4Arbb$PlqgqBt&ASTX{|4=S~aS|Nde|ee` z$YIh+hs{_QRzPaL|c7Glb zm_dUd;GY+zNB7Ho@2E%rtPR(#)GptDefZ|>{!2jo+}y={y*6)5W#hQ}l{p&GZ!cBQ zn}uJF?~K6sAVNCHmr~8Hqt*Vc_V+zU0W0DilUF@gzQca0zCGdQEYfu*uwAm1n+}*i zIN61;(fMG$rnIJwD8O!ae*>~a_bb_tZOw1}^eri{=yVd?mxxX(BKbtT(L$=$o_|W5 zW>rT6wZ%Bn-+O! z_E?3NnlBHgXUg>Q6wWaRnI*u39j(D|i9jNvy+Wgw#`EyijlLxJjot)}u6-^WY7iZ; zE$|6FPP0TboRT-ZYp0cc!Kih&Dd}?i`AF{!^KVxP6iGzS^3m>CTM`Wp!g3E_chvFz1;b*2sky( zHTZoO8N%^Lo%FZ^zMevs&2LX^_LJK-l`2{A3X21e-cpQ8BX~9yE%ENB7E12D^f&0I zS!nW;mhTdTtnl;*FHAn?2M3f;fgo*pL%|Op72w{}-v}eb+7S}0^Ahr9!?wP&YmSmzBi~5vhxok-XRA?QR-i`*?eX=o!R8g zV0w3M5;uCp$WI6gL0SvHm!8muE%Ui9H?fts-0Y1<+vwfjZD(lPS^Fh@a9&Ap$M(Yq zL!(GX+hnFm->-)%ik+}Nil)5|4(si(x!KpTi4!bhjH-6B^qOy%nZN^q+e&;(7qnb+al2WQ_W6P4He5G;Lp*ghU|d$rY30Ykd=7Z>o9 zODcljl_@sa?wKw#v#{9TUOJV(GtW4+G;?ZpPnCfZ!Qtq_aS;cqZqa$$iUBv1ztWTx z>CWE+e#7lDXTZggIULb&T98(*G*>vmW0Q-$+3JA{jeW=0TfSH@op10=V`ENQ9 z7P!l${<(SM$1I6@>s^GE|Jg#|W>3otbIVeXXyg5jTP&E>lhF?fvH9=F6|pc$sLgz) z5@i@6a&*K_ReXlUDWWL>S4-YyS6!}4jY4O8p+6>GSYVSQL68QVCVuux-!`;} zVJ$`eD-EVO+bDM~kExE|hAjJzSj|xgZ_0m5L1p=zP&8keYBD(MgK_ zs+&e^+zgK2t^bKb*Q|lHgn%F^WTP2KP0fVn^^qgFh=5=@i8n8E8Db>zo4Z#bNb){T zxcya*oPkF9o4b?i&l;MVeK``|c0I9cu4`vaqD4e_}+>YhU4Tu*`nQ_m~Bg z>@rbi(e4Hkeg8XzcK1Z7^zf?%XE zsC1|Z^=D`5JCC4(%iV2jpDW^ap=Ab~ZQP<+gnB(h4MQTs;gZdC9kQWUtCq8P@DBs% zfpl5#8s;Z=>uJmadXFKWaCe-(+z**FMg2f~g7~40Di6np zL6M_%?0W;8dEnK(6pl)Vqve)?-#an_Lbz}cSUxaDGog<8qvfmg4gD7h!2xSUAW<@_ z`jnJF>Y2YK!vneg%(s_E zme2ggq2{qQMQTu@ubFL)Dt6!4PT$GeF?2l(`-TxPL4~YO5+l>88Iroa*$Hhpay6NM z2AD_B_z1nJ>wy%hj?b0aVq*bMBnAm6HXeb}v470P0|f_Pkf=X^OCgS$KW+w`FeL)g zfyL*{hOZPsfu~qTg!fS7sS{1gcl})dW)CMvjMXgHmIS3Q45oE1e;S8@yFV{%>u1n0 z4V;GpoqqK*#@pOY%E9a9`e#X>a~1Av8J>sp0Zabtsz_iCIzuHf0_uRC8aB}72`^l= zkxg*`52kqm2lRky$K>bduiTO_L*tE4AcQjJ#TjY3-5J8Bo`n@4XI78s{&jhAu)~D| zq1qD=aGrodQM4L8J79@>lCeeBX6?;5#ejq}f=AkAB0z8~mFi~wn@%5gD{VoxT#H7g ze=xr{0j-g-W$Zo2(#1DgNDav`9V!?bSefUYZ~YC8v@ua9-S~fh&R8TxMn+CM@MtTa zzM*6J_g@ zV;kzUS&MIg6KzvWaFJF{BE^b0<5h0zOGQ@TYp-~S=2z*RKrrYe!qOO>Xod0X!ztkG z&Ce&z&C`sk%+8J4=o9$%@A5y;L|&Hf2@5JRw+66} z^?o(Pu8sy)Hasq(y;XWYpjo`;m=jrGfIxw5P&qo3Ix(=;2Re@>1CtpG#;XH6Z*K(_ z3L@S&rt4_H+yg-Zv68Kid7cA5g;v@>!vMa8glh*@;@uYj=fi?7#sAZm4tl@fbs9eq zqU4s&i*bS5{aOy6!+03S0Lj7qz4@pAy;NA5c^`rUiqSv}uxfLp&%Zw0U?+PX^Z5U} zT~`O&7-HZf9caJgwISobrsl}LqWxofT!5Dt~Vm;YS^qiL38+T>cg`nqteGx8++F zu|xdeOGB~w%Z_P;`~;rY1Xv z7^~G~&)*-HD+1#p`bP{bNb~U0E49N34$86Bmn(OMXUW|6Za?};FQ@=K>1nR;1mCkt zeu;wM>W{Ur9weIh{=W40XHN7Tn5o|}p))5S`ZFLUMpe_HQzvt$h03iiq@ZRb(M(W2 zt8~Y4wtcoy7P(wwo)c;JLU!Y}YkS{e@=t@{vy1jtVK8i&TJ&lB0D>4LH!Y$-;Iwns zxpC1+FKMQTGx=rhZlFr~&R};dNri4O+RyckW&}Z=`05o6FGq*WDc0g6orKpmuWd% znEsv}j2C}ta7cXlbBx|l>J{SWh_3t6PvL4_i=y^cF9FaXbaH?Dh2W!IA%E(5LhWXs z!mIgLxfc@blcu6q1a5CFlec@~7~eZ8*nMMgZyh+0E#JNTyjR@s?x_TKun~M&GqP-S zSma5AdQAg*+m(U+wO_mE>>?uW`a)SgAVMTA>cFD)viG<{pGg2?qGgI?*gNK&0RPXE z$LE%=Kfh|4j_v+!9h7Q)sU3H@a^y}Yu@aTY^=G0dL4(^erF{Fsl8Ws87h9=Bt_1C? zM>_k(p})D8zpbxY?t|K#;57&*@wiKEZ6&F=d(R=6+{jnAu)N_f`puPQ;3!^`ee|`E_bssl@1Vof+xTqnFSL`(0g+rf-kYwdM?854>=m zM}LdKJdz~k`S{v)V%{8W)Y;ocI4@g<|D?NA2Fr)3p=e+X(JJ&)n56FxM;c#eMB*Xj zYJR@Frab<9QVm_dfzjN*pX83Cx=*at)v!HSTVmB zKlE#8~GoQP0Z9z4memkcO$Gli0I9uh`7s(yr%;+ zl2j;6Zy4C>$5=m$WH3mZQhiic3~_#XKHh2J(=7P3r_dz&bM6_r{d;@j(QT!T6BiK1 z{hDt!h_~taei9u63{m9Yq4)RQFy!W~c=Igpezl#0@y!bWvCZ{xsHoS&-teT3ZI>~w zBLOA8FY~*xJWJ|H8*S4)5{y!UYuRCxs0`e_v_zFYbWQ0{Hy3L9E>=Fr6P9 z(aNB}JzU;niiI91(#GGAD`x^NY*sK71Suf38m-$Gh9Gl^w{-A(?X0s+3 z{{=`8@vlWB@+YthY{q5%Pm3V1FZjQg1IFdQgZba7 n^4}NXzc0dn*UbN$h;UCI!?Z9P`wKq>1pKKgYAJk|vkLlu+p#(w literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e63c90ea9057c22ee6abc787e0ea2bb2d13c9c7a GIT binary patch literal 38955 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 { 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/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/map.test.js b/test/unit/ui/map.test.js index deec79c18e9..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', () => { @@ -905,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 ]]) @@ -915,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]]) @@ -1310,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/marker.test.js b/test/unit/ui/marker.test.js index aa19db9a91f..72742a8abf0 100644 --- a/test/unit/ui/marker.test.js +++ b/test/unit/ui/marker.test.js @@ -830,7 +830,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]))