@deck.gl/core 9.3.0-alpha.1 → 9.3.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/debug.min.js +1 -1
  2. package/dist/controllers/controller.d.ts +5 -4
  3. package/dist/controllers/controller.d.ts.map +1 -1
  4. package/dist/controllers/controller.js +18 -7
  5. package/dist/controllers/controller.js.map +1 -1
  6. package/dist/controllers/first-person-controller.d.ts +3 -2
  7. package/dist/controllers/first-person-controller.d.ts.map +1 -1
  8. package/dist/controllers/first-person-controller.js +13 -5
  9. package/dist/controllers/first-person-controller.js.map +1 -1
  10. package/dist/controllers/globe-controller.d.ts +1 -0
  11. package/dist/controllers/globe-controller.d.ts.map +1 -1
  12. package/dist/controllers/globe-controller.js +66 -5
  13. package/dist/controllers/globe-controller.js.map +1 -1
  14. package/dist/controllers/map-controller.d.ts +7 -18
  15. package/dist/controllers/map-controller.d.ts.map +1 -1
  16. package/dist/controllers/map-controller.js +94 -50
  17. package/dist/controllers/map-controller.js.map +1 -1
  18. package/dist/controllers/orbit-controller.d.ts +12 -4
  19. package/dist/controllers/orbit-controller.d.ts.map +1 -1
  20. package/dist/controllers/orbit-controller.js +118 -10
  21. package/dist/controllers/orbit-controller.js.map +1 -1
  22. package/dist/controllers/orthographic-controller.d.ts +117 -9
  23. package/dist/controllers/orthographic-controller.d.ts.map +1 -1
  24. package/dist/controllers/orthographic-controller.js +302 -37
  25. package/dist/controllers/orthographic-controller.js.map +1 -1
  26. package/dist/controllers/view-state.d.ts +2 -1
  27. package/dist/controllers/view-state.d.ts.map +1 -1
  28. package/dist/controllers/view-state.js +2 -1
  29. package/dist/controllers/view-state.js.map +1 -1
  30. package/dist/debug/loggers.d.ts.map +1 -1
  31. package/dist/debug/loggers.js +1 -4
  32. package/dist/debug/loggers.js.map +1 -1
  33. package/dist/dist.dev.js +3705 -1678
  34. package/dist/effects/lighting/lighting-effect.d.ts +1 -0
  35. package/dist/effects/lighting/lighting-effect.d.ts.map +1 -1
  36. package/dist/effects/lighting/lighting-effect.js +14 -5
  37. package/dist/effects/lighting/lighting-effect.js.map +1 -1
  38. package/dist/index.cjs +685 -123
  39. package/dist/index.cjs.map +4 -4
  40. package/dist/lib/attribute/attribute-manager.d.ts.map +1 -1
  41. package/dist/lib/attribute/attribute-manager.js +2 -0
  42. package/dist/lib/attribute/attribute-manager.js.map +1 -1
  43. package/dist/lib/deck-picker.d.ts +6 -1
  44. package/dist/lib/deck-picker.d.ts.map +1 -1
  45. package/dist/lib/deck-picker.js +15 -3
  46. package/dist/lib/deck-picker.js.map +1 -1
  47. package/dist/lib/deck-renderer.d.ts +6 -1
  48. package/dist/lib/deck-renderer.d.ts.map +1 -1
  49. package/dist/lib/deck-renderer.js +14 -2
  50. package/dist/lib/deck-renderer.js.map +1 -1
  51. package/dist/lib/deck.d.ts +5 -0
  52. package/dist/lib/deck.d.ts.map +1 -1
  53. package/dist/lib/deck.js +13 -3
  54. package/dist/lib/deck.js.map +1 -1
  55. package/dist/lib/init.js +2 -2
  56. package/dist/lib/layer.d.ts.map +1 -1
  57. package/dist/lib/layer.js +1 -0
  58. package/dist/lib/layer.js.map +1 -1
  59. package/dist/passes/draw-layers-pass.d.ts +2 -0
  60. package/dist/passes/draw-layers-pass.d.ts.map +1 -1
  61. package/dist/passes/draw-layers-pass.js +3 -0
  62. package/dist/passes/draw-layers-pass.js.map +1 -1
  63. package/dist/passes/layers-pass.d.ts +2 -1
  64. package/dist/passes/layers-pass.d.ts.map +1 -1
  65. package/dist/passes/layers-pass.js +3 -0
  66. package/dist/passes/layers-pass.js.map +1 -1
  67. package/dist/passes/pick-layers-pass.d.ts +5 -2
  68. package/dist/passes/pick-layers-pass.d.ts.map +1 -1
  69. package/dist/passes/pick-layers-pass.js +3 -2
  70. package/dist/passes/pick-layers-pass.js.map +1 -1
  71. package/dist/shaderlib/project/project.glsl.d.ts.map +1 -1
  72. package/dist/shaderlib/project/project.glsl.js +3 -0
  73. package/dist/shaderlib/project/project.glsl.js.map +1 -1
  74. package/dist/utils/deep-merge.d.ts +5 -0
  75. package/dist/utils/deep-merge.d.ts.map +1 -0
  76. package/dist/utils/deep-merge.js +31 -0
  77. package/dist/utils/deep-merge.js.map +1 -0
  78. package/dist/utils/math-utils.d.ts +4 -0
  79. package/dist/utils/math-utils.d.ts.map +1 -1
  80. package/dist/utils/math-utils.js +8 -0
  81. package/dist/utils/math-utils.js.map +1 -1
  82. package/dist/viewports/globe-viewport.d.ts +1 -0
  83. package/dist/viewports/globe-viewport.d.ts.map +1 -1
  84. package/dist/viewports/globe-viewport.js +1 -1
  85. package/dist/viewports/globe-viewport.js.map +1 -1
  86. package/dist/viewports/orbit-viewport.d.ts.map +1 -1
  87. package/dist/viewports/orbit-viewport.js +7 -2
  88. package/dist/viewports/orbit-viewport.js.map +1 -1
  89. package/dist/viewports/orthographic-viewport.d.ts +8 -2
  90. package/dist/viewports/orthographic-viewport.d.ts.map +1 -1
  91. package/dist/viewports/orthographic-viewport.js.map +1 -1
  92. package/dist/views/orthographic-view.d.ts +38 -4
  93. package/dist/views/orthographic-view.d.ts.map +1 -1
  94. package/dist/views/orthographic-view.js.map +1 -1
  95. package/dist/views/view.d.ts.map +1 -1
  96. package/dist/views/view.js +2 -8
  97. package/dist/views/view.js.map +1 -1
  98. package/dist.min.js +220 -144
  99. package/package.json +9 -9
  100. package/src/controllers/controller.ts +23 -9
  101. package/src/controllers/first-person-controller.ts +18 -8
  102. package/src/controllers/globe-controller.ts +89 -5
  103. package/src/controllers/map-controller.ts +105 -56
  104. package/src/controllers/orbit-controller.ts +147 -13
  105. package/src/controllers/orthographic-controller.ts +417 -41
  106. package/src/controllers/view-state.ts +8 -1
  107. package/src/debug/loggers.ts +1 -5
  108. package/src/effects/lighting/lighting-effect.ts +20 -8
  109. package/src/lib/attribute/attribute-manager.ts +1 -0
  110. package/src/lib/deck-picker.ts +18 -4
  111. package/src/lib/deck-renderer.ts +17 -3
  112. package/src/lib/deck.ts +19 -3
  113. package/src/lib/layer.ts +1 -0
  114. package/src/passes/draw-layers-pass.ts +5 -0
  115. package/src/passes/layers-pass.ts +5 -1
  116. package/src/passes/pick-layers-pass.ts +8 -4
  117. package/src/shaderlib/project/project.glsl.ts +3 -0
  118. package/src/utils/deep-merge.ts +33 -0
  119. package/src/utils/math-utils.ts +12 -0
  120. package/src/viewports/globe-viewport.ts +1 -1
  121. package/src/viewports/orbit-viewport.ts +8 -2
  122. package/src/viewports/orthographic-viewport.ts +8 -2
  123. package/src/views/orthographic-view.ts +38 -4
  124. package/src/views/view.ts +2 -8
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "deck.gl core library",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
- "version": "9.3.0-alpha.1",
6
+ "version": "9.3.0-alpha.2",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -40,13 +40,13 @@
40
40
  "prepublishOnly": "npm run build-debugger && npm run build-bundle && npm run build-bundle -- --env=dev"
