maplibre-gl 6.2.0 → 6.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/dist/maplibre-gl-dev.mjs +1108 -427
- package/dist/maplibre-gl-dev.mjs.map +1 -1
- package/dist/maplibre-gl-shared-dev.mjs +359 -291
- 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 +467 -151
- package/dist/maplibre-gl.mjs +49 -49
- package/dist/maplibre-gl.mjs.map +1 -1
- package/package.json +12 -22
- package/src/data/bucket/fill_extrusion_bucket.ts +1 -13
- package/src/data/bucket/round_polygon_corners.test.ts +84 -36
- package/src/data/bucket/round_polygon_corners.ts +90 -39
- package/src/data/extent_bounds.ts +28 -1
- 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 +154 -1
- package/src/geo/projection/globe_transform.ts +7 -3
- package/src/geo/projection/globe_utils.ts +189 -2
- 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.test.ts +24 -0
- package/src/geo/projection/vertical_perspective_camera_helper.ts +71 -41
- 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 +180 -0
- package/src/render/image_atlas.ts +52 -6
- package/src/render/image_manager.test.ts +111 -0
- package/src/render/image_manager.ts +143 -144
- package/src/render/painter.ts +6 -14
- package/src/render/pattern_atlas.test.ts +131 -0
- package/src/render/pattern_atlas.ts +144 -0
- package/src/render/update_pattern_positions_in_program.test.ts +1 -1
- package/src/shaders/glsl/_prelude.vertex.glsl +10 -11
- 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/color_relief.fragment.glsl +1 -2
- package/src/shaders/glsl/color_relief.fragment.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/hillshade_prepare.fragment.glsl +12 -12
- package/src/shaders/glsl/hillshade_prepare.fragment.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 +246 -38
- package/src/source/image_source.ts +132 -19
- package/src/source/raster_dem_tile_source.test.ts +37 -3
- package/src/source/raster_dem_tile_source.ts +1 -2
- package/src/source/raster_dem_tile_worker_source.test.ts +1 -1
- package/src/source/raster_tile_source.test.ts +5 -26
- package/src/source/raster_tile_source.ts +4 -3
- 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 +75 -10
- package/src/style/style.ts +37 -64
- 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/cross_tile_symbol_index.test.ts +31 -0
- package/src/symbol/cross_tile_symbol_index.ts +67 -37
- 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.test.ts +69 -1
- package/src/tile/tile_manager.ts +16 -11
- 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 +17 -6
- package/src/ui/handler_inertia.test.ts +91 -0
- package/src/ui/handler_inertia.ts +65 -14
- 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 +55 -0
- package/src/ui/map_tests/map_style.test.ts +9 -8
- package/src/ui/map_tests/map_webgl.test.ts +30 -0
- package/src/ui/marker.test.ts +177 -42
- package/src/ui/marker.ts +120 -10
- 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/image_request.ts +31 -1
- 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_background.ts +1 -1
- package/src/webgl/draw/draw_raster.ts +11 -6
- package/src/webgl/program/pattern.ts +3 -3
- 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,91 @@
|
|
|
1
|
+
import {describe, beforeEach, afterEach, test, expect} from 'vitest';
|
|
2
|
+
import Point from '@mapbox/point-geometry';
|
|
3
|
+
import {DOM} from '../util/dom.ts';
|
|
4
|
+
import {beforeMapTest} from '../util/test/util.ts';
|
|
5
|
+
import {restoreNow, setNow} from '../util/time_control.ts';
|
|
6
|
+
import {Map} from './map.ts';
|
|
7
|
+
import type {EaseToOptions} from './camera.ts';
|
|
8
|
+
import {HandlerInertia} from './handler_inertia.ts';
|
|
9
|
+
|
|
10
|
+
type PanGesture = {
|
|
11
|
+
/**
|
|
12
|
+
* The pixels panned in each recorded frame, oldest first. `null` records a frame without
|
|
13
|
+
* any delta, which is what a terrain gesture does while the camera is not moving.
|
|
14
|
+
*/
|
|
15
|
+
frames: Array<number | null>;
|
|
16
|
+
/**
|
|
17
|
+
* The milliseconds between the recorded frames.
|
|
18
|
+
*/
|
|
19
|
+
frameTime?: number;
|
|
20
|
+
/**
|
|
21
|
+
* The milliseconds the gesture is held still between the last recorded frame and the release.
|
|
22
|
+
*/
|
|
23
|
+
holdTime?: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Records a horizontal pan gesture and returns the inertial ease it produces, if any.
|
|
28
|
+
*/
|
|
29
|
+
function panAndRelease(inertia: HandlerInertia, {frames, frameTime = 16, holdTime = 0}: PanGesture) {
|
|
30
|
+
let time = 1000;
|
|
31
|
+
setNow(time);
|
|
32
|
+
for (const pixels of frames) {
|
|
33
|
+
setNow(time += frameTime);
|
|
34
|
+
inertia.record(pixels === null ? {} : {panDelta: new Point(pixels, 0)});
|
|
35
|
+
}
|
|
36
|
+
setNow(time + holdTime);
|
|
37
|
+
return inertia._onMoveEnd();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns the horizontal distance the map keeps panning after the gesture ended.
|
|
42
|
+
*/
|
|
43
|
+
function panOnDistance(ease: EaseToOptions) {
|
|
44
|
+
return (ease.offset as Point).x;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('HandlerInertia', () => {
|
|
48
|
+
let map: Map;
|
|
49
|
+
let inertia: HandlerInertia;
|
|
50
|
+
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
beforeMapTest();
|
|
53
|
+
map = new Map({container: DOM.create('div', '', window.document.body)});
|
|
54
|
+
inertia = new HandlerInertia(map);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
restoreNow();
|
|
59
|
+
map.remove();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('pans on when the gesture is released while still moving', () => {
|
|
63
|
+
const ease = panAndRelease(inertia, {frames: [20, 20, 20, 20, 20]});
|
|
64
|
+
|
|
65
|
+
expect(panOnDistance(ease)).toBeGreaterThan(50);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('barely pans on when the gesture is held still before being released', () => {
|
|
69
|
+
const ease = panAndRelease(inertia, {frames: [20, 20, 20, 20, 20], holdTime: 100});
|
|
70
|
+
|
|
71
|
+
expect(Math.abs(panOnDistance(ease))).toBeLessThan(5);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('pans on at frame rates too low for two frames to fit into the velocity window', () => {
|
|
75
|
+
const ease = panAndRelease(inertia, {frames: [80, 80, 80], frameTime: 80});
|
|
76
|
+
|
|
77
|
+
expect(panOnDistance(ease)).toBeGreaterThan(20);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('does not pan on when a single frame was recorded, twice in a row', () => {
|
|
81
|
+
expect(panAndRelease(inertia, {frames: [20]})).toBeUndefined();
|
|
82
|
+
// the frame left over by the first gesture must not be measured as part of the second
|
|
83
|
+
expect(panAndRelease(inertia, {frames: [20]})).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('does not ease when the recorded frames hold no movement', () => {
|
|
87
|
+
const ease = panAndRelease(inertia, {frames: [20, 20, 20, null, null, null, null]});
|
|
88
|
+
|
|
89
|
+
expect(ease).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -3,6 +3,7 @@ import {bezier, clamp, extend, evaluateZoomSnap} from '../util/util.ts';
|
|
|
3
3
|
import Point from '@mapbox/point-geometry';
|
|
4
4
|
|
|
5
5
|
import type {DragPanOptions} from './handler/shim/drag_pan.ts';
|
|
6
|
+
import type {HandlerResult} from './handler_manager.ts';
|
|
6
7
|
import type {EaseToOptions} from './camera.ts';
|
|
7
8
|
import type {Map} from './map.ts';
|
|
8
9
|
|
|
@@ -43,12 +44,24 @@ export type InertiaOptions = {
|
|
|
43
44
|
maxSpeed: number;
|
|
44
45
|
};
|
|
45
46
|
|
|
47
|
+
/**
|
|
48
|
+
* The maximum age of a recorded gesture update, in milliseconds.
|
|
49
|
+
*/
|
|
50
|
+
const BUFFER_CUTOFF = 160;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The time window the gesture velocity is measured over, in milliseconds.
|
|
54
|
+
*/
|
|
55
|
+
const VELOCITY_WINDOW = 60;
|
|
56
|
+
|
|
57
|
+
type InertiaBufferEntry = {
|
|
58
|
+
time: number;
|
|
59
|
+
settings: HandlerResult;
|
|
60
|
+
};
|
|
61
|
+
|
|
46
62
|
export class HandlerInertia {
|
|
47
63
|
_map: Map;
|
|
48
|
-
_inertiaBuffer:
|
|
49
|
-
time: number;
|
|
50
|
-
settings: any;
|
|
51
|
-
}>;
|
|
64
|
+
_inertiaBuffer: InertiaBufferEntry[];
|
|
52
65
|
|
|
53
66
|
constructor(map: Map) {
|
|
54
67
|
this._map = map;
|
|
@@ -59,23 +72,54 @@ export class HandlerInertia {
|
|
|
59
72
|
this._inertiaBuffer = [];
|
|
60
73
|
}
|
|
61
74
|
|
|
62
|
-
record(settings:
|
|
75
|
+
record(settings: HandlerResult): void {
|
|
63
76
|
this._drainInertiaBuffer();
|
|
64
77
|
this._inertiaBuffer.push({time: now(), settings});
|
|
65
78
|
}
|
|
66
79
|
|
|
67
80
|
_drainInertiaBuffer(): void {
|
|
68
81
|
const inertia = this._inertiaBuffer,
|
|
69
|
-
currentTime = now()
|
|
70
|
-
cutoff = 160; //msec
|
|
82
|
+
currentTime = now();
|
|
71
83
|
|
|
72
|
-
while (inertia.length > 0 && currentTime - inertia[0].time >
|
|
84
|
+
while (inertia.length > 0 && currentTime - inertia[0].time > BUFFER_CUTOFF)
|
|
73
85
|
inertia.shift();
|
|
74
86
|
}
|
|
75
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Returns the entries the velocity is measured from: everything recorded within
|
|
90
|
+
* {@link VELOCITY_WINDOW} before now, but never fewer than the last two, so that
|
|
91
|
+
* inertia also works below two recorded updates per window.
|
|
92
|
+
*/
|
|
93
|
+
_getVelocityEntries(): InertiaBufferEntry[] {
|
|
94
|
+
const inertia = this._inertiaBuffer;
|
|
95
|
+
const windowStart = now() - VELOCITY_WINDOW;
|
|
96
|
+
|
|
97
|
+
let firstIndex = Math.max(0, inertia.length - 2);
|
|
98
|
+
while (firstIndex > 0 && inertia[firstIndex - 1].time >= windowStart) {
|
|
99
|
+
firstIndex--;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return inertia.slice(firstIndex);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Returns the ease which continues the gesture that has just ended.
|
|
107
|
+
*
|
|
108
|
+
* The velocity is measured over the time up to the release and not up to the last
|
|
109
|
+
* recorded movement, so that a gesture held still before being released has a lower
|
|
110
|
+
* velocity, down to no inertia at all. The delta of an entry happened before it was
|
|
111
|
+
* recorded, so the first entry only marks the start of the measured interval; anchors
|
|
112
|
+
* are the exception, as they describe the state at the moment of their entry.
|
|
113
|
+
*
|
|
114
|
+
* @param panInertiaOptions - overrides for the pan inertia defaults
|
|
115
|
+
* @returns the ease options, or `undefined` when the measured interval holds no
|
|
116
|
+
* movement, which terrain gestures produce by recording updates without any delta
|
|
117
|
+
*/
|
|
76
118
|
_onMoveEnd(panInertiaOptions?: DragPanOptions | boolean): EaseToOptions {
|
|
77
119
|
this._drainInertiaBuffer();
|
|
78
|
-
|
|
120
|
+
const entries = this._getVelocityEntries();
|
|
121
|
+
if (entries.length < 2) {
|
|
122
|
+
this.clear();
|
|
79
123
|
return;
|
|
80
124
|
}
|
|
81
125
|
|
|
@@ -89,18 +133,25 @@ export class HandlerInertia {
|
|
|
89
133
|
around: undefined
|
|
90
134
|
};
|
|
91
135
|
|
|
92
|
-
for (const {settings} of
|
|
136
|
+
for (const {settings} of entries) {
|
|
137
|
+
if (settings.around) deltas.around = settings.around;
|
|
138
|
+
if (settings.pinchAround) deltas.pinchAround = settings.pinchAround;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const {settings} of entries.slice(1)) {
|
|
93
142
|
deltas.zoom += settings.zoomDelta || 0;
|
|
94
143
|
deltas.bearing += settings.bearingDelta || 0;
|
|
95
144
|
deltas.pitch += settings.pitchDelta || 0;
|
|
96
145
|
deltas.roll += settings.rollDelta || 0;
|
|
97
146
|
if (settings.panDelta) deltas.pan._add(settings.panDelta);
|
|
98
|
-
if (settings.around) deltas.around = settings.around;
|
|
99
|
-
if (settings.pinchAround) deltas.pinchAround = settings.pinchAround;
|
|
100
147
|
}
|
|
101
148
|
|
|
102
|
-
|
|
103
|
-
|
|
149
|
+
if (!deltas.pan.mag() && !deltas.zoom && !deltas.bearing && !deltas.pitch && !deltas.roll) {
|
|
150
|
+
this.clear();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const duration = now() - entries[0].time;
|
|
104
155
|
|
|
105
156
|
const easeOptions = {} as any;
|
|
106
157
|
|
|
@@ -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
|
+
});
|
|
@@ -31,6 +31,12 @@ import type {Terrain} from '../render/terrain.ts';
|
|
|
31
31
|
|
|
32
32
|
const isMoving = (p: EventsInProgress) => p.zoom || p.drag || p.roll || p.pitch || p.rotate;
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* A terrain gesture's anchor plane must stay below this fraction of the camera's
|
|
36
|
+
* altitude over the center-elevation plane, or the ray-plane solve degenerates.
|
|
37
|
+
*/
|
|
38
|
+
const TERRAIN_ANCHOR_MAX_CAMERA_ALTITUDE_FRACTION = 0.9;
|
|
39
|
+
|
|
34
40
|
class RenderFrameEvent extends Event {
|
|
35
41
|
type: 'renderFrame';
|
|
36
42
|
timeStamp: number;
|
|
@@ -162,6 +168,11 @@ export class HandlerManager {
|
|
|
162
168
|
_updatingCamera: boolean;
|
|
163
169
|
_changes: Array<[HandlerResult, EventsInProgress, {[handlerName: string]: Event}]>;
|
|
164
170
|
_terrainMovement: boolean;
|
|
171
|
+
/**
|
|
172
|
+
* Elevation in meters of the terrain point grabbed at gesture start; kept for the
|
|
173
|
+
* same lifetime as the elevation freeze. Null when that terrain was not available.
|
|
174
|
+
*/
|
|
175
|
+
_terrainGestureAnchorElevation: number | null = null;
|
|
165
176
|
_zoom: {handlerName: string};
|
|
166
177
|
_previousActiveHandlers: {[x: string]: Handler};
|
|
167
178
|
_listeners: Array<[Window | Document | HTMLElement, string, {
|
|
@@ -542,17 +553,10 @@ export class HandlerManager {
|
|
|
542
553
|
// stop any ongoing camera animations (easeTo, flyTo)
|
|
543
554
|
this._camera.stop(true);
|
|
544
555
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
if (pinchAround !== undefined) {
|
|
548
|
-
around = pinchAround;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
around ||= this._camera.transform.centerPoint;
|
|
556
|
+
const {panDelta, zoomDelta, bearingDelta, pitchDelta, rollDelta} = combinedResult;
|
|
552
557
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
}
|
|
558
|
+
let {around, aroundOnSurface} = this._resolveAround(combinedResult, terrain, tr);
|
|
559
|
+
const aroundElevation = terrain ? this._terrainGestureElevation(terrain, around, aroundOnSurface, tr, combinedEventsInProgress) : undefined;
|
|
556
560
|
|
|
557
561
|
const deltasForHelper: MapControlsDeltas = {
|
|
558
562
|
panDelta,
|
|
@@ -561,17 +565,14 @@ export class HandlerManager {
|
|
|
561
565
|
pitchDelta,
|
|
562
566
|
bearingDelta,
|
|
563
567
|
around,
|
|
568
|
+
aroundElevation,
|
|
564
569
|
};
|
|
565
570
|
|
|
566
571
|
// Pre-zoom location under the mouse cursor is required for accurate mercator panning and zooming
|
|
567
572
|
if (this._camera.cameraHelper.useGlobeControls && !tr.isPointOnMapSurface(around)) {
|
|
568
573
|
around = tr.centerPoint;
|
|
569
574
|
}
|
|
570
|
-
|
|
571
|
-
// center directly, instead of computing it from the screen point
|
|
572
|
-
const preZoomAroundLoc = around.distSqr(tr.centerPoint) < 1.0e-2 ?
|
|
573
|
-
tr.center :
|
|
574
|
-
tr.screenPointToLocation(panDelta ? around.sub(panDelta) : around);
|
|
575
|
+
const preZoomAroundLoc = this._computePreZoomAroundLoc(tr, around, panDelta, aroundElevation);
|
|
575
576
|
|
|
576
577
|
this._handleMapControls({
|
|
577
578
|
terrain,
|
|
@@ -590,6 +591,65 @@ export class HandlerManager {
|
|
|
590
591
|
|
|
591
592
|
}
|
|
592
593
|
|
|
594
|
+
/**
|
|
595
|
+
* The gesture's anchor point: the pinch midpoint when pinching, otherwise the
|
|
596
|
+
* pan's anchor, clamped to the center point when it does not lie on the map.
|
|
597
|
+
*/
|
|
598
|
+
_resolveAround(combinedResult: HandlerResult, terrain: Terrain | null, tr: ITransform): {around: Point; aroundOnSurface: boolean} {
|
|
599
|
+
let around = combinedResult.pinchAround !== undefined ? combinedResult.pinchAround : combinedResult.around;
|
|
600
|
+
around ||= this._camera.transform.centerPoint;
|
|
601
|
+
if (terrain && !tr.isPointOnMapSurface(around)) {
|
|
602
|
+
return {around: tr.centerPoint, aroundOnSurface: false};
|
|
603
|
+
}
|
|
604
|
+
return {around, aroundOnSurface: true};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* The elevation of the plane a terrain gesture is solved on: the elevation of the
|
|
609
|
+
* terrain point grabbed at gesture start, captured here on the gesture's first
|
|
610
|
+
* frame. Undefined means the gesture is solved at the center's elevation instead —
|
|
611
|
+
* when the grabbed terrain is not loaded, the anchor is the center point (a
|
|
612
|
+
* center-point anchor is skipped by the camera helper and would lose the pan), or
|
|
613
|
+
* the anchor plane is too close to the camera for a stable ray-plane solve.
|
|
614
|
+
*/
|
|
615
|
+
_terrainGestureElevation(terrain: Terrain, around: Point, aroundOnSurface: boolean, tr: ITransform, combinedEventsInProgress: EventsInProgress): number | undefined {
|
|
616
|
+
if (!aroundOnSurface) {
|
|
617
|
+
return undefined;
|
|
618
|
+
}
|
|
619
|
+
if (!this._terrainMovement && (combinedEventsInProgress.drag || combinedEventsInProgress.zoom)) {
|
|
620
|
+
const anchor = terrain.pointCoordinate(around);
|
|
621
|
+
this._terrainGestureAnchorElevation = anchor ? anchor.z : null;
|
|
622
|
+
}
|
|
623
|
+
if (this._terrainGestureAnchorElevation === null) {
|
|
624
|
+
return undefined;
|
|
625
|
+
}
|
|
626
|
+
const elevation = this._terrainGestureAnchorElevation;
|
|
627
|
+
if (around.distSqr(tr.centerPoint) < 1.0e-2) {
|
|
628
|
+
return undefined;
|
|
629
|
+
}
|
|
630
|
+
if (elevation - tr.elevation >= TERRAIN_ANCHOR_MAX_CAMERA_ALTITUDE_FRACTION * (tr.getCameraAltitude() - tr.elevation)) {
|
|
631
|
+
return undefined;
|
|
632
|
+
}
|
|
633
|
+
return elevation;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* The location that was under `around` before this frame's deltas — the point the
|
|
638
|
+
* camera helper re-anchors after zooming — solved at `aroundElevation` when a
|
|
639
|
+
* terrain gesture provides one. When rotating about the center point, the
|
|
640
|
+
* transform's center is used directly to avoid numerical issues near the horizon.
|
|
641
|
+
*/
|
|
642
|
+
_computePreZoomAroundLoc(tr: ITransform, around: Point, panDelta: Point | undefined, aroundElevation: number | undefined): LngLat {
|
|
643
|
+
if (around.distSqr(tr.centerPoint) < 1.0e-2) {
|
|
644
|
+
return tr.center;
|
|
645
|
+
}
|
|
646
|
+
const aroundPreviousPoint = panDelta ? around.sub(panDelta) : around;
|
|
647
|
+
if (aroundElevation !== undefined) {
|
|
648
|
+
return tr.screenPointToLocationAtElevation(aroundPreviousPoint, aroundElevation);
|
|
649
|
+
}
|
|
650
|
+
return tr.screenPointToLocation(aroundPreviousPoint);
|
|
651
|
+
}
|
|
652
|
+
|
|
593
653
|
_handleMapControls({
|
|
594
654
|
terrain,
|
|
595
655
|
tr,
|
|
@@ -623,7 +683,9 @@ export class HandlerManager {
|
|
|
623
683
|
return;
|
|
624
684
|
}
|
|
625
685
|
|
|
626
|
-
if (
|
|
686
|
+
if (deltasForHelper.aroundElevation === undefined &&
|
|
687
|
+
combinedEventsInProgress.drag && this._terrainMovement && panDelta) {
|
|
688
|
+
// no usable anchor: drag by pixel-delta on the center-elevation plane
|
|
627
689
|
tr.setCenter(tr.screenPointToLocation(tr.centerPoint.sub(panDelta)));
|
|
628
690
|
return;
|
|
629
691
|
}
|
|
@@ -685,6 +747,7 @@ export class HandlerManager {
|
|
|
685
747
|
if (finishedMoving && this._terrainMovement) {
|
|
686
748
|
this._camera.elevationFreeze = false;
|
|
687
749
|
this._terrainMovement = false;
|
|
750
|
+
this._terrainGestureAnchorElevation = null;
|
|
688
751
|
const tr = this._camera.getTransformForUpdate();
|
|
689
752
|
if (this._map.getCenterClampedToGround()) {
|
|
690
753
|
tr.recalculateZoomAndCenter(this._map.terrain);
|