maplibre-gl 2.2.0-pre.3 → 2.2.1

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 (121) hide show
  1. package/README.md +73 -14
  2. package/build/generate-debug-index-file.ts +19 -0
  3. package/build/generate-style-code.ts +6 -1
  4. package/build/generate-style-spec.ts +151 -35
  5. package/build/generate-typings.ts +1 -1
  6. package/build/rollup_plugins.ts +4 -1
  7. package/dist/maplibre-gl-csp-worker.js +1 -1
  8. package/dist/maplibre-gl-csp-worker.js.map +1 -1
  9. package/dist/maplibre-gl-csp.js +1 -1
  10. package/dist/maplibre-gl-csp.js.map +1 -1
  11. package/dist/maplibre-gl-dev.js +364 -3124
  12. package/dist/maplibre-gl.css +1 -1
  13. package/dist/maplibre-gl.d.ts +439 -151
  14. package/dist/maplibre-gl.js +4 -4
  15. package/dist/maplibre-gl.js.map +1 -1
  16. package/package.json +68 -65
  17. package/src/css/maplibre-gl.css +39 -32
  18. package/src/data/bucket/fill_bucket.test.ts +1 -0
  19. package/src/data/bucket/symbol_bucket.test.ts +2 -0
  20. package/src/data/evaluation_feature.ts +1 -1
  21. package/src/data/program_configuration.ts +2 -2
  22. package/src/geo/transform.ts +4 -4
  23. package/src/gl/vertex_buffer.ts +4 -4
  24. package/src/index.ts +1 -1
  25. package/src/render/image_atlas.ts +1 -0
  26. package/src/render/image_manager.ts +1 -0
  27. package/src/render/program/debug_program.ts +1 -1
  28. package/src/render/render_to_texture.ts +3 -0
  29. package/src/render/terrain.ts +21 -21
  30. package/src/shaders/README.md +2 -2
  31. package/src/source/geojson_worker_source.test.ts +2 -2
  32. package/src/source/geojson_wrapper.test.ts +1 -1
  33. package/src/source/image_source.test.ts +8 -8
  34. package/src/source/image_source.ts +1 -1
  35. package/src/source/raster_tile_source.test.ts +1 -1
  36. package/src/source/source_cache.test.ts +12 -12
  37. package/src/source/source_cache.ts +1 -1
  38. package/src/source/terrain_source_cache.ts +3 -3
  39. package/src/source/vector_tile_source.test.ts +1 -1
  40. package/src/source/vector_tile_worker_source.test.ts +1 -1
  41. package/src/source/video_source.test.ts +2 -2
  42. package/src/source/worker.ts +2 -4
  43. package/src/style/light.test.ts +1 -1
  44. package/src/style/style.test.ts +3 -3
  45. package/src/style/style.ts +2 -2
  46. package/src/style/style_layer/background_style_layer_properties.g.ts +1 -0
  47. package/src/style/style_layer/circle_style_layer_properties.g.ts +1 -0
  48. package/src/style/style_layer/fill_extrusion_style_layer_properties.g.ts +1 -0
  49. package/src/style/style_layer/fill_style_layer_properties.g.ts +1 -0
  50. package/src/style/style_layer/heatmap_style_layer_properties.g.ts +1 -0
  51. package/src/style/style_layer/hillshade_style_layer_properties.g.ts +1 -0
  52. package/src/style/style_layer/line_style_layer_properties.g.ts +1 -0
  53. package/src/style/style_layer/raster_style_layer_properties.g.ts +1 -0
  54. package/src/style/style_layer/symbol_style_layer.ts +16 -1
  55. package/src/style/style_layer/symbol_style_layer_properties.g.ts +4 -3
  56. package/src/style-spec/CHANGELOG.md +5 -0
  57. package/src/style-spec/composite.test.ts +2 -0
  58. package/src/style-spec/diff.test.ts +3 -3
  59. package/src/style-spec/empty.ts +3 -2
  60. package/src/style-spec/expression/definitions/coercion.ts +13 -2
  61. package/src/style-spec/expression/definitions/interpolate.ts +1 -0
  62. package/src/style-spec/expression/expression.test.ts +1 -1
  63. package/src/style-spec/expression/expression.ts +3 -0
  64. package/src/style-spec/expression/index.ts +8 -2
  65. package/src/style-spec/expression/parsing_context.ts +2 -0
  66. package/src/style-spec/expression/types.ts +6 -1
  67. package/src/style-spec/expression/values.ts +9 -4
  68. package/src/style-spec/feature_filter/convert.ts +65 -65
  69. package/src/style-spec/feature_filter/feature_filter.test.ts +45 -4
  70. package/src/style-spec/feature_filter/index.ts +2 -1
  71. package/src/style-spec/function/index.test.ts +117 -1
  72. package/src/style-spec/function/index.ts +24 -12
  73. package/src/style-spec/migrate/expressions.ts +1 -1
  74. package/src/style-spec/migrate/v8.test.ts +2 -0
  75. package/src/style-spec/migrate/v9.test.ts +6 -4
  76. package/src/style-spec/migrate.test.ts +3 -1
  77. package/src/style-spec/package.json +1 -1
  78. package/src/style-spec/reference/latest.ts +2 -2
  79. package/src/style-spec/reference/v8.json +9 -6
  80. package/src/style-spec/style-spec.test.ts +2 -1
  81. package/src/style-spec/style-spec.ts +18 -9
  82. package/src/style-spec/types.g.ts +152 -36
  83. package/src/style-spec/util/interpolate.test.ts +5 -0
  84. package/src/style-spec/util/interpolate.ts +12 -0
  85. package/src/style-spec/util/padding.test.ts +27 -0
  86. package/src/style-spec/util/padding.ts +64 -0
  87. package/src/style-spec/validate/validate.ts +3 -1
  88. package/src/style-spec/validate/validate_padding.test.ts +82 -0
  89. package/src/style-spec/validate/validate_padding.ts +36 -0
  90. package/src/style-spec/validate_style.ts +1 -1
  91. package/src/symbol/check_max_angle.test.ts +5 -5
  92. package/src/symbol/collision_feature.test.ts +22 -5
  93. package/src/symbol/collision_feature.ts +7 -5
  94. package/src/symbol/collision_index.ts +1 -1
  95. package/src/symbol/get_anchors.test.ts +4 -4
  96. package/src/symbol/projection.ts +1 -1
  97. package/src/symbol/quads.test.ts +1 -1
  98. package/src/symbol/shaping.ts +10 -10
  99. package/src/symbol/symbol_layout.ts +5 -4
  100. package/src/symbol/symbol_style_layer.test.ts +1 -1
  101. package/src/ui/camera.test.ts +11 -11
  102. package/src/ui/control/geolocate_control.ts +1 -1
  103. package/src/ui/control/terrain_control.ts +4 -4
  104. package/src/ui/handler/cooperative_gestures.test.ts +167 -0
  105. package/src/ui/handler/drag_pan.test.ts +2 -1
  106. package/src/ui/handler/scroll_zoom.ts +7 -0
  107. package/src/ui/handler/touch_pan.ts +22 -2
  108. package/src/ui/handler/touch_zoom_rotate.ts +18 -1
  109. package/src/ui/handler_manager.ts +2 -2
  110. package/src/ui/map.test.ts +16 -16
  111. package/src/ui/map.ts +76 -8
  112. package/src/ui/map_events.test.ts +33 -32
  113. package/src/ui/popup.test.ts +2 -2
  114. package/src/util/ajax.test.ts +5 -5
  115. package/src/util/ajax.ts +1 -1
  116. package/src/util/classify_rings.test.ts +27 -27
  117. package/src/util/primitives.ts +4 -4
  118. package/src/util/resolve_tokens.test.ts +1 -1
  119. package/src/util/tile_request_cache.test.ts +5 -5
  120. package/src/util/util.test.ts +5 -5
  121. package/src/util/util.ts +2 -3