41
41
  },
42
42
  "dependencies": {
43
- "@loaders.gl/core": "^4.4.0-alpha.16",
44
- "@loaders.gl/images": "^4.4.0-alpha.16",
45
- "@luma.gl/constants": "^9.3.0-alpha.2",
46
- "@luma.gl/core": "^9.3.0-alpha.2",
47
- "@luma.gl/engine": "^9.3.0-alpha.2",
48
- "@luma.gl/shadertools": "^9.3.0-alpha.2",
49
- "@luma.gl/webgl": "^9.3.0-alpha.2",
43
+ "@loaders.gl/core": "^4.4.0-alpha.18",
44
+ "@loaders.gl/images": "^4.4.0-alpha.18",
45
+ "@luma.gl/constants": "^9.3.0-alpha.6",
46
+ "@luma.gl/core": "^9.3.0-alpha.6",
47
+ "@luma.gl/engine": "^9.3.0-alpha.6",
48
+ "@luma.gl/shadertools": "^9.3.0-alpha.6",
49
+ "@luma.gl/webgl": "^9.3.0-alpha.6",
50
50
  "@math.gl/core": "^4.1.0",
51
51
  "@math.gl/sun": "^4.1.0",
52
52
  "@math.gl/types": "^4.1.0",
@@ -58,5 +58,5 @@
58
58
  "gl-matrix": "^3.0.0",
59
59
  "mjolnir.js": "^3.0.0"
60
60
  },
