diff --git a/docs/api-reference/map-view.md b/docs/api-reference/map-view.md index d8315de4124..37794b70753 100644 --- a/docs/api-reference/map-view.md +++ b/docs/api-reference/map-view.md @@ -11,7 +11,9 @@ import {MapView} from '@deck.gl/core'; const view = new MapView({id, ...}); ``` -`MapView` takes the same parameters as the [View](/docs/api-reference/view.md) superclass constructor. +`MapView` takes the same parameters as the [View](/docs/api-reference/view.md) superclass constructor, plus the following: + +- `repeat` (`Boolean`) - Whether to render multiple copies of the map at low zoom levels. Default `false`. ## View State diff --git a/modules/core/src/passes/layers-pass.js b/modules/core/src/passes/layers-pass.js index 214ba8571f5..189415d2ea8 100644 --- a/modules/core/src/passes/layers-pass.js +++ b/modules/core/src/passes/layers-pass.js @@ -30,12 +30,16 @@ export default class LayersPass extends Pass { // Update context to point to this viewport onViewportActive(viewport); - props.viewport = viewport; props.view = view; // render this viewport - const stats = this._drawLayersInViewport(gl, props); - renderStats.push(stats); + const subViewports = viewport.subViewports || [viewport]; + for (const subViewport of subViewports) { + props.viewport = subViewport; + + const stats = this._drawLayersInViewport(gl, props); + renderStats.push(stats); + } }); return renderStats; } @@ -92,6 +96,8 @@ export default class LayersPass extends Pass { const uniforms = Object.assign({}, layer.context.uniforms, {layerIndex}); const layerParameters = this.getLayerParameters(layer, layerIndex); + // overwrite layer.context.viewport with the sub viewport + _moduleParameters.viewport = viewport; layer.drawLayer({ moduleParameters: _moduleParameters, uniforms, diff --git a/modules/core/src/viewports/web-mercator-viewport.js b/modules/core/src/viewports/web-mercator-viewport.js index 08c483dff46..a60bcca5215 100644 --- a/modules/core/src/viewports/web-mercator-viewport.js +++ b/modules/core/src/viewports/web-mercator-viewport.js @@ -32,6 +32,7 @@ import { // TODO - import from math.gl import * as vec2 from 'gl-matrix/vec2'; +import {Matrix4} from 'math.gl'; export default class WebMercatorViewport extends Viewport { /** @@ -50,7 +51,10 @@ export default class WebMercatorViewport extends Viewport { bearing = 0, nearZMultiplier = 0.1, farZMultiplier = 1.01, - orthographic = false + orthographic = false, + + repeat = false, + worldOffset = 0 } = opts; let {width, height, altitude = 1.5} = opts; @@ -77,7 +81,7 @@ export default class WebMercatorViewport extends Viewport { // shader (cheap) which gives a coordinate system that has its center in // the layer's center position. This makes rotations and other modelMatrx // transforms much more useful. - const viewMatrixUncentered = getViewMatrix({ + let viewMatrixUncentered = getViewMatrix({ height, pitch, bearing, @@ -85,6 +89,11 @@ export default class WebMercatorViewport extends Viewport { altitude }); + if (worldOffset) { + const viewOffset = new Matrix4().translate([512 * worldOffset, 0, 0]); + viewMatrixUncentered = viewOffset.multiplyLeft(viewMatrixUncentered); + } + const viewportOpts = Object.assign({}, opts, { // x, y, width, @@ -118,10 +127,39 @@ export default class WebMercatorViewport extends Viewport { this.orthographic = orthographic; + this._subViewports = repeat ? [] : null; + Object.freeze(this); } /* eslint-enable complexity, max-statements */ + get subViewports() { + if (this._subViewports && !this._subViewports.length) { + // Cache sub viewports so that we only calculate them once + const topLeft = this.unproject([0, 0]); + const topRight = this.unproject([this.width, 0]); + const bottomLeft = this.unproject([0, this.height]); + const bottomRight = this.unproject([this.width, this.height]); + + const minLon = Math.min(topLeft[0], topRight[0], bottomLeft[0], bottomRight[0]); + const maxLon = Math.max(topLeft[0], topRight[0], bottomLeft[0], bottomRight[0]); + + const minOffset = Math.floor((minLon + 180) / 360); + const maxOffset = Math.ceil((maxLon - 180) / 360); + + for (let x = minOffset; x <= maxOffset; x++) { + const offsetViewport = x + ? new WebMercatorViewport({ + ...this, + worldOffset: x + }) + : this; + this._subViewports.push(offsetViewport); + } + } + return this._subViewports; + } + /** * Add a meter delta to a base lnglat coordinate, returning a new lnglat array * diff --git a/test/modules/core/viewports/web-mercator-viewport.spec.js b/test/modules/core/viewports/web-mercator-viewport.spec.js index 5c6a836d70e..de5dc147755 100644 --- a/test/modules/core/viewports/web-mercator-viewport.spec.js +++ b/test/modules/core/viewports/web-mercator-viewport.spec.js @@ -242,6 +242,40 @@ test('WebMercatorViewport.getFrustumPlanes', t => { t.end(); }); +test('WebMercatorViewport.subViewports', t => { + let viewport = new WebMercatorViewport(TEST_VIEWPORTS[0]); + t.deepEqual(viewport.subViewports, null, 'gets correct subViewports'); + + viewport = new WebMercatorViewport({...TEST_VIEWPORTS[0], repeat: true}); + t.deepEqual(viewport.subViewports, [viewport], 'gets correct subViewports'); + + viewport = new WebMercatorViewport({ + width: 800, + height: 400, + longitude: 0, + latitude: 0, + zoom: 0, + repeat: true + }); + const {subViewports} = viewport; + t.is(subViewports.length, 3, 'gets correct subViewports'); + t.deepEqual( + subViewports[0].project([0, 0]), + [400 - 512, 200], + 'center offset in subViewports[0]' + ); + t.deepEqual(subViewports[1].project([0, 0]), [400, 200], 'center offset in subViewports[1]'); + t.deepEqual( + subViewports[2].project([0, 0]), + [400 + 512, 200], + 'center offset in subViewports[2]' + ); + + t.is(viewport.subViewports, subViewports, 'subViewports are cached'); + + t.end(); +}); + function getCulling(p, planes) { let outDir = null; p = new Vector3(p); diff --git a/test/render/golden-images/map-repeat.png b/test/render/golden-images/map-repeat.png new file mode 100644 index 00000000000..fbbca6c1268 Binary files /dev/null and b/test/render/golden-images/map-repeat.png differ diff --git a/test/render/test-cases.js b/test/render/test-cases.js index 844452e5412..b8d25bfc3bc 100644 --- a/test/render/test-cases.js +++ b/test/render/test-cases.js @@ -9,6 +9,7 @@ import { } from '@deck.gl/aggregation-layers'; import { COORDINATE_SYSTEM, + MapView, OrbitView, OrthographicView, FirstPersonView, @@ -1984,5 +1985,25 @@ export const TEST_CASES = [ }) ], goldenImage: './test/render/golden-images/binary.png' + }, + { + name: 'map-repeat', + views: new MapView({repeat: true}), + viewState: { + latitude: 0, + longitude: 0, + zoom: 0, + pitch: 0, + bearing: 0 + }, + layers: [ + new ScatterplotLayer({ + data: h3.getRes0Indexes(), + getPosition: d => h3.h3ToGeo(d).reverse(), + radiusMinPixels: 4, + getFillColor: [255, 0, 0] + }) + ], + goldenImage: './test/render/golden-images/map-repeat.png' } ];