Skip to content

Commit 348b948

Browse files
Arindam Bosemourner
authored andcommitted
Map API functions such as easeTo and flyTo now support padding: PaddingOptions which lets developers shift the center of perspective for a map when building floating sidebars.
Asymmetric viewport (mapbox#8638) Co-authored-by: Vladimir Agafonkin <agafonkin@gmail.com>
1 parent 3f25578 commit 348b948

File tree

21 files changed

+1114
-36
lines changed

21 files changed

+1114
-36
lines changed

src/geo/edge_insets.js

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// @flow
2+
import {number} from "../style-spec/util/interpolate";
3+
import Point from "@mapbox/point-geometry";
4+
import {clamp} from "../util/util";
5+
6+
/**
7+
* An `EdgeInset` object represents screen space padding applied to the edges of the viewport.
8+
* This shifts the apprent center or the vanishing point of the map. This is useful for adding floating UI elements
9+
* on top of the map and having the vanishing point shift as UI elements resize.
10+
*
11+
* @param {number} [top=0]
12+
* @param {number} [bottom=0]
13+
* @param {number} [left=0]
14+
* @param {number} [right=0]
15+
*/
16+
class EdgeInsets {
17+
top: number;
18+
bottom: number;
19+
left: number;
20+
right: number;
21+
22+
constructor(top: number = 0, bottom: number = 0, left: number = 0, right: number = 0) {
23+
if (isNaN(top) || top < 0 ||
24+
isNaN(bottom) || bottom < 0 ||
25+
isNaN(left) || left < 0 ||
26+
isNaN(right) || right < 0
27+
) {
28+
throw new Error('Invalid value for edge-insets, top, bottom, left and right must all be numbers');
29+
}
30+
31+
this.top = top;
32+
this.bottom = bottom;
33+
this.left = left;
34+
this.right = right;
35+
}
36+
37+
/**
38+
* Interpolates the inset in-place.
39+
* This maintains the current inset value for any inset not present in `target`.
40+
*
41+
* @param {PaddingOptions} target
42+
* @param {number} t
43+
* @returns {EdgeInsets}
44+
* @memberof EdgeInsets
45+
*/
46+
interpolate(start: PaddingOptions | EdgeInsets, target: PaddingOptions, t: number): EdgeInsets {
47+
if (target.top != null && start.top != null) this.top = number(start.top, target.top, t);
48+
if (target.bottom != null && start.bottom != null) this.bottom = number(start.bottom, target.bottom, t);
49+
if (target.left != null && start.left != null) this.left = number(start.left, target.left, t);
50+
if (target.right != null && start.right != null) this.right = number(start.right, target.right, t);
51+
52+
return this;
53+
}
54+
55+
/**
56+
* Utility method that computes the new apprent center or vanishing point after applying insets.
57+
* This is in pixels and with the top left being (0.0) and +y being downwards.
58+
*
59+
* @param {number} width
60+
* @param {number} height
61+
* @returns {Point}
62+
* @memberof EdgeInsets
63+
*/
64+
getCenter(width: number, height: number): Point {
65+
// Clamp insets so they never overflow width/height and always calculate a valid center
66+
const x = clamp((this.left + width - this.right) / 2, 0, width);
67+
const y = clamp((this.top + height - this.bottom) / 2, 0, height);
68+
69+
return new Point(x, y);
70+
}
71+
72+
equals(other: PaddingOptions): boolean {
73+
return this.top === other.top &&
74+
this.bottom === other.bottom &&
75+
this.left === other.left &&
76+
this.right === other.right;
77+
}
78+
79+
clone(): EdgeInsets {
80+
return new EdgeInsets(this.top, this.bottom, this.left, this.right);
81+
}
82+
83+
/**
84+
* Returns the current sdtate as json, useful when you want to have a
85+
* read-only representation of the inset.
86+
*
87+
* @returns {PaddingOptions}
88+
* @memberof EdgeInsets
89+
*/
90+
toJSON(): PaddingOptions {
91+
return {
92+
top: this.top,
93+
bottom: this.bottom,
94+
left: this.left,
95+
right: this.right
96+
};
97+
}
98+
}
99+
100+
export type PaddingOptions = {top: ?number, bottom: ?number, right: ?number, left: ?number};
101+
102+
export default EdgeInsets;

src/geo/transform.js

+67-9
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import {number as interpolate} from '../style-spec/util/interpolate';
99
import EXTENT from '../data/extent';
1010
import {vec4, mat4, mat2, vec2} from 'gl-matrix';
1111
import {Aabb, Frustum} from '../util/primitives.js';
12+
import EdgeInsets from './edge_insets';
1213

1314
import {UnwrappedTileID, OverscaledTileID, CanonicalTileID} from '../source/tile_id';
15+
import type {PaddingOptions} from './edge_insets';
1416

1517
/**
1618
* A single transform, generally used for a single tile to be
@@ -49,6 +51,7 @@ class Transform {
4951
_minPitch: number;
5052
_maxPitch: number;
5153
_center: LngLat;
54+
_edgeInsets: EdgeInsets;
5255
_constraining: boolean;
5356
_posMatrixCache: {[string]: Float32Array};
5457
_alignedPosMatrixCache: {[string]: Float32Array};
@@ -74,6 +77,7 @@ class Transform {
7477
this._fov = 0.6435011087932844;
7578
this._pitch = 0;
7679
this._unmodified = true;
80+
this._edgeInsets = new EdgeInsets();
7781
this._posMatrixCache = {};
7882
this._alignedPosMatrixCache = {};
7983
}
@@ -90,6 +94,7 @@ class Transform {
9094
clone._fov = this._fov;
9195
clone._pitch = this._pitch;
9296
clone._unmodified = this._unmodified;
97+
clone._edgeInsets = this._edgeInsets.clone();
9398
clone._calcMatrices();
9499
return clone;
95100
}
@@ -137,8 +142,8 @@ class Transform {
137142
return this.tileSize * this.scale;
138143
}
139144

140-
get centerPoint(): Point {
141-
return this.size._div(2);
145+
get centerOffset(): Point {
146+
return this.centerPoint._sub(this.size._div(2));
142147
}
143148

144149
get size(): Point {
@@ -204,6 +209,52 @@ class Transform {
204209
this._calcMatrices();
205210
}
206211

212+
get padding(): PaddingOptions { return this._edgeInsets.toJSON(); }
213+
set padding(padding: PaddingOptions) {
214+
if (this._edgeInsets.equals(padding)) return;
215+
this._unmodified = false;
216+
//Update edge-insets inplace
217+
this._edgeInsets.interpolate(this._edgeInsets, padding, 1);
218+
this._calcMatrices();
219+
}
220+
221+
/**
222+
* The center of the screen in pixels with the top-left corner being (0,0)
223+
* and +y axis pointing downwards. This accounts for padding.
224+
*
225+
* @readonly
226+
* @type {Point}
227+
* @memberof Transform
228+
*/
229+
get centerPoint(): Point {
230+
return this._edgeInsets.getCenter(this.width, this.height);
231+
}
232+
233+
/**
234+
* Returns if the padding params match
235+
*
236+
* @param {PaddingOptions} padding
237+
* @returns {boolean}
238+
* @memberof Transform
239+
*/
240+
isPaddingEqual(padding: PaddingOptions): boolean {
241+
return this._edgeInsets.equals(padding);
242+
}
243+
244+
/**
245+
* Helper method to upadte edge-insets inplace
246+
*
247+
* @param {PaddingOptions} target
248+
* @param {number} t
249+
* @memberof Transform
250+
*/
251+
interpolatePadding(start: PaddingOptions, target: PaddingOptions, t: number) {
252+
this._unmodified = false;
253+
this._edgeInsets.interpolate(start, target, t);
254+
this._constrain();
255+
this._calcMatrices();
256+
}
257+
207258
/**
208259
* Return a zoom level that will cover all tiles the transform
209260
* @param {Object} options
@@ -281,9 +332,10 @@ class Transform {
281332
const centerPoint = [numTiles * centerCoord.x, numTiles * centerCoord.y, 0];
282333
const cameraFrustum = Frustum.fromInvProjectionMatrix(this.invProjMatrix, this.worldSize, z);
283334

284-
// No change of LOD behavior for pitch lower than 60: return only tile ids from the requested zoom level
335+
// 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
285336
let minZoom = options.minzoom || 0;
286-
if (this.pitch <= 60.0)
337+
// Use 0.1 as an epsilon to avoid for explicit == 0.0 floating point checks
338+
if (this.pitch <= 60.0 && this._edgeInsets.top < 0.1)
287339
minZoom = z;
288340

289341
// There should always be a certain number of maximum zoom level tiles surrounding the center location
@@ -616,15 +668,17 @@ class Transform {
616668
_calcMatrices() {
617669
if (!this.height) return;
618670

619-
this.cameraToCenterDistance = 0.5 / Math.tan(this._fov / 2) * this.height;
671+
const halfFov = this._fov / 2;
672+
const offset = this.centerOffset;
673+
this.cameraToCenterDistance = 0.5 / Math.tan(halfFov) * this.height;
620674

621-
// Find the distance from the center point [width/2, height/2] to the
622-
// center top point [width/2, 0] in Z units, using the law of sines.
675+
// Find the distance from the center point [width/2 + offset.x, height/2 + offset.y] to the
676+
// center top point [width/2 + offset.x, 0] in Z units, using the law of sines.
623677
// 1 Z unit is equivalent to 1 horizontal px at the center of the map
624678
// (the distance between[width/2, height/2] and [width/2 + 1, height/2])
625-
const halfFov = this._fov / 2;
626679
const groundAngle = Math.PI / 2 + this._pitch;
627-
const topHalfSurfaceDistance = Math.sin(halfFov) * this.cameraToCenterDistance / Math.sin(clamp(Math.PI - groundAngle - halfFov, 0.01, Math.PI - 0.01));
680+
const fovAboveCenter = this._fov * (0.5 + offset.y / this.height);
681+
const topHalfSurfaceDistance = Math.sin(fovAboveCenter) * this.cameraToCenterDistance / Math.sin(clamp(Math.PI - groundAngle - fovAboveCenter, 0.01, Math.PI - 0.01));
628682
const point = this.point;
629683
const x = point.x, y = point.y;
630684

@@ -646,6 +700,10 @@ class Transform {
646700
let m = new Float64Array(16);
647701
mat4.perspective(m, this._fov, this.width / this.height, nearZ, farZ);
648702

703+
//Apply center of perspective offset
704+
m[8] = -offset.x * 2 / this.width;
705+
m[9] = offset.y * 2 / this.height;
706+
649707
mat4.scale(m, m, [1, -1, 1]);
650708
mat4.translate(m, m, [0, 0, -this.cameraToCenterDistance]);
651709
mat4.rotateX(m, m, this._pitch);

src/render/draw_debug.js

+50
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,63 @@ import StencilMode from '../gl/stencil_mode';
1111
import CullFaceMode from '../gl/cull_face_mode';
1212
import {debugUniformValues} from './program/debug_program';
1313
import Color from '../style-spec/util/color';
14+
import browser from '../util/browser';
1415

1516
import type Painter from './painter';
1617
import type SourceCache from '../source/source_cache';
1718
import type {OverscaledTileID} from '../source/tile_id';
1819

1920
export default drawDebug;
2021

22+
const topColor = new Color(1, 0, 0, 1);
23+
const btmColor = new Color(0, 1, 0, 1);
24+
const leftColor = new Color(0, 0, 1, 1);
25+
const rightColor = new Color(1, 0, 1, 1);
26+
const centerColor = new Color(0, 1, 1, 1);
27+
28+
export function drawDebugPadding(painter: Painter) {
29+
const padding = painter.transform.padding;
30+
const lineWidth = 3;
31+
// Top
32+
drawHorizontalLine(painter, painter.transform.height - (padding.top || 0), lineWidth, topColor);
33+
// Bottom
34+
drawHorizontalLine(painter, padding.bottom || 0, lineWidth, btmColor);
35+
// Left
36+
drawVerticalLine(painter, padding.left || 0, lineWidth, leftColor);
37+
// Right
38+
drawVerticalLine(painter, painter.transform.width - (padding.right || 0), lineWidth, rightColor);
39+
// Center
40+
const center = painter.transform.centerPoint;
41+
drawCrosshair(painter, center.x, painter.transform.height - center.y, centerColor);
42+
}
43+
44+
function drawCrosshair(painter: Painter, x: number, y: number, color: Color) {
45+
const size = 20;
46+
const lineWidth = 2;
47+
//Vertical line
48+
drawDebugSSRect(painter, x - lineWidth / 2, y - size / 2, lineWidth, size, color);
49+
//Horizontal line
50+
drawDebugSSRect(painter, x - size / 2, y - lineWidth / 2, size, lineWidth, color);
51+
}
52+
53+
function drawHorizontalLine(painter: Painter, y: number, lineWidth: number, color: Color) {
54+
drawDebugSSRect(painter, 0, y + lineWidth / 2, painter.transform.width, lineWidth, color);
55+
}
56+
57+
function drawVerticalLine(painter: Painter, x: number, lineWidth: number, color: Color) {
58+
drawDebugSSRect(painter, x - lineWidth / 2, 0, lineWidth, painter.transform.height, color);
59+
}
60+
61+
function drawDebugSSRect(painter: Painter, x: number, y: number, width: number, height: number, color: Color) {
62+
const context = painter.context;
63+
const gl = context.gl;
64+
65+
gl.enable(gl.SCISSOR_TEST);
66+
gl.scissor(x * browser.devicePixelRatio, y * browser.devicePixelRatio, width * browser.devicePixelRatio, height * browser.devicePixelRatio);
67+
context.clear({color});
68+
gl.disable(gl.SCISSOR_TEST);
69+
}
70+
2171
function drawDebug(painter: Painter, sourceCache: SourceCache, coords: Array<OverscaledTileID>) {
2272
for (let i = 0; i < coords.length; i++) {
2373
drawDebugTile(painter, sourceCache, coords[i]);

src/render/painter.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import fillExtrusion from './draw_fill_extrusion';
3333
import hillshade from './draw_hillshade';
3434
import raster from './draw_raster';
3535
import background from './draw_background';
36-
import debug from './draw_debug';
36+
import debug, {drawDebugPadding} from './draw_debug';
3737
import custom from './draw_custom';
3838

3939
const draw = {
@@ -69,6 +69,7 @@ export type RenderPass = 'offscreen' | 'opaque' | 'translucent';
6969
type PainterOptions = {
7070
showOverdrawInspector: boolean,
7171
showTileBoundaries: boolean,
72+
showPadding: boolean,
7273
rotating: boolean,
7374
zooming: boolean,
7475
moving: boolean,
@@ -473,6 +474,10 @@ class Painter {
473474
}
474475
}
475476

477+
if (this.options.showPadding) {
478+
drawDebugPadding(this);
479+
}
480+
476481
// Set defaults for most GL values so that anyone using the state after the render
477482
// encounters more expected values.
478483
this.context.setDefault();

0 commit comments

Comments
 (0)