@@ -1,5 +1,6 @@
1
1
  import Point from '@mapbox/point-geometry';
2
2
  import DOM from '../../util/dom';
3
+ import type Map from '../map';
3
4
 
4
5
  class TwoTouchHandler {
5
6
 
@@ -23,7 +24,6 @@ class TwoTouchHandler {
23
24
  _move(points: [Point, Point], pinchAround: Point, e: TouchEvent) { return {}; } //eslint-disable-line
24
25
 
25
26
  touchstart(e: TouchEvent, points: Array<Point>, mapTouches: Array<Touch>) {
26
- //console.log(e.target, e.targetTouches.length ? e.targetTouches[0].target : null);
27
27
  //log('touchstart', points, e.target.innerHTML, e.targetTouches.length ? e.targetTouches[0].target.innerHTML: undefined);
28
28
  if (this._firstTwoTouches || mapTouches.length < 2) return;
29
29
 
@@ -203,6 +203,13 @@ export class TouchPitchHandler extends TwoTouchHandler {
203
203
  _valid: boolean | void;
204
204
  _firstMove: number;
205
205
  _lastPoints: [Point, Point];
206
+ _map: Map;
207
+ _currentTouchCount: number;
208
+
209
+ constructor(map: Map) {
210
+ super();
211
+ this._map = map;
212
+ }
206
213
 
207
214
  reset() {
208
215
  super.reset();
@@ -211,6 +218,11 @@ export class TouchPitchHandler extends TwoTouchHandler {
211
218
  delete this._lastPoints;
212
219
  }
213
220
 
221
+ touchstart(e: TouchEvent, points: Array<Point>, mapTouches: Array<Touch>) {
222
+ super.touchstart(e, points, mapTouches);
223
+ this._currentTouchCount = mapTouches.length;
224
+ }
225
+
214
226
  _start(points: [Point, Point]) {
215
227
  this._lastPoints = points;
216
228
  if (isVertical(points[0].sub(points[1]))) {
@@ -221,6 +233,11 @@ export class TouchPitchHandler extends TwoTouchHandler {
221
233
  }
222
234
 
223
235
  _move(points: [Point, Point], center: Point, e: TouchEvent) {
236
+ // If cooperative gestures is enabled, we need a 3-finger minimum for this gesture to register
237
+ if (this._map._cooperativeGestures && this._currentTouchCount < 3) {
238
+ return;
239
+ }
240
+
224
241
  const vectorA = points[0].sub(this._lastPoints[0]);
225
242
  const vectorB = points[1].sub(this._lastPoints[1]);
226
243
 
@@ -195,7 +195,7 @@ class HandlerManager {
195
195
  const tapDragZoom = new TapDragZoomHandler();
196
196
  this._add('tapDragZoom', tapDragZoom);
197
197
 
198
- const touchPitch = map.touchPitch = new TouchPitchHandler();
198
+ const touchPitch = map.touchPitch = new TouchPitchHandler(map);
199
199
  this._add('touchPitch', touchPitch);
200
200
 
201
201
  const mouseRotate = new MouseRotateHandler(options);
@@ -205,7 +205,7 @@ class HandlerManager {
205
205
  this._add('mousePitch', mousePitch, ['mouseRotate']);
206
206
 
207
207
  const mousePan = new MousePanHandler(options);
208
- const touchPan = new TouchPanHandler(options);
208
+ const touchPan = new TouchPanHandler(options, map);
209
209
  map.dragPan = new DragPanHandler(el, mousePan, touchPan);
210
210
  this._add('mousePan', mousePan);
211
211
  this._add('touchPan', touchPan, ['touchZoom', 'touchRotate']);
@@ -598,8 +598,8 @@ describe('Map', () => {
598
598
  expect(parseFloat(map.getBounds().getCenter().lat.toFixed(10))).toBe(0);
599
599
 
600
600
  expect(toFixed(map.getBounds().toArray())).toEqual(toFixed([
601
- [ -70.31249999999976, -57.326521225216965 ],
602
- [ 70.31249999999977, 57.32652122521695 ] ]));
601
+ [-70.31249999999976, -57.326521225216965],
602
+ [70.31249999999977, 57.32652122521695]]));
603
603
  });
604
604
 
605
605
  test('rotated bounds', () => {
@@ -631,7 +631,7 @@ describe('Map', () => {
631
631
 
632
632
  describe('#setMaxBounds', () => {
633
633
  test('constrains map bounds', () => {
634
- const map = createMap({zoom:0});
634
+ const map = createMap({zoom: 0});
635
635
  map.setMaxBounds([[-130.4297, 50.0642], [-61.52344, 24.20688]]);
636
636
  expect(
637
637
  toFixed([[-130.4297000000, 7.0136641176], [-61.5234400000, 60.2398142283]])
@@ -639,7 +639,7 @@ describe('Map', () => {
639
639
  });
640
640
 
641
641
  test('when no argument is passed, map bounds constraints are removed', () => {
642
- const map = createMap({zoom:0});
642
+ const map = createMap({zoom: 0});
643
643
  map.setMaxBounds([[-130.4297, 50.0642], [-61.52344, 24.20688]]);
644
644
  expect(
645
645
  toFixed([[-166.28906999999964, -27.6835270554], [-25.664070000000066, 73.8248206697]])
@@ -664,12 +664,12 @@ describe('Map', () => {
664
664
 
665
665
  describe('#getMaxBounds', () => {
666
666
  test('returns null when no bounds set', () => {
667
- const map = createMap({zoom:0});
667
+ const map = createMap({zoom: 0});
668
668
  expect(map.getMaxBounds()).toBeNull();
669
669
  });
670
670
 
671
671
  test('returns bounds', () => {
672
- const map = createMap({zoom:0});
672
+ const map = createMap({zoom: 0});
673
673
  const bounds = [[-130.4297, 50.0642], [-61.52344, 24.20688]] as LngLatBoundsLike;
674
674
  map.setMaxBounds(bounds);
675
675
  expect(map.getMaxBounds().toArray()).toEqual(bounds);
@@ -718,14 +718,14 @@ describe('Map', () => {
718
718
  });
719
719
 
720
720
  test('#setMinZoom', () => {
721
- const map = createMap({zoom:5});
721
+ const map = createMap({zoom: 5});
722
722
  map.setMinZoom(3.5);
723
723
  map.setZoom(1);
724
724
  expect(map.getZoom()).toBe(3.5);
725
725
  });
726
726
 
727
727
  test('unset minZoom', () => {
728
- const map = createMap({minZoom:5});
728
+ const map = createMap({minZoom: 5});
729
729
  map.setMinZoom(null);
730
730
  map.setZoom(1);
731
731
  expect(map.getZoom()).toBe(1);
@@ -739,7 +739,7 @@ describe('Map', () => {
739
739
  });
740
740
 
741
741
  test('ignore minZooms over maxZoom', () => {
742
- const map = createMap({zoom:2, maxZoom:5});
742
+ const map = createMap({zoom: 2, maxZoom: 5});
743
743
  expect(() => {
744
744
  map.setMinZoom(6);
745
745
  }).toThrow();
@@ -748,14 +748,14 @@ describe('Map', () => {
748
748
  });
749
749
 
750
750
  test('#setMaxZoom', () => {
751
- const map = createMap({zoom:0});
751
+ const map = createMap({zoom: 0});
752
752
  map.setMaxZoom(3.5);
753
753
  map.setZoom(4);
754
754
  expect(map.getZoom()).toBe(3.5);
755
755
  });
756
756
 
757
757
  test('unset maxZoom', () => {
758
- const map = createMap({maxZoom:5});
758
+ const map = createMap({maxZoom: 5});
759
759
  map.setMaxZoom(null);
760
760
  map.setZoom(6);
761
761
  expect(map.getZoom()).toBe(6);
@@ -769,7 +769,7 @@ describe('Map', () => {
769
769
  });
770
770
 
771
771
  test('ignore maxZooms over minZoom', () => {
772
- const map = createMap({minZoom:5});
772
+ const map = createMap({minZoom: 5});
773
773
  expect(() => {
774
774
  map.setMaxZoom(4);
775
775
  }).toThrow();
@@ -779,13 +779,13 @@ describe('Map', () => {
779
779
 
780
780
  test('throw on maxZoom smaller than minZoom at init', () => {
781
781
  expect(() => {
782
- createMap({minZoom:10, maxZoom:5});
782
+ createMap({minZoom: 10, maxZoom: 5});
783
783
  }).toThrow(new Error('maxZoom must be greater than or equal to minZoom'));
784
784
  });
785
785
 
786
786
  test('throw on maxZoom smaller than minZoom at init with falsey maxZoom', () => {
787
787
  expect(() => {
788
- createMap({minZoom:1, maxZoom:0});
788
+ createMap({minZoom: 1, maxZoom: 0});
789
789
  }).toThrow(new Error('maxZoom must be greater than or equal to minZoom'));
790
790
  });
791
791
 
@@ -827,7 +827,7 @@ describe('Map', () => {
827
827
  });
828
828
 
829
829
  test('unset maxPitch', () => {
830
- const map = createMap({maxPitch:10});
830
+ const map = createMap({maxPitch: 10});
831
831
  map.setMaxPitch(null);
832
832
  map.setPitch(20);
833
833
  expect(map.getPitch()).toBe(20);
@@ -841,7 +841,7 @@ describe('Map', () => {
841
841
  });
842
842
 
843
843
  test('ignore maxPitchs over minPitch', () => {
844
- const map = createMap({minPitch:10});
844
+ const map = createMap({minPitch: 10});
845
845
  expect(() => {
846
846
  map.setMaxPitch(0);
847
847
  }).toThrow();
package/src/ui/map.ts CHANGED
@@ -59,7 +59,6 @@ import type {ControlPosition, IControl} from './control/control';
59
59
  import type {MapGeoJSONFeature} from '../util/vectortile_to_geojson';
60
60
 
61
61
  /* eslint-enable no-use-before-define */
62
-
63
62
  export type MapOptions = {
64
63
  hash?: boolean | string;
65
64
  interactive?: boolean;
@@ -86,6 +85,7 @@ export type MapOptions = {
86
85
  doubleClickZoom?: boolean;
87
86
  touchZoomRotate?: boolean;
88
87
  touchPitch?: boolean;
88
+ cooperativeGestures?: boolean | GestureOptions;
89
89
  trackResize?: boolean;
90
90
  center?: LngLatLike;
91
91
  zoom?: number;
@@ -107,6 +107,12 @@ export type MapOptions = {
107
107
  pixelRatio?: number;
108
108
  };
109
109
 
110
+ export type GestureOptions = {
111
+ windowsHelpText?: string;
112
+ macHelpText?: string;
113
+ mobileHelpText?: string;
114
+ };
115
+
110
116
  // See article here: https://medium.com/terria/typescript-transforming-optional-properties-to-required-properties-that-may-be-undefined-7482cb4e1585
111
117
  type Complete<T> = {
112
118
  [P in keyof Required<T>]: Pick<T, P> extends Required<Pick<T, P>> ? T[P] : (T[P] | undefined);
@@ -146,6 +152,7 @@ const defaultOptions = {
146
152
  doubleClickZoom: true,
147
153
  touchZoomRotate: true,
148
154
  touchPitch: true,
155
+ cooperativeGestures: undefined,
149
156
 
150
157
  bearingSnap: 7,
151
158
  clickTolerance: 3,
@@ -217,6 +224,13 @@ const defaultOptions = {
217
224
  * @param {boolean} [options.doubleClickZoom=true] If `true`, the "double click to zoom" interaction is enabled (see {@link DoubleClickZoomHandler}).
218
225
  * @param {boolean|Object} [options.touchZoomRotate=true] If `true`, the "pinch to rotate and zoom" interaction is enabled. An `Object` value is passed as options to {@link TouchZoomRotateHandler#enable}.
219
226
  * @param {boolean|Object} [options.touchPitch=true] If `true`, the "drag to pitch" interaction is enabled. An `Object` value is passed as options to {@link TouchPitchHandler#enable}.
227
+ * @param {boolean|GestureOptions} [options.cooperativeGestures=undefined] If `true` or set to an options object, map is only accessible on desktop while holding Command/Ctrl and only accessible on mobile with two fingers. Interacting with the map using normal gestures will trigger an informational screen. With this option enabled, "drag to pitch" requires a three-finger gesture.
228
+ * A valid options object includes the following properties to customize the text on the informational screen. The values below are the defaults.
229
+ * {
230
+ * windowsHelpText: "Use Ctrl + scroll to zoom the map",
231
+ * macHelpText: "Use ⌘ + scroll to zoom the map",
232
+ * mobileHelpText: "Use two fingers to move the map",
233
+ * }
220
234
  * @param {boolean} [options.trackResize=true] If `true`, the map will automatically resize when the browser window resizes.
221
235
  * @param {LngLatLike} [options.center=[0, 0]] The initial geographical centerpoint of the map. If `center` is not specified in the constructor options, MapLibre GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `[0, 0]` Note: MapLibre GL uses longitude, latitude coordinate order (as opposed to latitude, longitude) to match GeoJSON.
222
236
  * @param {number} [options.zoom=0] The initial zoom level of the map. If `zoom` is not specified in the constructor options, MapLibre GL JS will look for it in the map's style object. If it is not specified in the style, either, it will default to `0`.
@@ -271,6 +285,9 @@ class Map extends Camera {
271
285
  _controlContainer: HTMLElement;
272
286
  _controlPositions: {[_: string]: HTMLElement};
273
287
  _interactive: boolean;
288
+ _cooperativeGestures: boolean | GestureOptions;
289
+ _cooperativeGesturesScreen: HTMLElement;
290
+ _metaPress: boolean;
274
291
  _showTileBoundaries: boolean;
275
292
  _showCollisionBoxes: boolean;
276
293
  _showPadding: boolean;
@@ -381,6 +398,7 @@ class Map extends Camera {
381
398
  super(transform, {bearingSnap: options.bearingSnap});
382
399
 
383
400
  this._interactive = options.interactive;
401
+ this._cooperativeGestures = options.cooperativeGestures;
384
402
  this._maxTileCacheSize = options.maxTileCacheSize;
385
403
  this._failIfMajorPerformanceCaveat = options.failIfMajorPerformanceCaveat;
386
404
  this._preserveDrawingBuffer = options.preserveDrawingBuffer;
@@ -446,6 +464,10 @@ class Map extends Camera {
446
464
 
447
465
  this.handlers = new HandlerManager(this, options as CompleteMapOptions);
448
466
 
467
+ if (this._cooperativeGestures) {
468
+ this._setupCooperativeGestures();
469
+ }
470
+
449
471
  const hashName = (typeof options.hash === 'string' && options.hash) || undefined;
450
472
  this._hash = options.hash && (new Hash(hashName)).addTo(this);
451
473
  // don't set position from options if set through hash
@@ -1039,7 +1061,7 @@ class Map extends Camera {
1039
1061
  * | [`dataabort`](#map.event:dataabort) | |
1040
1062
  * | [`sourcedataabort`](#map.event:sourcedataabort) | |
1041
1063
  *
1042
- * @param {string | Listener} layerIdOrListener The ID of a style layer or a listener if no ID is provided. Event will only be triggered if its location
1064
+ * @param {string | Listener} layer The ID of a style layer or a listener if no ID is provided. Event will only be triggered if its location
1043
1065
  * is within a visible feature in this layer. The event will have a `features` property containing
1044
1066
  * an array of the matching features. If `layerIdOrListener` is not supplied, the event will not have a `features` property.
1045
1067
  * Please note that many event types are not compatible with the optional `layerIdOrListener` parameter.
@@ -1129,7 +1151,7 @@ class Map extends Camera {
1129
1151
  * a visible portion of the specified layer from outside that layer or outside the map canvas. `mouseleave`
1130
1152
  * and `mouseout` events are triggered when the cursor leaves a visible portion of the specified layer, or leaves
1131
1153
  * the map canvas.
1132
- * @param {string} layerIdOrListener The ID of a style layer or a listener if no ID is provided. Only events whose location is within a visible
1154
+ * @param {string} layer The ID of a style layer or a listener if no ID is provided. Only events whose location is within a visible
1133
1155
  * feature in this layer will trigger the listener. The event will have a `features` property containing
1134
1156
  * an array of the matching features.
1135
1157
  * @param {Function} listener The function to be called when the event is fired.
@@ -1173,7 +1195,7 @@ class Map extends Camera {
1173
1195
  * Removes an event listener for layer-specific events previously added with `Map#on`.
1174
1196
  *
1175
1197
  * @param {string} type The event type previously used to install the listener.
1176
- * @param {string} layerIdOrListener The layer ID or listener previously used to install the listener.
1198
+ * @param {string} layer The layer ID or listener previously used to install the listener.
1177
1199
  * @param {Function} listener The function previously installed as a listener.
1178
1200
  * @returns {Map} `this`
1179
1201
  */
@@ -1892,7 +1914,10 @@ class Map extends Camera {
1892
1914
  * A layer defines how data from a specified source will be styled. Read more about layer types
1893
1915
  * and available paint and layout properties in the [MapLibre Style Specification](https://maplibre.org/maplibre-gl-js-docs/style-spec/#layers).
1894
1916
  *
1895
- * @param {Object | CustomLayerInterface} layer The layer to add, conforming to either the MapLibre Style Specification's [layer definition](https://maplibre.org/maplibre-gl-js-docs/style-spec/#layers) or, less commonly, the {@link CustomLayerInterface} specification.
1917
+ * TODO: JSDoc can't pass @param {(LayerSpecification & {source?: string | SourceSpecification}) | CustomLayerInterface} layer The layer to add,
1918
+ * @param {Object} layer
1919
+ * conforming to either the MapLibre Style Specification's [layer definition](https://maplibre.org/maplibre-gl-js-docs/style-spec/#layers) or,
1920
+ * less commonly, the {@link CustomLayerInterface} specification.
1896
1921
  * The MapLibre Style Specification's layer definition is appropriate for most layers.
1897
1922
  *
1898
1923
  * @param {string} layer.id A unique identifer that you define.
@@ -1900,10 +1925,10 @@ class Map extends Camera {
1900
1925
  * A list of layer types is available in the [MapLibre Style Specification](https://maplibre.org/maplibre-gl-js-docs/style-spec/layers/#type).
1901
1926
  *
1902
1927
  * (This can also be `custom`. For more information, see {@link CustomLayerInterface}.)
1903
- * @param {string | Object} [layer.source] The data source for the layer.
1928
+ * @param {string | SourceSpecification} [layer.source] The data source for the layer.
1904
1929
  * Reference a source that has _already been defined_ using the source's unique id.
1905
1930
  * Reference a _new source_ using a source object (as defined in the [MapLibre Style Specification](https://maplibre.org/maplibre-gl-js-docs/style-spec/sources/)) directly.
1906
- * This is **required** for all `layer.type` options _except_ for `custom`.
1931
+ * This is **required** for all `layer.type` options _except_ for `custom` and `background`.
1907
1932
  * @param {string} [layer.sourceLayer] (optional) The name of the source layer within the specified `layer.source` to use for this style layer.
1908
1933
  * This is only applicable for vector tile sources and is **required** when `layer.source` is of the type `vector`.
1909
1934
  * @param {array} [layer.filter] (optional) An expression specifying conditions on source features.
@@ -1994,7 +2019,7 @@ class Map extends Camera {
1994
2019
  * @see [Add a vector tile source](https://maplibre.org/maplibre-gl-js-docs/example/vector-source/)
1995
2020
  * @see [Add a WMS source](https://maplibre.org/maplibre-gl-js-docs/example/wms/)
1996
2021
  */
1997
- addLayer(layer: LayerSpecification | CustomLayerInterface, beforeId?: string) {
2022
+ addLayer(layer: (LayerSpecification & {source?: string | SourceSpecification}) | CustomLayerInterface, beforeId?: string) {
1998
2023
  this._lazyInitEmptyStyle();
1999
2024
  this.style.addLayer(layer, beforeId);
2000
2025
  return this._update(true);
@@ -2410,6 +2435,35 @@ class Map extends Camera {
2410
2435
  this._container.addEventListener('scroll', this._onMapScroll, false);
2411
2436
  }
2412
2437
 
2438
+ _setupCooperativeGestures() {
2439
+ const container = this._container;
2440
+ this._metaPress = false;
2441
+ this._cooperativeGesturesScreen = DOM.create('div', 'maplibregl-cooperative-gesture-screen', container);
2442
+ let modifierKeyName = 'Control';
2443
+ let desktopMessage = typeof this._cooperativeGestures !== 'boolean' && this._cooperativeGestures.windowsHelpText ? this._cooperativeGestures.windowsHelpText : 'Use Ctrl + scroll to zoom the map';
2444
+ if (navigator.platform.indexOf('Mac') === 0) {
2445
+ desktopMessage = typeof this._cooperativeGestures !== 'boolean' && this._cooperativeGestures.macHelpText ? this._cooperativeGestures.macHelpText : 'Use ⌘ + scroll to zoom the map';
2446
+ modifierKeyName = 'Meta';
2447
+ }
2448
+ const mobileMessage = typeof this._cooperativeGestures !== 'boolean' && this._cooperativeGestures.mobileHelpText ? this._cooperativeGestures.mobileHelpText : 'Use two fingers to move the map';
2449
+ this._cooperativeGesturesScreen.innerHTML = `
2450
+ <div class="maplibregl-desktop-message">${desktopMessage}</div>
2451
+ <div class="maplibregl-mobile-message">${mobileMessage}</div>
2452
+ `;
2453
+ document.addEventListener('keydown', (event) => {
2454
+ if (event.key === modifierKeyName) this._metaPress = true;
2455
+ });
2456
+ document.addEventListener('keyup', (event) => {
2457
+ if (event.key === modifierKeyName) this._metaPress = false;
2458
+ });
2459
+ // Add event to canvas container since gesture container is pointer-events: none
2460
+ this._canvasContainer.addEventListener('wheel', (e) => {
2461
+ this._onCooperativeGesture(e, this._metaPress, 1);
2462
+ }, false);
2463
+ // Remove the traditional pan classes
2464
+ this._canvasContainer.classList.remove('mapboxgl-touch-drag-pan', 'maplibregl-touch-drag-pan');
2465
+ }
2466
+
2413
2467
  _resizeCanvas(width: number, height: number, pixelRatio: number) {
2414
2468
  // Request the required canvas size taking the pixelratio into account.
2415
2469
  this._canvas.width = pixelRatio * width;
@@ -2465,6 +2519,17 @@ class Map extends Camera {
2465
2519
  return false;
2466
2520
  }
2467
2521
 
2522
+ _onCooperativeGesture(event: any, metaPress, touches) {
2523
+ if (!metaPress && touches < 2) {
2524
+ // Alert user how to scroll/pan
2525
+ this._cooperativeGesturesScreen.classList.add('maplibregl-show');
2526
+ setTimeout(() => {
2527
+ this._cooperativeGesturesScreen.classList.remove('maplibregl-show');
2528
+ }, 100);
2529
+ }
2530
+ return false;
2531
+ }
2532
+
2468
2533
  /**
2469
2534
  * Returns a Boolean indicating whether the map is fully loaded.
2470
2535
  *
@@ -2714,6 +2779,9 @@ class Map extends Camera {
2714
2779
  this._canvas.removeEventListener('webglcontextlost', this._contextLost, false);
2715
2780
  DOM.remove(this._canvasContainer);
2716
2781
  DOM.remove(this._controlContainer);
2782
+ if (this._cooperativeGestures) {
2783
+ DOM.remove(this._cooperativeGesturesScreen);
2784
+ }
2717
2785
  this._container.classList.remove('maplibregl-map', 'mapboxgl-map');
2718
2786
 
2719
2787
  PerformanceUtils.clearMetrics();
@@ -1,6 +1,7 @@
1
1
  import simulate, {window} from '../../test/unit/lib/simulate_interaction';
2
2
  import StyleLayer from '../style/style_layer';
3
3
  import {createMap, setPerformance, setWebGlContext} from '../util/test/util';
4
+ import {MapGeoJSONFeature} from '../util/vectortile_to_geojson';
4
5
  import {MapLayerEventType} from './events';
5
6
 
6
7
  beforeEach(() => {
@@ -37,10 +38,10 @@ describe('map events', () => {
37
38
 
38
39
  test('Map#on adds a listener for an event on a given layer', () => {
39
40
  const map = createMap();
40
- const features = [{}];
41
+ const features = [{} as MapGeoJSONFeature];
41
42
 
42
43
  jest.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer);
43
- jest.spyOn(map, 'queryRenderedFeatures').mockImplementation((point, options) => {
44
+ jest.spyOn(map, 'queryRenderedFeatures').mockImplementation((_point, options) => {
44
45
  expect(options).toEqual({layers: ['layer']});
45
46
  return features;
46
47
  });
@@ -79,7 +80,7 @@ describe('map events', () => {
79
80
  test('Map#on adds a listener not triggered when the specified layer does not exiist', () => {
80
81
  const map = createMap();
81
82
 
82
- jest.spyOn(map, 'getLayer').mockReturnValue(null);
83
+ jest.spyOn(map, 'getLayer').mockReturnValue(null as unknown as StyleLayer);
83
84
 
84
85
  const spy = jest.fn();
85
86
 
@@ -94,7 +95,7 @@ describe('map events', () => {
94
95
  const map = createMap();
95
96
 
96
97
  jest.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer);
97
- jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{}]);
98
+ jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]);
98
99
 
99
100
  const spyDown = jest.fn((e) => {
100
101
  expect(e.type).toBe('mousedown');
@@ -114,11 +115,11 @@ describe('map events', () => {
114
115
 
115
116
  test('Map#on distinguishes distinct layers', () => {
116
117
  const map = createMap();
117
- const featuresA = [{}];
118
- const featuresB = [{}];
118
+ const featuresA = [{} as MapGeoJSONFeature];
119
+ const featuresB = [{} as MapGeoJSONFeature];
119
120
 
120
121
  jest.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer);
121
- jest.spyOn(map, 'queryRenderedFeatures').mockImplementation((point, options) => {
122
+ jest.spyOn(map, 'queryRenderedFeatures').mockImplementation((_point, options) => {
122
123
  return (options as any).layers[0] === 'A' ? featuresA : featuresB;
123
124
  });
124
125
 
@@ -142,7 +143,7 @@ describe('map events', () => {
142
143
  const map = createMap();
143
144
 
144
145
  jest.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer);
145
- jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{}]);
146
+ jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]);
146
147
 
147
148
  const spyA = jest.fn();
148
149
  const spyB = jest.fn();
@@ -159,7 +160,7 @@ describe('map events', () => {
159
160
  const map = createMap();
160
161
 
161
162
  jest.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer);
162
- jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{}]);
163
+ jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]);
163
164
 
164
165
  const spy = jest.fn();
165
166
 
@@ -175,7 +176,7 @@ describe('map events', () => {
175
176
  const map = createMap();
176
177
 
177
178
  jest.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer);
178
- jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{}]);
179
+ jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]);
179
180
 
180
181
  const spy = jest.fn((e) => {
181
182
  expect(e.type).toBe('mousedown');
@@ -191,7 +192,7 @@ describe('map events', () => {
191
192
 
192
193
  test('Map#off distinguishes distinct layers', () => {
193
194
  const map = createMap();
194
- const featuresA = [{}];
195
+ const featuresA = [{} as MapGeoJSONFeature];
195
196
 
196
197
  jest.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer);
197
198
  jest.spyOn(map, 'queryRenderedFeatures').mockImplementation((point, options) => {
@@ -215,7 +216,7 @@ describe('map events', () => {
215
216
  const map = createMap();
216
217
 
217
218
  jest.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer);
218
- jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{}]);
219
+ jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]);
219
220
 
220
221
  const spyA = jest.fn();
221
222
  const spyB = jest.fn();
@@ -233,7 +234,7 @@ describe('map events', () => {
233
234
  test(`Map#on ${event} does not fire if the specified layer does not exist`, () => {
234
235
  const map = createMap();
235
236
 
236
- jest.spyOn(map, 'getLayer').mockReturnValue(null);
237
+ jest.spyOn(map, 'getLayer').mockReturnValue(null as unknown as StyleLayer);
237
238
 
238
239
  const spy = jest.fn();
239
240
 
@@ -247,10 +248,10 @@ describe('map events', () => {
247
248
 
248
249
  test(`Map#on ${event} fires when entering the specified layer`, () => {
249
250
  const map = createMap();
250
- const features = [{}];
251
+ const features = [{} as MapGeoJSONFeature];
251
252
 
252
253
  jest.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer);
253
- jest.spyOn(map, 'queryRenderedFeatures').mockImplementation((point, options) => {
254
+ jest.spyOn(map, 'queryRenderedFeatures').mockImplementation((_point, options) => {
254
255
  expect(options).toEqual({layers: ['layer']});
255
256
  return features;
256
257
  });
@@ -272,7 +273,7 @@ describe('map events', () => {
272
273
  const map = createMap();
273
274
 
274
275
  jest.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer);
275
- jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{}]);
276
+ jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]);
276
277
 
277
278
  const spy = jest.fn();
278
279
 
@@ -288,9 +289,9 @@ describe('map events', () => {
288
289
 
289
290
  jest.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer);
290
291
  jest.spyOn(map, 'queryRenderedFeatures')
291
- .mockReturnValueOnce([{}])
292
+ .mockReturnValueOnce([{} as MapGeoJSONFeature])
292
293
  .mockReturnValueOnce([])
293
- .mockReturnValueOnce([{}]);
294
+ .mockReturnValueOnce([{} as MapGeoJSONFeature]);
294
295
 
295
296
  const spy = jest.fn();
296
297
 
@@ -306,7 +307,7 @@ describe('map events', () => {
306
307
  const map = createMap();
307
308
 
308
309
  jest.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer);
309
- jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{}]);
310
+ jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]);
310
311
 
311
312
  const spy = jest.fn();
312
313
 
@@ -320,11 +321,11 @@ describe('map events', () => {
320
321
 
321
322
  test(`Map#on ${event} distinguishes distinct layers`, () => {
322
323
  const map = createMap();
323
- const featuresA = [{}];
324
- const featuresB = [{}];
324
+ const featuresA = [{} as MapGeoJSONFeature];
325
+ const featuresB = [{} as MapGeoJSONFeature];
325
326
 
326
327
  jest.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer);
327
- jest.spyOn(map, 'queryRenderedFeatures').mockImplementation((point, options) => {
328
+ jest.spyOn(map, 'queryRenderedFeatures').mockImplementation((_point, options) => {
328
329
  return (options as any).layers[0] === 'A' ? featuresA : featuresB;
329
330
  });
330
331
 
@@ -350,7 +351,7 @@ describe('map events', () => {
350
351
  const map = createMap();
351
352
 
352
353
  jest.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer);
353
- jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{}]);
354
+ jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]);
354
355
 
355
356
  const spyA = jest.fn();
356
357
  const spyB = jest.fn();
@@ -367,7 +368,7 @@ describe('map events', () => {
367
368
  const map = createMap();
368
369
 
369
370
  jest.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer);
370
- jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{}]);
371
+ jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]);
371
372
 
372
373
  const spy = jest.fn();
373
374
 
@@ -381,10 +382,10 @@ describe('map events', () => {
381
382
 
382
383
  test(`Map#off ${event} distinguishes distinct layers`, () => {
383
384
  const map = createMap();
384
- const featuresA = [{}];
385
+ const featuresA = [{} as MapGeoJSONFeature];
385
386
 
386
387
  jest.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer);
387
- jest.spyOn(map, 'queryRenderedFeatures').mockImplementation((point, options) => {
388
+ jest.spyOn(map, 'queryRenderedFeatures').mockImplementation((_point, options) => {
388
389
  expect(options).toEqual({layers: ['A']});
389
390
  return featuresA;
390
391
  });
@@ -405,7 +406,7 @@ describe('map events', () => {
405
406
  const map = createMap();
406
407
 
407
408
  jest.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer);
408
- jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{}]);
409
+ jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]);
409
410
 
410
411
  const spyA = jest.fn();
411
412
  const spyB = jest.fn();
@@ -424,7 +425,7 @@ describe('map events', () => {
424
425
  test(`Map#on ${event} does not fire if the specified layer does not exiist`, () => {
425
426
  const map = createMap();
426
427
 
427
- jest.spyOn(map, 'getLayer').mockReturnValue(null);
428
+ jest.spyOn(map, 'getLayer').mockReturnValue(null as unknown as StyleLayer);
428
429
 
429
430
  const spy = jest.fn();
430
431
 
@@ -440,7 +441,7 @@ describe('map events', () => {
440
441
  const map = createMap();
441
442
 
442
443
  jest.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer);
443
- jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{}]);
444
+ jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]);
444
445
 
445
446
  const spy = jest.fn();
446
447
 
@@ -457,7 +458,7 @@ describe('map events', () => {
457
458
 
458
459
  jest.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer);
459
460
  jest.spyOn(map, 'queryRenderedFeatures')
460
- .mockReturnValueOnce([{}])
461
+ .mockReturnValueOnce([{} as MapGeoJSONFeature])
461
462
  .mockReturnValueOnce([]);
462
463
 
463
464
  const spy = jest.fn(function (e) {
@@ -477,7 +478,7 @@ describe('map events', () => {
477
478
  const map = createMap();
478
479
 
479
480
  jest.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer);
480
- jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{}]);
481
+ jest.spyOn(map, 'queryRenderedFeatures').mockReturnValue([{} as MapGeoJSONFeature]);
481
482
 
482
483
  const spy = jest.fn(function (e) {
483
484
  expect(this).toBe(map);
@@ -497,7 +498,7 @@ describe('map events', () => {
497
498
 
498
499
  jest.spyOn(map, 'getLayer').mockReturnValue({} as StyleLayer);
499
500
  jest.spyOn(map, 'queryRenderedFeatures')
500
- .mockReturnValueOnce([{}])
501
+ .mockReturnValueOnce([{} as MapGeoJSONFeature])
501
502
  .mockReturnValueOnce([]);
502
503
 
503
504
  const spy = jest.fn();
@@ -625,8 +625,8 @@ describe('popup', () => {
625
625
  .trackPointer()
626
626
  .addTo(map);
627
627
 
628
- simulate.mousemove(map.getCanvas(), {screenX:0, screenY:0});
629
- expect(popup._pos).toEqual({x:0, y:0});
628
+ simulate.mousemove(map.getCanvas(), {screenX: 0, screenY: 0});
629
+ expect(popup._pos).toEqual({x: 0, y: 0});
630
630
  });
631
631
 
632
632
  test('Popup closes on Map#remove', () => {