61
- "gitHead": "eb5dc776addbcd766f6ffcb1eb2179eee0664d97"
61
+ "gitHead": "135d329f4a4b596ae2c16e4eb801eda30252f3bc"
62
62
  }
@@ -7,6 +7,7 @@ import TransitionManager, {TransitionProps} from './transition-manager';
7
7
  import LinearInterpolator from '../transitions/linear-interpolator';
8
8
  import {IViewState} from './view-state';
9
9
  import {ConstructorOf} from '../types/types';
10
+ import {deepEqual} from '../utils/deep-equal';
10
11
 
11
12
  import type Viewport from '../viewports/viewport';
12
13
 
@@ -65,6 +66,8 @@ export type ControllerOptions = {
65
66
  dragMode?: 'pan' | 'rotate';
66
67
  /** Enable inertia after panning/pinching. If a number is provided, indicates the duration of time over which the velocity reduces to zero, in milliseconds. Default `false`. */
67
68
  inertia?: boolean | number;
69
+ /** Bounding box of content that the controller is constrained in */
70
+ maxBounds?: [min: [number, number], max: [number, number]] | [min: [number, number, number], max: [number, number, number]] | null;
68
71
  };
69
72
 
70
73
  export type ControllerProps = {
@@ -122,6 +125,7 @@ export default abstract class Controller<ControllerState extends IViewState<Cont
122
125
  protected onViewStateChange: (params: ViewStateChangeParameters) => void;
123
126
  protected onStateChange: (state: InteractionState) => void;
124
127
  protected makeViewport: (opts: Record<string, any>) => Viewport;
128
+ protected pickPosition?: (x: number, y: number) => {coordinate?: number[]} | null;
125
129
 
126
130
  private _controllerState?: ControllerState;
127
131
  private _events: Record<string, boolean> = {};
@@ -171,6 +175,7 @@ export default abstract class Controller<ControllerState extends IViewState<Cont
171
175
  this.onViewStateChange = opts.onViewStateChange || (() => {});
172
176
  this.onStateChange = opts.onStateChange || (() => {});
173
177
  this.makeViewport = opts.makeViewport;
178
+ this.pickPosition = opts.pickPosition;
174
179
  }
175
180
 
176
181
  set events(customEvents) {
@@ -240,7 +245,7 @@ export default abstract class Controller<ControllerState extends IViewState<Cont
240
245
  ...this.props,
241
246
  ...this.state
242
247
  });
243
- return this._controllerState ;
248
+ return this._controllerState;
244
249
  }
245
250
 
246
251
  getCenter(event: MjolnirGestureEvent | MjolnirWheelEvent) : [number, number] {
@@ -291,6 +296,7 @@ export default abstract class Controller<ControllerState extends IViewState<Cont
291
296
  if (props.dragMode) {
292
297
  this.dragMode = props.dragMode;
293
298
  }
299
+ const oldProps = this.props;
294
300
  this.props = props;
295
301
 
296
302
  if (!('transitionInterpolator' in props)) {
@@ -332,6 +338,19 @@ export default abstract class Controller<ControllerState extends IViewState<Cont
332
338
  this.touchZoom = touchZoom;
333
339
  this.touchRotate = touchRotate;
334
340
  this.keyboard = keyboard;
341
+
342
+ // Normalize view state if maxBounds is defined
343
+ const dimensionChanged = !oldProps || oldProps.height !== props.height || oldProps.width !== props.width || oldProps.maxBounds !== props.maxBounds;
344
+ if (dimensionChanged && props.maxBounds) {
345
+ // Dimensions changed, try re-normalize the props
346
+ const controllerState = new this.ControllerState({...props, makeViewport: this.makeViewport});
347
+ const normalizedProps = controllerState.getViewportProps();
348
+ const changed = Object.keys(normalizedProps).some(key => !deepEqual(normalizedProps[key], props[key], 1));
349
+ if (changed) {
350
+ // some props are updated after normalization
351
+ this.updateViewport(controllerState);
352
+ }
353
+ }
335
354
  }
336
355
 
337
356
  updateTransition() {
@@ -400,19 +419,14 @@ export default abstract class Controller<ControllerState extends IViewState<Cont
400
419
  alternateMode = !alternateMode;
401
420
  }
402
421
 
403
- const newControllerState = alternateMode
404
- ? this.controllerState.panStart({pos})
405
- : this.controllerState.rotateStart(this._getRotateStartParams(pos));
422
+ const newControllerState = this.controllerState[alternateMode ? 'panStart' : 'rotateStart']({
423
+ pos
424
+ });
406
425
  this._panMove = alternateMode;
407
426
  this.updateViewport(newControllerState, NO_TRANSITION_PROPS, {isDragging: true});
408
427
  return true;
409
428
  }
