diff --git a/src/geo/edge_insets.js b/src/geo/edge_insets.js new file mode 100644 index 00000000000..5835d7c161d --- /dev/null +++ b/src/geo/edge_insets.js @@ -0,0 +1,102 @@ +// @flow +import {number} from "../style-spec/util/interpolate"; +import Point from "@mapbox/point-geometry"; +import {clamp} from "../util/util"; + +/** + * An `EdgeInset` object represents screen space padding applied to the edges of the viewport. + * This shifts the apprent center or the vanishing point of the map. This is useful for adding floating UI elements + * on top of the map and having the vanishing point shift as UI elements resize. + * + * @param {number} [top=0] + * @param {number} [bottom=0] + * @param {number} [left=0] + * @param {number} [right=0] + */ +class EdgeInsets { + top: number; + bottom: number; + left: number; + right: number; + + constructor(top: number = 0, bottom: number = 0, left: number = 0, right: number = 0) { + if (isNaN(top) || top < 0 || + isNaN(bottom) || bottom < 0 || + isNaN(left) || left < 0 || + isNaN(right) || right < 0 + ) { + throw new Error('Invalid value for edge-insets, top, bottom, left and right must all be numbers'); + } + + this.top = top; + this.bottom = bottom; + this.left = left; + this.right = right; + } + + /** + * Interpolates the inset in-place. + * This maintains the current inset value for any inset not present in `target`. + * + * @param {PaddingOptions} target + * @param {number} t + * @returns {EdgeInsets} + * @memberof EdgeInsets + */ + interpolate(start: PaddingOptions | EdgeInsets, target: PaddingOptions, t: number): EdgeInsets { + if (target.top != null && start.top != null) this.top = number(start.top, target.top, t); + if (target.bottom != null && start.bottom != null) this.bottom = number(start.bottom, target.bottom, t); + if (target.left != null && start.left != null) this.left = number(start.left, target.left, t); + if (target.right != null && start.right != null) this.right = number(start.right, target.right, t); + + return this; + } + + /** + * Utility method that computes the new apprent center or vanishing point after applying insets. + * This is in pixels and with the top left being (0.0) and +y being downwards. + * + * @param {number} width + * @param {number} height + * @returns {Point} + * @memberof EdgeInsets + */ + getCenter(width: number, height: number): Point { + // Clamp insets so they never overflow width/height and always calculate a valid center + const x = clamp((this.left + width - this.right) / 2, 0, width); + const y = clamp((this.top + height - this.bottom) / 2, 0, height); + + return new Point(x, y); + } + + equals(other: PaddingOptions): boolean { + return this.top === other.top && + this.bottom === other.bottom && + this.left === other.left && + this.right === other.right; + } + + clone(): EdgeInsets { + return new EdgeInsets(this.top, this.bottom, this.left, this.right); + } + + /** + * Returns the current sdtate as json, useful when you want to have a + * read-only representation of the inset. + * + * @returns {PaddingOptions} + * @memberof EdgeInsets + */ + toJSON(): PaddingOptions { + return { + top: this.top, + bottom: this.bottom, + left: this.left, + right: this.right + }; + } +} + +export type PaddingOptions = {top: ?number, bottom: ?number, right: ?number, left: ?number}; + +export default EdgeInsets; diff --git a/src/geo/transform.js b/src/geo/transform.js index af33f5922d9..7e5b232dc94 100644 --- a/src/geo/transform.js +++ b/src/geo/transform.js @@ -9,8 +9,10 @@ import {number as interpolate} from '../style-spec/util/interpolate'; import EXTENT from '../data/extent'; import {vec4, mat4, mat2, vec2} from 'gl-matrix'; import {Aabb, Frustum} from '../util/primitives.js'; +import EdgeInsets from './edge_insets'; import {UnwrappedTileID, OverscaledTileID, CanonicalTileID} from '../source/tile_id'; +import type {PaddingOptions} from './edge_insets'; /** * A single transform, generally used for a single tile to be @@ -49,6 +51,7 @@ class Transform { _minPitch: number; _maxPitch: number; _center: LngLat; + _edgeInsets: EdgeInsets; _constraining: boolean; _posMatrixCache: {[string]: Float32Array}; _alignedPosMatrixCache: {[string]: Float32Array}; @@ -74,6 +77,7 @@ class Transform { this._fov = 0.6435011087932844; this._pitch = 0; this._unmodified = true; + this._edgeInsets = new EdgeInsets(); this._posMatrixCache = {}; this._alignedPosMatrixCache = {}; } @@ -90,6 +94,7 @@ class Transform { clone._fov = this._fov; clone._pitch = this._pitch; clone._unmodified = this._unmodified; + clone._edgeInsets = this._edgeInsets.clone(); clone._calcMatrices(); return clone; } @@ -137,8 +142,8 @@ class Transform { return this.tileSize * this.scale; } - get centerPoint(): Point { - return this.size._div(2); + get centerOffset(): Point { + return this.centerPoint._sub(this.size._div(2)); } get size(): Point { @@ -204,6 +209,52 @@ class Transform { this._calcMatrices(); } + get padding(): PaddingOptions { return this._edgeInsets.toJSON(); } + set padding(padding: PaddingOptions) { + if (this._edgeInsets.equals(padding)) return; + this._unmodified = false; + //Update edge-insets inplace + this._edgeInsets.interpolate(this._edgeInsets, padding, 1); + this._calcMatrices(); + } + + /** + * The center of the screen in pixels with the top-left corner being (0,0) + * and +y axis pointing downwards. This accounts for padding. + * + * @readonly + * @type {Point} + * @memberof Transform + */ + get centerPoint(): Point { + return this._edgeInsets.getCenter(this.width, this.height); + } + + /** + * Returns if the padding params match + * + * @param {PaddingOptions} padding + * @returns {boolean} + * @memberof Transform + */ + isPaddingEqual(padding: PaddingOptions): boolean { + return this._edgeInsets.equals(padding); + } + + /** + * Helper method to upadte edge-insets inplace + * + * @param {PaddingOptions} target + * @param {number} t + * @memberof Transform + */ + interpolatePadding(start: PaddingOptions, target: PaddingOptions, t: number) { + this._unmodified = false; + this._edgeInsets.interpolate(start, target, t); + this._constrain(); + this._calcMatrices(); + } + /** * Return a zoom level that will cover all tiles the transform * @param {Object} options @@ -281,9 +332,10 @@ class Transform { const centerPoint = [numTiles * centerCoord.x, numTiles * centerCoord.y, 0]; const cameraFrustum = Frustum.fromInvProjectionMatrix(this.invProjMatrix, this.worldSize, z); - // No change of LOD behavior for pitch lower than 60: return only tile ids from the requested zoom level + // 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 let minZoom = options.minzoom || 0; - if (this.pitch <= 60.0) + // Use 0.1 as an epsilon to avoid for explicit == 0.0 floating point checks + if (this.pitch <= 60.0 && this._edgeInsets.top < 0.1) minZoom = z; // There should always be a certain number of maximum zoom level tiles surrounding the center location @@ -616,15 +668,17 @@ class Transform { _calcMatrices() { if (!this.height) return; - this.cameraToCenterDistance = 0.5 / Math.tan(this._fov / 2) * this.height; + const halfFov = this._fov / 2; + const offset = this.centerOffset; + this.cameraToCenterDistance = 0.5 / Math.tan(halfFov) * this.height; - // Find the distance from the center point [width/2, height/2] to the - // center top point [width/2, 0] in Z units, using the law of sines. + // Find the distance from the center point [width/2 + offset.x, height/2 + offset.y] to the + // center top point [width/2 + offset.x, 0] in Z units, using the law of sines. // 1 Z unit is equivalent to 1 horizontal px at the center of the map // (the distance between[width/2, height/2] and [width/2 + 1, height/2]) - const halfFov = this._fov / 2; const groundAngle = Math.PI / 2 + this._pitch; - const topHalfSurfaceDistance = Math.sin(halfFov) * this.cameraToCenterDistance / Math.sin(clamp(Math.PI - groundAngle - halfFov, 0.01, Math.PI - 0.01)); + const fovAboveCenter = this._fov * (0.5 + offset.y / this.height); + const topHalfSurfaceDistance = Math.sin(fovAboveCenter) * this.cameraToCenterDistance / Math.sin(clamp(Math.PI - groundAngle - fovAboveCenter, 0.01, Math.PI - 0.01)); const point = this.point; const x = point.x, y = point.y; @@ -646,6 +700,10 @@ class Transform { let m = new Float64Array(16); mat4.perspective(m, this._fov, this.width / this.height, nearZ, farZ); + //Apply center of perspective offset + m[8] = -offset.x * 2 / this.width; + m[9] = offset.y * 2 / this.height; + mat4.scale(m, m, [1, -1, 1]); mat4.translate(m, m, [0, 0, -this.cameraToCenterDistance]); mat4.rotateX(m, m, this._pitch); diff --git a/src/render/draw_debug.js b/src/render/draw_debug.js index 56cb6cfc473..8071b8444e9 100644 --- a/src/render/draw_debug.js +++ b/src/render/draw_debug.js @@ -11,6 +11,7 @@ import StencilMode from '../gl/stencil_mode'; import CullFaceMode from '../gl/cull_face_mode'; import {debugUniformValues} from './program/debug_program'; import Color from '../style-spec/util/color'; +import browser from '../util/browser'; import type Painter from './painter'; import type SourceCache from '../source/source_cache'; @@ -18,6 +19,55 @@ import type {OverscaledTileID} from '../source/tile_id'; export default drawDebug; +const topColor = new Color(1, 0, 0, 1); +const btmColor = new Color(0, 1, 0, 1); +const leftColor = new Color(0, 0, 1, 1); +const rightColor = new Color(1, 0, 1, 1); +const centerColor = new Color(0, 1, 1, 1); + +export function drawDebugPadding(painter: Painter) { + const padding = painter.transform.padding; + const lineWidth = 3; + // Top + drawHorizontalLine(painter, painter.transform.height - (padding.top || 0), lineWidth, topColor); + // Bottom + drawHorizontalLine(painter, padding.bottom || 0, lineWidth, btmColor); + // Left + drawVerticalLine(painter, padding.left || 0, lineWidth, leftColor); + // Right + drawVerticalLine(painter, painter.transform.width - (padding.right || 0), lineWidth, rightColor); + // Center + const center = painter.transform.centerPoint; + drawCrosshair(painter, center.x, painter.transform.height - center.y, centerColor); +} + +function drawCrosshair(painter: Painter, x: number, y: number, color: Color) { + const size = 20; + const lineWidth = 2; + //Vertical line + drawDebugSSRect(painter, x - lineWidth / 2, y - size / 2, lineWidth, size, color); + //Horizontal line + drawDebugSSRect(painter, x - size / 2, y - lineWidth / 2, size, lineWidth, color); +} + +function drawHorizontalLine(painter: Painter, y: number, lineWidth: number, color: Color) { + drawDebugSSRect(painter, 0, y + lineWidth / 2, painter.transform.width, lineWidth, color); +} + +function drawVerticalLine(painter: Painter, x: number, lineWidth: number, color: Color) { + drawDebugSSRect(painter, x - lineWidth / 2, 0, lineWidth, painter.transform.height, color); +} + +function drawDebugSSRect(painter: Painter, x: number, y: number, width: number, height: number, color: Color) { + const context = painter.context; + const gl = context.gl; + + gl.enable(gl.SCISSOR_TEST); + gl.scissor(x * browser.devicePixelRatio, y * browser.devicePixelRatio, width * browser.devicePixelRatio, height * browser.devicePixelRatio); + context.clear({color}); + gl.disable(gl.SCISSOR_TEST); +} + function drawDebug(painter: Painter, sourceCache: SourceCache, coords: Array) { for (let i = 0; i < coords.length; i++) { drawDebugTile(painter, sourceCache, coords[i]); diff --git a/src/render/painter.js b/src/render/painter.js index 5499e109d57..01f51643923 100644 --- a/src/render/painter.js +++ b/src/render/painter.js @@ -33,7 +33,7 @@ import fillExtrusion from './draw_fill_extrusion'; import hillshade from './draw_hillshade'; import raster from './draw_raster'; import background from './draw_background'; -import debug from './draw_debug'; +import debug, {drawDebugPadding} from './draw_debug'; import custom from './draw_custom'; const draw = { @@ -69,6 +69,7 @@ export type RenderPass = 'offscreen' | 'opaque' | 'translucent'; type PainterOptions = { showOverdrawInspector: boolean, showTileBoundaries: boolean, + showPadding: boolean, rotating: boolean, zooming: boolean, moving: boolean, @@ -473,6 +474,10 @@ class Painter { } } + if (this.options.showPadding) { + drawDebugPadding(this); + } + // Set defaults for most GL values so that anyone using the state after the render // encounters more expected values. this.context.setDefault(); diff --git a/src/ui/camera.js b/src/ui/camera.js index 7b88df5eca5..c5abb8b86bb 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -3,7 +3,6 @@ import { bindAll, extend, - deepEqual, warnOnce, clamp, wrap, @@ -22,6 +21,7 @@ import type {LngLatLike} from '../geo/lng_lat'; import type {LngLatBoundsLike} from '../geo/lng_lat_bounds'; import type {TaskID} from '../util/task_queue'; import type {PointLike} from '@mapbox/point-geometry'; +import type {PaddingOptions} from '../geo/edge_insets'; /** * Options common to {@link Map#jumpTo}, {@link Map#easeTo}, and {@link Map#flyTo}, controlling the desired location, @@ -35,13 +35,15 @@ import type {PointLike} from '@mapbox/point-geometry'; * is "up"; for example, a bearing of 90° orients the map so that east is up. * @property {number} pitch The desired pitch, in degrees. * @property {LngLatLike} around If `zoom` is specified, `around` determines the point around which the zoom is centered. + * @property {PaddingOptions} padding Dimensions in pixels applied on eachs side of the viewport for shifting the vanishing point. */ export type CameraOptions = { center?: LngLatLike, zoom?: number, bearing?: number, pitch?: number, - around?: LngLatLike + around?: LngLatLike, + padding?: PaddingOptions }; /** @@ -83,6 +85,7 @@ class Camera extends Evented { _zooming: boolean; _rotating: boolean; _pitching: boolean; + _padding: boolean; _bearingSnap: number; _easeEndTimeoutID: TimeoutID; @@ -284,6 +287,34 @@ class Camera extends Evented { return this; } + /** + * Returns the current padding applied around the map viewport. + * + * @memberof Map# + * @returns The current padding around the map viewport. + */ + getPadding(): PaddingOptions { return this.transform.padding; } + + /** + * Sets the padding in pixels around the viewport. + * + * Equivalent to `jumpTo({padding: padding})`. + * + * @memberof Map# + * @param padding The desired padding. Format: { left: number, right: number, top: number, bottom: number } + * @param eventData Additional properties to be added to event objects of events triggered by this method. + * @fires movestart + * @fires moveend + * @returns {Map} `this` + * @example + * // Sets a left padding of 300px, and a top padding of 50px + * map.setPadding({ left: 300, top: 50 }); + */ + setPadding(padding: PaddingOptions, eventData?: Object) { + this.jumpTo({padding}, eventData); + return this; + } + /** * Rotates the map to the specified bearing, with an animated transition. The bearing is the compass direction * that is \"up\"; for example, a bearing of 90° orients the map so that east is up. @@ -424,13 +455,14 @@ class Camera extends Evented { * }); */ _cameraForBoxAndBearing(p0: LngLatLike, p1: LngLatLike, bearing: number, options?: CameraOptions): void | CameraOptions & AnimationOptions { + const defaultPadding = { + top: 0, + bottom: 0, + right: 0, + left: 0 + }; options = extend({ - padding: { - top: 0, - bottom: 0, - right: 0, - left: 0 - }, + padding: defaultPadding, offset: [0, 0], maxZoom: this.transform.maxZoom }, options); @@ -444,18 +476,10 @@ class Camera extends Evented { left: p }; } - if (!deepEqual(Object.keys(options.padding).sort((a, b) => { - if (a < b) return -1; - if (a > b) return 1; - return 0; - }), ["bottom", "left", "right", "top"])) { - warnOnce( - "options.padding must be a positive number, or an Object with keys 'bottom', 'left', 'right', 'top'" - ); - return; - } + options.padding = extend(defaultPadding, options.padding); const tr = this.transform; + const edgePadding = tr.padding; // We want to calculate the upper right and lower left of the box defined by p0 and p1 // in a coordinate system rotate to match the destination bearing. @@ -469,8 +493,8 @@ class Camera extends Evented { // Calculate zoom: consider the original bbox and padding. const size = upperRight.sub(lowerLeft); - const scaleX = (tr.width - options.padding.left - options.padding.right) / size.x; - const scaleY = (tr.height - options.padding.top - options.padding.bottom) / size.y; + const scaleX = (tr.width - (edgePadding.left + edgePadding.right + options.padding.left + options.padding.right)) / size.x; + const scaleY = (tr.height - (edgePadding.top + edgePadding.bottom + options.padding.top + options.padding.bottom)) / size.y; if (scaleY < 0 || scaleX < 0) { warnOnce( @@ -628,6 +652,10 @@ class Camera extends Evented { tr.pitch = +options.pitch; } + if (options.padding != null && !tr.isPaddingEqual(options.padding)) { + tr.padding = options.padding; + } + this.fire(new Event('movestart', eventData)) .fire(new Event('move', eventData)); @@ -653,7 +681,7 @@ class Camera extends Evented { } /** - * Changes any combination of center, zoom, bearing, and pitch, with an animated transition + * Changes any combination of center, zoom, bearing, pitch, and padding with an animated transition * between old and new values. The map will retain its current values for any * details not specified in `options`. * @@ -693,12 +721,15 @@ class Camera extends Evented { startZoom = this.getZoom(), startBearing = this.getBearing(), startPitch = this.getPitch(), + startPadding = this.getPadding(), zoom = 'zoom' in options ? +options.zoom : startZoom, bearing = 'bearing' in options ? this._normalizeBearing(options.bearing, startBearing) : startBearing, - pitch = 'pitch' in options ? +options.pitch : startPitch; + pitch = 'pitch' in options ? +options.pitch : startPitch, + padding = 'padding' in options ? options.padding : tr.padding; - const pointAtOffset = tr.centerPoint.add(Point.convert(options.offset)); + const offsetAsPoint = Point.convert(options.offset); + let pointAtOffset = tr.centerPoint.add(offsetAsPoint); const locationAtOffset = tr.pointLocation(pointAtOffset); const center = LngLat.convert(options.center || locationAtOffset); this._normalizeCenter(center); @@ -717,6 +748,7 @@ class Camera extends Evented { this._zooming = (zoom !== startZoom); this._rotating = (startBearing !== bearing); this._pitching = (pitch !== startPitch); + this._padding = !tr.isPaddingEqual(padding); this._prepareEase(eventData, options.noMoveStart); @@ -732,6 +764,12 @@ class Camera extends Evented { if (this._pitching) { tr.pitch = interpolate(startPitch, pitch, k); } + if (this._padding) { + tr.interpolatePadding(startPadding, padding, k); + // When padding is being applied, Transform#centerPoint is changing continously, + // thus we need to recalculate offsetPoint every fra,e + pointAtOffset = tr.centerPoint.add(offsetAsPoint); + } if (around) { tr.setLocationAtPoint(around, aroundPoint); @@ -796,6 +834,7 @@ class Camera extends Evented { this._zooming = false; this._rotating = false; this._pitching = false; + this._padding = false; if (wasZooming) { this.fire(new Event('zoomend', eventData)); @@ -895,14 +934,17 @@ class Camera extends Evented { const tr = this.transform, startZoom = this.getZoom(), startBearing = this.getBearing(), - startPitch = this.getPitch(); + startPitch = this.getPitch(), + startPadding = this.getPadding(); const zoom = 'zoom' in options ? clamp(+options.zoom, tr.minZoom, tr.maxZoom) : startZoom; const bearing = 'bearing' in options ? this._normalizeBearing(options.bearing, startBearing) : startBearing; const pitch = 'pitch' in options ? +options.pitch : startPitch; + const padding = 'padding' in options ? options.padding : tr.padding; const scale = tr.zoomScale(zoom - startZoom); - const pointAtOffset = tr.centerPoint.add(Point.convert(options.offset)); + const offsetAsPoint = Point.convert(options.offset); + let pointAtOffset = tr.centerPoint.add(offsetAsPoint); const locationAtOffset = tr.pointLocation(pointAtOffset); const center = LngLat.convert(options.center || locationAtOffset); this._normalizeCenter(center); @@ -990,6 +1032,7 @@ class Camera extends Evented { this._zooming = true; this._rotating = (startBearing !== bearing); this._pitching = (pitch !== startPitch); + this._padding = !tr.isPaddingEqual(padding); this._prepareEase(eventData, false); @@ -1005,6 +1048,12 @@ class Camera extends Evented { if (this._pitching) { tr.pitch = interpolate(startPitch, pitch, k); } + if (this._padding) { + tr.interpolatePadding(startPadding, padding, k); + // When padding is being applied, Transform#centerPoint is changing continously, + // thus we need to recalculate offsetPoint every frame + pointAtOffset = tr.centerPoint.add(offsetAsPoint); + } const newCenter = k === 1 ? center : tr.unproject(from.add(delta.mult(u(s))).mult(scale)); tr.setLocationAtPoint(tr.renderWorldCopies ? newCenter.wrap() : newCenter, pointAtOffset); diff --git a/src/ui/map.js b/src/ui/map.js index c00e326197f..63f2cdfe4d9 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -271,6 +271,7 @@ class Map extends Camera { _interactive: ?boolean; _showTileBoundaries: ?boolean; _showCollisionBoxes: ?boolean; + _showPadding: ?boolean; _showOverdrawInspector: boolean; _repaint: ?boolean; _vertices: ?boolean; @@ -2198,8 +2199,9 @@ class Map extends Camera { rotating: this.isRotating(), zooming: this.isZooming(), moving: this.isMoving(), + fadeDuration: this._fadeDuration, + showPadding: this.showPadding, gpuTiming: !!this.listens('gpu-timing-layer'), - fadeDuration: this._fadeDuration }); this.fire(new Event('render')); @@ -2349,6 +2351,22 @@ class Map extends Camera { this._update(); } + /** + * Gets and sets a Boolean indicating whether the map will visualize + * the padding offsets. + * + * @name showPadding + * @type {boolean} + * @instance + * @memberof Map + */ + get showPadding(): boolean { return !!this._showPadding; } + set showPadding(value: boolean) { + if (this._showPadding === value) return; + this._showPadding = value; + this._update(); + } + /** * Gets and sets a Boolean indicating whether the map will render boxes * around all symbols in the data source, revealing which symbols diff --git a/test/integration/render-tests/debug/padding/ease-to-btm-distort/expected.png b/test/integration/render-tests/debug/padding/ease-to-btm-distort/expected.png new file mode 100644 index 00000000000..f7ddba62f96 Binary files /dev/null and b/test/integration/render-tests/debug/padding/ease-to-btm-distort/expected.png differ diff --git a/test/integration/render-tests/debug/padding/ease-to-btm-distort/style.json b/test/integration/render-tests/debug/padding/ease-to-btm-distort/style.json new file mode 100644 index 00000000000..df80784d22b --- /dev/null +++ b/test/integration/render-tests/debug/padding/ease-to-btm-distort/style.json @@ -0,0 +1,112 @@ +{ + "version": 8, + "metadata": { + "test": { + "showPadding": true, + "height": 256, + "operations": [ + [ + "easeTo", + { + "padding": { + "top": 10, + "left": 50, + "bottom": 100, + "right": 10 + } + } + ], + [ + "wait", + 500 + ] + ] + } + }, + "center": [ 0, 0 ], + "zoom": 8, + "pitch": 60, + "sources": { + "northward-road": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ 0, -25], + [ 0, 25] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ -0.2, -25], + [ -0.2, 25] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ 0.2, -25], + [ 0.2, 25] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ -25, 0.2], + [ 25, 0.2] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ -25, -0.2], + [ 25, -0.2] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ -25, 0], + [ 25, 0] + ] + } + } + ] + } + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "line", + "type": "line", + "source": "northward-road", + "layout": {}, + "paint": { + "line-width": 10 + } + } + ] +} diff --git a/test/integration/render-tests/debug/padding/ease-to-left-distort/expected.png b/test/integration/render-tests/debug/padding/ease-to-left-distort/expected.png new file mode 100644 index 00000000000..2103cffb96c Binary files /dev/null and b/test/integration/render-tests/debug/padding/ease-to-left-distort/expected.png differ diff --git a/test/integration/render-tests/debug/padding/ease-to-left-distort/style.json b/test/integration/render-tests/debug/padding/ease-to-left-distort/style.json new file mode 100644 index 00000000000..9f9d70ad524 --- /dev/null +++ b/test/integration/render-tests/debug/padding/ease-to-left-distort/style.json @@ -0,0 +1,112 @@ +{ + "version": 8, + "metadata": { + "test": { + "showPadding": true, + "height": 256, + "operations": [ + [ + "easeTo", + { + "padding": { + "top": 20, + "left": 250, + "bottom": 10, + "right": 15 + } + } + ], + [ + "wait", + 500 + ] + ] + } + }, + "center": [ 0, 0 ], + "zoom": 8, + "pitch": 60, + "sources": { + "northward-road": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ 0, -25], + [ 0, 25] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ -0.2, -25], + [ -0.2, 25] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ 0.2, -25], + [ 0.2, 25] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ -25, 0.2], + [ 25, 0.2] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ -25, -0.2], + [ 25, -0.2] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ -25, 0], + [ 25, 0] + ] + } + } + ] + } + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "line", + "type": "line", + "source": "northward-road", + "layout": {}, + "paint": { + "line-width": 10 + } + } + ] +} diff --git a/test/integration/render-tests/debug/padding/ease-to-no-distort/expected.png b/test/integration/render-tests/debug/padding/ease-to-no-distort/expected.png new file mode 100644 index 00000000000..e709e1c98ed Binary files /dev/null and b/test/integration/render-tests/debug/padding/ease-to-no-distort/expected.png differ diff --git a/test/integration/render-tests/debug/padding/ease-to-no-distort/style.json b/test/integration/render-tests/debug/padding/ease-to-no-distort/style.json new file mode 100644 index 00000000000..46ec2f25030 --- /dev/null +++ b/test/integration/render-tests/debug/padding/ease-to-no-distort/style.json @@ -0,0 +1,127 @@ +{ + "version": 8, + "metadata": { + "test": { + "showPadding": true, + "height": 256, + "operations": [ + [ + "easeTo", + { + "padding": { + "top": 20, + "left": 50, + "bottom": 10, + "right": 250 + } + } + ], + [ + "wait", + 500 + ], + [ + "easeTo", + { + "padding": { + "top": 10, + "left": 10, + "bottom": 10, + "right": 10 + } + } + ], + [ + "wait", + 500 + ] + ] + } + }, + "center": [ 0, 0 ], + "zoom": 8, + "pitch": 60, + "sources": { + "northward-road": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ 0, -25], + [ 0, 25] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ -0.2, -25], + [ -0.2, 25] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ 0.2, -25], + [ 0.2, 25] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ -25, 0.2], + [ 25, 0.2] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ -25, -0.2], + [ 25, -0.2] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ -25, 0], + [ 25, 0] + ] + } + } + ] + } + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "line", + "type": "line", + "source": "northward-road", + "layout": {}, + "paint": { + "line-width": 10 + } + } + ] +} diff --git a/test/integration/render-tests/debug/padding/ease-to-right-distort/expected.png b/test/integration/render-tests/debug/padding/ease-to-right-distort/expected.png new file mode 100644 index 00000000000..ddf26422755 Binary files /dev/null and b/test/integration/render-tests/debug/padding/ease-to-right-distort/expected.png differ diff --git a/test/integration/render-tests/debug/padding/ease-to-right-distort/style.json b/test/integration/render-tests/debug/padding/ease-to-right-distort/style.json new file mode 100644 index 00000000000..ccc36c279f8 --- /dev/null +++ b/test/integration/render-tests/debug/padding/ease-to-right-distort/style.json @@ -0,0 +1,112 @@ +{ + "version": 8, + "metadata": { + "test": { + "showPadding": true, + "height": 256, + "operations": [ + [ + "easeTo", + { + "padding": { + "top": 20, + "left": 50, + "bottom": 10, + "right": 250 + } + } + ], + [ + "wait", + 500 + ] + ] + } + }, + "center": [ 0, 0 ], + "zoom": 8, + "pitch": 60, + "sources": { + "northward-road": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ 0, -25], + [ 0, 25] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ -0.2, -25], + [ -0.2, 25] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ 0.2, -25], + [ 0.2, 25] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ -25, 0.2], + [ 25, 0.2] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ -25, -0.2], + [ 25, -0.2] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ -25, 0], + [ 25, 0] + ] + } + } + ] + } + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "line", + "type": "line", + "source": "northward-road", + "layout": {}, + "paint": { + "line-width": 10 + } + } + ] +} diff --git a/test/integration/render-tests/debug/padding/ease-to-top-distort/expected.png b/test/integration/render-tests/debug/padding/ease-to-top-distort/expected.png new file mode 100644 index 00000000000..57adfa9d76a Binary files /dev/null and b/test/integration/render-tests/debug/padding/ease-to-top-distort/expected.png differ diff --git a/test/integration/render-tests/debug/padding/ease-to-top-distort/style.json b/test/integration/render-tests/debug/padding/ease-to-top-distort/style.json new file mode 100644 index 00000000000..183d7da9c15 --- /dev/null +++ b/test/integration/render-tests/debug/padding/ease-to-top-distort/style.json @@ -0,0 +1,112 @@ +{ + "version": 8, + "metadata": { + "test": { + "showPadding": true, + "height": 256, + "operations": [ + [ + "easeTo", + { + "padding": { + "top": 100, + "left": 50, + "bottom": 10, + "right": 250 + } + } + ], + [ + "wait", + 500 + ] + ] + } + }, + "center": [ 0, 0 ], + "zoom": 8, + "pitch": 60, + "sources": { + "northward-road": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ 0, -25], + [ 0, 25] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ -0.2, -25], + [ -0.2, 25] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ 0.2, -25], + [ 0.2, 25] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ -25, 0.2], + [ 25, 0.2] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ -25, -0.2], + [ 25, -0.2] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ -25, 0], + [ 25, 0] + ] + } + } + ] + } + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "line", + "type": "line", + "source": "northward-road", + "layout": {}, + "paint": { + "line-width": 10 + } + } + ] +} diff --git a/test/integration/render-tests/debug/padding/set-padding/expected.png b/test/integration/render-tests/debug/padding/set-padding/expected.png new file mode 100644 index 00000000000..c1d5e83e1eb Binary files /dev/null and b/test/integration/render-tests/debug/padding/set-padding/expected.png differ diff --git a/test/integration/render-tests/debug/padding/set-padding/style.json b/test/integration/render-tests/debug/padding/set-padding/style.json new file mode 100644 index 00000000000..c54c836ed35 --- /dev/null +++ b/test/integration/render-tests/debug/padding/set-padding/style.json @@ -0,0 +1,82 @@ +{ + "version": 8, + "metadata": { + "test": { + "showPadding": true, + "height": 256, + "operations": [ + [ + "setPadding", + { + "top": 20, + "left": 125, + "bottom": 10, + "right": 15 + } + ], + [ + "wait" + ] + ] + } + }, + "center": [ 0, 0 ], + "zoom": 8, + "pitch": 60, + "sources": { + "northward-road": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ 0, -25], + [ 0, 25] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ -0.2, -25], + [ -0.2, 25] + ] + } + },{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ 0.2, -25], + [ 0.2, 25] + ] + } + } + ] + } + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "line", + "type": "line", + "source": "northward-road", + "layout": {}, + "paint": { + "line-width": 10 + } + } + ] +} diff --git a/test/suite_implementation.js b/test/suite_implementation.js index 5aef1a905d3..0b33c0803dc 100644 --- a/test/suite_implementation.js +++ b/test/suite_implementation.js @@ -66,6 +66,7 @@ module.exports = function(style, options, _callback) { // eslint-disable-line im if (options.debug) map.showTileBoundaries = true; if (options.showOverdrawInspector) map.showOverdrawInspector = true; + if (options.showPadding) map.showPadding = true; const gl = map.painter.context.gl; diff --git a/test/unit/geo/edge_insets.test.js b/test/unit/geo/edge_insets.test.js new file mode 100644 index 00000000000..ccfc1982cf4 --- /dev/null +++ b/test/unit/geo/edge_insets.test.js @@ -0,0 +1,99 @@ +import {test} from '../../util/test'; +import EdgeInsets from '../../../src/geo/edge_insets'; + +test('EdgeInsets', (t) => { + t.test('#constructor', (t) => { + t.ok(new EdgeInsets() instanceof EdgeInsets, 'creates an object with default values'); + t.throws(() => { + new EdgeInsets(NaN, 10); + }, `Invalid input EdgeInsets(NaN, 10) gets detected and error is thrown`); + t.throws(() => { + new EdgeInsets(-10, 10, 20, 10); + }, `Invalid input EdgeInsets(-10, 10, 20, 10) gets detected and error is thrown`); + + t.test('valid initialization', (t) => { + const top = 10; + const bottom = 15; + const left = 26; + const right = 19; + + const inset = new EdgeInsets(top, bottom, left, right); + t.equal(inset.top, top); + t.equal(inset.bottom, bottom); + t.equal(inset.left, left); + t.equal(inset.right, right); + t.end(); + }); + t.end(); + }); + + t.test('#getCenter', (t) => { + t.test('valid input', (t) => { + const inset = new EdgeInsets(10, 15, 50, 10); + const center = inset.getCenter(600, 400); + t.equal(center.x, 320); + t.equal(center.y, 197.5); + t.end(); + }); + + t.test('center clamping', (t) => { + const inset = new EdgeInsets(300, 200, 500, 200); + const center = inset.getCenter(600, 400); + // Midpoint of the overlap when padding overlaps + t.equal(center.x, 450); + t.equal(center.y, 250); + t.end(); + }); + t.end(); + }); + + t.test('#interpolate', (t) => { + t.test('it works', (t) => { + const inset1 = new EdgeInsets(10, 15, 50, 10); + const inset2 = new EdgeInsets(20, 30, 100, 10); + const inset3 = inset1.interpolate(inset1, inset2, 0.5); + // inset1 is mutated in-place + t.equal(inset3, inset1); + + t.equal(inset3.top, 15); + t.equal(inset3.bottom, 22.5); + t.equal(inset3.left, 75); + t.equal(inset3.right, 10); + t.end(); + }); + + t.test('it retains insets that dont have new parameters passed in', (t) => { + const inset = new EdgeInsets(10, 15, 50, 10); + const target = { + top: 20 + }; + inset.interpolate(inset, target, 0.5); + t.equal(inset.top, 15); + t.equal(inset.bottom, 15); + t.equal(inset.left, 50); + t.equal(inset.right, 10); + t.end(); + }); + + t.end(); + }); + + t.test('#equals', (t) => { + const inset1 = new EdgeInsets(10, 15, 50, 10); + const inset2 = new EdgeInsets(10, 15, 50, 10); + const inset3 = new EdgeInsets(10, 15, 50, 11); + t.ok(inset1.equals(inset2)); + t.notOk(inset2.equals(inset3)); + t.end(); + }); + + t.test('#clone', (t) => { + const inset1 = new EdgeInsets(10, 15, 50, 10); + const inset2 = inset1.clone(); + t.notOk(inset2 === inset1); + t.ok(inset1.equals(inset2)); + t.end(); + }); + + t.end(); +}); diff --git a/test/unit/ui/camera.test.js b/test/unit/ui/camera.test.js index c801dd87f16..fe656b558ff 100644 --- a/test/unit/ui/camera.test.js +++ b/test/unit/ui/camera.test.js @@ -316,6 +316,45 @@ test('camera', (t) => { t.end(); }); + t.test('#setPadding', (t) => { + t.test('sets padding', (t) => { + const camera = createCamera(); + const padding = {left: 300, top: 100, right: 50, bottom: 10}; + camera.setPadding(padding); + t.deepEqual(camera.getPadding(), padding); + t.end(); + }); + + t.test('existing padding is retained if no new values are passed in', (t) => { + const camera = createCamera(); + const padding = {left: 300, top: 100, right: 50, bottom: 10}; + camera.setPadding(padding); + camera.setPadding({}); + + const currentPadding = camera.getPadding(); + t.deepEqual(currentPadding, padding); + t.end(); + }); + + t.test('doesnt change padding thats already present if new value isnt passed in', (t) => { + const camera = createCamera(); + const padding = {left: 300, top: 100, right: 50, bottom: 10}; + camera.setPadding(padding); + const padding1 = {right: 100}; + camera.setPadding(padding1); + + const currentPadding = camera.getPadding(); + t.equal(currentPadding.left, padding.left); + t.equal(currentPadding.top, padding.top); + // padding1 here + t.equal(currentPadding.right, padding1.right); + t.equal(currentPadding.bottom, padding.bottom); + t.end(); + }); + + t.end(); + }); + t.test('#panBy', (t) => { t.test('pans by specified amount', (t) => { const camera = createCamera();