maplibre-gl 2.3.1-pre.2 → 2.4.0

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "maplibre-gl",
3
3
  "description": "BSD licensed community fork of mapbox-gl, a WebGL interactive maps library",
4
- "version": "2.3.1-pre.2",
4
+ "version": "2.4.0",
5
5
  "main": "dist/maplibre-gl.js",
6
6
  "style": "dist/maplibre-gl.css",
7
7
  "license": "BSD-3-Clause",
@@ -52,10 +52,10 @@
52
52
  "@types/d3": "^7.4.0",
53
53
  "@types/diff": "^5.0.2",
54
54
  "@types/earcut": "^2.1.1",
55
- "@types/eslint": "^8.4.5",
55
+ "@types/eslint": "^8.4.6",
56
56
  "@types/gl": "^4.1.1",
57
57
  "@types/glob": "^7.2.0",
58
- "@types/jest": "^28.1.6",
58
+ "@types/jest": "^28.1.7",
59
59
  "@types/jsdom": "^20.0.0",
60
60
  "@types/minimist": "^1.2.2",
61
61
  "@types/murmurhash-js": "^1.0.3",
@@ -70,8 +70,8 @@
70
70
  "@types/shuffle-seed": "^1.1.0",
71
71
  "@types/supercluster": "^7.1.0",
72
72
  "@types/window-or-global": "^1.0.4",
73
- "@typescript-eslint/eslint-plugin": "^5.32.0",
74
- "@typescript-eslint/parser": "^5.32.0",
73
+ "@typescript-eslint/eslint-plugin": "^5.34.0",
74
+ "@typescript-eslint/parser": "^5.33.1",
75
75
  "acorn-import-assertions": "^1.8.0",
76
76
  "address": "^1.2.0",
77
77
  "benchmark": "^2.1.4",
@@ -80,13 +80,13 @@
80
80
  "d3": "^7.6.1",
81
81
  "d3-queue": "^3.0.7",
82
82
  "diff": "^5.1.0",
83
- "documentation": "14.0.0-alpha.1",
83
+ "documentation": "14.0.0",
84
84
  "dts-bundle-generator": "^6.12.0",
85
85
  "eslint": "^8.22.0",
86
86
  "eslint-config-mourner": "^3.0.0",
87
87
  "eslint-plugin-html": "^7.1.0",
88
88
  "eslint-plugin-import": "^2.26.0",
89
- "eslint-plugin-jest": "^26.7.0",
89
+ "eslint-plugin-jest": "^26.8.7",
90
90
  "eslint-plugin-jsdoc": "^39.3.4",
91
91
  "eslint-plugin-react": "^7.30.1",
92
92
  "gl": "^5.0.3",
@@ -116,7 +116,7 @@
116
116
  "react": "^18.2.0",
117
117
  "react-dom": "^18.2.0",
118
118
  "request": "^2.88.0",
119
- "rollup": "^2.78.0",
119
+ "rollup": "^2.78.1",
120
120
  "rollup-plugin-import-assert": "^2.1.0",
121
121
  "rollup-plugin-sourcemaps": "^0.6.3",
122
122
  "rollup-plugin-terser": "^7.0.2",
@@ -125,8 +125,8 @@
125
125
  "shuffle-seed": "^1.1.6",
126
126
  "source-map-explorer": "^2.5.2",
127
127
  "st": "^3.0.0",
128
- "stylelint": "^14.10.0",
129
- "stylelint-config-standard": "^27.0.0",
128
+ "stylelint": "^14.11.0",
129
+ "stylelint-config-standard": "^28.0.0",
130
130
  "ts-jest": "^28.0.8",
131
131
  "ts-node": "^10.9.1",
132
132
  "typescript": "^4.7.4"
@@ -139,6 +139,7 @@
139
139
  "generate-typings": "node --loader ts-node/esm --experimental-specifier-resolution=node build/generate-typings.ts",
140
140
  "generate-query-test-fixtures": "node --loader ts-node/esm --experimental-specifier-resolution=node build/generate-query-test-fixtures.ts",
141
141
  "generate-debug-index-file": "node --loader ts-node/esm --experimental-specifier-resolution=node build/generate-debug-index-file.ts",
