maplibre-gl 6.2.0 → 6.3.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/dist/maplibre-gl-dev.mjs +522 -223
- package/dist/maplibre-gl-dev.mjs.map +1 -1
- package/dist/maplibre-gl-shared-dev.mjs +103 -58
- package/dist/maplibre-gl-shared-dev.mjs.map +1 -1
- package/dist/maplibre-gl-shared.mjs +2 -2
- package/dist/maplibre-gl-shared.mjs.map +1 -1
- package/dist/maplibre-gl-worker-dev.mjs +23 -49
- package/dist/maplibre-gl-worker-dev.mjs.map +1 -1
- package/dist/maplibre-gl-worker.mjs +2 -2
- package/dist/maplibre-gl-worker.mjs.map +1 -1
- package/dist/maplibre-gl.css +1 -1
- package/dist/maplibre-gl.d.ts +214 -59
- package/dist/maplibre-gl.mjs +44 -44
- package/dist/maplibre-gl.mjs.map +1 -1
- package/package.json +10 -20
- package/src/data/feature_index.bench.ts +21 -0
- package/src/data/segment.test.ts +2 -2
- package/src/geo/edge_insets.test.ts +1 -1
- package/src/geo/lng_lat.test.ts +7 -7
- package/src/geo/lng_lat_bounds.test.ts +4 -4
- package/src/geo/mercator_coordinate.test.ts +2 -2
- package/src/geo/projection/camera_helper.ts +5 -0
- package/src/geo/projection/globe_transform.test.ts +25 -0
- package/src/geo/projection/globe_transform.ts +7 -3
- package/src/geo/projection/mercator_camera_helper.ts +1 -1
- package/src/geo/projection/mercator_transform.ts +21 -5
- package/src/geo/projection/vertical_perspective_camera_helper.ts +60 -31
- package/src/geo/projection/vertical_perspective_transform.ts +7 -1
- package/src/geo/transform_interface.ts +13 -1
- package/src/index.ts +7 -2
- package/src/render/image_atlas.test.ts +72 -0
- package/src/render/image_atlas.ts +17 -2
- package/src/render/image_manager.test.ts +46 -0
- package/src/render/image_manager.ts +9 -2
- package/src/render/painter.ts +1 -12
- package/src/render/update_pattern_positions_in_program.test.ts +1 -1
- package/src/shaders/glsl/_prelude.vertex.glsl +2 -3
- package/src/shaders/glsl/_prelude.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/circle.vertex.glsl +4 -4
- package/src/shaders/glsl/circle.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/fill_extrusion.vertex.glsl +3 -3
- package/src/shaders/glsl/fill_extrusion.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/fill_extrusion_pattern.vertex.glsl +5 -5
- package/src/shaders/glsl/fill_extrusion_pattern.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/heatmap.vertex.glsl +4 -4
- package/src/shaders/glsl/heatmap.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/line.vertex.glsl +7 -7
- package/src/shaders/glsl/line.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/line_gradient.vertex.glsl +6 -6
- package/src/shaders/glsl/line_gradient.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/line_gradient_sdf.vertex.glsl +7 -7
- package/src/shaders/glsl/line_gradient_sdf.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/line_pattern.vertex.glsl +7 -7
- package/src/shaders/glsl/line_pattern.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/line_sdf.vertex.glsl +7 -7
- package/src/shaders/glsl/line_sdf.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/raster.fragment.glsl +4 -4
- package/src/shaders/glsl/raster.fragment.glsl.g.ts +1 -1
- package/src/shaders/glsl/raster.vertex.glsl +11 -6
- package/src/shaders/glsl/raster.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/symbol_icon.vertex.glsl +5 -5
- package/src/shaders/glsl/symbol_icon.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/symbol_sdf.vertex.glsl +5 -5
- package/src/shaders/glsl/symbol_sdf.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/symbol_text_and_icon.vertex.glsl +6 -6
- package/src/shaders/glsl/symbol_text_and_icon.vertex.glsl.g.ts +1 -1
- package/src/source/canvas_source.test.ts +31 -0
- package/src/source/canvas_source.ts +2 -1
- package/src/source/geojson_worker_source.test.ts +88 -0
- package/src/source/geojson_worker_source.ts +22 -39
- package/src/source/image_source.test.ts +205 -7
- package/src/source/image_source.ts +118 -4
- package/src/source/raster_dem_tile_worker_source.test.ts +1 -1
- package/src/source/vector_tile_worker_source.test.ts +94 -2
- package/src/source/vector_tile_worker_source.ts +18 -27
- package/src/source/video_source.test.ts +12 -8
- package/src/source/worker_tile.bench.ts +10 -0
- package/src/source/worker_tile.ts +0 -3
- package/src/style/create_style_layer.bench.ts +15 -0
- package/src/style/properties.bench.ts +81 -0
- package/src/style/style.test.ts +11 -10
- package/src/style/style_image.ts +67 -9
- package/src/style/style_layer.bench.ts +65 -0
- package/src/style/style_layer.test.ts +1 -1
- package/src/style/style_layer.ts +2 -2
- package/src/style/validate_style.bench.ts +11 -0
- package/src/style/validate_style.test.ts +2 -2
- package/src/symbol/anchor.test.ts +2 -2
- package/src/symbol/collision_index.bench.ts +116 -0
- package/src/symbol/cross_tile_symbol_index.bench.ts +49 -0
- package/src/symbol/shaping.test.ts +2 -1
- package/src/symbol/symbol_layout.bench.ts +30 -0
- package/src/tile/tile_id.test.ts +1 -1
- package/src/tile/tile_manager.ts +2 -2
- package/src/ui/control/fullscreen_control.test.ts +10 -10
- package/src/ui/control/geolocate_control.test.ts +3 -5
- package/src/ui/control/globe_control.test.ts +6 -6
- package/src/ui/events.ts +1 -1
- package/src/ui/handler/scroll_zoom.test.ts +50 -0
- package/src/ui/handler/two_fingers_touch.test.ts +3 -3
- package/src/ui/handler_manager.test.ts +243 -3
- package/src/ui/handler_manager.ts +79 -16
- package/src/ui/map.ts +17 -3
- package/src/ui/map_tests/map_cross_window.test.ts +2 -2
- package/src/ui/map_tests/map_events.test.ts +3 -3
- package/src/ui/map_tests/map_images.test.ts +29 -0
- package/src/ui/map_tests/map_style.test.ts +9 -8
- package/src/ui/map_tests/map_webgl.test.ts +29 -0
- package/src/ui/marker.test.ts +26 -42
- package/src/ui/popup.test.ts +14 -14
- package/src/util/evented.test.ts +33 -28
- package/src/util/evented.ts +69 -43
- package/src/util/test/null_gl.ts +10 -0
- package/src/util/test/util.ts +2 -1
- package/src/util/util.test.ts +1 -1
- package/src/util/web_worker_transfer.test.ts +5 -5
- package/src/webgl/context.ts +20 -0
- package/src/webgl/draw/draw_raster.ts +11 -6
- package/src/webgl/program/raster_program.ts +11 -0
- package/src/webgl/program.ts +31 -16
- package/src/webgl/texture.test.ts +16 -1
- package/src/webgl/texture.ts +3 -0
- package/src/webgl/vertex_buffer.test.ts +17 -2
- package/src/webgl/vertex_buffer.ts +24 -13
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {bench} from 'vitest';
|
|
2
|
+
import {performSymbolLayout} from './symbol_layout.ts';
|
|
3
|
+
import {SymbolBucket} from '../data/bucket/symbol_bucket.ts';
|
|
4
|
+
import {SubdivisionGranularitySetting} from '../render/subdivision_granularity_settings.ts';
|
|
5
|
+
import {fixtureTiles, parseTile} from '../../test/bench/lib/parse_tile.ts';
|
|
6
|
+
|
|
7
|
+
import type {WorkerTileWithData} from '../source/worker_source.ts';
|
|
8
|
+
|
|
9
|
+
const parsedTiles = await Promise.all(fixtureTiles.map(async (tile): Promise<WorkerTileWithData> => {
|
|
10
|
+
return await parseTile(tile, true) as WorkerTileWithData;
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
bench('performSymbolLayout', () => {
|
|
14
|
+
for (const tileResult of parsedTiles) {
|
|
15
|
+
for (const bucket of tileResult.buckets) {
|
|
16
|
+
if (bucket instanceof SymbolBucket) {
|
|
17
|
+
performSymbolLayout({
|
|
18
|
+
bucket,
|
|
19
|
+
glyphMap: tileResult.glyphMap,
|
|
20
|
+
glyphPositions: tileResult.glyphPositions,
|
|
21
|
+
imageMap: tileResult.iconMap,
|
|
22
|
+
imagePositions: tileResult.imageAtlas.iconPositions,
|
|
23
|
+
showCollisionBoxes: false,
|
|
24
|
+
canonical: tileResult.featureIndex.tileID.canonical,
|
|
25
|
+
subdivisionGranularity: SubdivisionGranularitySetting.noSubdivision
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
package/src/tile/tile_id.test.ts
CHANGED
|
@@ -63,7 +63,7 @@ describe('CanonicalTileID', () => {
|
|
|
63
63
|
|
|
64
64
|
describe('OverscaledTileID', () => {
|
|
65
65
|
test('constructor', () => {
|
|
66
|
-
expect(new OverscaledTileID(0, 0, 0, 0, 0)
|
|
66
|
+
expect(new OverscaledTileID(0, 0, 0, 0, 0)).toBeInstanceOf(OverscaledTileID);
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
test('constructor - deeper canonicalZ than overscaledZ disallowed', () => {
|
package/src/tile/tile_manager.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {GEOJSON_TILE_LAYER_NAME} from '../data/feature_index.ts';
|
|
|
16
16
|
import {hasRasterTransition, isRasterType, updateFadingTiles} from './tile_manager_raster.ts';
|
|
17
17
|
import {backfillDEM} from './tile_manager_raster_dem.ts';
|
|
18
18
|
import {InViewTiles} from './tile_manager_in_view_tiles.ts';
|
|
19
|
-
import {MapSourceDataEvent} from '../ui/events.ts';
|
|
19
|
+
import {MapSourceDataEvent, type SourceEventType} from '../ui/events.ts';
|
|
20
20
|
|
|
21
21
|
import type Point from '@mapbox/point-geometry';
|
|
22
22
|
import type {Context} from '../webgl/context.ts';
|
|
@@ -55,7 +55,7 @@ type TileResult = {
|
|
|
55
55
|
* - unloading cached tiles not needed to render a given viewport
|
|
56
56
|
* - managing tile state and feature state
|
|
57
57
|
*/
|
|
58
|
-
export class TileManager extends Evented {
|
|
58
|
+
export class TileManager extends Evented<SourceEventType> {
|
|
59
59
|
id: string;
|
|
60
60
|
dispatcher: Dispatcher;
|
|
61
61
|
map: Map;
|
|
@@ -45,15 +45,15 @@ describe('FullscreenControl', () => {
|
|
|
45
45
|
|
|
46
46
|
const click = new window.Event('click');
|
|
47
47
|
|
|
48
|
-
expect(mapContainer.classList.
|
|
48
|
+
expect(mapContainer.classList).not.toContain('maplibregl-pseudo-fullscreen');
|
|
49
49
|
|
|
50
50
|
fullscreen._fullscreenButton.dispatchEvent(click);
|
|
51
51
|
|
|
52
|
-
expect(mapContainer.classList.
|
|
52
|
+
expect(mapContainer.classList).toContain('maplibregl-pseudo-fullscreen');
|
|
53
53
|
|
|
54
54
|
fullscreen._fullscreenButton.dispatchEvent(click);
|
|
55
55
|
|
|
56
|
-
expect(mapContainer.classList.
|
|
56
|
+
expect(mapContainer.classList).not.toContain('maplibregl-pseudo-fullscreen');
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
test('start and end events fire for fullscreen button clicks', () => {
|
|
@@ -129,11 +129,11 @@ describe('FullscreenControl', () => {
|
|
|
129
129
|
|
|
130
130
|
const click = new window.Event('click');
|
|
131
131
|
|
|
132
|
-
expect(mapContainer.classList.
|
|
132
|
+
expect(mapContainer.classList).not.toContain('maplibregl-pseudo-fullscreen');
|
|
133
133
|
fullscreen._fullscreenButton.dispatchEvent(click);
|
|
134
|
-
expect(mapContainer.classList.
|
|
134
|
+
expect(mapContainer.classList).toContain('maplibregl-pseudo-fullscreen');
|
|
135
135
|
fullscreen._fullscreenButton.dispatchEvent(click);
|
|
136
|
-
expect(mapContainer.classList.
|
|
136
|
+
expect(mapContainer.classList).not.toContain('maplibregl-pseudo-fullscreen');
|
|
137
137
|
});
|
|
138
138
|
|
|
139
139
|
test('pseudo option forces pseudo fullscreen even when native fullscreen is available', () => {
|
|
@@ -151,7 +151,7 @@ describe('FullscreenControl', () => {
|
|
|
151
151
|
fullscreen._fullscreenButton.dispatchEvent(click);
|
|
152
152
|
|
|
153
153
|
expect(requestFullscreenSpy).not.toHaveBeenCalled();
|
|
154
|
-
expect(mapContainer.classList.
|
|
154
|
+
expect(mapContainer.classList).toContain('maplibregl-pseudo-fullscreen');
|
|
155
155
|
});
|
|
156
156
|
|
|
157
157
|
test('pseudo fullscreen can be used on custom container', () => {
|
|
@@ -166,11 +166,11 @@ describe('FullscreenControl', () => {
|
|
|
166
166
|
|
|
167
167
|
const click = new window.Event('click');
|
|
168
168
|
|
|
169
|
-
expect(container.classList.
|
|
169
|
+
expect(container.classList).not.toContain('maplibregl-pseudo-fullscreen');
|
|
170
170
|
fullscreen._fullscreenButton.dispatchEvent(click);
|
|
171
|
-
expect(container.classList.
|
|
171
|
+
expect(container.classList).toContain('maplibregl-pseudo-fullscreen');
|
|
172
172
|
expect(fullscreen._container.tagName).toBe('BODY');
|
|
173
173
|
fullscreen._fullscreenButton.dispatchEvent(click);
|
|
174
|
-
expect(container.classList.
|
|
174
|
+
expect(container.classList).not.toContain('maplibregl-pseudo-fullscreen');
|
|
175
175
|
});
|
|
176
176
|
});
|
|
@@ -418,7 +418,7 @@ describe('GeolocateControl with no options', () => {
|
|
|
418
418
|
geolocate.trigger();
|
|
419
419
|
geolocation.sendError({code: 2, message: 'error message'});
|
|
420
420
|
expect(geolocate._watchState).toBe('ACTIVE_ERROR');
|
|
421
|
-
expect(geolocate._geolocateButton.classList.
|
|
421
|
+
expect(geolocate._geolocateButton.classList).toContain('maplibregl-ctrl-geolocate-active-error');
|
|
422
422
|
});
|
|
423
423
|
|
|
424
424
|
test('trigger before added to map', () => {
|
|
@@ -538,9 +538,7 @@ describe('GeolocateControl with no options', () => {
|
|
|
538
538
|
|
|
539
539
|
expect(lngLatAsFixed(map.getCenter(), 4)).toEqual({lat: '10.0000', lng: '20.0000'});
|
|
540
540
|
expect(geolocate._userLocationDotMarker._map).toBeTruthy();
|
|
541
|
-
expect(
|
|
542
|
-
geolocate._userLocationDotMarker._element.classList.contains('maplibregl-user-location-dot-stale')
|
|
543
|
-
).toBeFalsy();
|
|
541
|
+
expect(geolocate._userLocationDotMarker._element.classList).not.toContain('maplibregl-user-location-dot-stale');
|
|
544
542
|
const secontMoveEnd = map.once('moveend');
|
|
545
543
|
geolocation.change({latitude: 40, longitude: 50, accuracy: 60});
|
|
546
544
|
await secontMoveEnd;
|
|
@@ -549,7 +547,7 @@ describe('GeolocateControl with no options', () => {
|
|
|
549
547
|
geolocation.changeError({code: 2, message: 'position unavailable'});
|
|
550
548
|
await errorPromise;
|
|
551
549
|
expect(geolocate._userLocationDotMarker._map).toBeTruthy();
|
|
552
|
-
expect(geolocate._userLocationDotMarker._element.classList.
|
|
550
|
+
expect(geolocate._userLocationDotMarker._element.classList).toContain('maplibregl-user-location-dot-stale');
|
|
553
551
|
});
|
|
554
552
|
|
|
555
553
|
/**
|
|
@@ -73,8 +73,8 @@ describe('GlobeControl', () => {
|
|
|
73
73
|
test('default without call to setProjection', () => {
|
|
74
74
|
const button = map.getContainer().querySelector('.maplibregl-ctrl-globe');
|
|
75
75
|
expect(map.style.projection.name).toBe('mercator');
|
|
76
|
-
expect(button.classList.
|
|
77
|
-
expect(button.classList.
|
|
76
|
+
expect(button.classList).toContain('maplibregl-ctrl-globe');
|
|
77
|
+
expect(button.classList).not.toContain('maplibregl-ctrl-globe-enabled');
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
test('setProjection({type: "mercator" -> "globe")', () => {
|
|
@@ -85,8 +85,8 @@ describe('GlobeControl', () => {
|
|
|
85
85
|
const button = map.getContainer().querySelector('.maplibregl-ctrl-globe-enabled');
|
|
86
86
|
expect(map.style.projection.name).toBe('globe');
|
|
87
87
|
expect(button).not.toBeNull();
|
|
88
|
-
expect(button.classList.
|
|
89
|
-
expect(button.classList.
|
|
88
|
+
expect(button.classList).toContain('maplibregl-ctrl-globe-enabled');
|
|
89
|
+
expect(button.classList).not.toContain('maplibregl-ctrl-globe');
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
test('setProjection({type: "globe" -> "mercator")', () => {
|
|
@@ -96,8 +96,8 @@ describe('GlobeControl', () => {
|
|
|
96
96
|
// mercator = disabled state
|
|
97
97
|
const button = map.getContainer().querySelector('.maplibregl-ctrl-globe');
|
|
98
98
|
expect(map.style.projection.name).toBe('mercator');
|
|
99
|
-
expect(button.classList.
|
|
100
|
-
expect(button.classList.
|
|
99
|
+
expect(button.classList).toContain('maplibregl-ctrl-globe');
|
|
100
|
+
expect(button.classList).not.toContain('maplibregl-ctrl-globe-enabled');
|
|
101
101
|
});
|
|
102
102
|
});
|
|
103
103
|
});
|
package/src/ui/events.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {describe, beforeEach, test, expect, vi, type MockInstance} from 'vitest';
|
|
2
|
+
import Point from '@mapbox/point-geometry';
|
|
2
3
|
import * as timeControl from '../../util/time_control.ts';
|
|
3
4
|
import {Map, type MapOptions} from '../../ui/map.ts';
|
|
4
5
|
import {DOM} from '../../util/dom.ts';
|
|
@@ -656,6 +657,55 @@ describe('ScrollZoomHandler', () => {
|
|
|
656
657
|
map.remove();
|
|
657
658
|
});
|
|
658
659
|
|
|
660
|
+
describe('globe zooms around the pointer', () => {
|
|
661
|
+
async function zoomInAtOffset(offset: Point) {
|
|
662
|
+
const timeControlNow = vi.spyOn(timeControl, 'now');
|
|
663
|
+
timeControlNow.mockReturnValue(1555555555555);
|
|
664
|
+
const map = createMap();
|
|
665
|
+
await map.once('load');
|
|
666
|
+
map.setProjection({type: 'globe'});
|
|
667
|
+
map.setCenter([0, 0]);
|
|
668
|
+
map.setZoom(0);
|
|
669
|
+
map._renderTaskQueue.run();
|
|
670
|
+
|
|
671
|
+
const pointer = map._camera.transform.centerPoint.add(offset);
|
|
672
|
+
const locationBefore = map.unproject(pointer);
|
|
673
|
+
|
|
674
|
+
simulate.wheel(map.getCanvas(), {deltaY: -simulate.magicWheelZoomDelta * 4, clientX: pointer.x, clientY: pointer.y});
|
|
675
|
+
map._renderTaskQueue.run();
|
|
676
|
+
|
|
677
|
+
const locationAfter = map.unproject(pointer);
|
|
678
|
+
const result = {
|
|
679
|
+
driftLng: locationAfter.lng - locationBefore.lng,
|
|
680
|
+
driftLat: locationAfter.lat - locationBefore.lat,
|
|
681
|
+
center: map.getCenter()
|
|
682
|
+
};
|
|
683
|
+
map.remove();
|
|
684
|
+
return result;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
test('keeps the location under the pointer while the globe is small on screen, and still zooms towards it', async () => {
|
|
688
|
+
const wellInsideTheGlobe = new Point(0, -40);
|
|
689
|
+
const {driftLng, driftLat, center} = await zoomInAtOffset(wellInsideTheGlobe);
|
|
690
|
+
expect(driftLng).toBeCloseTo(0, 6);
|
|
691
|
+
expect(driftLat).toBeCloseTo(0, 6);
|
|
692
|
+
expect(center.lat).toBeCloseTo(0.5972, 3);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
test('eases the anchoring off near the horizon instead of dropping it', async () => {
|
|
696
|
+
const nearTheHorizon = new Point(0, -67.5);
|
|
697
|
+
const {driftLat} = await zoomInAtOffset(nearTheHorizon);
|
|
698
|
+
expect(Math.abs(driftLat)).toBeGreaterThan(0.5);
|
|
699
|
+
expect(Math.abs(driftLat)).toBeLessThan(1.5);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
test('leaves zooming with the pointer off the planet unchanged', async () => {
|
|
703
|
+
const offThePlanet = new Point(0, -85);
|
|
704
|
+
const {center} = await zoomInAtOffset(offThePlanet);
|
|
705
|
+
expect(center.lat).toBeCloseTo(0.3545, 3);
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
|
|
659
709
|
test('Clamps to min/max zoom when using mercator projection', async () => {
|
|
660
710
|
const timeControlNow = vi.spyOn(timeControl, 'now');
|
|
661
711
|
const map = createMap();
|
|
@@ -312,11 +312,11 @@ describe('touch zoom rotate', () => {
|
|
|
312
312
|
const map = createMap();
|
|
313
313
|
|
|
314
314
|
const className = 'maplibregl-touch-zoom-rotate';
|
|
315
|
-
expect(map.getCanvasContainer().classList.
|
|
315
|
+
expect(map.getCanvasContainer().classList).toContain(className);
|
|
316
316
|
map.touchZoomRotate.disable();
|
|
317
|
-
expect(map.getCanvasContainer().classList.
|
|
317
|
+
expect(map.getCanvasContainer().classList).not.toContain(className);
|
|
318
318
|
map.touchZoomRotate.enable();
|
|
319
|
-
expect(map.getCanvasContainer().classList.
|
|
319
|
+
expect(map.getCanvasContainer().classList).toContain(className);
|
|
320
320
|
});
|
|
321
321
|
|
|
322
322
|
test('TwoFingersTouchZoomRotateHandler zooms when touching two markers on the same map', () => {
|
|
@@ -8,15 +8,15 @@ import type {ICameraHelper, MapControlsDeltas} from '../geo/projection/camera_he
|
|
|
8
8
|
import type {Terrain} from '../render/terrain.ts';
|
|
9
9
|
import type {ITransform} from '../geo/transform_interface.ts';
|
|
10
10
|
import {Event as MapEvent} from '../util/evented.ts';
|
|
11
|
-
import {
|
|
11
|
+
import {MercatorCoordinate} from '../geo/mercator_coordinate.ts';
|
|
12
|
+
import {beforeMapTest, createMap, createTerrain} from '../util/test/util.ts';
|
|
13
|
+
import simulate from '../../test/unit/lib/simulate_interaction.ts';
|
|
12
14
|
|
|
13
15
|
let map: Map;
|
|
14
16
|
let manager: HandlerManager;
|
|
15
17
|
|
|
16
18
|
beforeEach(() => {
|
|
17
19
|
beforeMapTest();
|
|
18
|
-
map = createMap();
|
|
19
|
-
manager = map._handlers;
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
afterEach(() => {
|
|
@@ -25,6 +25,11 @@ afterEach(() => {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
describe('HandlerManager terrain scenarios', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
map = createMap();
|
|
30
|
+
manager = map._handlers;
|
|
31
|
+
});
|
|
32
|
+
|
|
28
33
|
test('_handleMapControls keeps terrain movement disabled when terrain is not enabled', () => {
|
|
29
34
|
const handleZoom = vi.fn();
|
|
30
35
|
const handlePan = vi.fn();
|
|
@@ -291,3 +296,238 @@ function createEventInProgress(name: keyof EventsInProgress): EventInProgress {
|
|
|
291
296
|
originalEvent: new MapEvent(`${String(name)}start`),
|
|
292
297
|
};
|
|
293
298
|
}
|
|
299
|
+
|
|
300
|
+
describe('terrain gesture anchoring', () => {
|
|
301
|
+
async function setupGestureMap(pitch: number = 0): Promise<HTMLElement> {
|
|
302
|
+
map = createMap({interactive: true, zoom: 11, center: [7.5, 45.9], pitch, bearing: 0});
|
|
303
|
+
map.touchZoomRotate.disableRotation();
|
|
304
|
+
map._handlers._handlersById.tapZoom.disable();
|
|
305
|
+
map.touchPitch.disable();
|
|
306
|
+
await map.once('style.load');
|
|
307
|
+
return map.getCanvas();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function gestureStep(kind: 'touchstart' | 'touchmove', target: HTMLElement, positions: Point[]): void {
|
|
311
|
+
const touches = positions.map((p, i) => ({target, identifier: i + 1, clientX: p.x, clientY: p.y}));
|
|
312
|
+
simulate[kind](target, {touches});
|
|
313
|
+
map._renderTaskQueue.run();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function endGesture(target: HTMLElement): void {
|
|
317
|
+
simulate.touchend(target, {touches: []});
|
|
318
|
+
map._renderTaskQueue.run();
|
|
319
|
+
map._renderTaskQueue.run();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function pinchTouches(mid: Point, halfSpread: number): Point[] {
|
|
323
|
+
return [
|
|
324
|
+
new Point(mid.x, mid.y - halfSpread),
|
|
325
|
+
new Point(mid.x, mid.y + halfSpread),
|
|
326
|
+
];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function slipOf(anchor: LngLat, expectedAt: Point): number {
|
|
330
|
+
return map.project(anchor).dist(expectedAt);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
test('moving-centroid pinch with terrain keeps the grabbed terrain point under the fingers', async () => {
|
|
334
|
+
const target = await setupGestureMap();
|
|
335
|
+
const anchor = new LngLat(7.49, 45.905);
|
|
336
|
+
const anchorCoordinate = MercatorCoordinate.fromLngLat(anchor);
|
|
337
|
+
let raycastElevation = 1000;
|
|
338
|
+
map.terrain = {
|
|
339
|
+
...createTerrain(),
|
|
340
|
+
pointCoordinate: () => new MercatorCoordinate(anchorCoordinate.x, anchorCoordinate.y, raycastElevation),
|
|
341
|
+
} as any as Terrain;
|
|
342
|
+
|
|
343
|
+
// Start the pinch with the finger midpoint exactly where the grabbed point renders
|
|
344
|
+
const start = map.project(anchor);
|
|
345
|
+
expect(start.x).toBeGreaterThan(20);
|
|
346
|
+
expect(start.x).toBeLessThan(180);
|
|
347
|
+
expect(start.y).toBeGreaterThan(20);
|
|
348
|
+
expect(start.y).toBeLessThan(180);
|
|
349
|
+
|
|
350
|
+
gestureStep('touchstart', target, pinchTouches(start, 30));
|
|
351
|
+
let mid = start;
|
|
352
|
+
let halfSpread = 30;
|
|
353
|
+
for (let step = 0; step < 3; step++) {
|
|
354
|
+
mid = mid.add(new Point(15, 10));
|
|
355
|
+
halfSpread += 10;
|
|
356
|
+
gestureStep('touchmove', target, pinchTouches(mid, halfSpread));
|
|
357
|
+
// terrain "loading" mid-gesture must not move the anchor plane
|
|
358
|
+
raycastElevation = 4000;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
expect(map.getZoom()).toBeGreaterThan(11.5);
|
|
362
|
+
expect(map._handlers._terrainGestureAnchorElevation).toBe(1000);
|
|
363
|
+
|
|
364
|
+
const slip = slipOf(anchor, mid);
|
|
365
|
+
endGesture(target);
|
|
366
|
+
expect(slip).toBeLessThan(0.5);
|
|
367
|
+
expect(map._handlers._terrainGestureAnchorElevation).toBeNull();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test('pitched moving-centroid pinch with terrain keeps the grabbed terrain point under the fingers', async () => {
|
|
371
|
+
const target = await setupGestureMap(60);
|
|
372
|
+
const anchor = new LngLat(7.494, 45.904);
|
|
373
|
+
const anchorCoordinate = MercatorCoordinate.fromLngLat(anchor);
|
|
374
|
+
map.terrain = {
|
|
375
|
+
...createTerrain(),
|
|
376
|
+
pointCoordinate: () => new MercatorCoordinate(anchorCoordinate.x, anchorCoordinate.y, 1000),
|
|
377
|
+
} as any as Terrain;
|
|
378
|
+
|
|
379
|
+
const start = map.project(anchor);
|
|
380
|
+
expect(start.x).toBeGreaterThan(20);
|
|
381
|
+
expect(start.x).toBeLessThan(180);
|
|
382
|
+
expect(start.y).toBeGreaterThan(20);
|
|
383
|
+
expect(start.y).toBeLessThan(180);
|
|
384
|
+
|
|
385
|
+
gestureStep('touchstart', target, pinchTouches(start, 25));
|
|
386
|
+
let mid = start;
|
|
387
|
+
let halfSpread = 25;
|
|
388
|
+
for (let step = 0; step < 3; step++) {
|
|
389
|
+
mid = mid.add(new Point(12, 8));
|
|
390
|
+
halfSpread += 8;
|
|
391
|
+
gestureStep('touchmove', target, pinchTouches(mid, halfSpread));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
expect(map.getZoom()).toBeGreaterThan(11.5);
|
|
395
|
+
|
|
396
|
+
const slip = slipOf(anchor, mid);
|
|
397
|
+
endGesture(target);
|
|
398
|
+
expect(slip).toBeLessThan(0.5);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test('touch drag with terrain keeps a grabbed point elevated above the center plane under the finger', async () => {
|
|
402
|
+
const target = await setupGestureMap();
|
|
403
|
+
// a 2000 m ridge east of the center, with the center itself at 300 m
|
|
404
|
+
const ridgeStartLng = map.getCenter().lng + 0.003;
|
|
405
|
+
const elevationAt = (lngLat: LngLat) => lngLat.lng > ridgeStartLng ? 2000 : 300;
|
|
406
|
+
const anchor = new LngLat(map.getCenter().lng + 0.012, map.getCenter().lat);
|
|
407
|
+
const anchorCoordinate = MercatorCoordinate.fromLngLat(anchor);
|
|
408
|
+
map.terrain = {
|
|
409
|
+
...createTerrain(),
|
|
410
|
+
getElevationForLngLat: (lngLat: LngLat) => elevationAt(lngLat),
|
|
411
|
+
getElevationForLngLatZoom: (lngLat: LngLat) => elevationAt(lngLat),
|
|
412
|
+
pointCoordinate: () => new MercatorCoordinate(anchorCoordinate.x, anchorCoordinate.y, 2000),
|
|
413
|
+
} as any as Terrain;
|
|
414
|
+
map._camera.transform.setElevation(300);
|
|
415
|
+
|
|
416
|
+
const start = map.project(anchor);
|
|
417
|
+
expect(start.x).toBeGreaterThan(20);
|
|
418
|
+
expect(start.x).toBeLessThan(180);
|
|
419
|
+
|
|
420
|
+
gestureStep('touchstart', target, [start]);
|
|
421
|
+
let finger = start;
|
|
422
|
+
for (let step = 0; step < 3; step++) {
|
|
423
|
+
finger = finger.add(new Point(-20, 0));
|
|
424
|
+
gestureStep('touchmove', target, [finger]);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
expect(map.getCenter().lng).not.toBeCloseTo(7.5, 5);
|
|
428
|
+
|
|
429
|
+
const slip = slipOf(anchor, finger);
|
|
430
|
+
endGesture(target);
|
|
431
|
+
expect(slip).toBeLessThan(0.5);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test('center-anchored pinch over terrain still pans with the centroid', async () => {
|
|
435
|
+
const target = await setupGestureMap();
|
|
436
|
+
map.touchZoomRotate.enable({around: 'center'});
|
|
437
|
+
const anchor = new LngLat(7.49, 45.905);
|
|
438
|
+
const anchorCoordinate = MercatorCoordinate.fromLngLat(anchor);
|
|
439
|
+
map.terrain = {
|
|
440
|
+
...createTerrain(),
|
|
441
|
+
pointCoordinate: () => new MercatorCoordinate(anchorCoordinate.x, anchorCoordinate.y, 1000),
|
|
442
|
+
} as any as Terrain;
|
|
443
|
+
|
|
444
|
+
const startCenter = map.getCenter();
|
|
445
|
+
const start = new Point(80, 90);
|
|
446
|
+
gestureStep('touchstart', target, pinchTouches(start, 30));
|
|
447
|
+
let mid = start;
|
|
448
|
+
let halfSpread = 30;
|
|
449
|
+
for (let step = 0; step < 3; step++) {
|
|
450
|
+
mid = mid.add(new Point(15, 10));
|
|
451
|
+
halfSpread += 10;
|
|
452
|
+
gestureStep('touchmove', target, pinchTouches(mid, halfSpread));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
expect(map.getZoom()).toBeGreaterThan(11.5);
|
|
456
|
+
// center-anchored zoom keeps the center fixed, so only the pan moves it
|
|
457
|
+
const centerMovedPx = map.project(startCenter).dist(new Point(100, 100));
|
|
458
|
+
endGesture(target);
|
|
459
|
+
expect(centerMovedPx).toBeGreaterThan(20);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test('falls back to the center-elevation behavior when the terrain under the gesture is not loaded', async () => {
|
|
463
|
+
const target = await setupGestureMap();
|
|
464
|
+
map.terrain = {
|
|
465
|
+
...createTerrain(),
|
|
466
|
+
pointCoordinate: () => null,
|
|
467
|
+
} as any as Terrain;
|
|
468
|
+
|
|
469
|
+
const startCenter = map.getCenter();
|
|
470
|
+
const start = new Point(80, 90);
|
|
471
|
+
gestureStep('touchstart', target, pinchTouches(start, 30));
|
|
472
|
+
let mid = start;
|
|
473
|
+
let halfSpread = 30;
|
|
474
|
+
for (let step = 0; step < 3; step++) {
|
|
475
|
+
mid = mid.add(new Point(15, 10));
|
|
476
|
+
halfSpread += 10;
|
|
477
|
+
gestureStep('touchmove', target, pinchTouches(mid, halfSpread));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// the gesture still zooms and pans sanely near where it started
|
|
481
|
+
expect(map.getZoom()).toBeGreaterThan(11.5);
|
|
482
|
+
expect(Math.abs(map.getCenter().lng - startCenter.lng)).toBeLessThan(0.5);
|
|
483
|
+
expect(Math.abs(map.getCenter().lat - startCenter.lat)).toBeLessThan(0.5);
|
|
484
|
+
endGesture(target);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test('falls back to the center-elevation behavior when the grabbed terrain point is above the camera altitude', async () => {
|
|
488
|
+
const target = await setupGestureMap();
|
|
489
|
+
map.terrain = {
|
|
490
|
+
...createTerrain(),
|
|
491
|
+
pointCoordinate: () => new MercatorCoordinate(0.5, 0.35, 1e6),
|
|
492
|
+
} as any as Terrain;
|
|
493
|
+
|
|
494
|
+
const startCenter = map.getCenter();
|
|
495
|
+
const start = new Point(80, 90);
|
|
496
|
+
gestureStep('touchstart', target, pinchTouches(start, 30));
|
|
497
|
+
let mid = start;
|
|
498
|
+
let halfSpread = 30;
|
|
499
|
+
for (let step = 0; step < 3; step++) {
|
|
500
|
+
mid = mid.add(new Point(15, 10));
|
|
501
|
+
halfSpread += 10;
|
|
502
|
+
gestureStep('touchmove', target, pinchTouches(mid, halfSpread));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// the gesture still zooms and pans sanely near where it started
|
|
506
|
+
expect(map.getZoom()).toBeGreaterThan(11.5);
|
|
507
|
+
expect(Math.abs(map.getCenter().lng - startCenter.lng)).toBeLessThan(0.5);
|
|
508
|
+
expect(Math.abs(map.getCenter().lat - startCenter.lat)).toBeLessThan(0.5);
|
|
509
|
+
endGesture(target);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test('control: the same moving-centroid pinch without terrain keeps the grabbed point under the fingers', async () => {
|
|
513
|
+
const target = await setupGestureMap();
|
|
514
|
+
|
|
515
|
+
const start = new Point(67, 76);
|
|
516
|
+
const anchor = map.unproject(start);
|
|
517
|
+
|
|
518
|
+
gestureStep('touchstart', target, pinchTouches(start, 30));
|
|
519
|
+
let mid = start;
|
|
520
|
+
let halfSpread = 30;
|
|
521
|
+
for (let step = 0; step < 3; step++) {
|
|
522
|
+
mid = mid.add(new Point(15, 10));
|
|
523
|
+
halfSpread += 10;
|
|
524
|
+
gestureStep('touchmove', target, pinchTouches(mid, halfSpread));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
expect(map.getZoom()).toBeGreaterThan(11.5);
|
|
528
|
+
|
|
529
|
+
const slip = slipOf(anchor, mid);
|
|
530
|
+
endGesture(target);
|
|
531
|
+
expect(slip).toBeLessThan(0.5);
|
|
532
|
+
});
|
|
533
|
+
});
|