UNPKG

mapbox-gl

Version:
1,140 lines (1,011 loc) 77.2 kB
// @flow import { bindAll, extend, warnOnce, clamp, wrap, ease as defaultEasing, pick, degToRad } from '../util/util.js'; import {number as interpolate} from '../style-spec/util/interpolate.js'; import browser from '../util/browser.js'; import LngLat, {earthRadius} from '../geo/lng_lat.js'; import LngLatBounds from '../geo/lng_lat_bounds.js'; import Point from '@mapbox/point-geometry'; import {Event, Evented} from '../util/evented.js'; import assert from 'assert'; import {Debug} from '../util/debug.js'; import MercatorCoordinate, { mercatorZfromAltitude, mercatorXfromLng, mercatorYfromLat, latFromMercatorY, lngFromMercatorX } from '../geo/mercator_coordinate.js'; import { latLngToECEF, ecefToLatLng, GLOBE_RADIUS, GLOBE_ZOOM_THRESHOLD_MAX, GLOBE_ZOOM_THRESHOLD_MIN } from '../geo/projection/globe_util.js'; import {vec3, vec4, mat4} from 'gl-matrix'; import type {FreeCameraOptions} from './free_camera.js'; import type Transform from '../geo/transform.js'; import type {LngLatLike} from '../geo/lng_lat.js'; import type {LngLatBoundsLike} from '../geo/lng_lat_bounds.js'; import type {ElevationQueryOptions} from '../terrain/elevation.js'; import type {TaskID} from '../util/task_queue.js'; import type {Callback} from '../types/callback.js'; import type {PointLike} from '@mapbox/point-geometry'; import {Aabb} from '../util/primitives.js'; import type {PaddingOptions} from '../geo/edge_insets.js'; import type {MapEvent} from './events.js'; /** * A helper type: converts all Object type values to non-maybe types. */ type Required<T> = $ObjMap<T, <V>(v: V) => $NonMaybeType<V>>; /** * Options common to {@link Map#jumpTo}, {@link Map#easeTo}, and {@link Map#flyTo}, controlling the desired location, * zoom, bearing, and pitch of the camera. All properties are optional, and when a property is omitted, the current * camera value for that property will remain unchanged. * * @typedef {Object} CameraOptions * @property {LngLatLike} center The location to place at the screen center. * @property {number} zoom The desired zoom level. * @property {number} bearing The desired bearing in degrees. The bearing is the compass direction that * is "up". For example, `bearing: 90` orients the map so that east is up. * @property {number} pitch The desired pitch in degrees. The pitch is the angle towards the horizon * measured in degrees with a range between 0 and 85 degrees. For example, pitch: 0 provides the appearance * of looking straight down at the map, while pitch: 60 tilts the user's perspective towards the horizon. * Increasing the pitch value is often used to display 3D objects. * @property {LngLatLike} around The location serving as the origin for a change in `zoom`, `pitch` and/or `bearing`. * This location will remain at the same screen position following the transform. * This is useful for drawing attention to a location that is not in the screen center. * `center` is ignored if `around` is included. * @property {PaddingOptions} padding Dimensions in pixels applied on each side of the viewport for shifting the vanishing point. * @example * // set the map's initial perspective with CameraOptions * const map = new mapboxgl.Map({ * container: 'map', * style: 'mapbox://styles/mapbox/streets-v11', * center: [-73.5804, 45.53483], * pitch: 60, * bearing: -60, * zoom: 10 * }); * @see [Example: Set pitch and bearing](https://docs.mapbox.com/mapbox-gl-js/example/set-perspective/) * @see [Example: Jump to a series of locations](https://docs.mapbox.com/mapbox-gl-js/example/jump-to/) * @see [Example: Fly to a location](https://docs.mapbox.com/mapbox-gl-js/example/flyto/) * @see [Example: Display buildings in 3D](https://docs.mapbox.com/mapbox-gl-js/example/3d-buildings/) */ export type CameraOptions = { center?: LngLatLike, zoom?: number, bearing?: number, pitch?: number, around?: LngLatLike, padding?: PaddingOptions }; export type FullCameraOptions = { maxZoom: number, offset: PointLike, padding: Required<PaddingOptions> } & CameraOptions /** * Options common to map movement methods that involve animation, such as {@link Map#panBy} and * {@link Map#easeTo}, controlling the duration and easing function of the animation. All properties * are optional. * * @typedef {Object} AnimationOptions * @property {number} duration The animation's duration, measured in milliseconds. * @property {Function} easing A function taking a time in the range 0..1 and returning a number where 0 is * the initial state and 1 is the final state. * @property {PointLike} offset The target center's offset relative to real map container center at the end of animation. * @property {boolean} animate If `false`, no animation will occur. * @property {boolean} essential If `true`, then the animation is considered essential and will not be affected by * [`prefers-reduced-motion`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion). * @property {boolean} preloadOnly If `true`, it will trigger tiles loading across the animation path, but no animation will occur. * @property {number} curve The zooming "curve" that will occur along the * flight path. A high value maximizes zooming for an exaggerated animation, while a low * value minimizes zooming for an effect closer to {@link Map#easeTo}. 1.42 is the average * value selected by participants in the user study discussed in * [van Wijk (2003)](https://www.win.tue.nl/~vanwijk/zoompan.pdf). A value of * `Math.pow(6, 0.25)` would be equivalent to the root mean squared average velocity. A * value of 1 would produce a circular motion. If `minZoom` is specified, this option will be ignored. * @property {number} minZoom The zero-based zoom level at the peak of the flight path. If * this option is specified, `curve` will be ignored. * @property {number} speed The average speed of the animation defined in relation to * `curve`. A speed of 1.2 means that the map appears to move along the flight path * by 1.2 times `curve` screenfuls every second. A _screenful_ is the map's visible span. * It does not correspond to a fixed physical distance, but varies by zoom level. * @property {number} screenSpeed The average speed of the animation measured in screenfuls * per second, assuming a linear timing curve. If `speed` is specified, this option is ignored. * @property {number} maxDuration The animation's maximum duration, measured in milliseconds. * If duration exceeds maximum duration, it resets to 0. * @see [Example: Slowly fly to a location](https://docs.mapbox.com/mapbox-gl-js/example/flyto-options/) * @see [Example: Customize camera animations](https://docs.mapbox.com/mapbox-gl-js/example/camera-animation/) * @see [Example: Navigate the map with game-like controls](https://docs.mapbox.com/mapbox-gl-js/example/game-controls/) */ export type AnimationOptions = { duration?: number, easing?: (_: number) => number, offset?: PointLike, animate?: boolean, essential?: boolean, preloadOnly?: boolean }; export type EasingOptions = CameraOptions & AnimationOptions; export type ElevationBoxRaycast = { minLngLat: LngLat, maxLngLat: LngLat, minAltitude: number, maxAltitude: number }; const freeCameraNotSupportedWarning = 'map.setFreeCameraOptions(...) and map.getFreeCameraOptions() are not yet supported for non-mercator projections.'; /** * Options for setting padding on calls to methods such as {@link Map#fitBounds}, {@link Map#fitScreenCoordinates}, and {@link Map#setPadding}. Adjust these options to set the amount of padding in pixels added to the edges of the canvas. Set a uniform padding on all edges or individual values for each edge. All properties of this object must be * non-negative integers. * * @typedef {Object} PaddingOptions * @property {number} top Padding in pixels from the top of the map canvas. * @property {number} bottom Padding in pixels from the bottom of the map canvas. * @property {number} left Padding in pixels from the left of the map canvas. * @property {number} right Padding in pixels from the right of the map canvas. * * @example * const bbox = [[-79, 43], [-73, 45]]; * map.fitBounds(bbox, { * padding: {top: 10, bottom:25, left: 15, right: 5} * }); * * @example * const bbox = [[-79, 43], [-73, 45]]; * map.fitBounds(bbox, { * padding: 20 * }); * @see [Example: Fit to the bounds of a LineString](https://docs.mapbox.com/mapbox-gl-js/example/zoomto-linestring/) * @see [Example: Fit a map to a bounding box](https://docs.mapbox.com/mapbox-gl-js/example/fitbounds/) */ class Camera extends Evented { transform: Transform; _moving: boolean; _zooming: boolean; _rotating: boolean; _pitching: boolean; _padding: boolean; _bearingSnap: number; _easeStart: number; _easeOptions: {duration: number, easing: (_: number) => number}; _easeId: string | void; _respectPrefersReducedMotion: boolean; _onEaseFrame: ?(_: number) => Transform | void; _onEaseEnd: ?(easeId?: string) => void; _easeFrameId: ?TaskID; +_requestRenderFrame: (() => void) => TaskID; +_cancelRenderFrame: (_: TaskID) => void; +_preloadTiles: (transform: Transform | Array<Transform>, callback?: Callback<any>) => any; constructor(transform: Transform, options: {bearingSnap: number, respectPrefersReducedMotion: ?boolean}) { super(); this._moving = false; this._zooming = false; this.transform = transform; this._bearingSnap = options.bearingSnap; this._respectPrefersReducedMotion = options.respectPrefersReducedMotion !== false; bindAll(['_renderFrameCallback'], this); //addAssertions(this); } /** @section {Camera} * @method * @instance * @memberof Map */ /** * Returns the map's geographical centerpoint. * * @memberof Map# * @returns {LngLat} The map's geographical centerpoint. * @example * // Return a LngLat object such as {lng: 0, lat: 0}. * const center = map.getCenter(); * // Access longitude and latitude values directly. * const {lng, lat} = map.getCenter(); * @see [Tutorial: Use Mapbox GL JS in a React app](https://docs.mapbox.com/help/tutorials/use-mapbox-gl-js-with-react/#store-the-new-coordinates) */ getCenter(): LngLat { return new LngLat(this.transform.center.lng, this.transform.center.lat); } /** * Sets the map's geographical centerpoint. Equivalent to `jumpTo({center: center})`. * * @memberof Map# * @param {LngLatLike} center The centerpoint to set. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} Returns itself to allow for method chaining. * @example * map.setCenter([-74, 38]); */ setCenter(center: LngLatLike, eventData?: Object): this { return this.jumpTo({center}, eventData); } /** * Pans the map by the specified offset. * * @memberof Map# * @param {PointLike} offset The `x` and `y` coordinates by which to pan the map. * @param {AnimationOptions | null} options An options object describing the destination and animation of the transition. We do not recommend using `options.offset` since this value will override the value of the `offset` parameter. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} `this` Returns itself to allow for method chaining. * @example * map.panBy([-74, 38]); * @example * // panBy with an animation of 5 seconds. * map.panBy([-74, 38], {duration: 5000}); * @see [Example: Navigate the map with game-like controls](https://www.mapbox.com/mapbox-gl-js/example/game-controls/) */ panBy(offset: PointLike, options?: AnimationOptions, eventData?: Object): this { offset = Point.convert(offset).mult(-1); return this.panTo(this.transform.center, extend({offset}, options), eventData); } /** * Pans the map to the specified location with an animated transition. * * @memberof Map# * @param {LngLatLike} lnglat The location to pan the map to. * @param {AnimationOptions | null} options Options describing the destination and animation of the transition. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} Returns itself to allow for method chaining. * @example * map.panTo([-74, 38]); * @example * // Specify that the panTo animation should last 5000 milliseconds. * map.panTo([-74, 38], {duration: 5000}); * @see [Example: Update a feature in realtime](https://docs.mapbox.com/mapbox-gl-js/example/live-update-feature/) */ panTo(lnglat: LngLatLike, options?: AnimationOptions, eventData?: Object): this { return this.easeTo(extend({ center: lnglat }, options), eventData); } /** * Returns the map's current zoom level. * * @memberof Map# * @returns {number} The map's current zoom level. * @example * map.getZoom(); */ getZoom(): number { return this.transform.zoom; } /** * Sets the map's zoom level. Equivalent to `jumpTo({zoom: zoom})`. * * @memberof Map# * @param {number} zoom The zoom level to set (0-20). * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:zoomstart * @fires Map.event:move * @fires Map.event:zoom * @fires Map.event:moveend * @fires Map.event:zoomend * @returns {Map} Returns itself to allow for method chaining. * @example * // Zoom to the zoom level 5 without an animated transition * map.setZoom(5); */ setZoom(zoom: number, eventData?: Object): this { this.jumpTo({zoom}, eventData); return this; } /** * Zooms the map to the specified zoom level, with an animated transition. * * @memberof Map# * @param {number} zoom The zoom level to transition to. * @param {AnimationOptions | null} options Options object. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:zoomstart * @fires Map.event:move * @fires Map.event:zoom * @fires Map.event:moveend * @fires Map.event:zoomend * @returns {Map} Returns itself to allow for method chaining. * @example * // Zoom to the zoom level 5 without an animated transition * map.zoomTo(5); * // Zoom to the zoom level 8 with an animated transition * map.zoomTo(8, { * duration: 2000, * offset: [100, 50] * }); */ zoomTo(zoom: number, options: ? AnimationOptions, eventData?: Object): this { return this.easeTo(extend({ zoom }, options), eventData); } /** * Increases the map's zoom level by 1. * * @memberof Map# * @param {AnimationOptions | null} options Options object. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:zoomstart * @fires Map.event:move * @fires Map.event:zoom * @fires Map.event:moveend * @fires Map.event:zoomend * @returns {Map} Returns itself to allow for method chaining. * @example * // zoom the map in one level with a custom animation duration * map.zoomIn({duration: 1000}); */ zoomIn(options?: AnimationOptions, eventData?: Object): this { this.zoomTo(this.getZoom() + 1, options, eventData); return this; } /** * Decreases the map's zoom level by 1. * * @memberof Map# * @param {AnimationOptions | null} options Options object. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:zoomstart * @fires Map.event:move * @fires Map.event:zoom * @fires Map.event:moveend * @fires Map.event:zoomend * @returns {Map} Returns itself to allow for method chaining. * @example * // zoom the map out one level with a custom animation offset * map.zoomOut({offset: [80, 60]}); */ zoomOut(options?: AnimationOptions, eventData?: Object): this { this.zoomTo(this.getZoom() - 1, options, eventData); return this; } /** * Returns the map's current bearing. The bearing is the compass direction that is "up"; for example, a bearing * of 90° orients the map so that east is up. * * @memberof Map# * @returns {number} The map's current bearing. * @example * 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; } /** * Sets the map's bearing (rotation). The bearing is the compass direction that is "up"; for example, a bearing * of 90° orients the map so that east is up. * * Equivalent to `jumpTo({bearing: bearing})`. * * @memberof Map# * @param {number} bearing The desired bearing. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} Returns itself to allow for method chaining. * @example * // Rotate the map to 90 degrees. * map.setBearing(90); */ setBearing(bearing: number, eventData?: Object): this { this.jumpTo({bearing}, eventData); return this; } /** * Returns the current padding applied around the map viewport. * * @memberof Map# * @returns {PaddingOptions} The current padding around the map viewport. * @example * const padding = map.getPadding(); */ getPadding(): PaddingOptions { return this.transform.padding; } /** * Sets the padding in pixels around the viewport. * * Equivalent to `jumpTo({padding: padding})`. * * @memberof Map# * @param {PaddingOptions} padding The desired padding. Format: {left: number, right: number, top: number, bottom: number}. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} Returns itself to allow for method chaining. * @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 { 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. * * @memberof Map# * @param {number} bearing The desired bearing. * @param {EasingOptions | null} options Options describing the destination and animation of the transition. * Accepts {@link CameraOptions} and {@link AnimationOptions}. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} Returns itself to allow for method chaining. * @example * map.rotateTo(30); * @example * // rotateTo with an animation of 2 seconds. * map.rotateTo(30, {duration: 2000}); */ rotateTo(bearing: number, options?: EasingOptions, eventData?: Object): this { return this.easeTo(extend({ bearing }, options), eventData); } /** * Rotates the map so that north is up (0° bearing), with an animated transition. * * @memberof Map# * @param {EasingOptions | null} options Options describing the destination and animation of the transition. * Accepts {@link CameraOptions} and {@link AnimationOptions}. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} Returns itself to allow for method chaining. * @example * // resetNorth with an animation of 2 seconds. * map.resetNorth({duration: 2000}); */ resetNorth(options?: EasingOptions, eventData?: Object): this { this.rotateTo(0, extend({duration: 1000}, options), eventData); return this; } /** * Rotates and pitches the map so that north is up (0° bearing) and pitch is 0°, with an animated transition. * * @memberof Map# * @param {EasingOptions | null} options Options describing the destination and animation of the transition. * Accepts {@link CameraOptions} and {@link AnimationOptions}. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} Returns itself to allow for method chaining. * @example * // resetNorthPitch with an animation of 2 seconds. * map.resetNorthPitch({duration: 2000}); */ resetNorthPitch(options?: EasingOptions, eventData?: Object): this { this.easeTo(extend({ bearing: 0, pitch: 0, duration: 1000 }, options), eventData); return this; } /** * Snaps the map so that north is up (0° bearing), if the current bearing is * close enough to it (within the `bearingSnap` threshold). * * @memberof Map# * @param {EasingOptions | null} options Options describing the destination and animation of the transition. * Accepts {@link CameraOptions} and {@link AnimationOptions}. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} Returns itself to allow for method chaining. * @example * // snapToNorth with an animation of 2 seconds. * map.snapToNorth({duration: 2000}); */ snapToNorth(options?: EasingOptions, eventData?: Object): this { if (Math.abs(this.getBearing()) < this._bearingSnap) { return this.resetNorth(options, eventData); } return this; } /** * Returns the map's current [pitch](https://docs.mapbox.com/help/glossary/camera/) (tilt). * * @memberof Map# * @returns {number} The map's current pitch, measured in degrees away from the plane of the screen. * @example * const pitch = map.getPitch(); */ getPitch(): number { return this.transform.pitch; } /** * Sets the map's [pitch](https://docs.mapbox.com/help/glossary/camera/) (tilt). Equivalent to `jumpTo({pitch: pitch})`. * * @memberof Map# * @param {number} pitch The pitch to set, measured in degrees away from the plane of the screen (0-60). * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:pitchstart * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} Returns itself to allow for method chaining. * @example * // setPitch with an animation of 2 seconds. * map.setPitch(80, {duration: 2000}); */ setPitch(pitch: number, eventData?: Object): this { this.jumpTo({pitch}, eventData); return this; } /** * Returns a {@link CameraOptions} object for the highest zoom level * up to and including `Map#getMaxZoom()` that fits the bounds * in the viewport at the specified bearing. * * @memberof Map# * @param {LngLatBoundsLike} 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. LngLatBounds represent a box that is always axis-aligned with bearing 0. * @param {CameraOptions | null} options Options object. * @param {number | PaddingOptions} [options.padding] The amount of padding in pixels to add to the given bounds. * @param {number} [options.bearing=0] Desired map bearing at end of animation, in degrees. * @param {number} [options.pitch=0] Desired map pitch at end of animation, in degrees. * @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 camera would transition to the specified bounds. * @returns {CameraOptions | void} If map is able to fit to provided bounds, returns `CameraOptions` with * `center`, `zoom`, and `bearing`. If map is unable to fit, method will warn and return undefined. * @example * const bbox = [[-79, 43], [-73, 45]]; * const newCameraTransform = map.cameraForBounds(bbox, { * padding: {top: 10, bottom:25, left: 15, right: 5} * }); */ cameraForBounds(bounds: LngLatBoundsLike, options?: CameraOptions): ?EasingOptions { bounds = LngLatBounds.convert(bounds); const bearing = (options && options.bearing) || 0; const pitch = (options && options.pitch) || 0; const lnglat0 = bounds.getNorthWest(); const lnglat1 = bounds.getSouthEast(); return this._cameraForBounds(this.transform, lnglat0, lnglat1, bearing, pitch, options); } _extendCameraOptions(options?: CameraOptions): FullCameraOptions { const defaultPadding = { top: 0, bottom: 0, right: 0, left: 0 }; options = extend({ padding: defaultPadding, offset: [0, 0], maxZoom: this.transform.maxZoom }, options); if (typeof options.padding === 'number') { const p = options.padding; options.padding = { top: p, bottom: p, right: p, left: p }; } options.padding = extend(defaultPadding, options.padding); return options; } _minimumAABBFrustumDistance(tr: Transform, aabb: Aabb): number { const aabbW = aabb.max[0] - aabb.min[0]; const aabbH = aabb.max[1] - aabb.min[1]; const aabbAspectRatio = aabbW / aabbH; const selectXAxis = aabbAspectRatio > tr.aspect; const minimumDistance = selectXAxis ? aabbW / (2 * Math.tan(tr.fovX * 0.5) * tr.aspect) : aabbH / (2 * Math.tan(tr.fovY * 0.5) * tr.aspect); return minimumDistance; } _cameraForBoundsOnGlobe(transform: Transform, p0: LngLatLike, p1: LngLatLike, bearing: number, pitch: number, options?: CameraOptions): ?EasingOptions { const tr = transform.clone(); const eOptions = this._extendCameraOptions(options); tr.bearing = bearing; tr.pitch = pitch; const coord0 = LngLat.convert(p0); const coord1 = LngLat.convert(p1); const midLat = (coord0.lat + coord1.lat) * 0.5; const midLng = (coord0.lng + coord1.lng) * 0.5; const origin = latLngToECEF(midLat, midLng); const zAxis = vec3.normalize([], origin); const xAxis = vec3.normalize([], vec3.cross([], zAxis, [0, 1, 0])); const yAxis = vec3.cross([], xAxis, zAxis); const aabbOrientation = [ xAxis[0], xAxis[1], xAxis[2], 0, yAxis[0], yAxis[1], yAxis[2], 0, zAxis[0], zAxis[1], zAxis[2], 0, 0, 0, 0, 1 ]; const ecefCoords = [ origin, latLngToECEF(coord0.lat, coord0.lng), latLngToECEF(coord1.lat, coord0.lng), latLngToECEF(coord1.lat, coord1.lng), latLngToECEF(coord0.lat, coord1.lng), latLngToECEF(midLat, coord0.lng), latLngToECEF(midLat, coord1.lng), latLngToECEF(coord0.lat, midLng), latLngToECEF(coord1.lat, midLng), ]; let aabb = Aabb.fromPoints(ecefCoords.map(p => [vec3.dot(xAxis, p), vec3.dot(yAxis, p), vec3.dot(zAxis, p)])); const center = vec3.transformMat4([], aabb.center, aabbOrientation); if (vec3.squaredLength(center) === 0) { vec3.set(center, 0, 0, 1); } vec3.normalize(center, center); vec3.scale(center, center, GLOBE_RADIUS); tr.center = ecefToLatLng(center); const worldToCamera = tr.getWorldToCameraMatrix(); const cameraToWorld = mat4.invert(new Float64Array(16), worldToCamera); aabb = Aabb.applyTransform(aabb, mat4.multiply([], worldToCamera, aabbOrientation)); vec3.transformMat4(center, center, worldToCamera); const aabbHalfExtentZ = (aabb.max[2] - aabb.min[2]) * 0.5; const frustumDistance = this._minimumAABBFrustumDistance(tr, aabb); const offsetZ = vec3.scale([], [0, 0, 1], aabbHalfExtentZ); const aabbClosestPoint = vec3.add(offsetZ, center, offsetZ); const offsetDistance = frustumDistance + (tr.pitch === 0 ? 0 : vec3.distance(center, aabbClosestPoint)); const globeCenter = tr.globeCenterInViewSpace; const normal = vec3.sub([], center, [globeCenter[0], globeCenter[1], globeCenter[2]]); vec3.normalize(normal, normal); vec3.scale(normal, normal, offsetDistance); const cameraPosition = vec3.add([], center, normal); vec3.transformMat4(cameraPosition, cameraPosition, cameraToWorld); const meterPerECEF = earthRadius / GLOBE_RADIUS; const altitudeECEF = vec3.length(cameraPosition); const altitudeMeter = altitudeECEF * meterPerECEF - earthRadius; const mercatorZ = mercatorZfromAltitude(Math.max(altitudeMeter, Number.EPSILON), 0); const zoom = Math.min(tr.zoomFromMercatorZAdjusted(mercatorZ), eOptions.maxZoom); const halfZoomTransition = (GLOBE_ZOOM_THRESHOLD_MIN + GLOBE_ZOOM_THRESHOLD_MAX) * 0.5; if (zoom > halfZoomTransition) { tr.setProjection({name: 'mercator'}); tr.zoom = zoom; return this._cameraForBounds(tr, p0, p1, bearing, pitch, options); } return {center: tr.center, zoom, bearing, pitch}; } /** @section {Querying features} */ /** * Queries the currently loaded data for elevation at a geographical location. The elevation is returned in `meters` relative to mean sea-level. * Returns `null` if `terrain` is disabled or if terrain data for the location hasn't been loaded yet. * * In order to guarantee that the terrain data is loaded ensure that the geographical location is visible and wait for the `idle` event to occur. * * @memberof Map# * @param {LngLatLike} lnglat The geographical location at which to query. * @param {ElevationQueryOptions} [options] Options object. * @param {boolean} [options.exaggerated=true] When `true` returns the terrain elevation with the value of `exaggeration` from the style already applied. * When `false`, returns the raw value of the underlying data without styling applied. * @returns {number | null} The elevation in meters. * @example * const coordinate = [-122.420679, 37.772537]; * const elevation = map.queryTerrainElevation(coordinate); * @see [Example: Query terrain elevation](https://docs.mapbox.com/mapbox-gl-js/example/query-terrain-elevation/) */ queryTerrainElevation(lnglat: LngLatLike, options: ?ElevationQueryOptions): ?number { const elevation = this.transform.elevation; if (elevation) { options = extend({}, {exaggerated: true}, options); return elevation.getAtPoint(MercatorCoordinate.fromLngLat(lnglat), null, options.exaggerated); } return null; } /** * Calculate the center of these two points in the viewport and use * the highest zoom level up to and including `Map#getMaxZoom()` that fits * the points in the viewport at the specified bearing. * @memberof Map# * @param {LngLatLike} p0 First point * @param {LngLatLike} p1 Second point * @param {number} bearing Desired map bearing at end of animation, in degrees * @param {number} pitch Desired map pitch at end of animation, in degrees * @param {CameraOptions | null} options * @param {number | PaddingOptions} [options.padding] The amount of padding in pixels to add to the given bounds. * @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 camera would transition to the specified bounds. * @returns {CameraOptions | void} If map is able to fit to provided bounds, returns `CameraOptions` with * `center`, `zoom`, and `bearing`. If map is unable to fit, method will warn and return undefined. * @private * @example * var p0 = [-79, 43]; * var p1 = [-73, 45]; * var bearing = 90; * var newCameraTransform = map._cameraForBounds(p0, p1, bearing, pitch, { * padding: {top: 10, bottom:25, left: 15, right: 5} * }); */ _cameraForBounds(transform: Transform, p0: LngLatLike, p1: LngLatLike, bearing: number, pitch: number, options?: CameraOptions): ?EasingOptions { if (transform.projection.name === 'globe') { return this._cameraForBoundsOnGlobe(transform, p0, p1, bearing, pitch, options); } const tr = transform.clone(); const eOptions = this._extendCameraOptions(options); const edgePadding = tr.padding; tr.bearing = bearing; tr.pitch = pitch; const coord0 = LngLat.convert(p0); const coord1 = LngLat.convert(p1); const coord2 = new LngLat(coord0.lng, coord1.lat); const coord3 = new LngLat(coord1.lng, coord0.lat); const p0world = tr.project(coord0); const p1world = tr.project(coord1); const z0 = this.queryTerrainElevation(coord0); const z1 = this.queryTerrainElevation(coord1); const z2 = this.queryTerrainElevation(coord2); const z3 = this.queryTerrainElevation(coord3); const worldCoords = [ [p0world.x, p0world.y, Math.min(z0 || 0, z1 || 0, z2 || 0, z3 || 0)], [p1world.x, p1world.y, Math.max(z0 || 0, z1 || 0, z2 || 0, z3 || 0)] ]; let aabb = Aabb.fromPoints(worldCoords); const worldToCamera = tr.getWorldToCameraMatrix(); const cameraToWorld = mat4.invert(new Float64Array(16), worldToCamera); aabb = Aabb.applyTransform(aabb, worldToCamera); const size = vec3.sub([], aabb.max, aabb.min); const screenPadL = edgePadding.left || 0; const screenPadR = edgePadding.right || 0; const screenPadB = edgePadding.bottom || 0; const screenPadT = edgePadding.top || 0; const {left: padL, right: padR, top: padT, bottom: padB} = eOptions.padding; const halfScreenPadX = (screenPadL + screenPadR) * 0.5; const halfScreenPadY = (screenPadT + screenPadB) * 0.5; const scaleX = (tr.width - (screenPadL + screenPadR + padL + padR)) / size[0]; const scaleY = (tr.height - (screenPadB + screenPadT + padB + padT)) / size[1]; const zoomRef = Math.min(tr.scaleZoom(tr.scale * Math.min(scaleX, scaleY)), eOptions.maxZoom); const scaleRatio = tr.scale / tr.zoomScale(zoomRef); aabb = new Aabb( [aabb.min[0] - (padL + halfScreenPadX) * scaleRatio, aabb.min[1] - (padB + halfScreenPadY) * scaleRatio, aabb.min[2]], [aabb.max[0] + (padR + halfScreenPadX) * scaleRatio, aabb.max[1] + (padT + halfScreenPadY) * scaleRatio, aabb.max[2]]); const aabbHalfExtentZ = size[2] * 0.5; const frustumDistance = this._minimumAABBFrustumDistance(tr, aabb); const normalZ = [0, 0, 1, 0]; vec4.transformMat4(normalZ, normalZ, worldToCamera); vec4.normalize(normalZ, normalZ); const offset = vec3.scale([], normalZ, frustumDistance + aabbHalfExtentZ); const cameraPosition = vec3.add([], aabb.center, offset); const centerOffset = (typeof eOptions.offset.x === 'number' && typeof eOptions.offset.y === 'number') ? new Point(eOptions.offset.x, eOptions.offset.y) : Point.convert(eOptions.offset); const rotatedOffset = centerOffset.rotate(-degToRad(bearing)); aabb.center[0] -= rotatedOffset.x * scaleRatio; aabb.center[1] += rotatedOffset.y * scaleRatio; vec3.transformMat4(aabb.center, aabb.center, cameraToWorld); vec3.transformMat4(cameraPosition, cameraPosition, cameraToWorld); const mercator = [aabb.center[0], aabb.center[1], cameraPosition[2] * tr.pixelsPerMeter]; vec3.scale(mercator, mercator, 1.0 / tr.worldSize); const lng = lngFromMercatorX(mercator[0]); const lat = latFromMercatorY(mercator[1]); const zoom = Math.min(tr._zoomFromMercatorZ(mercator[2]), eOptions.maxZoom); const center = new LngLat(lng, lat); const halfZoomTransition = (GLOBE_ZOOM_THRESHOLD_MIN + GLOBE_ZOOM_THRESHOLD_MAX) * 0.5; if (tr.mercatorFromTransition && zoom < halfZoomTransition) { tr.setProjection({name: 'globe'}); tr.zoom = zoom; return this._cameraForBounds(tr, p0, p1, bearing, pitch, options); } return {center, zoom, bearing, pitch}; } /** * Pans and zooms the map to contain its visible area within the specified geographical bounds. * If a padding is set on the map, the bounds are fit to the inset. * * @memberof Map# * @param {LngLatBoundsLike} 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 {Object} [options] Options supports all properties from {@link AnimationOptions} and {@link CameraOptions} in addition to the fields below. * @param {number | PaddingOptions} [options.padding] The amount of padding in pixels to add to the given bounds. * @param {number} [options.pitch=0] Desired map pitch at end of animation, in degrees. * @param {number} [options.bearing=0] Desired map bearing at end of animation, in degrees. * @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 {Object} [eventData] Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} Returns itself to allow for method chaining. * @example * const bbox = [[-79, 43], [-73, 45]]; * map.fitBounds(bbox, { * padding: {top: 10, bottom:25, left: 15, right: 5} * }); * @see [Example: Fit a map to a bounding box](https://www.mapbox.com/mapbox-gl-js/example/fitbounds/) */ fitBounds(bounds: LngLatBoundsLike, options?: EasingOptions, eventData?: Object): this { const cameraPlacement = this.cameraForBounds(bounds, options); return this._fitInternal(cameraPlacement, options, eventData); } /** * Pans, rotates and zooms the map to to fit the box made by points p0 and p1 * once the map is rotated to the specified bearing. To zoom without rotating, * pass in the current map bearing. * * @memberof Map# * @param {PointLike} p0 First point on screen, in pixel coordinates. * @param {PointLike} p1 Second point on screen, in pixel coordinates. * @param {number} bearing Desired map bearing at end of animation, in degrees. * @param {EasingOptions | null} options Options object. * Accepts {@link CameraOptions} and {@link AnimationOptions}. * @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 {number} [options.pitch=0] Desired map pitch at end of animation, in degrees. * @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 {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:moveend * @returns {Map} Returns itself to allow for method chaining. * @example * const p0 = [220, 400]; * const p1 = [500, 900]; * map.fitScreenCoordinates(p0, p1, map.getBearing(), { * padding: {top: 10, bottom:25, left: 15, right: 5} * }); * @see Used by {@link BoxZoomHandler} */ fitScreenCoordinates(p0: PointLike, p1: PointLike, bearing: number, options?: EasingOptions, eventData?: Object): this { const screen0 = Point.convert(p0); const screen1 = Point.convert(p1); const min = new Point(Math.min(screen0.x, screen1.x), Math.min(screen0.y, screen1.y)); const max = new Point(Math.max(screen0.x, screen1.x), Math.max(screen0.y, screen1.y)); if (this.transform.projection.name === 'mercator' && this.transform.anyCornerOffEdge(screen0, screen1)) { return this; } const lnglat0 = this.transform.pointLocation3D(min); const lnglat1 = this.transform.pointLocation3D(max); const lnglat2 = this.transform.pointLocation3D(new Point(min.x, max.y)); const lnglat3 = this.transform.pointLocation3D(new Point(max.x, min.y)); const p0coord = [ Math.min(lnglat0.lng, lnglat1.lng, lnglat2.lng, lnglat3.lng), Math.min(lnglat0.lat, lnglat1.lat, lnglat2.lat, lnglat3.lat), ]; const p1coord = [ Math.max(lnglat0.lng, lnglat1.lng, lnglat2.lng, lnglat3.lng), Math.max(lnglat0.lat, lnglat1.lat, lnglat2.lat, lnglat3.lat), ]; const pitch = options && options.pitch ? options.pitch : this.getPitch(); const cameraPlacement = this._cameraForBounds(this.transform, p0coord, p1coord, bearing, pitch, options); return this._fitInternal(cameraPlacement, options, eventData); } _fitInternal(calculatedOptions?: ?EasingOptions, options?: EasingOptions, eventData?: Object): this { // cameraForBounds warns + returns undefined if unable to fit: if (!calculatedOptions) return this; options = extend(calculatedOptions, options); // Explicitly remove the padding field because, calculatedOptions already accounts for padding by setting zoom and center accordingly. delete options.padding; return options.linear ? this.easeTo(options, eventData) : this.flyTo(options, eventData); } /** * Changes any combination of center, zoom, bearing, and pitch, without * an animated transition. The map will retain its current values for any * details not specified in `options`. * * @memberof Map# * @param {CameraOptions} options Options object. * @param {Object | null} eventData Additional properties to be added to event objects of events triggered by this method. * @fires Map.event:movestart * @fires Map.event:zoomstart * @fires Map.event:pitchstart * @fires Map.event:rotate * @fires Map.event:move * @fires Map.event:zoom * @fires Map.event:pitch * @fires Map.event:moveend * @fires Map.event:zoomend * @fires Map.event:pitchend * @returns {Map} Returns itself to allow for method chaining. * @example * // jump to coordinates at current zoom * map.jumpTo({center: [0, 0]}); * // jump with zoom, pitch, and bearing options * map.jumpTo({ * center: [0, 0], * zoom: 8, * pitch: 45, * bearing: 90 * }); * @see [Example: Jump to a series of locations](https://docs.mapbox.com/mapbox-gl-js/example/jump-to/) * @see [Example: Update a feature in realtime](https://docs.mapbox.com/mapbox-gl-js/example/live-update-feature/) */ jumpTo(options: CameraOptions & {preloadOnly?: $PropertyType<AnimationOptions, 'preloadOnly'>}, eventData?: Object): this { this.stop(); const tr = options.preloadOnly ? this.transform.clone() : this.transform; let zoomChanged = false, bearingChanged = false, pitchChanged = false; if ('zoom' in options && tr.zoom !== +options.zoom) { zoomChanged = true; tr.zoom = +options.zoom; } if (options.center !== undefined) { tr.center = LngLat.convert(options.center); } if ('bearing' in options && tr.bearing !== +options.bearing) { bearingChanged = true; tr.bearing = +options.bearing; } if ('pitch' in options && tr.pitch !== +options.pitch) { pitchChanged = true; tr.pitch = +options.pitch; } if (options.padding != null && !tr.isPaddingEqual(options.padding)) { // $FlowFixMe[incompatible-type] - Flow can't infer that padding is not null here tr.padding = options.padding; } if (options.preloadOnly) { this._preloadTiles(tr); return this; } this.fire(new Event('movestart', eventData)) .fire(new Event('move', eventData)); if (zoomChanged) { this.fire(new Event('zoomstart', eventData)) .fire(new Event('zoom', eventData)) .fire(new Event('zoomend', eventData)); } if (bearingChanged) { this.fire(new Event('rotatestart', eventData)) .fire(new Event('rotate', eventData)) .fire(new Event('rotateend', eventData)); } if (pitchChanged) { this.fire(new Event('pitchstart', eventData)) .fire(new Event('pitch', eventData)) .fire(new Event('pitchend', eventData)); } return this.fire(new Event('moveend', eventData)); } /** * Returns position and orientation of the camera entity. * * This method is not supported for projections other than mercator. * * @memberof Map# * @returns {FreeCameraOptions} The camera state. * @example * const camera = map.getFreeCameraOptions(); * * const position = [138.72649, 35.33974]; * const altitude = 3000; * * camera.position = mapboxgl.MercatorCoordinate.fromLngLat(position, altitude); * camera.lookAtPoint([138.73036, 35.36197]); * * map.setFreeCameraOptions(camera); */ getFreeCameraOptions(): FreeCameraOptions { if (!this.transform.projection.supportsFreeCamera) { warnOnce(freeCameraNotSupportedWarning); } return this.transform.getFreeCameraOptions(); } /** * `FreeCameraOptions` provides more direct access to the underlying camera entity. * For backwards compatibility the state set using this API must be representable with * `CameraOptions` as well. Parameters are clamped into a valid range or discarded as invalid * if the conversion to the pitch and bearing presentation is ambiguous. For example orientation * can be invalid if it leads to the camera being upside down, the quaternion has zero length, * or the pitch is over the maximum pitch limit. * * This method is not supported for projections other than mercator. * * @memberof Map# * @param {FreeCameraOptions} options `FreeCamera