142
+ "build-dist": "npm run generate-typings && npm run build-dev && npm run build-prod && npm run build-csp && npm run build-css",
142
143
  "build-dev": "rollup --configPlugin @rollup/plugin-typescript -c --environment BUILD:dev",
143
144
  "watch-dev": "rollup --configPlugin @rollup/plugin-typescript -c --environment BUILD:dev --watch",
144
145
  "build-prod": "rollup --configPlugin @rollup/plugin-typescript -c --environment BUILD:production",
@@ -66,7 +66,7 @@ function drawDebug(painter: Painter, sourceCache: SourceCache, coords: Array<Ove
66
66
  }
67
67
  }
68
68
 
69
- function drawDebugTile(painter, sourceCache, coord: OverscaledTileID) {
69
+ function drawDebugTile(painter: Painter, sourceCache: SourceCache, coord: OverscaledTileID) {
70
70
  const context = painter.context;
71
71
  const gl = context.gl;
72
72
 
@@ -1,6 +1,7 @@
1
1
  import Color from '../style-spec/util/color';
2
2
  import DepthMode from '../gl/depth_mode';
3
3
  import CullFaceMode from '../gl/cull_face_mode';
4
+ import ColorMode from '../gl/color_mode';
4
5
  import {
5
6
  fillUniformValues,
6
7
  fillPatternUniformValues,
@@ -56,7 +57,14 @@ function drawFill(painter: Painter, sourceCache: SourceCache, layer: FillStyleLa
56
57
  }
57
58
  }
58
59
 
59
- function drawFillTiles(painter, sourceCache, layer, coords, depthMode, colorMode, isOutline) {
60
+ function drawFillTiles(
61
+ painter: Painter,
62
+ sourceCache: SourceCache,
63
+ layer: FillStyleLayer,
64
+ coords: Array<OverscaledTileID>,
65
+ depthMode: Readonly<DepthMode>,
66
+ colorMode: Readonly<ColorMode>,
67
+ isOutline: boolean) {
60
68
  const gl = painter.context.gl;
61
69
 
62
70
  const patternProperty = layer.paint.get('fill-pattern');
@@ -45,7 +45,14 @@ function draw(painter: Painter, source: SourceCache, layer: FillExtrusionStyleLa
45
45
  }
46
46
  }
47
47
 
48
- function drawExtrusionTiles(painter, source, layer, coords, depthMode, stencilMode, colorMode) {
48
+ function drawExtrusionTiles(
49
+ painter: Painter,
50
+ source: SourceCache,
51
+ layer: FillExtrusionStyleLayer,
52
+ coords: OverscaledTileID[],
53
+ depthMode: DepthMode,
54
+ stencilMode: Readonly<StencilMode>,
55
+ colorMode: Readonly<ColorMode>) {
49
56
  const context = painter.context;
50
57
  const gl = context.gl;
51
58
  const patternProperty = layer.paint.get('fill-extrusion-pattern');
@@ -4,6 +4,8 @@ import DepthMode from '../gl/depth_mode';
4
4
  import StencilMode from '../gl/stencil_mode';
5
5
  import ColorMode from '../gl/color_mode';
6
6
  import CullFaceMode from '../gl/cull_face_mode';
7
+ import Context from '../gl/context';
8
+ import Framebuffer from '../gl/framebuffer';
7
9
  import {
8
10
  heatmapUniformValues,
9
11
  heatmapTextureUniformValues
@@ -67,7 +69,7 @@ function drawHeatmap(painter: Painter, sourceCache: SourceCache, layer: HeatmapS
67
69
  }
68
70
  }
69
71
 
70
- function bindFramebuffer(context, painter, layer) {
72
+ function bindFramebuffer(context: Context, painter: Painter, layer: HeatmapStyleLayer) {
71
73
  const gl = context.gl;
72
74
  context.activeTexture.set(gl.TEXTURE1);
73
75
 
@@ -94,7 +96,7 @@ function bindFramebuffer(context, painter, layer) {
94
96
  }
95
97
  }
96
98
 
97
- function bindTextureToFramebuffer(context, painter, texture, fbo) {
99
+ function bindTextureToFramebuffer(context: Context, painter: Painter, texture: WebGLTexture, fbo: Framebuffer) {
98
100
  const gl = context.gl;
99
101
  // Use the higher precision half-float texture where available (producing much smoother looking heatmaps);
100
102
  // Otherwise, fall back to a low precision texture
@@ -103,7 +105,7 @@ function bindTextureToFramebuffer(context, painter, texture, fbo) {
103
105
  fbo.colorAttachment.set(texture);
104
106
  }
105
107
 
106
- function renderTextureToMap(painter, layer) {
108
+ function renderTextureToMap(painter: Painter, layer: HeatmapStyleLayer) {
107
109
  const context = painter.context;
108
110
  const gl = context.gl;
109
111
 
@@ -2,6 +2,8 @@ import Texture from './texture';
2
2
  import StencilMode from '../gl/stencil_mode';
3
3
  import DepthMode from '../gl/depth_mode';
4
4
  import CullFaceMode from '../gl/cull_face_mode';
5
+ import ColorMode from '../gl/color_mode';
6
+ import Tile from '../source/tile';
5
7
  import {
6
8
  hillshadeUniformValues,
7
9
  hillshadeUniformPrepareValues
@@ -37,7 +39,14 @@ function drawHillshade(painter: Painter, sourceCache: SourceCache, layer: Hillsh
37
39
  context.viewport.set([0, 0, painter.width, painter.height]);
38
40
  }
39
41
 
40
- function renderHillshade(painter, coord, tile, layer, depthMode, stencilMode, colorMode) {
42
+ function renderHillshade(
43
+ painter: Painter,
44
+ coord: OverscaledTileID,
45
+ tile: Tile,
46
+ layer: HillshadeStyleLayer,
47
+ depthMode: Readonly<DepthMode>,
48
+ stencilMode: Readonly<StencilMode>,
49
+ colorMode: Readonly<ColorMode>) {
41
50
  const context = painter.context;
42
51
  const gl = context.gl;
43
52
  const fbo = tile.fbo;
@@ -58,7 +67,13 @@ function renderHillshade(painter, coord, tile, layer, depthMode, stencilMode, co
58
67
 
59
68
  // hillshade rendering is done in two steps. the prepare step first calculates the slope of the terrain in the x and y
60
69
  // directions for each pixel, and saves those values to a framebuffer texture in the r and g channels.
61
- function prepareHillshade(painter, tile, layer, depthMode, stencilMode, colorMode) {
70
+ function prepareHillshade(
71
+ painter: Painter,
72
+ tile: Tile,
73
+ layer: HillshadeStyleLayer,
74
+ depthMode: Readonly<DepthMode>,
75
+ stencilMode: Readonly<StencilMode>,
76
+ colorMode: Readonly<ColorMode>) {
62
77
  const context = painter.context;
63
78
  const gl = context.gl;
64
79
  const dem = tile.dem;
@@ -186,7 +186,7 @@ function calculateTileRatio(tile: Tile, transform: Transform) {
186
186
  return 1 / pixelsToTileUnits(tile, 1, transform.tileZoom);
187
187
  }
188
188
 
189
- function calculateMatrix(painter, tile, layer, coord) {
189
+ function calculateMatrix(painter: Painter, tile: Tile, layer: LineStyleLayer, coord: OverscaledTileID) {
190
190
  return painter.translatePosMatrix(
191
191
  coord ? coord.posMatrix : tile.tileID.posMatrix,
192
192
  tile,
@@ -499,6 +499,7 @@ class Style extends Evented {
499
499
 
500
500
  // remove terrain
501
501
  if (!options) {
502
+ if (this.terrain) this.terrain.sourceCache.destruct();
502
503
  this.terrain = null;
503
504
  this.map.transform.updateElevation(this.terrain);
504
505
 
@@ -24,7 +24,7 @@ import type {BucketParameters} from '../../data/bucket';
24
24
  import type {SymbolLayoutProps, SymbolPaintProps} from './symbol_style_layer_properties.g';
25
25
  import type EvaluationParameters from '../evaluation_parameters';
26
26
  import type {LayerSpecification} from '../../style-spec/types.g';
27
- import type {Feature, SourceExpression, CompositeExpression} from '../../style-spec/expression';
27
+ import type {Feature, SourceExpression} from '../../style-spec/expression';
28
28
  import type {Expression} from '../../style-spec/expression/expression';
29
29
  import type {CanonicalTileID} from '../../source/tile_id';
30
30
  import {FormattedType} from '../../style-spec/expression/types';
@@ -122,12 +122,11 @@ class SymbolStyleLayer extends StyleLayer {
122
122
  const styleExpression = new StyleExpression(override, overriden.property.specification);
123
123
  let expression = null;
124
124
  if (overriden.value.kind === 'constant' || overriden.value.kind === 'source') {
125
- expression = (new ZoomConstantExpression('source', styleExpression) as SourceExpression);
125
+ expression = new ZoomConstantExpression('source', styleExpression) as SourceExpression;
126
126
  } else {
127
- expression = (new ZoomDependentExpression('composite',
127
+ expression = new ZoomDependentExpression('composite',
128
128
  styleExpression,
129
- overriden.value.zoomStops,
130
- (overriden.value as any)._interpolationType) as CompositeExpression);
129
+ overriden.value.zoomStops);
131
130
  }
132
131
  this.paint._values[overridable] = new PossiblyEvaluatedPropertyValue(overriden.property,
133
132
  expression,
@@ -6,7 +6,7 @@
6
6
 
7
7
  ### Breaking changes
8
8
 
9
- * Renamed `ParsingError` to `ExpressionParsingError` as there were two with the same name and added typescript typings [1468](https://github.com/maplibre/maplibre-gl-js/pull/1468)
9
+ * Renamed `ParsingError` to `ExpressionParsingError` as there were two with the same name and added typescript typings [#1468](https://github.com/maplibre/maplibre-gl-js/pull/1468)
10
10
 
11
11
  ## 16.1.0
12
12
 
@@ -55,7 +55,7 @@
55
55
  ## 13.16.0
56
56
 
57
57
  ### ✨ Features and improvements
58
- * Added `volatile` source property to control storing the tiles in local storage. ([9702](https://github.com/mapbox/mapbox-gl-js/pull/9702))
58
+ * Added `volatile` source property to control storing the tiles in local storage. ([#9702](https://github.com/mapbox/mapbox-gl-js/pull/9702))
59
59
 
60
60
  * Added `clusterMinPoints` option for clustered GeoJSON sources that defines the minimum number of points to form a cluster. ([#9748](https://github.com/mapbox/mapbox-gl-js/pull/9748))
61
61
 
@@ -1,9 +1,10 @@
1
- import Camera from '../ui/camera';
1
+ import Camera, {CameraOptions} from '../ui/camera';
2
2
  import Transform from '../geo/transform';
3
3
  import TaskQueue, {TaskID} from '../util/task_queue';
4
4
  import browser from '../util/browser';
5
5
  import {fixedLngLat, fixedNum} from '../../test/unit/lib/fixed';
6
6
  import {setMatchMedia} from '../util/test/util';
7
+ import {mercatorZfromAltitude} from '../geo/mercator_coordinate';
7
8
 
8
9
  beforeEach(() => {
9
10
  setMatchMedia();
@@ -55,6 +56,68 @@ function assertTransitionTime(done, camera, min, max) {
55
56
  });
56
57
  }
57
58
 
59
+ describe('#calculateCameraOptionsFromTo', () => {
60
+ // Choose initial zoom to avoid center being constrained by mercator latitude limits.
61
+ const camera = createCamera({zoom: 1});
62
+
63
+ test('look at north', () => {
64
+ const cameraOptions: CameraOptions = camera.calculateCameraOptionsFromTo({lng: 1, lat: 0}, 0, {lng: 1, lat: 1});
65
+ expect(cameraOptions).toBeDefined();
66
+ expect(cameraOptions.center).toBeDefined();
67
+ expect(cameraOptions.bearing).toBeCloseTo(0);
68
+ });
69
+
70
+ test('look at west', () => {
71
+ const cameraOptions = camera.calculateCameraOptionsFromTo({lng: 1, lat: 0}, 0, {lng: 0, lat: 0});
72
+ expect(cameraOptions).toBeDefined();
73
+ expect(cameraOptions.bearing).toBeCloseTo(-90);
74
+ });
75
+
76
+ test('pitch 45', () => {
77
+ // altitude same as grounddistance => 45°
78
+ // distance between lng x and lng x+1 is 111.2km at same lat
79
+ const cameraOptions: CameraOptions = camera.calculateCameraOptionsFromTo({lng: 1, lat: 0}, 111200, {lng: 0, lat: 0});
80
+ expect(cameraOptions).toBeDefined();
81
+ expect(cameraOptions.pitch).toBeCloseTo(45);
82
+ });
83
+
84
+ test('pitch 90', () => {
85
+ const cameraOptions = camera.calculateCameraOptionsFromTo({lng: 1, lat: 0}, 0, {lng: 0, lat: 0});
86
+ expect(cameraOptions).toBeDefined();
87
+ expect(cameraOptions.pitch).toBeCloseTo(90);
88
+ });
89
+
90
+ test('pitch 153.435', () => {
91
+
92
+ // distance between lng x and lng x+1 is 111.2km at same lat
93
+ // (elevation difference of cam and center) / 2 = grounddistance =>
94
+ // acos(111.2 / sqrt(111.2² + (111.2 * 2)²)) = acos(1/sqrt(5)) => 63.435 + 90 (looking up) = 153.435
95
+ const cameraOptions: CameraOptions = camera.calculateCameraOptionsFromTo({lng: 1, lat: 0}, 111200, {lng: 0, lat: 0}, 111200 * 3);
96
+ expect(cameraOptions).toBeDefined();
97
+ expect(cameraOptions.pitch).toBeCloseTo(153.435);
98
+ });
99
+
100
+ test('zoom distance 1000', () => {
101
+ const expectedZoom = Math.log2(camera.transform.cameraToCenterDistance / mercatorZfromAltitude(1000, 0) / camera.transform.tileSize);
102
+ const cameraOptions = camera.calculateCameraOptionsFromTo({lng: 0, lat: 0}, 0, {lng: 0, lat: 0}, 1000);
103
+
104
+ expect(cameraOptions).toBeDefined();
105
+ expect(cameraOptions.zoom).toBeCloseTo(expectedZoom);
106
+ });
107
+
108
+ test('zoom distance 1 lng (111.2km), 111.2km altitude away', () => {
109
+ const expectedZoom = Math.log2(camera.transform.cameraToCenterDistance / mercatorZfromAltitude(Math.hypot(111200, 111200), 0) / camera.transform.tileSize);
110
+ const cameraOptions = camera.calculateCameraOptionsFromTo({lng: 0, lat: 0}, 0, {lng: 1, lat: 0}, 111200);
111
+
112
+ expect(cameraOptions).toBeDefined();
113
+ expect(cameraOptions.zoom).toBeCloseTo(expectedZoom);
114
+ });
115
+
116
+ test('same To as From error', () => {
117
+ expect(() => { camera.calculateCameraOptionsFromTo({lng: 0, lat: 0}, 0, {lng: 0, lat: 0}, 0); }).toThrow();
118
+ });
119
+ });
120
+
58
121
  describe('#jumpTo', () => {
59
122
  // Choose initial zoom to avoid center being constrained by mercator latitude limits.
60
123
  const camera = createCamera({zoom: 1});
package/src/ui/camera.ts CHANGED
@@ -12,6 +12,7 @@ import type {LngLatLike} from '../geo/lng_lat';
12
12
  import type {LngLatBoundsLike} from '../geo/lng_lat_bounds';
13
13
  import type {TaskID} from '../util/task_queue';
14
14
  import type {PaddingOptions} from '../geo/edge_insets';
15
+ import MercatorCoordinate from '../geo/mercator_coordinate';
15
16
 
16
17
  /**
17
18
  * A [Point](https://github.com/mapbox/point-geometry) or an array of two numbers representing `x` and `y` screen coordinates in pixels.
@@ -774,6 +775,41 @@ abstract class Camera extends Evented {
774
775
  return this.fire(new Event('moveend', eventData));
775
776
  }
776
777
 
778
+ /**
779
+ * Calculates pitch, zoom and bearing for looking at @param newCenter with the camera position being @param newCenter
780
+ * and returns them as Cameraoptions.
781
+ * @memberof Map#
782
+ * @param from The camera to look from
783
+ * @param altitudeFrom The altitude of the camera to look from
784
+ * @param to The center to look at
785
+ * @param altitudeTo Optional altitude of the center to look at. If none given the ground height will be used.
786
+ * @returns {CameraOptions} the calculated camera options
787
+ */
788
+ calculateCameraOptionsFromTo(from: LngLat, altitudeFrom: number, to: LngLat, altitudeTo: number = 0) : CameraOptions {
789
+ const fromMerc = MercatorCoordinate.fromLngLat(from, altitudeFrom);
790
+ const toMerc = MercatorCoordinate.fromLngLat(to, altitudeTo);
791
+ const dx = toMerc.x - fromMerc.x;
792
+ const dy = toMerc.y - fromMerc.y;
793
+ const dz = toMerc.z - fromMerc.z;
794
+
795
+ const distance3D = Math.hypot(dx, dy, dz);
796
+ if (distance3D === 0) throw new Error('Can\'t calculate camera options with same From and To');
797
+
798
+ const groundDistance = Math.hypot(dx, dy);
799
+
800
+ const zoom = this.transform.scaleZoom(this.transform.cameraToCenterDistance / distance3D / this.transform.tileSize);
801
+ const bearing = (Math.atan2(dx, -dy) * 180) / Math.PI;
802
+ let pitch = (Math.acos(groundDistance / distance3D) * 180) / Math.PI;
803
+ pitch = dz < 0 ? 90 - pitch : 90 + pitch;
804
+
805
+ return {
806
+ center: toMerc.toLngLat(),
807
+ zoom,
808
+ pitch,
809
+ bearing
810
+ };
811
+ }
812
+
777
813
  /**
778
814
  * Changes any combination of `center`, `zoom`, `bearing`, `pitch`, and `padding` with an animated transition
779
815
  * between old and new values. The map will retain its current values for any
@@ -1,6 +1,7 @@
1
1
  import AttributionControl from './attribution_control';
2
2
  import {createMap as globalCreateMap, setWebGlContext, setPerformance, setMatchMedia} from '../../util/test/util';
3
3
  import simulate from '../../../test/unit/lib/simulate_interaction';
4
+ import {fakeServer} from 'nise';
4
5
 
5
6
  function createMap() {
6
7
 
@@ -272,6 +273,67 @@ describe('AttributionControl', () => {
272
273
  });
273
274
  });
274
275
 
276
+ test('does not show attributions for sources that are used for terrain when they are not in use', done => {
277
+ global.fetch = null;
278
+ const server = fakeServer.create();
279
+ server.respondWith('/source.json', JSON.stringify({
280
+ minzoom: 5,
281
+ maxzoom: 12,
282
+ attribution: 'Terrain',
283
+ tiles: ['http://example.com/{z}/{x}/{y}.pngraw'],
284
+ bounds: [-47, -7, -45, -5]
285
+ }));
286
+
287
+ const attribution = new AttributionControl();
288
+ map.addControl(attribution);
289
+
290
+ map.on('load', () => {
291
+ map.addSource('1', {type: 'raster-dem', url: '/source.json'});
292
+ server.respond();
293
+ });
294
+
295
+ let times = 0;
296
+ map.on('data', (e) => {
297
+ if (e.dataType === 'source' && e.sourceDataType === 'visibility') {
298
+ if (++times === 1) {
299
+ expect(attribution._innerContainer.innerHTML).toBe('');
300
+ done();
301
+ }
302
+ }
303
+ });
304
+ });
305
+
306
+ test('shows attributions for sources that are used for terrain', done => {
307
+ global.fetch = null;
308
+ const server = fakeServer.create();
309
+ server.respondWith('/source.json', JSON.stringify({
310
+ minzoom: 5,
311
+ maxzoom: 12,
312
+ attribution: 'Terrain',
313
+ tiles: ['http://example.com/{z}/{x}/{y}.pngraw'],
314
+ bounds: [-47, -7, -45, -5]
315
+ }));
316
+
317
+ const attribution = new AttributionControl();
318
+ map.addControl(attribution);
319
+
320
+ map.on('load', () => {
321
+ map.addSource('1', {type: 'raster-dem', url: '/source.json'});
322
+ server.respond();
323
+ map.setTerrain({source: '1'});
324
+ });
325
+
326
+ let times = 0;
327
+ map.on('data', (e) => {
328
+ if (e.dataType === 'source' && e.sourceDataType === 'visibility') {
329
+ if (++times === 1) {
330
+ expect(attribution._innerContainer.innerHTML).toBe('Terrain');
331
+ done();
332
+ }
333
+ }
334
+ });
335
+ });
336
+
275
337
  test('toggles attributions for sources whose visibility changes when zooming', done => {
276
338
  const attribution = new AttributionControl();
277
339
  map.addControl(attribution);
@@ -63,6 +63,7 @@ class AttributionControl implements IControl {
63
63
 
64
64
  this._map.on('styledata', this._updateData);
65
65
  this._map.on('sourcedata', this._updateData);
66
+ this._map.on('terrain', this._updateData);
66
67
  this._map.on('resize', this._updateCompact);
67
68
  this._map.on('drag', this._updateCompactMinimize);
68
69
 
@@ -74,6 +75,7 @@ class AttributionControl implements IControl {
74
75
 
75
76
  this._map.off('styledata', this._updateData);
76
77
  this._map.off('sourcedata', this._updateData);
78
+ this._map.off('terrain', this._updateData);
77
79
  this._map.off('resize', this._updateCompact);
78
80
  this._map.off('drag', this._updateCompactMinimize);
79
81
 
@@ -101,7 +103,7 @@ class AttributionControl implements IControl {
101
103
  }
102
104
 
103
105
  _updateData(e: any) {
104
- if (e && (e.sourceDataType === 'metadata' || e.sourceDataType === 'visibility' || e.dataType === 'style')) {
106
+ if (e && (e.sourceDataType === 'metadata' || e.sourceDataType === 'visibility' || e.dataType === 'style' || e.type === 'terrain')) {
105
107
  this._updateAttributions();
106
108
  }
107
109
  }
@@ -131,7 +133,7 @@ class AttributionControl implements IControl {
131
133
  const sourceCaches = this._map.style.sourceCaches;
132
134
  for (const id in sourceCaches) {
133
135
  const sourceCache = sourceCaches[id];
134
- if (sourceCache.used) {
136
+ if (sourceCache.used || sourceCache.usedForTerrain) {
135
137
  const source = sourceCache.getSource();
136
138
  if (source.attribution && attributions.indexOf(source.attribution) < 0) {
137
139
  attributions.push(source.attribution);
@@ -0,0 +1,60 @@
1
+ import TerrainControl from './terrain_control';
2
+ import {createMap as globalCreateMap, setWebGlContext, setPerformance, setMatchMedia} from '../../util/test/util';
3
+
4
+ function createMap() {
5
+
6
+ return globalCreateMap({
7
+ attributionControl: false,
8
+ style: {
9
+ version: 8,
10
+ sources: {
11
+ terrain: {
12
+ minzoom: 5,
13
+ maxzoom: 12,
14
+ attribution: 'MapLibre',
15
+ tiles: ['http://example.com/{z}/{x}/{y}.pngraw'],
16
+ bounds: [-47, -7, -45, -5]
17
+ },
18
+ },
19
+ layers: [],
20
+ owner: 'mapblibre',
21
+ id: 'demotiles',
22
+ },
23
+ hash: true
24
+ }, undefined);
25
+ }
26
+
27
+ let map;
28
+
29
+ beforeEach(() => {
30
+ setWebGlContext();
31
+ setPerformance();
32
+ setMatchMedia();
33
+ map = createMap();
34
+ });
35
+
36
+ afterEach(() => {
37
+ map.remove();
38
+ });
39
+
40
+ describe('TerrainControl', () => {
41
+ test('appears in top-right by default', () => {
42
+ map.addControl(new TerrainControl({source: 'terrain'}));
43
+
44
+ expect(
45
+ map.getContainer().querySelectorAll('.maplibregl-ctrl-top-right .maplibregl-ctrl-terrain')
46
+ ).toHaveLength(1);
47
+ });
48
+
49
+ test('appears in the position specified by the position option', () => {
50
+ map.addControl(new TerrainControl({source: 'terrain'}), 'bottom-right');
51
+
52
+ expect(
53
+ map.getContainer().querySelectorAll('.maplibregl-ctrl-bottom-right .maplibregl-ctrl-terrain')
54
+ ).toHaveLength(1);
55
+
56
+ expect(
57
+ map.getContainer().querySelectorAll('.maplibregl-ctrl-top-right .maplibregl-ctrl-terrain')
58
+ ).toHaveLength(0);
59
+ });
60
+ });
@@ -4,7 +4,7 @@ import simulate from '../../../test/unit/lib/simulate_interaction';
4
4
  import {setMatchMedia, setPerformance, setWebGlContext} from '../../util/test/util';
5
5
 
6
6
  function createMap() {
7
- return new Map({interactive: false, container: DOM.create('div', '', window.document.body)} as any as MapOptions);
7
+ return new Map({interactive: true, container: DOM.create('div', '', window.document.body)} as any as MapOptions);
8
8
  }
9
9
 
10
10
  beforeEach(() => {
@@ -93,4 +93,68 @@ describe('map events', () => {
93
93
 
94
94
  map.remove();
95
95
  });
96
+
97
+ test('MapEvent handler fires contextmenu on MacOS/Linux, but only at mouseup', () => {
98
+ const map = createMap();
99
+ const target = map.getCanvas();
100
+ map.dragPan.enable();
101
+
102
+ const contextmenu = jest.fn();
103
+
104
+ map.on('contextmenu', contextmenu);
105
+
106
+ simulate.mousedown(map.getCanvas(), {target, button: 2, clientX: 10, clientY: 10});
107
+ simulate.contextmenu(map.getCanvas(), {target}); // triggered immediately after mousedown
108
+ expect(contextmenu).toHaveBeenCalledTimes(0);
109
+ simulate.mouseup(map.getCanvas(), {target, button: 2, clientX: 10, clientY: 10});
110
+ expect(contextmenu).toHaveBeenCalledTimes(1);
111
+ });
112
+
113
+ test('MapEvent handler does not fire contextmenu on MacOS/Linux, when moved', () => {
114
+ const map = createMap();
115
+ const target = map.getCanvas();
116
+ map.dragPan.enable();
117
+
118
+ const contextmenu = jest.fn();
119
+
120
+ map.on('contextmenu', contextmenu);
121
+
122
+ simulate.mousedown(map.getCanvas(), {target, button: 2, clientX: 10, clientY: 10});
123
+ simulate.contextmenu(map.getCanvas(), {target}); // triggered immediately after mousedown
124
+ simulate.mousemove(map.getCanvas(), {target, buttons: 2, clientX: 50, clientY: 10});
125
+ simulate.mouseup(map.getCanvas(), {target, button: 2, clientX: 70, clientY: 10});
126
+ expect(contextmenu).toHaveBeenCalledTimes(0);
127
+ });
128
+
129
+ test('MapEvent handler fires contextmenu on Windows', () => {
130
+ const map = createMap();
131
+ const target = map.getCanvas();
132
+ map.dragPan.enable();
133
+
134
+ const contextmenu = jest.fn();
135
+
136
+ map.on('contextmenu', contextmenu);
137
+
138
+ simulate.mousedown(map.getCanvas(), {target, button: 2, clientX: 10, clientY: 10});
139
+ simulate.mouseup(map.getCanvas(), {target, button: 2, clientX: 10, clientY: 10});
140
+ expect(contextmenu).toHaveBeenCalledTimes(0);
141
+ simulate.contextmenu(map.getCanvas(), {target, button: 2, clientX: 10, clientY: 10}); // triggered only after mouseup
142
+ expect(contextmenu).toHaveBeenCalledTimes(1);
143
+ });
144
+
145
+ test('MapEvent handler does not fire contextmenu on Windows, when moved', () => {
146
+ const map = createMap();
147
+ const target = map.getCanvas();
148
+ map.dragPan.enable();
149
+
150
+ const contextmenu = jest.fn();
151
+
152
+ map.on('contextmenu', contextmenu);
153
+
154
+ simulate.mousedown(map.getCanvas(), {target, button: 2, clientX: 10, clientY: 10});
155
+ simulate.mousemove(map.getCanvas(), {target, buttons: 2, clientX: 50, clientY: 10});
156
+ simulate.mouseup(map.getCanvas(), {target, button: 2, clientX: 50, clientY: 10});
157
+ simulate.contextmenu(map.getCanvas(), {target, button: 2, clientX: 10, clientY: 10}); // triggered only after mouseup
158
+ expect(contextmenu).toHaveBeenCalledTimes(0);
159
+ });
96
160
  });