410
429
 
411
- /** Returns parameters for rotateStart. Override to add extra params (e.g. altitude). */
412
- protected _getRotateStartParams(pos: [number, number]): {pos: [number, number]} {
413
- return {pos};
414
- }
415
-
416
430
  // Default handler for the `panmove` and `panend` event.
417
431
  protected _onPan(event: MjolnirGestureEvent): boolean {
418
432
  if (!this.isDragging()) {
@@ -2,7 +2,7 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import Controller from './controller';
5
+ import Controller, {ControllerProps} from './controller';
6
6
  import ViewState from './view-state';
7
7
  import {mod} from '../utils/math-utils';
8
8
  import type Viewport from '../viewports/viewport';
@@ -27,6 +27,8 @@ type FirstPersonStateProps = {
27
27
 
28
28
  maxPitch?: number;
29
29
  minPitch?: number;
30
+
31
+ maxBounds?: ControllerProps['maxBounds'];
30
32
  };
31
33
 
32
34
  type FirstPersonStateInternal = {
@@ -43,8 +45,6 @@ class FirstPersonState extends ViewState<
43
45
  FirstPersonStateProps,
44
46
  FirstPersonStateInternal
45
47
  > {
46
- makeViewport: (props: Record<string, any>) => Viewport;
47
-
48
48
  constructor(
49
49
  options: FirstPersonStateProps &
50
50
  FirstPersonStateInternal & {
@@ -69,6 +69,8 @@ class FirstPersonState extends ViewState<
69
69
  maxPitch = 90,
70
70
  minPitch = -90,
71
71
 
72
+ maxBounds = null,
73
+
72
74
  // Model state when the rotate operation first started
73
75
  startRotatePos,
74
76
  startBearing,
@@ -88,7 +90,8 @@ class FirstPersonState extends ViewState<
88
90
  longitude,
89
91
  latitude,
90
92
  maxPitch,
91
- minPitch
93
+ minPitch,
94
+ maxBounds
92
95
  },
93
96
  {
94
97
  startRotatePos,
@@ -97,10 +100,9 @@ class FirstPersonState extends ViewState<
97
100
  startZoomPosition,
98
101
  startPanPos,
99
102
  startPanPosition
100
- }
103
+ },
104
+ options.makeViewport
101
105
  );
102
-
103
- this.makeViewport = options.makeViewport;
104
106
  }
105
107
 
106
108
  /* Public API */
@@ -366,7 +368,7 @@ class FirstPersonState extends ViewState<
366
368
  // Apply any constraints (mathematical or defined by _viewportProps) to map state
367
369
  applyConstraints(props: Required<FirstPersonStateProps>): Required<FirstPersonStateProps> {
368
370
  // Ensure pitch and zoom are within specified range
369
- const {pitch, maxPitch, minPitch, longitude, bearing} = props;
371
+ const {pitch, maxPitch, minPitch, longitude, position, bearing, maxBounds} = props;
370
372
  props.pitch = clamp(pitch, minPitch, maxPitch);
371
373
 
372
374
  // Normalize degrees
@@ -376,6 +378,14 @@ class FirstPersonState extends ViewState<
376
378
  if (bearing < -180 || bearing > 180) {
377
379
  props.bearing = mod(bearing + 180, 360) - 180;
378
380
  }
381
+ if (maxBounds) {
382
+ const x = clamp(position[0], maxBounds[0][0], maxBounds[1][0]);
383
+ const y = clamp(position[1], maxBounds[0][1], maxBounds[1][1]);
384
+ const z = clamp(position[2] ?? 0, maxBounds[0][2] ?? 0, maxBounds[1][2] ?? 0);
385
+ if (x !== position[0] || y !== position[1] || z !== position[2]) {
386
+ props.position = [x, y, z];
387
+ }
388
+ }
379
389
 
380
390
  return props;
381
391
  }
@@ -9,10 +9,24 @@ import {MapState, MapStateProps} from './map-controller';
9
9
  import type {MapStateInternal} from './map-controller';
10
10
  import {mod} from '../utils/math-utils';
11
11
  import LinearInterpolator from '../transitions/linear-interpolator';
12
- import {zoomAdjust} from '../viewports/globe-viewport';
12
+ import {zoomAdjust, GLOBE_RADIUS} from '../viewports/globe-viewport';
13
13
 
14
14
  import {MAX_LATITUDE} from '@math.gl/web-mercator';
15
15
 
16
+ const DEGREES_TO_RADIANS = Math.PI / 180;
17
+ const RADIANS_TO_DEGREES = 180 / Math.PI;
18
+
19
+ function degreesToPixels(angle: number, zoom: number = 0): number {
20
+ const radians = Math.min(180, angle) * DEGREES_TO_RADIANS;
21
+ const size = GLOBE_RADIUS * 2 * Math.sin(radians / 2);
22
+ return size * Math.pow(2, zoom);
23
+ }
24
+ function pixelsToDegrees(pixels: number, zoom: number = 0): number {
25
+ const size = pixels / Math.pow(2, zoom);
26
+ const radians = Math.asin(Math.min(1, size / GLOBE_RADIUS / 2)) * 2;
27
+ return radians * RADIANS_TO_DEGREES;
28
+ }
29
+
16
30
  type GlobeStateInternal = MapStateInternal & {
17
31
  startPanPos?: [number, number];
18
32
  };
@@ -25,6 +39,7 @@ class GlobeState extends MapState {
25
39
  }
26
40
  ) {
27
41
  const {startPanPos, ...mapStateOptions} = options;
42
+ mapStateOptions.normalize = false; // disable MapState default normalization
28
43
  super(mapStateOptions);
29
44
 
30
45
  if (startPanPos !== undefined) {
@@ -71,19 +86,88 @@ class GlobeState extends MapState {
71
86
 
72
87
  applyConstraints(props: Required<MapStateProps>): Required<MapStateProps> {
73
88
  // Ensure zoom is within specified range
74
- const {longitude, latitude, maxZoom, minZoom, zoom} = props;
89
+ const {longitude, latitude, maxBounds} = props;
75
90
 
76
- const ZOOM0 = zoomAdjust(0);
77
- const zoomAdjustment = zoomAdjust(latitude) - ZOOM0;
78
- props.zoom = clamp(zoom, minZoom + zoomAdjustment, maxZoom + zoomAdjustment);
91
+ props.zoom = this._constrainZoom(props.zoom, props);
79
92
 
80
93
  if (longitude < -180 || longitude > 180) {
81
94
  props.longitude = mod(longitude + 180, 360) - 180;
82
95
  }
83
96
  props.latitude = clamp(latitude, -MAX_LATITUDE, MAX_LATITUDE);
97
+ if (maxBounds) {
98
+ props.longitude = clamp(props.longitude, maxBounds[0][0], maxBounds[1][0]);
99
+ props.latitude = clamp(props.latitude, maxBounds[0][1], maxBounds[1][1]);
100
+ }
101
+
102
+ if (maxBounds) {
103
+ // calculate center and zoom ranges at pitch=0 and bearing=0
104
+ // to maintain visual stability when rotating
105
+ const effectiveZoom = props.zoom - zoomAdjust(latitude);
106
+ const lngSpan = maxBounds[1][0] - maxBounds[0][0];
107
+ const latSpan = maxBounds[1][1] - maxBounds[0][1];
108
+ if (latSpan > 0 && latSpan < MAX_LATITUDE * 2) {
109
+ const halfHeightDegrees =
110
+ Math.min(pixelsToDegrees(props.height, effectiveZoom), latSpan) / 2;
111
+ props.latitude = clamp(
112
+ props.latitude,
113
+ maxBounds[0][1] + halfHeightDegrees,
114
+ maxBounds[1][1] - halfHeightDegrees
115
+ );
116
+ }
117
+ if (lngSpan > 0 && lngSpan < 360) {
118
+ const halfWidthDegrees =
119
+ Math.min(
120
+ pixelsToDegrees(
121
+ props.width / Math.cos(props.latitude * DEGREES_TO_RADIANS),
122
+ effectiveZoom
123
+ ),
124
+ lngSpan
125
+ ) / 2;
126
+ props.longitude = clamp(
127
+ props.longitude,
128
+ maxBounds[0][0] + halfWidthDegrees,
129
+ maxBounds[1][0] - halfWidthDegrees
130
+ );
131
+ }
132
+ }
133
+ if (props.latitude !== latitude) {
134
+ props.zoom += zoomAdjust(props.latitude) - zoomAdjust(latitude);
135
+ }
84
136
 
85
137
  return props;
86
138
  }
139
+
140
+ _constrainZoom(zoom: number, props?: Required<MapStateProps>): number {
141
+ props ||= this.getViewportProps();
142
+ const {latitude, maxZoom, maxBounds} = props;
143
+ let {minZoom} = props;
144
+ const ZOOM0 = zoomAdjust(0);
145
+ const zoomAdjustment = zoomAdjust(latitude) - ZOOM0;
146
+
147
+ const shouldApplyMaxBounds = maxBounds !== null && props.width > 0 && props.height > 0;
148
+ if (shouldApplyMaxBounds) {
149
+ const minLatitude = maxBounds[0][1];
150
+ const maxLatitude = maxBounds[1][1];
151
+ // latitude at which the bounding box is the widest
152
+ const fitLatitude =
153
+ Math.sign(minLatitude) === Math.sign(maxLatitude)
154
+ ? Math.min(Math.abs(minLatitude), Math.abs(maxLatitude))
155
+ : 0;
156
+ const w =
157
+ degreesToPixels(maxBounds[1][0] - maxBounds[0][0]) *
158
+ Math.cos(fitLatitude * DEGREES_TO_RADIANS);
159
+ const h = degreesToPixels(maxBounds[1][1] - maxBounds[0][1]);
160
+ if (w > 0) {
161
+ minZoom = Math.max(minZoom, Math.log2(props.width / w) + ZOOM0);
162
+ }
163
+ if (h > 0) {
164
+ minZoom = Math.max(minZoom, Math.log2(props.height / h) + ZOOM0);
165
+ }
166
+ if (minZoom > maxZoom) minZoom = maxZoom;
167
+ }
168
+
169
+ return clamp(zoom, minZoom + zoomAdjustment, maxZoom + zoomAdjustment);
170
+ }
87
171
  }
88
172
 
89
173
  export default class GlobeController extends Controller<MapState> {
@@ -5,14 +5,34 @@
5
5
  import {clamp} from '@math.gl/core';
6
6
  import Controller, {ControllerProps, InteractionState} from './controller';
7
7
  import ViewState from './view-state';
8
- import {normalizeViewportProps} from '@math.gl/web-mercator';
8
+ import {worldToLngLat, lngLatToWorld as _lngLatToWorld} from '@math.gl/web-mercator';
9
9
  import assert from '../utils/assert';
10
+ import {mod} from '../utils/math-utils';
10
11
 
11
12
  import LinearInterpolator from '../transitions/linear-interpolator';
12
13
  import type Viewport from '../viewports/viewport';
13
14
 
14
15
  const PITCH_MOUSE_THRESHOLD = 5;
15
16
  const PITCH_ACCEL = 1.2;
17
+ const WEB_MERCATOR_TILE_SIZE = 512;
18
+ const WEB_MERCATOR_MAX_BOUNDS = [
19
+ [-Infinity, -90],
20
+ [Infinity, 90]
21
+ ] satisfies ControllerProps['maxBounds'];
22
+
23
+ /** The web mercator utility `lngLatToWorld` throws if invalid coordinates are provided.
24
+ * This wrapper clamps user input to calculate common positions safely. */
25
+ function lngLatToWorld([lng, lat]: number[]): number[] {
26
+ if (Math.abs(lat) > 90) {
27
+ lat = Math.sign(lat) * 90;
28
+ }
29
+ if (Number.isFinite(lng)) {
30
+ const [x, y] = _lngLatToWorld([lng, lat]);
31
+ return [x, clamp(y, 0, WEB_MERCATOR_TILE_SIZE)];
32
+ }
33
+ const [, y] = _lngLatToWorld([0, lat]);
34
+ return [lng, clamp(y, 0, WEB_MERCATOR_TILE_SIZE)];
35
+ }
16
36
 
17
37
  export type MapStateProps = {
18
38
  /** Mapbox viewport properties */
@@ -47,6 +67,8 @@ export type MapStateProps = {
47
67
 
48
68
  /** Normalize viewport props to fit map height into viewport. Default `true` */
49
69
  normalize?: boolean;
70
+
71
+ maxBounds?: ControllerProps['maxBounds'];
50
72
  };
51
73
 
52
74
  export type MapStateInternal = {
@@ -70,12 +92,18 @@ export type MapStateInternal = {
70
92
  /* Utils */
71
93
 
72
94
  export class MapState extends ViewState<MapState, MapStateProps, MapStateInternal> {
73
- makeViewport: (props: Record<string, any>) => Viewport;
95
+ /* get optional altitude for rotation pivot
96
+ * - undefined: rotate around viewport center (no pivot point)
97
+ * - 0: rotate around pointer position at ground level
98
+ * - other value: rotate around pointer position at specified altitude
99
+ */
100
+ getAltitude?: (pos: [number, number]) => number | undefined;
74
101
 
75
102
  constructor(
76
103
  options: MapStateProps &
77
104
  MapStateInternal & {
78
105
  makeViewport: (props: Record<string, any>) => Viewport;
106
+ getAltitude?: (pos: [number, number]) => number | undefined;
79
107
  }
80
108
  ) {
81
109
  const {
@@ -133,6 +161,8 @@ export class MapState extends ViewState<MapState, MapStateProps, MapStateInterna
133
161
  assert(Number.isFinite(latitude)); // `latitude` must be supplied
134
162
  assert(Number.isFinite(zoom)); // `zoom` must be supplied
135
163
 
164
+ const maxBounds = options.maxBounds || (normalize ? WEB_MERCATOR_MAX_BOUNDS : null);
165
+
136
166
  super(
137
167
  {
138
168
  width,
@@ -148,7 +178,8 @@ export class MapState extends ViewState<MapState, MapStateProps, MapStateInterna
148
178
  maxPitch,
149
179
  minPitch,
150
180
  normalize,
151
- position
181
+ position,
182
+ maxBounds
152
183
  },
153
184
  {
154
185
  startPanLngLat,
@@ -158,10 +189,11 @@ export class MapState extends ViewState<MapState, MapStateProps, MapStateInterna
158
189
  startBearing,
159
190
  startPitch,
160
191
  startZoom
161
- }
192
+ },
193
+ options.makeViewport
162
194
  );
163
195
 
164
- this.makeViewport = options.makeViewport;
196
+ this.getAltitude = options.getAltitude;
165
197
  }
166
198
 
167
199
  /**
@@ -206,12 +238,10 @@ export class MapState extends ViewState<MapState, MapStateProps, MapStateInterna
206
238
  /**
207
239
  * Start rotating
208
240
  * @param {[Number, Number]} pos - position on screen where the center is
209
- * @param {Number} altitude - optional altitude for rotation pivot
210
- * - undefined: rotate around viewport center (no pivot point)
211
- * - 0: rotate around pointer position at ground level
212
- * - other value: rotate around pointer position at specified altitude
213
241
  */
214
- rotateStart({pos, altitude}: {pos: [number, number]; altitude?: number}): MapState {
242
+ rotateStart({pos}: {pos: [number, number]}): MapState {
243
+ const altitude = this.getAltitude?.(pos);
244
+
215
245
  return this._getUpdatedState({
216
246
  startRotatePos: pos,
217
247
  startRotateLngLat: altitude !== undefined ? this._unproject3D(pos, altitude) : undefined,
@@ -323,10 +353,7 @@ export class MapState extends ViewState<MapState, MapStateProps, MapStateInterna
323
353
  return this;
324
354
  }
325
355
 
326
- const {maxZoom, minZoom} = this.getViewportProps();
327
- let zoom = (startZoom as number) + Math.log2(scale);
328
- zoom = clamp(zoom, minZoom, maxZoom);
329
-
356
+ const zoom = this._constrainZoom((startZoom as number) + Math.log2(scale));
330
357
  const zoomedViewport = this.makeViewport({...this.getViewportProps(), zoom});
331
358
 
332
359
  return this._getUpdatedState({
@@ -411,18 +438,33 @@ export class MapState extends ViewState<MapState, MapStateProps, MapStateInterna
411
438
 
412
439
  // Apply any constraints (mathematical or defined by _viewportProps) to map state
413
440
  applyConstraints(props: Required<MapStateProps>): Required<MapStateProps> {
414
- // Ensure zoom is within specified range
415
- const {maxZoom, minZoom, zoom} = props;
416
- props.zoom = clamp(zoom, minZoom, maxZoom);
417
-
418
441
  // Ensure pitch is within specified range
419
- const {maxPitch, minPitch, pitch} = props;
420
- props.pitch = clamp(pitch, minPitch, maxPitch);
442
+ const {maxPitch, minPitch, pitch, longitude, bearing, normalize, maxBounds} = props;
421
443
 
422
- // Normalize viewport props to fit map height into viewport
423
- const {normalize = true} = props;
424
444
  if (normalize) {
425
- Object.assign(props, normalizeViewportProps(props));
445
+ if (longitude < -180 || longitude > 180) {
446
+ props.longitude = mod(longitude + 180, 360) - 180;
447
+ }
448
+ if (bearing < -180 || bearing > 180) {
449
+ props.bearing = mod(bearing + 180, 360) - 180;
450
+ }
451
+ }
452
+ props.pitch = clamp(pitch, minPitch, maxPitch);
453
+
454
+ props.zoom = this._constrainZoom(props.zoom, props);
455
+
456
+ if (maxBounds) {
457
+ const bl = lngLatToWorld(maxBounds[0]);
458
+ const tr = lngLatToWorld(maxBounds[1]);
459
+ // calculate center and zoom ranges at pitch=0 and bearing=0
460
+ // to maintain visual stability when rotating
461
+ const scale = 2 ** props.zoom;
462
+ const halfWidth = props.width / 2 / scale;
463
+ const halfHeight = props.height / 2 / scale;
464
+ const [minLng, minLat] = worldToLngLat([bl[0] + halfWidth, bl[1] + halfHeight]);
465
+ const [maxLng, maxLat] = worldToLngLat([tr[0] - halfWidth, tr[1] - halfHeight]);
466
+ props.longitude = clamp(props.longitude, minLng, maxLng);
467
+ props.latitude = clamp(props.latitude, minLat, maxLat);
426
468
  }
427
469
 
428
470
  return props;
@@ -430,6 +472,30 @@ export class MapState extends ViewState<MapState, MapStateProps, MapStateInterna
430
472
 
431
473
  /* Private methods */
432
474
 
475
+ _constrainZoom(zoom: number, props?: Required<MapStateProps>): number {
476
+ props ||= this.getViewportProps();
477
+ const {maxZoom, maxBounds} = props;
478
+
479
+ const shouldApplyMaxBounds = maxBounds !== null && props.width > 0 && props.height > 0;
480
+ let {minZoom} = props;
481
+
482
+ if (shouldApplyMaxBounds) {
483
+ const bl = lngLatToWorld(maxBounds[0]);
484
+ const tr = lngLatToWorld(maxBounds[1]);
485
+ const w = tr[0] - bl[0];
486
+ const h = tr[1] - bl[1];
487
+ // ignore bound size of 0 or Infinity
488
+ if (Number.isFinite(w) && w > 0) {
489
+ minZoom = Math.max(minZoom, Math.log2(props.width / w));
490
+ }
491
+ if (Number.isFinite(h) && h > 0) {
492
+ minZoom = Math.max(minZoom, Math.log2(props.height / h));
493
+ }
494
+ if (minZoom > maxZoom) minZoom = maxZoom;
495
+ }
496
+ return clamp(zoom, minZoom, maxZoom);
497
+ }
498
+
433
499
  _zoomFromCenter(scale) {
434
500
  const {width, height} = this.getViewportProps();
435
501
  return this.zoom({
@@ -542,36 +608,23 @@ export default class MapController extends Controller<MapState> {
542
608
  */
543
609
  protected rotationPivot: 'center' | '2d' | '3d' = 'center';
544
610
 
545
- /**
546
- * Internal callback to access deck picking engine. Populated by ViewManager
547
- */
548
- protected pickPosition?: (x: number, y: number) => {coordinate?: number[]} | null;
549
-
550
- constructor(opts: ConstructorParameters<typeof Controller>[0]) {
551
- super(opts);
552
- this.pickPosition = opts.pickPosition;
553
- }
554
-
555
- setProps(props: ControllerProps & MapStateProps & {rotationPivot?: 'center' | '2d' | '3d'}) {
611
+ setProps(
612
+ props: ControllerProps &
613
+ MapStateProps & {
614
+ rotationPivot?: 'center' | '2d' | '3d';
615
+ getAltitude?: (pos: [number, number]) => number | undefined;
616
+ }
617
+ ) {
556
618
  if ('rotationPivot' in props) {
557
619
  this.rotationPivot = props.rotationPivot || 'center';
558
620
  }
621
+ // this will be passed to MapState constructor
622
+ props.getAltitude = this._getAltitude;
559
623
  props.position = props.position || [0, 0, 0];
560
- const oldProps = this.props;
624
+ props.maxBounds =
625
+ props.maxBounds || (props.normalize === false ? null : WEB_MERCATOR_MAX_BOUNDS);
561
626
 
562
627
  super.setProps(props);
563
-
564
- const dimensionChanged = !oldProps || oldProps.height !== props.height;
565
- if (dimensionChanged) {
566
- // Dimensions changed, normalize the props
567
- this.updateViewport(
568
- new this.ControllerState({
569
- makeViewport: this.makeViewport,
570
- ...props,
571
- ...this.state
572
- })
573
- );
574
- }
575
628
  }
576
629
 
577
630
  protected updateViewport(
@@ -595,22 +648,18 @@ export default class MapController extends Controller<MapState> {
595
648
  }
596
649
 
597
650
  /** Add altitude to rotateStart params based on rotationPivot mode */
598
- protected _getRotateStartParams(pos: [number, number]): {
599
- pos: [number, number];
600
- altitude?: number;
601
- } {
602
- let altitude: number | undefined;
651
+ protected _getAltitude = (pos: [number, number]): number | undefined => {
603
652
  if (this.rotationPivot === '2d') {
604
- altitude = 0;
653
+ return 0;
605
654
  } else if (this.rotationPivot === '3d') {
606
655
  if (this.pickPosition) {
607
656
  const {x, y} = this.props;
608
657
  const pickResult = this.pickPosition(x + pos[0], y + pos[1]);
609
658
  if (pickResult && pickResult.coordinate && pickResult.coordinate.length >= 3) {
610
- altitude = pickResult.coordinate[2];
659
+ return pickResult.coordinate[2];
611
660
  }
612
661
  }
613
662
  }
614
- return {pos, altitude};
615
- }
663
+ return undefined;
664
+ };
616
665
  }