1 | import {bindAll, extend, warnOnce, clamp, wrap, ease as defaultEasing, pick} from '../util/util';
|
2 | import {number as interpolate} from '../style-spec/util/interpolate';
|
3 | import browser from '../util/browser';
|
4 | import LngLat from '../geo/lng_lat';
|
5 | import LngLatBounds from '../geo/lng_lat_bounds';
|
6 | import Point from '@mapbox/point-geometry';
|
7 | import {Event, Evented} from '../util/evented';
|
8 | import assert from 'assert';
|
9 | import {Debug} from '../util/debug';
|
10 |
|
11 | import type Transform from '../geo/transform';
|
12 | import type {LngLatLike} from '../geo/lng_lat';
|
13 | import type {LngLatBoundsLike} from '../geo/lng_lat_bounds';
|
14 | import type {TaskID} from '../util/task_queue';
|
15 | import type {PaddingOptions} from '../geo/edge_insets';
|
16 |
|
17 | /**
|
18 | * A [Point](https://github.com/mapbox/point-geometry) or an array of two numbers representing `x` and `y` screen coordinates in pixels.
|
19 | *
|
20 | * @typedef {(Point | [number, number])} PointLike
|
21 | * @example
|
22 | * var p1 = new Point(-77, 38); // a PointLike which is a Point
|
23 | * var p2 = [-77, 38]; // a PointLike which is an array of two numbers
|
24 | */
|
25 | export type PointLike = Point | [number, number];
|
26 |
|
27 | export type RequireAtLeastOne<T> = { [K in keyof T]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<keyof T, K>>>; }[keyof T];
|
28 |
|
29 | /**
|
30 | * Options common to {@link Map#jumpTo}, {@link Map#easeTo}, and {@link Map#flyTo}, controlling the desired location,
|
31 | * zoom, bearing, and pitch of the camera. All properties are optional, and when a property is omitted, the current
|
32 | * camera value for that property will remain unchanged.
|
33 | *
|
34 | * @typedef {Object} CameraOptions
|
35 | * @property {LngLatLike} center The desired center.
|
36 | * @property {number} zoom The desired zoom level.
|
37 | * @property {number} bearing The desired bearing in degrees. The bearing is the compass direction that
|
38 | * is "up". For example, `bearing: 90` orients the map so that east is up.
|
39 | * @property {number} pitch The desired pitch in degrees. The pitch is the angle towards the horizon
|
40 | * measured in degrees with a range between 0 and 60 degrees. For example, pitch: 0 provides the appearance
|
41 | * of looking straight down at the map, while pitch: 60 tilts the user's perspective towards the horizon.
|
42 | * Increasing the pitch value is often used to display 3D objects.
|
43 | * @property {LngLatLike} around If `zoom` is specified, `around` determines the point around which the zoom is centered.
|
44 | * @property {PaddingOptions} padding Dimensions in pixels applied on each side of the viewport for shifting the vanishing point.
|
45 | * @example
|
46 | * // set the map's initial perspective with CameraOptions
|
47 | * var map = new maplibregl.Map({
|
48 | * container: 'map',
|
49 | * style: 'https://demotiles.maplibre.org/style.json',
|
50 | * center: [-73.5804, 45.53483],
|
51 | * pitch: 60,
|
52 | * bearing: -60,
|
53 | * zoom: 10
|
54 | * });
|
55 | * @see [Set pitch and bearing](https://maplibre.org/maplibre-gl-js-docs/example/set-perspective/)
|
56 | * @see [Jump to a series of locations](https://maplibre.org/maplibre-gl-js-docs/example/jump-to/)
|
57 | * @see [Fly to a location](https://maplibre.org/maplibre-gl-js-docs/example/flyto/)
|
58 | * @see [Display buildings in 3D](https://maplibre.org/maplibre-gl-js-docs/example/3d-buildings/)
|
59 | */
|
60 | export type CameraOptions = CenterZoomBearing & {
|
61 | pitch?: number;
|
62 | around?: LngLatLike;
|
63 | };
|
64 |
|
65 | export type CenterZoomBearing = {
|
66 | center?: LngLatLike;
|
67 | zoom?: number;
|
68 | bearing?: number;
|
69 | }
|
70 |
|
71 | export type JumpToOptions = CameraOptions & {
|
72 | padding?: PaddingOptions;
|
73 | }
|
74 |
|
75 | export type CameraForBoundsOptions = CameraOptions & {
|
76 | padding?: number | RequireAtLeastOne<PaddingOptions>;
|
77 | offset?: PointLike;
|
78 | maxZoom?: number;
|
79 | }
|
80 |
|
81 | export type FlyToOptions = AnimationOptions & CameraOptions & {
|
82 | curve?: number;
|
83 | minZoom?: number;
|
84 | speed?: number;
|
85 | screenSpeed?: number;
|
86 | maxDuration?: number;
|
87 | padding?: number | RequireAtLeastOne<PaddingOptions>;
|
88 | }
|
89 |
|
90 | export type EaseToOptions = AnimationOptions & CameraOptions & {
|
91 | delayEndEvents?: number;
|
92 | padding?: number | RequireAtLeastOne<PaddingOptions>;
|
93 | }
|
94 |
|
95 | export type FitBoundsOptions = FlyToOptions & {
|
96 | linear?: boolean;
|
97 | offset?: PointLike;
|
98 | maxZoom?: number;
|
99 | maxDuration?: number;
|
100 | padding?: number | RequireAtLeastOne<PaddingOptions>;
|
101 | }
|
102 |
|
103 | /**
|
104 | * Options common to map movement methods that involve animation, such as {@link Map#panBy} and
|
105 | * {@link Map#easeTo}, controlling the duration and easing function of the animation. All properties
|
106 | * are optional.
|
107 | *
|
108 | * @typedef {Object} AnimationOptions
|
109 | * @property {number} duration The animation's duration, measured in milliseconds.
|
110 | * @property {Function} easing A function taking a time in the range 0..1 and returning a number where 0 is
|
111 | * the initial state and 1 is the final state.
|
112 | * @property {PointLike} offset of the target center relative to real map container center at the end of animation.
|
113 | * @property {boolean} animate If `false`, no animation will occur.
|
114 | * @property {boolean} essential If `true`, then the animation is considered essential and will not be affected by
|
115 | * [`prefers-reduced-motion`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion).
|
116 | */
|
117 | export type AnimationOptions = {
|
118 | duration?: number;
|
119 | easing?: (_: number) => number;
|
120 | offset?: PointLike;
|
121 | animate?: boolean;
|
122 | essential?: boolean;
|
123 | };
|
124 |
|
125 | abstract class Camera extends Evented {
|
126 | transform: Transform;
|
127 | _moving: boolean;
|
128 | _zooming: boolean;
|
129 | _rotating: boolean;
|
130 | _pitching: boolean;
|
131 | _padding: boolean;
|
132 |
|
133 | _bearingSnap: number;
|
134 | _easeStart: number;
|
135 | _easeOptions: {
|
136 | duration?: number;
|
137 | easing?: (_: number) => number;
|
138 | };
|
139 | _easeId: string | void;
|
140 |
|
141 | _onEaseFrame: (_: number) => void;
|
142 | _onEaseEnd: (easeId?: string) => void;
|
143 | _easeFrameId: TaskID;
|
144 |
|
145 | abstract _requestRenderFrame(a: () => void): TaskID;
|
146 | abstract _cancelRenderFrame(_: TaskID): void;
|
147 |
|
148 | constructor(transform: Transform, options: {
|
149 | bearingSnap: number;
|
150 | }) {
|
151 | super();
|
152 | this._moving = false;
|
153 | this._zooming = false;
|
154 | this.transform = transform;
|
155 | this._bearingSnap = options.bearingSnap;
|
156 |
|
157 | bindAll(['_renderFrameCallback'], this);
|
158 |
|
159 | //addAssertions(this);
|
160 | }
|
161 |
|
162 | /**
|
163 | * Returns the map's geographical centerpoint.
|
164 | *
|
165 | * @memberof Map#
|
166 | * @returns The map's geographical centerpoint.
|
167 | * @example
|
168 | * // return a LngLat object such as {lng: 0, lat: 0}
|
169 | * var center = map.getCenter();
|
170 | * // access longitude and latitude values directly
|
171 | * var {lng, lat} = map.getCenter();
|
172 | */
|
173 | getCenter(): LngLat { return new LngLat(this.transform.center.lng, this.transform.center.lat); }
|
174 |
|
175 | /**
|
176 | * Sets the map's geographical centerpoint. Equivalent to `jumpTo({center: center})`.
|
177 | *
|
178 | * @memberof Map#
|
179 | * @param center The centerpoint to set.
|
180 | * @param eventData Additional properties to be added to event objects of events triggered by this method.
|
181 | * @fires movestart
|
182 | * @fires moveend
|
183 | * @returns {Map} `this`
|
184 | * @example
|
185 | * map.setCenter([-74, 38]);
|
186 | */
|
187 | setCenter(center: LngLatLike, eventData?: any) {
|
188 | return this.jumpTo({center}, eventData);
|
189 | }
|
190 |
|
191 | /**
|
192 | * Pans the map by the specified offset.
|
193 | *
|
194 | * @memberof Map#
|
195 | * @param offset `x` and `y` coordinates by which to pan the map.
|
196 | * @param options Options object
|
197 | * @param eventData Additional properties to be added to event objects of events triggered by this method.
|
198 | * @fires movestart
|
199 | * @fires moveend
|
200 | * @returns {Map} `this`
|
201 | * @see [Navigate the map with game-like controls](https://maplibre.org/maplibre-gl-js-docs/example/game-controls/)
|
202 | */
|
203 | panBy(offset: PointLike, options?: AnimationOptions, eventData?: any) {
|
204 | offset = Point.convert(offset).mult(-1);
|
205 | return this.panTo(this.transform.center, extend({offset}, options), eventData);
|
206 | }
|
207 |
|
208 | /**
|
209 | * Pans the map to the specified location with an animated transition.
|
210 | *
|
211 | * @memberof Map#
|
212 | * @param lnglat The location to pan the map to.
|
213 | * @param options Options describing the destination and animation of the transition.
|
214 | * @param eventData Additional properties to be added to event objects of events triggered by this method.
|
215 | * @fires movestart
|
216 | * @fires moveend
|
217 | * @returns {Map} `this`
|
218 | * @example
|
219 | * map.panTo([-74, 38]);
|
220 | * @example
|
221 | * // Specify that the panTo animation should last 5000 milliseconds.
|
222 | * map.panTo([-74, 38], {duration: 5000});
|
223 | * @see [Update a feature in realtime](https://maplibre.org/maplibre-gl-js-docs/example/live-update-feature/)
|
224 | */
|
225 | panTo(lnglat: LngLatLike, options?: AnimationOptions, eventData?: any) {
|
226 | return this.easeTo(extend({
|
227 | center: lnglat
|
228 | }, options), eventData);
|
229 | }
|
230 |
|
231 | /**
|
232 | * Returns the map's current zoom level.
|
233 | *
|
234 | * @memberof Map#
|
235 | * @returns The map's current zoom level.
|
236 | * @example
|
237 | * map.getZoom();
|
238 | */
|
239 | getZoom(): number { return this.transform.zoom; }
|
240 |
|
241 | /**
|
242 | * Sets the map's zoom level. Equivalent to `jumpTo({zoom: zoom})`.
|
243 | *
|
244 | * @memberof Map#
|
245 | * @param zoom The zoom level to set (0-20).
|
246 | * @param eventData Additional properties to be added to event objects of events triggered by this method.
|
247 | * @fires movestart
|
248 | * @fires zoomstart
|
249 | * @fires move
|
250 | * @fires zoom
|
251 | * @fires moveend
|
252 | * @fires zoomend
|
253 | * @returns {Map} `this`
|
254 | * @example
|
255 | * // Zoom to the zoom level 5 without an animated transition
|
256 | * map.setZoom(5);
|
257 | */
|
258 | setZoom(zoom: number, eventData?: any) {
|
259 | this.jumpTo({zoom}, eventData);
|
260 | return this;
|
261 | }
|
262 |
|
263 | /**
|
264 | * Zooms the map to the specified zoom level, with an animated transition.
|
265 | *
|
266 | * @memberof Map#
|
267 | * @param zoom The zoom level to transition to.
|
268 | * @param options Options object
|
269 | * @param eventData Additional properties to be added to event objects of events triggered by this method.
|
270 | * @fires movestart
|
271 | * @fires zoomstart
|
272 | * @fires move
|
273 | * @fires zoom
|
274 | * @fires moveend
|
275 | * @fires zoomend
|
276 | * @returns {Map} `this`
|
277 | * @example
|
278 | * // Zoom to the zoom level 5 without an animated transition
|
279 | * map.zoomTo(5);
|
280 | * // Zoom to the zoom level 8 with an animated transition
|
281 | * map.zoomTo(8, {
|
282 | * duration: 2000,
|
283 | * offset: [100, 50]
|
284 | * });
|
285 | */
|
286 | zoomTo(zoom: number, options?: AnimationOptions | null, eventData?: any) {
|
287 | return this.easeTo(extend({
|
288 | zoom
|
289 | }, options), eventData);
|
290 | }
|
291 |
|
292 | /**
|
293 | * Increases the map's zoom level by 1.
|
294 | *
|
295 | * @memberof Map#
|
296 | * @param options Options object
|
297 | * @param eventData Additional properties to be added to event objects of events triggered by this method.
|
298 | * @fires movestart
|
299 | * @fires zoomstart
|
300 | * @fires move
|
301 | * @fires zoom
|
302 | * @fires moveend
|
303 | * @fires zoomend
|
304 | * @returns {Map} `this`
|
305 | * @example
|
306 | * // zoom the map in one level with a custom animation duration
|
307 | * map.zoomIn({duration: 1000});
|
308 | */
|
309 | zoomIn(options?: AnimationOptions, eventData?: any) {
|
310 | this.zoomTo(this.getZoom() + 1, options, eventData);
|
311 | return this;
|
312 | }
|
313 |
|
314 | /**
|
315 | * Decreases the map's zoom level by 1.
|
316 | *
|
317 | * @memberof Map#
|
318 | * @param options Options object
|
319 | * @param eventData Additional properties to be added to event objects of events triggered by this method.
|
320 | * @fires movestart
|
321 | * @fires zoomstart
|
322 | * @fires move
|
323 | * @fires zoom
|
324 | * @fires moveend
|
325 | * @fires zoomend
|
326 | * @returns {Map} `this`
|
327 | * @example
|
328 | * // zoom the map out one level with a custom animation offset
|
329 | * map.zoomOut({offset: [80, 60]});
|
330 | */
|
331 | zoomOut(options?: AnimationOptions, eventData?: any) {
|
332 | this.zoomTo(this.getZoom() - 1, options, eventData);
|
333 | return this;
|
334 | }
|
335 |
|
336 | /**
|
337 | * Returns the map's current bearing. The bearing is the compass direction that is "up"; for example, a bearing
|
338 | * of 90° orients the map so that east is up.
|
339 | *
|
340 | * @memberof Map#
|
341 | * @returns The map's current bearing.
|
342 | * @see [Navigate the map with game-like controls](https://maplibre.org/maplibre-gl-js-docs/example/game-controls/)
|
343 | */
|
344 | getBearing(): number { return this.transform.bearing; }
|
345 |
|
346 | /**
|
347 | * Sets the map's bearing (rotation). The bearing is the compass direction that is "up"; for example, a bearing
|
348 | * of 90° orients the map so that east is up.
|
349 | *
|
350 | * Equivalent to `jumpTo({bearing: bearing})`.
|
351 | *
|
352 | * @memberof Map#
|
353 | * @param bearing The desired bearing.
|
354 | * @param eventData Additional properties to be added to event objects of events triggered by this method.
|
355 | * @fires movestart
|
356 | * @fires moveend
|
357 | * @returns {Map} `this`
|
358 | * @example
|
359 | * // rotate the map to 90 degrees
|
360 | * map.setBearing(90);
|
361 | */
|
362 | setBearing(bearing: number, eventData?: any) {
|
363 | this.jumpTo({bearing}, eventData);
|
364 | return this;
|
365 | }
|
366 |
|
367 | /**
|
368 | * Returns the current padding applied around the map viewport.
|
369 | *
|
370 | * @memberof Map#
|
371 | * @returns The current padding around the map viewport.
|
372 | */
|
373 | getPadding(): PaddingOptions { return this.transform.padding; }
|
374 |
|
375 | /**
|
376 | * Sets the padding in pixels around the viewport.
|
377 | *
|
378 | * Equivalent to `jumpTo({padding: padding})`.
|
379 | *
|
380 | * @memberof Map#
|
381 | * @param padding The desired padding. Format: { left: number, right: number, top: number, bottom: number }
|
382 | * @param eventData Additional properties to be added to event objects of events triggered by this method.
|
383 | * @fires movestart
|
384 | * @fires moveend
|
385 | * @returns {Map} `this`
|
386 | * @example
|
387 | * // Sets a left padding of 300px, and a top padding of 50px
|
388 | * map.setPadding({ left: 300, top: 50 });
|
389 | */
|
390 | setPadding(padding: PaddingOptions, eventData?: any) {
|
391 | this.jumpTo({padding}, eventData);
|
392 | return this;
|
393 | }
|
394 |
|
395 | /**
|
396 | * Rotates the map to the specified bearing, with an animated transition. The bearing is the compass direction
|
397 | * that is \"up\"; for example, a bearing of 90° orients the map so that east is up.
|
398 | *
|
399 | * @memberof Map#
|
400 | * @param bearing The desired bearing.
|
401 | * @param options Options object
|
402 | * @param eventData Additional properties to be added to event objects of events triggered by this method.
|
403 | * @fires movestart
|
404 | * @fires moveend
|
405 | * @returns {Map} `this`
|
406 | */
|
407 | rotateTo(bearing: number, options?: AnimationOptions, eventData?: any) {
|
408 | return this.easeTo(extend({
|
409 | bearing
|
410 | }, options), eventData);
|
411 | }
|
412 |
|
413 | /**
|
414 | * Rotates the map so that north is up (0° bearing), with an animated transition.
|
415 | *
|
416 | * @memberof Map#
|
417 | * @param options Options object
|
418 | * @param eventData Additional properties to be added to event objects of events triggered by this method.
|
419 | * @fires movestart
|
420 | * @fires moveend
|
421 | * @returns {Map} `this`
|
422 | */
|
423 | resetNorth(options?: AnimationOptions, eventData?: any) {
|
424 | this.rotateTo(0, extend({duration: 1000}, options), eventData);
|
425 | return this;
|
426 | }
|
427 |
|
428 | /**
|
429 | * Rotates and pitches the map so that north is up (0° bearing) and pitch is 0°, with an animated transition.
|
430 | *
|
431 | * @memberof Map#
|
432 | * @param options Options object
|
433 | * @param eventData Additional properties to be added to event objects of events triggered by this method.
|
434 | * @fires movestart
|
435 | * @fires moveend
|
436 | * @returns {Map} `this`
|
437 | */
|
438 | resetNorthPitch(options?: AnimationOptions, eventData?: any) {
|
439 | this.easeTo(extend({
|
440 | bearing: 0,
|
441 | pitch: 0,
|
442 | duration: 1000
|
443 | }, options), eventData);
|
444 | return this;
|
445 | }
|
446 |
|
447 | /**
|
448 | * Snaps the map so that north is up (0° bearing), if the current bearing is close enough to it (i.e. within the
|
449 | * `bearingSnap` threshold).
|
450 | *
|
451 | * @memberof Map#
|
452 | * @param options Options object
|
453 | * @param eventData Additional properties to be added to event objects of events triggered by this method.
|
454 | * @fires movestart
|
455 | * @fires moveend
|
456 | * @returns {Map} `this`
|
457 | */
|
458 | snapToNorth(options?: AnimationOptions, eventData?: any) {
|
459 | if (Math.abs(this.getBearing()) < this._bearingSnap) {
|
460 | return this.resetNorth(options, eventData);
|
461 | }
|
462 | return this;
|
463 | }
|
464 |
|
465 | /**
|
466 | * Returns the map's current pitch (tilt).
|
467 | *
|
468 | * @memberof Map#
|
469 | * @returns The map's current pitch, measured in degrees away from the plane of the screen.
|
470 | */
|
471 | getPitch(): number { return this.transform.pitch; }
|
472 |
|
473 | /**
|
474 | * Sets the map's pitch (tilt). Equivalent to `jumpTo({pitch: pitch})`.
|
475 | *
|
476 | * @memberof Map#
|
477 | * @param pitch The pitch to set, measured in degrees away from the plane of the screen (0-60).
|
478 | * @param eventData Additional properties to be added to event objects of events triggered by this method.
|
479 | * @fires pitchstart
|
480 | * @fires movestart
|
481 | * @fires moveend
|
482 | * @returns {Map} `this`
|
483 | */
|
484 | setPitch(pitch: number, eventData?: any) {
|
485 | this.jumpTo({pitch}, eventData);
|
486 | return this;
|
487 | }
|
488 |
|
489 | /**
|
490 | * @memberof Map#
|
491 | * @param {LngLatBoundsLike} bounds Calculate the center for these bounds in the viewport and use
|
492 | * the highest zoom level up to and including `Map#getMaxZoom()` that fits
|
493 | * in the viewport. LngLatBounds represent a box that is always axis-aligned with bearing 0.
|
494 | * @param options Options object
|
495 | * @param {number | PaddingOptions} [options.padding] The amount of padding in pixels to add to the given bounds.
|
496 | * @param {number} [options.bearing=0] Desired map bearing at end of animation, in degrees.
|
497 | * @param {PointLike} [options.offset=[0, 0]] The center of the given bounds relative to the map's center, measured in pixels.
|
498 | * @param {number} [options.maxZoom] The maximum zoom level to allow when the camera would transition to the specified bounds.
|
499 | * @returns {CenterZoomBearing} If map is able to fit to provided bounds, returns `center`, `zoom`, and `bearing`.
|
500 | * If map is unable to fit, method will warn and return undefined.
|
501 | * @example
|
502 | * var bbox = [[-79, 43], [-73, 45]];
|
503 | * var newCameraTransform = map.cameraForBounds(bbox, {
|
504 | * padding: {top: 10, bottom:25, left: 15, right: 5}
|
505 | * });
|
506 | */
|
507 | cameraForBounds(bounds: LngLatBoundsLike, options?: CameraForBoundsOptions): CenterZoomBearing {
|
508 | bounds = LngLatBounds.convert(bounds);
|
509 | const bearing = options && options.bearing || 0;
|
510 | return this._cameraForBoxAndBearing(bounds.getNorthWest(), bounds.getSouthEast(), bearing, options);
|
511 | }
|
512 |
|
513 | /**
|
514 | * Calculate the center of these two points in the viewport and use
|
515 | * the highest zoom level up to and including `Map#getMaxZoom()` that fits
|
516 | * the points in the viewport at the specified bearing.
|
517 | * @memberof Map#
|
518 | * @param {LngLatLike} p0 First point
|
519 | * @param {LngLatLike} p1 Second point
|
520 | * @param bearing Desired map bearing at end of animation, in degrees
|
521 | * @param options
|
522 | * @param {number | PaddingOptions} [options.padding] The amount of padding in pixels to add to the given bounds.
|
523 | * @param {PointLike} [options.offset=[0, 0]] The center of the given bounds relative to the map's center, measured in pixels.
|
524 | * @param {number} [options.maxZoom] The maximum zoom level to allow when the camera would transition to the specified bounds.
|
525 | * @returns {CenterZoomBearing} If map is able to fit to provided bounds, returns `center`, `zoom`, and `bearing`.
|
526 | * If map is unable to fit, method will warn and return undefined.
|
527 | * @private
|
528 | * @example
|
529 | * var p0 = [-79, 43];
|
530 | * var p1 = [-73, 45];
|
531 | * var bearing = 90;
|
532 | * var newCameraTransform = map._cameraForBoxAndBearing(p0, p1, bearing, {
|
533 | * padding: {top: 10, bottom:25, left: 15, right: 5}
|
534 | * });
|
535 | */
|
536 | _cameraForBoxAndBearing(p0: LngLatLike, p1: LngLatLike, bearing: number, options?: CameraForBoundsOptions): CenterZoomBearing {
|
537 | const defaultPadding = {
|
538 | top: 0,
|
539 | bottom: 0,
|
540 | right: 0,
|
541 | left: 0
|
542 | };
|
543 | options = extend({
|
544 | padding: defaultPadding,
|
545 | offset: [0, 0],
|
546 | maxZoom: this.transform.maxZoom
|
547 | }, options);
|
548 |
|
549 | if (typeof options.padding === 'number') {
|
550 | const p = options.padding;
|
551 | options.padding = {
|
552 | top: p,
|
553 | bottom: p,
|
554 | right: p,
|
555 | left: p
|
556 | };
|
557 | }
|
558 |
|
559 | options.padding = extend(defaultPadding, options.padding) as PaddingOptions;
|
560 | const tr = this.transform;
|
561 | const edgePadding = tr.padding;
|
562 |
|
563 | // We want to calculate the upper right and lower left of the box defined by p0 and p1
|
564 | // in a coordinate system rotate to match the destination bearing.
|
565 | const p0world = tr.project(LngLat.convert(p0));
|
566 | const p1world = tr.project(LngLat.convert(p1));
|
567 | const p0rotated = p0world.rotate(-bearing * Math.PI / 180);
|
568 | const p1rotated = p1world.rotate(-bearing * Math.PI / 180);
|
569 |
|
570 | const upperRight = new Point(Math.max(p0rotated.x, p1rotated.x), Math.max(p0rotated.y, p1rotated.y));
|
571 | const lowerLeft = new Point(Math.min(p0rotated.x, p1rotated.x), Math.min(p0rotated.y, p1rotated.y));
|
572 |
|
573 | // Calculate zoom: consider the original bbox and padding.
|
574 | const size = upperRight.sub(lowerLeft);
|
575 | const scaleX = (tr.width - (edgePadding.left + edgePadding.right + options.padding.left + options.padding.right)) / size.x;
|
576 | const scaleY = (tr.height - (edgePadding.top + edgePadding.bottom + options.padding.top + options.padding.bottom)) / size.y;
|
577 |
|
578 | if (scaleY < 0 || scaleX < 0) {
|
579 | warnOnce(
|
580 | 'Map cannot fit within canvas with the given bounds, padding, and/or offset.'
|
581 | );
|
582 | return undefined;
|
583 | }
|
584 |
|
585 | const zoom = Math.min(tr.scaleZoom(tr.scale * Math.min(scaleX, scaleY)), options.maxZoom);
|
586 |
|
587 | // Calculate center: apply the zoom, the configured offset, as well as offset that exists as a result of padding.
|
588 | const offset = Point.convert(options.offset);
|
589 | const paddingOffsetX = (options.padding.left - options.padding.right) / 2;
|
590 | const paddingOffsetY = (options.padding.top - options.padding.bottom) / 2;
|
591 | const paddingOffset = new Point(paddingOffsetX, paddingOffsetY);
|
592 | const rotatedPaddingOffset = paddingOffset.rotate(bearing * Math.PI / 180);
|
593 | const offsetAtInitialZoom = offset.add(rotatedPaddingOffset);
|
594 | const offsetAtFinalZoom = offsetAtInitialZoom.mult(tr.scale / tr.zoomScale(zoom));
|
595 |
|
596 | const center = tr.unproject(p0world.add(p1world).div(2).sub(offsetAtFinalZoom));
|
597 |
|
598 | return {
|
599 | center,
|
600 | zoom,
|
601 | bearing
|
602 | };
|
603 | }
|
604 |
|
605 | /**
|
606 | * Pans and zooms the map to contain its visible area within the specified geographical bounds.
|
607 | * This function will also reset the map's bearing to 0 if bearing is nonzero.
|
608 | *
|
609 | * @memberof Map#
|
610 | * @param bounds Center these bounds in the viewport and use the highest
|
611 | * zoom level up to and including `Map#getMaxZoom()` that fits them in the viewport.
|
612 | * @param {FitBoundsOptions} [options] Options supports all properties from {@link AnimationOptions} and {@link CameraOptions} in addition to the fields below.
|
613 | * @param {number | PaddingOptions} [options.padding] The amount of padding in pixels to add to the given bounds.
|
614 | * @param {boolean} [options.linear=false] If `true`, the map transitions using
|
615 | * {@link Map#easeTo}. If `false`, the map transitions using {@link Map#flyTo}. See
|
616 | * those functions and {@link AnimationOptions} for information about options available.
|
617 | * @param {Function} [options.easing] An easing function for the animated transition. See {@link AnimationOptions}.
|
618 | * @param {PointLike} [options.offset=[0, 0]] The center of the given bounds relative to the map's center, measured in pixels.
|
619 | * @param {number} [options.maxZoom] The maximum zoom level to allow when the map view transitions to the specified bounds.
|
620 | * @param {Object} [eventData] Additional properties to be added to event objects of events triggered by this method.
|
621 | * @fires movestart
|
622 | * @fires moveend
|
623 | * @returns {Map} `this`
|
624 | * @example
|
625 | * var bbox = [[-79, 43], [-73, 45]];
|
626 | * map.fitBounds(bbox, {
|
627 | * padding: {top: 10, bottom:25, left: 15, right: 5}
|
628 | * });
|
629 | * @see [Fit a map to a bounding box](https://maplibre.org/maplibre-gl-js-docs/example/fitbounds/)
|
630 | */
|
631 | fitBounds(bounds: LngLatBoundsLike, options?: FitBoundsOptions, eventData?: any) {
|
632 | return this._fitInternal(
|
633 | this.cameraForBounds(bounds, options),
|
634 | options,
|
635 | eventData);
|
636 | }
|
637 |
|
638 | /**
|
639 | * Pans, rotates and zooms the map to to fit the box made by points p0 and p1
|
640 | * once the map is rotated to the specified bearing. To zoom without rotating,
|
641 | * pass in the current map bearing.
|
642 | *
|
643 | * @memberof Map#
|
644 | * @param p0 First point on screen, in pixel coordinates
|
645 | * @param p1 Second point on screen, in pixel coordinates
|
646 | * @param bearing Desired map bearing at end of animation, in degrees
|
647 | * @param options Options object
|
648 | * @param {number | PaddingOptions} [options.padding] The amount of padding in pixels to add to the given bounds.
|
649 | * @param {boolean} [options.linear=false] If `true`, the map transitions using
|
650 | * {@link Map#easeTo}. If `false`, the map transitions using {@link Map#flyTo}. See
|
651 | * those functions and {@link AnimationOptions} for information about options available.
|
652 | * @param {Function} [options.easing] An easing function for the animated transition. See {@link AnimationOptions}.
|
653 | * @param {PointLike} [options.offset=[0, 0]] The center of the given bounds relative to the map's center, measured in pixels.
|
654 | * @param {number} [options.maxZoom] The maximum zoom level to allow when the map view transitions to the specified bounds.
|
655 | * @param eventData Additional properties to be added to event objects of events triggered by this method.
|
656 | * @fires movestart
|
657 | * @fires moveend
|
658 | * @returns {Map} `this`
|
659 | * @example
|
660 | * var p0 = [220, 400];
|
661 | * var p1 = [500, 900];
|
662 | * map.fitScreenCoordinates(p0, p1, map.getBearing(), {
|
663 | * padding: {top: 10, bottom:25, left: 15, right: 5}
|
664 | * });
|
665 | * @see Used by {@link BoxZoomHandler}
|
666 | */
|
667 | fitScreenCoordinates(p0: PointLike, p1: PointLike, bearing: number, options?: FitBoundsOptions, eventData?: any) {
|
668 | return this._fitInternal(
|
669 | this._cameraForBoxAndBearing(
|
670 | this.transform.pointLocation(Point.convert(p0)),
|
671 | this.transform.pointLocation(Point.convert(p1)),
|
672 | bearing,
|
673 | options),
|
674 | options,
|
675 | eventData);
|
676 | }
|
677 |
|
678 | _fitInternal(calculatedOptions?: CenterZoomBearing, options?: FitBoundsOptions, eventData?: any) {
|
679 | // cameraForBounds warns + returns undefined if unable to fit:
|
680 | if (!calculatedOptions) return this;
|
681 |
|
682 | options = extend(calculatedOptions, options);
|
683 | // Explictly remove the padding field because, calculatedOptions already accounts for padding by setting zoom and center accordingly.
|
684 | delete options.padding;
|
685 |
|
686 | return options.linear ?
|
687 | this.easeTo(options, eventData) :
|
688 | this.flyTo(options, eventData);
|
689 | }
|
690 |
|
691 | /**
|
692 | * Changes any combination of center, zoom, bearing, and pitch, without
|
693 | * an animated transition. The map will retain its current values for any
|
694 | * details not specified in `options`.
|
695 | *
|
696 | * @memberof Map#
|
697 | * @param options Options object
|
698 | * @param eventData Additional properties to be added to event objects of events triggered by this method.
|
699 | * @fires movestart
|
700 | * @fires zoomstart
|
701 | * @fires pitchstart
|
702 | * @fires rotate
|
703 | * @fires move
|
704 | * @fires zoom
|
705 | * @fires pitch
|
706 | * @fires moveend
|
707 | * @fires zoomend
|
708 | * @fires pitchend
|
709 | * @returns {Map} `this`
|
710 | * @example
|
711 | * // jump to coordinates at current zoom
|
712 | * map.jumpTo({center: [0, 0]});
|
713 | * // jump with zoom, pitch, and bearing options
|
714 | * map.jumpTo({
|
715 | * center: [0, 0],
|
716 | * zoom: 8,
|
717 | * pitch: 45,
|
718 | * bearing: 90
|
719 | * });
|
720 | * @see [Jump to a series of locations](https://maplibre.org/maplibre-gl-js-docs/example/jump-to/)
|
721 | * @see [Update a feature in realtime](https://maplibre.org/maplibre-gl-js-docs/example/live-update-feature/)
|
722 | */
|
723 | jumpTo(options: JumpToOptions, eventData?: any) {
|
724 | this.stop();
|
725 |
|
726 | const tr = this.transform;
|
727 | let zoomChanged = false,
|
728 | bearingChanged = false,
|
729 | pitchChanged = false;
|
730 |
|
731 | if ('zoom' in options && tr.zoom !== +options.zoom) {
|
732 | zoomChanged = true;
|
733 | tr.zoom = +options.zoom;
|
734 | }
|
735 |
|
736 | if (options.center !== undefined) {
|
737 | tr.center = LngLat.convert(options.center);
|
738 | }
|
739 |
|
740 | if ('bearing' in options && tr.bearing !== +options.bearing) {
|
741 | bearingChanged = true;
|
742 | tr.bearing = +options.bearing;
|
743 | }
|
744 |
|
745 | if ('pitch' in options && tr.pitch !== +options.pitch) {
|
746 | pitchChanged = true;
|
747 | tr.pitch = +options.pitch;
|
748 | }
|
749 |
|
750 | if (options.padding != null && !tr.isPaddingEqual(options.padding)) {
|
751 | tr.padding = options.padding;
|
752 | }
|
753 |
|
754 | this.fire(new Event('movestart', eventData))
|
755 | .fire(new Event('move', eventData));
|
756 |
|
757 | if (zoomChanged) {
|
758 | this.fire(new Event('zoomstart', eventData))
|
759 | .fire(new Event('zoom', eventData))
|
760 | .fire(new Event('zoomend', eventData));
|
761 | }
|
762 |
|
763 | if (bearingChanged) {
|
764 | this.fire(new Event('rotatestart', eventData))
|
765 | .fire(new Event('rotate', eventData))
|
766 | .fire(new Event('rotateend', eventData));
|
767 | }
|
768 |
|
769 | if (pitchChanged) {
|
770 | this.fire(new Event('pitchstart', eventData))
|
771 | .fire(new Event('pitch', eventData))
|
772 | .fire(new Event('pitchend', eventData));
|
773 | }
|
774 |
|
775 | return this.fire(new Event('moveend', eventData));
|
776 | }
|
777 |
|
778 | /**
|
779 | * Changes any combination of `center`, `zoom`, `bearing`, `pitch`, and `padding` with an animated transition
|
780 | * between old and new values. The map will retain its current values for any
|
781 | * details not specified in `options`.
|
782 | *
|
783 | * Note: The transition will happen instantly if the user has enabled
|
784 | * the `reduced motion` accesibility feature enabled in their operating system,
|
785 | * unless `options` includes `essential: true`.
|
786 | *
|
787 | * @memberof Map#
|
788 | * @param options Options describing the destination and animation of the transition.
|
789 | * Accepts {@link CameraOptions} and {@link AnimationOptions}.
|
790 | * @param eventData Additional properties to be added to event objects of events triggered by this method.
|
791 | * @fires movestart
|
792 | * @fires zoomstart
|
793 | * @fires pitchstart
|
794 | * @fires rotate
|
795 | * @fires move
|
796 | * @fires zoom
|
797 | * @fires pitch
|
798 | * @fires moveend
|
799 | * @fires zoomend
|
800 | * @fires pitchend
|
801 | * @returns {Map} `this`
|
802 | * @see [Navigate the map with game-like controls](https://maplibre.org/maplibre-gl-js-docs/example/game-controls/)
|
803 | */
|
804 | easeTo(options: EaseToOptions & {
|
805 | easeId?: string;
|
806 | noMoveStart?: boolean;
|
807 | }, eventData?: any) {
|
808 | this._stop(false, options.easeId);
|
809 |
|
810 | options = extend({
|
811 | offset: [0, 0],
|
812 | duration: 500,
|
813 | easing: defaultEasing
|
814 | }, options);
|
815 |
|
816 | if (options.animate === false || (!options.essential && browser.prefersReducedMotion)) options.duration = 0;
|
817 |
|
818 | const tr = this.transform,
|
819 | startZoom = this.getZoom(),
|
820 | startBearing = this.getBearing(),
|
821 | startPitch = this.getPitch(),
|
822 | startPadding = this.getPadding(),
|
823 |
|
824 | zoom = 'zoom' in options ? +options.zoom : startZoom,
|
825 | bearing = 'bearing' in options ? this._normalizeBearing(options.bearing, startBearing) : startBearing,
|
826 | pitch = 'pitch' in options ? +options.pitch : startPitch,
|
827 | padding = 'padding' in options ? options.padding : tr.padding;
|
828 |
|
829 | const offsetAsPoint = Point.convert(options.offset);
|
830 | let pointAtOffset = tr.centerPoint.add(offsetAsPoint);
|
831 | const locationAtOffset = tr.pointLocation(pointAtOffset);
|
832 | const center = LngLat.convert(options.center || locationAtOffset);
|
833 | this._normalizeCenter(center);
|
834 |
|
835 | const from = tr.project(locationAtOffset);
|
836 | const delta = tr.project(center).sub(from);
|
837 | const finalScale = tr.zoomScale(zoom - startZoom);
|
838 |
|
839 | let around, aroundPoint;
|
840 |
|
841 | if (options.around) {
|
842 | around = LngLat.convert(options.around);
|
843 | aroundPoint = tr.locationPoint(around);
|
844 | }
|
845 |
|
846 | const currently = {
|
847 | moving: this._moving,
|
848 | zooming: this._zooming,
|
849 | rotating: this._rotating,
|
850 | pitching: this._pitching
|
851 | };
|
852 |
|
853 | this._zooming = this._zooming || (zoom !== startZoom);
|
854 | this._rotating = this._rotating || (startBearing !== bearing);
|
855 | this._pitching = this._pitching || (pitch !== startPitch);
|
856 | this._padding = !tr.isPaddingEqual(padding as PaddingOptions);
|
857 |
|
858 | this._easeId = options.easeId;
|
859 | this._prepareEase(eventData, options.noMoveStart, currently);
|
860 |
|
861 | this._ease((k) => {
|
862 | if (this._zooming) {
|
863 | tr.zoom = interpolate(startZoom, zoom, k);
|
864 | }
|
865 | if (this._rotating) {
|
866 | tr.bearing = interpolate(startBearing, bearing, k);
|
867 | }
|
868 | if (this._pitching) {
|
869 | tr.pitch = interpolate(startPitch, pitch, k);
|
870 | }
|
871 | if (this._padding) {
|
872 | tr.interpolatePadding(startPadding, padding as PaddingOptions, k);
|
873 | // When padding is being applied, Transform#centerPoint is changing continously,
|
874 | // thus we need to recalculate offsetPoint every frame
|
875 | pointAtOffset = tr.centerPoint.add(offsetAsPoint);
|
876 | }
|
877 |
|
878 | if (around) {
|
879 | tr.setLocationAtPoint(around, aroundPoint);
|
880 | } else {
|
881 | const scale = tr.zoomScale(tr.zoom - startZoom);
|
882 | const base = zoom > startZoom ?
|
883 | Math.min(2, finalScale) :
|
884 | Math.max(0.5, finalScale);
|
885 | const speedup = Math.pow(base, 1 - k);
|
886 | const newCenter = tr.unproject(from.add(delta.mult(k * speedup)).mult(scale));
|
887 | tr.setLocationAtPoint(tr.renderWorldCopies ? newCenter.wrap() : newCenter, pointAtOffset);
|
888 | }
|
889 |
|
890 | this._fireMoveEvents(eventData);
|
891 |
|
892 | }, (interruptingEaseId?: string) => {
|
893 | this._afterEase(eventData, interruptingEaseId);
|
894 | }, options as any);
|
895 |
|
896 | return this;
|
897 | }
|
898 |
|
899 | _prepareEase(eventData: any, noMoveStart: boolean, currently: any = {}) {
|
900 | this._moving = true;
|
901 |
|
902 | if (!noMoveStart && !currently.moving) {
|
903 | this.fire(new Event('movestart', eventData));
|
904 | }
|
905 | if (this._zooming && !currently.zooming) {
|
906 | this.fire(new Event('zoomstart', eventData));
|
907 | }
|
908 | if (this._rotating && !currently.rotating) {
|
909 | this.fire(new Event('rotatestart', eventData));
|
910 | }
|
911 | if (this._pitching && !currently.pitching) {
|
912 | this.fire(new Event('pitchstart', eventData));
|
913 | }
|
914 | }
|
915 |
|
916 | _fireMoveEvents(eventData?: any) {
|
917 | this.fire(new Event('move', eventData));
|
918 | if (this._zooming) {
|
919 | this.fire(new Event('zoom', eventData));
|
920 | }
|
921 | if (this._rotating) {
|
922 | this.fire(new Event('rotate', eventData));
|
923 | }
|
924 | if (this._pitching) {
|
925 | this.fire(new Event('pitch', eventData));
|
926 | }
|
927 | }
|
928 |
|
929 | _afterEase(eventData?: any, easeId?: string) {
|
930 | // if this easing is being stopped to start another easing with
|
931 | // the same id then don't fire any events to avoid extra start/stop events
|
932 | if (this._easeId && easeId && this._easeId === easeId) {
|
933 | return;
|
934 | }
|
935 | delete this._easeId;
|
936 |
|
937 | const wasZooming = this._zooming;
|
938 | const wasRotating = this._rotating;
|
939 | const wasPitching = this._pitching;
|
940 | this._moving = false;
|
941 | this._zooming = false;
|
942 | this._rotating = false;
|
943 | this._pitching = false;
|
944 | this._padding = false;
|
945 |
|
946 | if (wasZooming) {
|
947 | this.fire(new Event('zoomend', eventData));
|
948 | }
|
949 | if (wasRotating) {
|
950 | this.fire(new Event('rotateend', eventData));
|
951 | }
|
952 | if (wasPitching) {
|
953 | this.fire(new Event('pitchend', eventData));
|
954 | }
|
955 | this.fire(new Event('moveend', eventData));
|
956 | }
|
957 |
|
958 | /**
|
959 | * Changes any combination of center, zoom, bearing, and pitch, animating the transition along a curve that
|
960 | * evokes flight. The animation seamlessly incorporates zooming and panning to help
|
961 | * the user maintain her bearings even after traversing a great distance.
|
962 | *
|
963 | * Note: The animation will be skipped, and this will behave equivalently to `jumpTo`
|
964 | * if the user has the `reduced motion` accesibility feature enabled in their operating system,
|
965 | * unless 'options' includes `essential: true`.
|
966 | *
|
967 | * @memberof Map#
|
968 | * @param {FlyToOptions} options Options describing the destination and animation of the transition.
|
969 | * Accepts {@link CameraOptions}, {@link AnimationOptions},
|
970 | * and the following additional options.
|
971 | * @param {number} [options.curve=1.42] The zooming "curve" that will occur along the
|
972 | * flight path. A high value maximizes zooming for an exaggerated animation, while a low
|
973 | * value minimizes zooming for an effect closer to {@link Map#easeTo}. 1.42 is the average
|
974 | * value selected by participants in the user study discussed in
|
975 | * [van Wijk (2003)](https://www.win.tue.nl/~vanwijk/zoompan.pdf). A value of
|
976 | * `Math.pow(6, 0.25)` would be equivalent to the root mean squared average velocity. A
|
977 | * value of 1 would produce a circular motion.
|
978 | * @param {number} [options.minZoom] The zero-based zoom level at the peak of the flight path. If
|
979 | * `options.curve` is specified, this option is ignored.
|
980 | * @param {number} [options.speed=1.2] The average speed of the animation defined in relation to
|
981 | * `options.curve`. A speed of 1.2 means that the map appears to move along the flight path
|
982 | * by 1.2 times `options.curve` screenfuls every second. A _screenful_ is the map's visible span.
|
983 | * It does not correspond to a fixed physical distance, but varies by zoom level.
|
984 | * @param {number} [options.screenSpeed] The average speed of the animation measured in screenfuls
|
985 | * per second, assuming a linear timing curve. If `options.speed` is specified, this option is ignored.
|
986 | * @param {number} [options.maxDuration] The animation's maximum duration, measured in milliseconds.
|
987 | * If duration exceeds maximum duration, it resets to 0.
|
988 | * @param eventData Additional properties to be added to event objects of events triggered by this method.
|
989 | * @fires movestart
|
990 | * @fires zoomstart
|
991 | * @fires pitchstart
|
992 | * @fires move
|
993 | * @fires zoom
|
994 | * @fires rotate
|
995 | * @fires pitch
|
996 | * @fires moveend
|
997 | * @fires zoomend
|
998 | * @fires pitchend
|
999 | * @returns {Map} `this`
|
1000 | * @example
|
1001 | * // fly with default options to null island
|
1002 | * map.flyTo({center: [0, 0], zoom: 9});
|
1003 | * // using flyTo options
|
1004 | * map.flyTo({
|
1005 | * center: [0, 0],
|
1006 | * zoom: 9,
|
1007 | * speed: 0.2,
|
1008 | * curve: 1,
|
1009 | * easing(t) {
|
1010 | * return t;
|
1011 | * }
|
1012 | * });
|
1013 | * @see [Fly to a location](https://maplibre.org/maplibre-gl-js-docs/example/flyto/)
|
1014 | * @see [Slowly fly to a location](https://maplibre.org/maplibre-gl-js-docs/example/flyto-options/)
|
1015 | * @see [Fly to a location based on scroll position](https://maplibre.org/maplibre-gl-js-docs/example/scroll-fly-to/)
|
1016 | */
|
1017 | flyTo(options: FlyToOptions, eventData?: any) {
|
1018 | // Fall through to jumpTo if user has set prefers-reduced-motion
|
1019 | if (!options.essential && browser.prefersReducedMotion) {
|
1020 | const coercedOptions = pick(options, ['center', 'zoom', 'bearing', 'pitch', 'around']) as CameraOptions;
|
1021 | return this.jumpTo(coercedOptions, eventData);
|
1022 | }
|
1023 |
|
1024 | // This method implements an “optimal path” animation, as detailed in:
|
1025 | //
|
1026 | // Van Wijk, Jarke J.; Nuij, Wim A. A. “Smooth and efficient zooming and panning.” INFOVIS
|
1027 | // ’03. pp. 15–22. <https://www.win.tue.nl/~vanwijk/zoompan.pdf#page=5>.
|
1028 | //
|
1029 | // Where applicable, local variable documentation begins with the associated variable or
|
1030 | // function in van Wijk (2003).
|
1031 |
|
1032 | this.stop();
|
1033 |
|
1034 | options = extend({
|
1035 | offset: [0, 0],
|
1036 | speed: 1.2,
|
1037 | curve: 1.42,
|
1038 | easing: defaultEasing
|
1039 | }, options);
|
1040 |
|
1041 | const tr = this.transform,
|
1042 | startZoom = this.getZoom(),
|
1043 | startBearing = this.getBearing(),
|
1044 | startPitch = this.getPitch(),
|
1045 | startPadding = this.getPadding();
|
1046 |
|
1047 | const zoom = 'zoom' in options ? clamp(+options.zoom, tr.minZoom, tr.maxZoom) : startZoom;
|
1048 | const bearing = 'bearing' in options ? this._normalizeBearing(options.bearing, startBearing) : startBearing;
|
1049 | const pitch = 'pitch' in options ? +options.pitch : startPitch;
|
1050 | const padding = 'padding' in options ? options.padding : tr.padding;
|
1051 |
|
1052 | const scale = tr.zoomScale(zoom - startZoom);
|
1053 | const offsetAsPoint = Point.convert(options.offset);
|
1054 | let pointAtOffset = tr.centerPoint.add(offsetAsPoint);
|
1055 | const locationAtOffset = tr.pointLocation(pointAtOffset);
|
1056 | const center = LngLat.convert(options.center || locationAtOffset);
|
1057 | this._normalizeCenter(center);
|
1058 |
|
1059 | const from = tr.project(locationAtOffset);
|
1060 | const delta = tr.project(center).sub(from);
|
1061 |
|
1062 | let rho = options.curve;
|
1063 |
|
1064 | // w₀: Initial visible span, measured in pixels at the initial scale.
|
1065 | const w0 = Math.max(tr.width, tr.height),
|
1066 | // w₁: Final visible span, measured in pixels with respect to the initial scale.
|
1067 | w1 = w0 / scale,
|
1068 | // Length of the flight path as projected onto the ground plane, measured in pixels from
|
1069 | // the world image origin at the initial scale.
|
1070 | u1 = delta.mag();
|
1071 |
|
1072 | if ('minZoom' in options) {
|
1073 | const minZoom = clamp(Math.min(options.minZoom, startZoom, zoom), tr.minZoom, tr.maxZoom);
|
1074 | // w<sub>m</sub>: Maximum visible span, measured in pixels with respect to the initial
|
1075 | // scale.
|
1076 | const wMax = w0 / tr.zoomScale(minZoom - startZoom);
|
1077 | rho = Math.sqrt(wMax / u1 * 2);
|
1078 | }
|
1079 |
|
1080 | // ρ²
|
1081 | const rho2 = rho * rho;
|
1082 |
|
1083 | /**
|
1084 | * rᵢ: Returns the zoom-out factor at one end of the animation.
|
1085 | *
|
1086 | * @param i 0 for the ascent or 1 for the descent.
|
1087 | * @private
|
1088 | */
|
1089 | function r(i) {
|
1090 | const b = (w1 * w1 - w0 * w0 + (i ? -1 : 1) * rho2 * rho2 * u1 * u1) / (2 * (i ? w1 : w0) * rho2 * u1);
|
1091 | return Math.log(Math.sqrt(b * b + 1) - b);
|
1092 | }
|
1093 |
|
1094 | function sinh(n) { return (Math.exp(n) - Math.exp(-n)) / 2; }
|
1095 | function cosh(n) { return (Math.exp(n) + Math.exp(-n)) / 2; }
|
1096 | function tanh(n) { return sinh(n) / cosh(n); }
|
1097 |
|
1098 | // r₀: Zoom-out factor during ascent.
|
1099 | const r0 = r(0);
|
1100 |
|
1101 | // w(s): Returns the visible span on the ground, measured in pixels with respect to the
|
1102 | // initial scale. Assumes an angular field of view of 2 arctan ½ ≈ 53°.
|
1103 | let w: (_: number) => number = function (s) {
|
1104 | return (cosh(r0) / cosh(r0 + rho * s));
|
1105 | };
|
1106 |
|
1107 | // u(s): Returns the distance along the flight path as projected onto the ground plane,
|
1108 | // measured in pixels from the world image origin at the initial scale.
|
1109 | let u: (_: number) => number = function (s) {
|
1110 | return w0 * ((cosh(r0) * tanh(r0 + rho * s) - sinh(r0)) / rho2) / u1;
|
1111 | };
|
1112 |
|
1113 | // S: Total length of the flight path, measured in ρ-screenfuls.
|
1114 | let S = (r(1) - r0) / rho;
|
1115 |
|
1116 | // When u₀ = u₁, the optimal path doesn’t require both ascent and descent.
|
1117 | if (Math.abs(u1) < 0.000001 || !isFinite(S)) {
|
1118 | // Perform a more or less instantaneous transition if the path is too short.
|
1119 | if (Math.abs(w0 - w1) < 0.000001) return this.easeTo(options, eventData);
|
1120 |
|
1121 | const k = w1 < w0 ? -1 : 1;
|
1122 | S = Math.abs(Math.log(w1 / w0)) / rho;
|
1123 |
|
1124 | u = function() { return 0; };
|
1125 | w = function(s) { return Math.exp(k * rho * s); };
|
1126 | }
|
1127 |
|
1128 | if ('duration' in options) {
|
1129 | options.duration = +options.duration;
|
1130 | } else {
|
1131 | const V = 'screenSpeed' in options ? +options.screenSpeed / rho : +options.speed;
|
1132 | options.duration = 1000 * S / V;
|
1133 | }
|
1134 |
|
1135 | if (options.maxDuration && options.duration > options.maxDuration) {
|
1136 | options.duration = 0;
|
1137 | }
|
1138 |
|
1139 | this._zooming = true;
|
1140 | this._rotating = (startBearing !== bearing);
|
1141 | this._pitching = (pitch !== startPitch);
|
1142 | this._padding = !tr.isPaddingEqual(padding as PaddingOptions);
|
1143 |
|
1144 | this._prepareEase(eventData, false);
|
1145 |
|
1146 | this._ease((k) => {
|
1147 | // s: The distance traveled along the flight path, measured in ρ-screenfuls.
|
1148 | const s = k * S;
|
1149 | const scale = 1 / w(s);
|
1150 | tr.zoom = k === 1 ? zoom : startZoom + tr.scaleZoom(scale);
|
1151 |
|
1152 | if (this._rotating) {
|
1153 | tr.bearing = interpolate(startBearing, bearing, k);
|
1154 | }
|
1155 | if (this._pitching) {
|
1156 | tr.pitch = interpolate(startPitch, pitch, k);
|
1157 | }
|
1158 | if (this._padding) {
|
1159 | tr.interpolatePadding(startPadding, padding as PaddingOptions, k);
|
1160 | // When padding is being applied, Transform#centerPoint is changing continously,
|
1161 | // thus we need to recalculate offsetPoint every frame
|
1162 | pointAtOffset = tr.centerPoint.add(offsetAsPoint);
|
1163 | }
|
1164 |
|
1165 | const newCenter = k === 1 ? center : tr.unproject(from.add(delta.mult(u(s))).mult(scale));
|
1166 | tr.setLocationAtPoint(tr.renderWorldCopies ? newCenter.wrap() : newCenter, pointAtOffset);
|
1167 |
|
1168 | this._fireMoveEvents(eventData);
|
1169 |
|
1170 | }, () => this._afterEase(eventData), options);
|
1171 |
|
1172 | return this;
|
1173 | }
|
1174 |
|
1175 | isEasing() {
|
1176 | return !!this._easeFrameId;
|
1177 | }
|
1178 |
|
1179 | /**
|
1180 | * Stops any animated transition underway.
|
1181 | *
|
1182 | * @memberof Map#
|
1183 | * @returns {Map} `this`
|
1184 | */
|
1185 | stop(): this {
|
1186 | return this._stop();
|
1187 | }
|
1188 |
|
1189 | _stop(allowGestures?: boolean, easeId?: string): this {
|
1190 | if (this._easeFrameId) {
|
1191 | this._cancelRenderFrame(this._easeFrameId);
|
1192 | delete this._easeFrameId;
|
1193 | delete this._onEaseFrame;
|
1194 | }
|
1195 |
|
1196 | if (this._onEaseEnd) {
|
1197 | // The _onEaseEnd function might emit events which trigger new
|
1198 | // animation, which sets a new _onEaseEnd. Ensure we don't delete
|
1199 | // it unintentionally.
|
1200 | const onEaseEnd = this._onEaseEnd;
|
1201 | delete this._onEaseEnd;
|
1202 | onEaseEnd.call(this, easeId);
|
1203 | }
|
1204 | if (!allowGestures) {
|
1205 | const handlers = (this as any).handlers;
|
1206 | if (handlers) handlers.stop(false);
|
1207 | }
|
1208 | return this;
|
1209 | }
|
1210 |
|
1211 | _ease(frame: (_: number) => void,
|
1212 | finish: () => void,
|
1213 | options: {
|
1214 | animate?: boolean;
|
1215 | duration?: number;
|
1216 | easing?: (_: number) => number;
|
1217 | }) {
|
1218 | if (options.animate === false || options.duration === 0) {
|
1219 | frame(1);
|
1220 | finish();
|
1221 | } else {
|
1222 | this._easeStart = browser.now();
|
1223 | this._easeOptions = options;
|
1224 | this._onEaseFrame = frame;
|
1225 | this._onEaseEnd = finish;
|
1226 | this._easeFrameId = this._requestRenderFrame(this._renderFrameCallback);
|
1227 | }
|
1228 | }
|
1229 |
|
1230 | // Callback for map._requestRenderFrame
|
1231 | _renderFrameCallback() {
|
1232 | const t = Math.min((browser.now() - this._easeStart) / this._easeOptions.duration, 1);
|
1233 | this._onEaseFrame(this._easeOptions.easing(t));
|
1234 | if (t < 1) {
|
1235 | this._easeFrameId = this._requestRenderFrame(this._renderFrameCallback);
|
1236 | } else {
|
1237 | this.stop();
|
1238 | }
|
1239 | }
|
1240 |
|
1241 | // convert bearing so that it's numerically close to the current one so that it interpolates properly
|
1242 | _normalizeBearing(bearing: number, currentBearing: number) {
|
1243 | bearing = wrap(bearing, -180, 180);
|
1244 | const diff = Math.abs(bearing - currentBearing);
|
1245 | if (Math.abs(bearing - 360 - currentBearing) < diff) bearing -= 360;
|
1246 | if (Math.abs(bearing + 360 - currentBearing) < diff) bearing += 360;
|
1247 | return bearing;
|
1248 | }
|
1249 |
|
1250 | // If a path crossing the antimeridian would be shorter, extend the final coordinate so that
|
1251 | // interpolating between the two endpoints will cross it.
|
1252 | _normalizeCenter(center: LngLat) {
|
1253 | const tr = this.transform;
|
1254 | if (!tr.renderWorldCopies || tr.lngRange) return;
|
1255 |
|
1256 | const delta = center.lng - tr.center.lng;
|
1257 | center.lng +=
|
1258 | delta > 180 ? -360 :
|
1259 | delta < -180 ? 360 : 0;
|
1260 | }
|
1261 | }
|
1262 |
|
1263 | // In debug builds, check that camera change events are fired in the correct order.
|
1264 | // - ___start events needs to be fired before ___ and ___end events
|
1265 | // - another ___start event can't be fired before a ___end event has been fired for the previous one
|
1266 | function addAssertions(camera: Camera) { //eslint-disable-line
|
1267 | Debug.run(() => {
|
1268 | const inProgress = {} as any;
|
1269 |
|
1270 | ['drag', 'zoom', 'rotate', 'pitch', 'move'].forEach(name => {
|
1271 | inProgress[name] = false;
|
1272 |
|
1273 | camera.on(`${name}start`, () => {
|
1274 | assert(!inProgress[name], `"${name}start" fired twice without a "${name}end"`);
|
1275 | inProgress[name] = true;
|
1276 | assert(inProgress.move);
|
1277 | });
|
1278 |
|
1279 | camera.on(name, () => {
|
1280 | assert(inProgress[name]);
|
1281 | assert(inProgress.move);
|
1282 | });
|
1283 |
|
1284 | camera.on(`${name}end`, () => {
|
1285 | assert(inProgress.move);
|
1286 | assert(inProgress[name]);
|
1287 | inProgress[name] = false;
|
1288 | });
|
1289 | });
|
1290 |
|
1291 | // Canary used to test whether this function is stripped in prod build
|
1292 | canary = 'canary debug run'; // eslint-disable-line
|
1293 | });
|
1294 | }
|
1295 |
|
1296 | let canary; // eslint-disable-line
|
1297 |
|
1298 | export default Camera;
|