diff --git a/src/ui/camera.js b/src/ui/camera.js index 5987a8312c5..6216369aa25 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -355,35 +355,25 @@ class Camera extends Evented { return this; } - /** - * Pans and zooms the map to contain its visible area within the specified geographical bounds. - * This function will also reset the map's bearing to 0 if bearing is nonzero. - * * @memberof Map# - * @param bounds Center these bounds in the viewport and use the highest - * zoom level up to and including `Map#getMaxZoom()` that fits them in the viewport. + * @param bounds Calculate the center for these bounds in the viewport and use + * the highest zoom level up to and including `Map#getMaxZoom()` that fits + * in the viewport. * @param options * @param {number | PaddingOptions} [options.padding] The amount of padding in pixels to add to the given bounds. - * @param {boolean} [options.linear=false] If `true`, the map transitions using - * {@link Map#easeTo}. If `false`, the map transitions using {@link Map#flyTo}. See - * those functions and {@link AnimationOptions} for information about options available. - * @param {Function} [options.easing] An easing function for the animated transition. See {@link AnimationOptions}. * @param {PointLike} [options.offset=[0, 0]] The center of the given bounds relative to the map's center, measured in pixels. - * @param {number} [options.maxZoom] The maximum zoom level to allow when the map view transitions to the specified bounds. - * @param eventData Additional properties to be added to event objects of events triggered by this method. - * @fires movestart - * @fires moveend - * @returns {Map} `this` - * @example + * @param {number} [options.maxZoom] The maximum zoom level to allow when the camera would transition to the specified bounds. + * @returns {CameraOptions | void} If map is able to fit to provided bounds, returns `CameraOptions` with + * at least `center`, `zoom`, `bearing`, `offset`, `padding`, and `maxZoom`, as well as any other + * `options` provided in arguments. If map is unable to fit, method will warn and return undefined. + * @example * var bbox = [[-79, 43], [-73, 45]]; - * map.fitBounds(bbox, { + * var newCameraTransform = map.cameraForBounds(bbox, { * padding: {top: 10, bottom:25, left: 15, right: 5} * }); - * @see [Fit a map to a bounding box](https://www.mapbox.com/mapbox-gl-js/example/fitbounds/) */ - fitBounds(bounds: LngLatBoundsLike, options?: AnimationOptions & CameraOptions, eventData?: Object) { - + cameraForBounds(bounds: LngLatBoundsLike, options?: CameraOptions): void | CameraOptions & AnimationOptions { options = extend({ padding: { top: 0, @@ -412,7 +402,7 @@ class Camera extends Evented { warnOnce( "options.padding must be a positive number, or an Object with keys 'bottom', 'left', 'right', 'top'" ); - return this; + return; } bounds = LngLatBounds.convert(bounds); @@ -438,13 +428,50 @@ class Camera extends Evented { warnOnce( 'Map cannot fit within canvas with the given bounds, padding, and/or offset.' ); - return this; + return; } options.center = tr.unproject(nw.add(se).div(2)); options.zoom = Math.min(tr.scaleZoom(tr.scale * Math.min(scaleX, scaleY)), options.maxZoom); options.bearing = 0; + return options; + } + + /** + * Pans and zooms the map to contain its visible area within the specified geographical bounds. + * This function will also reset the map's bearing to 0 if bearing is nonzero. + * + * @memberof Map# + * @param bounds Center these bounds in the viewport and use the highest + * zoom level up to and including `Map#getMaxZoom()` that fits them in the viewport. + * @param options + * @param {number | PaddingOptions} [options.padding] The amount of padding in pixels to add to the given bounds. + * @param {boolean} [options.linear=false] If `true`, the map transitions using + * {@link Map#easeTo}. If `false`, the map transitions using {@link Map#flyTo}. See + * those functions and {@link AnimationOptions} for information about options available. + * @param {Function} [options.easing] An easing function for the animated transition. See {@link AnimationOptions}. + * @param {PointLike} [options.offset=[0, 0]] The center of the given bounds relative to the map's center, measured in pixels. + * @param {number} [options.maxZoom] The maximum zoom level to allow when the map view transitions to the specified bounds. + * @param eventData Additional properties to be added to event objects of events triggered by this method. + * @fires movestart + * @fires moveend + * @returns {Map} `this` + * @example + * var bbox = [[-79, 43], [-73, 45]]; + * map.fitBounds(bbox, { + * padding: {top: 10, bottom:25, left: 15, right: 5} + * }); + * @see [Fit a map to a bounding box](https://www.mapbox.com/mapbox-gl-js/example/fitbounds/) + */ + fitBounds(bounds: LngLatBoundsLike, options?: AnimationOptions & CameraOptions, eventData?: Object) { + const calculatedOptions = this.cameraForBounds(bounds, options); + + // cameraForBounds warns + returns undefined if unable to fit: + if (!calculatedOptions) return this; + + options = extend(calculatedOptions, options); + return options.linear ? this.easeTo(options, eventData) : this.flyTo(options, eventData); diff --git a/test/unit/ui/camera.test.js b/test/unit/ui/camera.test.js index 161a6aed921..5efba5f8aa1 100644 --- a/test/unit/ui/camera.test.js +++ b/test/unit/ui/camera.test.js @@ -1664,6 +1664,39 @@ test('camera', (t) => { t.end(); }); + t.test('#cameraForBounds', (t) => { + t.test('no padding passed', (t) => { + const camera = createCamera(); + const bb = [[-133, 16], [-68, 50]]; + + const transform = camera.cameraForBounds(bb); + t.deepEqual(fixedLngLat(transform.center, 4), { lng: -100.5, lat: 34.7171 }, 'correctly calculates coordinates for new bounds'); + t.equal(fixedNum(transform.zoom, 3), 2.469); + t.end(); + }); + + t.test('padding number', (t) => { + const camera = createCamera(); + const bb = [[-133, 16], [-68, 50]]; + + const transform = camera.cameraForBounds(bb, { padding: 15 }); + t.deepEqual(fixedLngLat(transform.center, 4), { lng: -100.5, lat: 34.7171 }, 'correctly calculates coordinates for bounds with padding option as number applied'); + t.equal(fixedNum(transform.zoom, 3), 2.382); + t.end(); + }); + + t.test('padding object', (t) => { + const camera = createCamera(); + const bb = [[-133, 16], [-68, 50]]; + + const transform = camera.cameraForBounds(bb, { padding: {top: 10, right: 75, bottom: 50, left: 25}, duration: 0 }); + t.deepEqual(fixedLngLat(transform.center, 4), { lng: -100.5, lat: 34.7171 }, 'correctly calculates coordinates for bounds with padding option as object applied'); + t.end(); + }); + + t.end(); + }); + t.test('#fitBounds', (t) => { t.test('no padding passed', (t) => { const camera = createCamera();