maplibre-gl 6.3.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 +590 -208
- package/dist/maplibre-gl-dev.mjs.map +1 -1
- package/dist/maplibre-gl-shared-dev.mjs +258 -235
- 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 +2 -2
- package/dist/maplibre-gl-worker.mjs +2 -2
- package/dist/maplibre-gl-worker.mjs.map +1 -1
- package/dist/maplibre-gl.d.ts +258 -97
- package/dist/maplibre-gl.mjs +18 -18
- package/dist/maplibre-gl.mjs.map +1 -1
- package/package.json +7 -7
- 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/geo/projection/globe_transform.test.ts +129 -1
- package/src/geo/projection/globe_utils.ts +189 -2
- package/src/geo/projection/vertical_perspective_camera_helper.test.ts +24 -0
- package/src/geo/projection/vertical_perspective_camera_helper.ts +11 -10
- package/src/render/image_atlas.test.ts +110 -2
- package/src/render/image_atlas.ts +35 -4
- package/src/render/image_manager.test.ts +66 -1
- package/src/render/image_manager.ts +136 -144
- package/src/render/painter.ts +5 -2
- package/src/render/pattern_atlas.test.ts +131 -0
- package/src/render/pattern_atlas.ts +144 -0
- package/src/shaders/glsl/_prelude.vertex.glsl +8 -8
- package/src/shaders/glsl/_prelude.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/hillshade_prepare.fragment.glsl +12 -12
- package/src/shaders/glsl/hillshade_prepare.fragment.glsl.g.ts +1 -1
- package/src/source/image_source.test.ts +41 -31
- package/src/source/image_source.ts +14 -15
- 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_tile_source.test.ts +5 -26
- package/src/source/raster_tile_source.ts +4 -3
- package/src/style/style.test.ts +64 -0
- package/src/style/style.ts +37 -64
- package/src/symbol/cross_tile_symbol_index.test.ts +31 -0
- package/src/symbol/cross_tile_symbol_index.ts +67 -37
- package/src/tile/tile_manager.test.ts +69 -1
- package/src/tile/tile_manager.ts +14 -9
- package/src/ui/handler/two_fingers_touch.test.ts +14 -3
- package/src/ui/handler_inertia.test.ts +91 -0
- package/src/ui/handler_inertia.ts +65 -14
- package/src/ui/map_tests/map_images.test.ts +26 -0
- package/src/ui/map_tests/map_webgl.test.ts +2 -1
- package/src/ui/marker.test.ts +151 -0
- package/src/ui/marker.ts +120 -10
- package/src/util/image_request.ts +31 -1
- package/src/webgl/draw/draw_background.ts +1 -1
- package/src/webgl/program/pattern.ts +3 -3
|
@@ -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
|
|
|
@@ -95,6 +95,32 @@ test('map fires `styleimagemissing` when missing style image resolver returns no
|
|
|
95
95
|
expect(map.hasImage(id)).toBeFalsy();
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
+
test('map preserves resolved images when missing style image resolver rejects another image', async () => {
|
|
99
|
+
const map = createMap();
|
|
100
|
+
|
|
101
|
+
const resolvedId = 'resolved-style-image';
|
|
102
|
+
const rejectedId = 'rejected-style-image';
|
|
103
|
+
const sampleImage = {width: 2, height: 1, data: new Uint8Array(8)};
|
|
104
|
+
const missingImageEventSpy = vi.fn();
|
|
105
|
+
|
|
106
|
+
map.setMissingStyleImageResolver(async (imageId) => {
|
|
107
|
+
if (imageId === rejectedId) {
|
|
108
|
+
throw new Error('Failed to resolve image');
|
|
109
|
+
}
|
|
110
|
+
map.addImage(imageId, sampleImage);
|
|
111
|
+
});
|
|
112
|
+
map.on('styleimagemissing', missingImageEventSpy);
|
|
113
|
+
|
|
114
|
+
const generatedImages = await map.style.imageManager.getImages([resolvedId, rejectedId]);
|
|
115
|
+
|
|
116
|
+
expect(generatedImages[resolvedId].data.width).toEqual(sampleImage.width);
|
|
117
|
+
expect(generatedImages[rejectedId]).toBeUndefined();
|
|
118
|
+
expect(missingImageEventSpy).toHaveBeenCalledTimes(1);
|
|
119
|
+
expect(missingImageEventSpy.mock.calls[0][0].id).toBe(rejectedId);
|
|
120
|
+
expect(map.hasImage(resolvedId)).toBeTruthy();
|
|
121
|
+
expect(map.hasImage(rejectedId)).toBeFalsy();
|
|
122
|
+
});
|
|
123
|
+
|
|
98
124
|
test('map keeps missing style image resolver after replacing the style', async () => {
|
|
99
125
|
const map = createMap();
|
|
100
126
|
|
|
@@ -104,6 +104,7 @@ test('a WebGL style image is told to release its GPU resources on context loss,
|
|
|
104
104
|
onRemove: vi.fn()
|
|
105
105
|
};
|
|
106
106
|
map.addImage('gpu-image', userImage);
|
|
107
|
+
const versionBeforeContextLoss = map.getImage('gpu-image').version;
|
|
107
108
|
|
|
108
109
|
const contextLostPromise = map.once('webglcontextlost');
|
|
109
110
|
canvas.dispatchEvent(new window.Event('webglcontextlost'));
|
|
@@ -117,7 +118,7 @@ test('a WebGL style image is told to release its GPU resources on context loss,
|
|
|
117
118
|
|
|
118
119
|
expect(map.hasImage('gpu-image')).toBe(true);
|
|
119
120
|
expect(map.getImage('gpu-image').userImage).toBe(userImage);
|
|
120
|
-
expect(map.
|
|
121
|
+
expect(map.getImage('gpu-image').version).toBe(versionBeforeContextLoss + 1);
|
|
121
122
|
|
|
122
123
|
map.remove();
|
|
123
124
|
});
|
package/src/ui/marker.test.ts
CHANGED
|
@@ -812,6 +812,157 @@ describe('marker', () => {
|
|
|
812
812
|
map.remove();
|
|
813
813
|
});
|
|
814
814
|
|
|
815
|
+
test('Marker with draggable:true is keyboard-focusable, and stops being focusable when dragging is disabled', () => {
|
|
816
|
+
const map = createMap();
|
|
817
|
+
const marker = new Marker({draggable: true}).setLngLat([0, 0]).addTo(map);
|
|
818
|
+
expect(marker.getElement().getAttribute('tabindex')).toBe('0');
|
|
819
|
+
marker.setDraggable(false);
|
|
820
|
+
expect(marker.getElement().hasAttribute('tabindex')).toBe(false);
|
|
821
|
+
map.remove();
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
test('Marker.setDraggable preserves an application-provided tabindex', () => {
|
|
825
|
+
const marker = new Marker();
|
|
826
|
+
marker.getElement().setAttribute('tabindex', '5');
|
|
827
|
+
|
|
828
|
+
marker.setDraggable(true);
|
|
829
|
+
expect(marker.getElement().getAttribute('tabindex')).toBe('5');
|
|
830
|
+
|
|
831
|
+
marker.setDraggable(false);
|
|
832
|
+
expect(marker.getElement().getAttribute('tabindex')).toBe('5');
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
test('Marker keeps a tabindex the application overrode after the marker managed it', () => {
|
|
836
|
+
const marker = new Marker({draggable: true});
|
|
837
|
+
marker.setDraggable(true);
|
|
838
|
+
expect(marker.getElement().getAttribute('tabindex')).toBe('0');
|
|
839
|
+
|
|
840
|
+
marker.getElement().setAttribute('tabindex', '5');
|
|
841
|
+
marker.setDraggable(false);
|
|
842
|
+
expect(marker.getElement().getAttribute('tabindex')).toBe('5');
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
test('Marker starts a fresh keyboard gesture after being removed mid-drag and re-added', () => {
|
|
846
|
+
const map = createMap();
|
|
847
|
+
const marker = new Marker({draggable: true}).setLngLat([0, 0]).addTo(map);
|
|
848
|
+
const el = marker.getElement();
|
|
849
|
+
const dragstart = vi.fn();
|
|
850
|
+
marker.on('dragstart', dragstart);
|
|
851
|
+
|
|
852
|
+
el.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true, cancelable: true}));
|
|
853
|
+
expect(dragstart).toHaveBeenCalledTimes(1);
|
|
854
|
+
|
|
855
|
+
// removed mid-gesture (no keyup), then re-added
|
|
856
|
+
marker.remove();
|
|
857
|
+
marker.addTo(map);
|
|
858
|
+
|
|
859
|
+
el.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true, cancelable: true}));
|
|
860
|
+
expect(dragstart).toHaveBeenCalledTimes(2);
|
|
861
|
+
map.remove();
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
test('Marker keeps its tabindex while either dragging or a popup keeps it interactive', () => {
|
|
865
|
+
const marker = new Marker({draggable: true}).setPopup(new Popup());
|
|
866
|
+
|
|
867
|
+
marker.setPopup();
|
|
868
|
+
expect(marker.getElement().getAttribute('tabindex')).toBe('0');
|
|
869
|
+
|
|
870
|
+
marker.setDraggable(false);
|
|
871
|
+
expect(marker.getElement().hasAttribute('tabindex')).toBe(false);
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
test('Marker with a custom element leaves focusability and keyboard handling to the application', () => {
|
|
875
|
+
const map = createMap();
|
|
876
|
+
const element = window.document.createElement('div');
|
|
877
|
+
const marker = new Marker({draggable: true, element}).setLngLat([0, 0]).addTo(map);
|
|
878
|
+
const startLngLat = marker.getLngLat();
|
|
879
|
+
const event = new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true, cancelable: true});
|
|
880
|
+
|
|
881
|
+
element.dispatchEvent(event);
|
|
882
|
+
|
|
883
|
+
expect(element.hasAttribute('tabindex')).toBe(false);
|
|
884
|
+
expect(marker.getLngLat()).toEqual(startLngLat);
|
|
885
|
+
expect(event.defaultPrevented).toBe(false);
|
|
886
|
+
map.remove();
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
test('Marker with draggable:true moves with arrow keys and fires drag events', () => {
|
|
890
|
+
const map = createMap();
|
|
891
|
+
const marker = new Marker({draggable: true}).setLngLat([0, 0]).addTo(map);
|
|
892
|
+
const el = marker.getElement();
|
|
893
|
+
const startPos = map.project(marker.getLngLat());
|
|
894
|
+
const events: string[] = [];
|
|
895
|
+
const positionsAtDragstart: Point[] = [];
|
|
896
|
+
marker.on('dragstart', () => {
|
|
897
|
+
events.push('dragstart');
|
|
898
|
+
positionsAtDragstart.push(map.project(marker.getLngLat()));
|
|
899
|
+
});
|
|
900
|
+
marker.on('drag', () => events.push('drag'));
|
|
901
|
+
marker.on('dragend', () => events.push('dragend'));
|
|
902
|
+
|
|
903
|
+
const ignoredEvent = new KeyboardEvent('keydown', {key: 'Enter', bubbles: true, cancelable: true});
|
|
904
|
+
el.dispatchEvent(ignoredEvent);
|
|
905
|
+
expect(map.project(marker.getLngLat())).toEqual(startPos);
|
|
906
|
+
expect(ignoredEvent.defaultPrevented).toBe(false);
|
|
907
|
+
|
|
908
|
+
const nestedElement = window.document.createElement('button');
|
|
909
|
+
el.attachShadow({mode: 'open'}).appendChild(nestedElement);
|
|
910
|
+
const nestedEvent = new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true, composed: true, cancelable: true});
|
|
911
|
+
nestedElement.dispatchEvent(nestedEvent);
|
|
912
|
+
expect(map.project(marker.getLngLat())).toEqual(startPos);
|
|
913
|
+
expect(nestedEvent.defaultPrevented).toBe(false);
|
|
914
|
+
|
|
915
|
+
const easeTo = vi.spyOn(map, 'easeTo');
|
|
916
|
+
const containerHandler = vi.fn();
|
|
917
|
+
map.getCanvasContainer().addEventListener('keydown', containerHandler);
|
|
918
|
+
const event = new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true, cancelable: true});
|
|
919
|
+
el.dispatchEvent(event);
|
|
920
|
+
|
|
921
|
+
expect(map.project(marker.getLngLat()).x).toBeCloseTo(startPos.x + 1);
|
|
922
|
+
expect(map.project(marker.getLngLat()).y).toBeCloseTo(startPos.y);
|
|
923
|
+
expect(event.defaultPrevented).toBe(true);
|
|
924
|
+
expect(containerHandler).not.toHaveBeenCalled();
|
|
925
|
+
expect(easeTo).not.toHaveBeenCalled();
|
|
926
|
+
expect(events).toEqual(['dragstart', 'drag']);
|
|
927
|
+
expect(positionsAtDragstart[0].x).toBeCloseTo(startPos.x + 1);
|
|
928
|
+
|
|
929
|
+
el.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', repeat: true, bubbles: true, cancelable: true}));
|
|
930
|
+
el.dispatchEvent(new KeyboardEvent('keyup', {key: 'ArrowRight', bubbles: true, cancelable: true}));
|
|
931
|
+
expect(events).toEqual(['dragstart', 'drag', 'drag', 'dragend']);
|
|
932
|
+
|
|
933
|
+
el.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true, cancelable: true}));
|
|
934
|
+
el.dispatchEvent(new FocusEvent('blur'));
|
|
935
|
+
expect(events).toEqual(['dragstart', 'drag', 'drag', 'dragend', 'dragstart', 'drag', 'dragend']);
|
|
936
|
+
|
|
937
|
+
el.dispatchEvent(new KeyboardEvent('keyup', {key: 'ArrowRight', bubbles: true, cancelable: true}));
|
|
938
|
+
expect(events).toEqual(['dragstart', 'drag', 'drag', 'dragend', 'dragstart', 'drag', 'dragend']);
|
|
939
|
+
map.remove();
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
test('Marker with draggable:true moves ten pixels per Shift+arrow keydown', () => {
|
|
943
|
+
const map = createMap();
|
|
944
|
+
const marker = new Marker({draggable: true}).setLngLat([0, 0]).addTo(map);
|
|
945
|
+
const startPos = map.project(marker.getLngLat());
|
|
946
|
+
|
|
947
|
+
marker.getElement().dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', shiftKey: true, bubbles: true, cancelable: true}));
|
|
948
|
+
|
|
949
|
+
const endPos = map.project(marker.getLngLat());
|
|
950
|
+
expect(endPos.x).toBeCloseTo(startPos.x);
|
|
951
|
+
expect(endPos.y).toBeCloseTo(startPos.y + 10);
|
|
952
|
+
map.remove();
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
test('Marker stops responding to arrow keys after dragging is disabled', () => {
|
|
956
|
+
const map = createMap();
|
|
957
|
+
const marker = new Marker({draggable: true}).setLngLat([0, 0]).addTo(map).setDraggable(false);
|
|
958
|
+
const startLngLat = marker.getLngLat();
|
|
959
|
+
|
|
960
|
+
marker.getElement().dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true, cancelable: true}));
|
|
961
|
+
|
|
962
|
+
expect(marker.getLngLat()).toEqual(startLngLat);
|
|
963
|
+
map.remove();
|
|
964
|
+
});
|
|
965
|
+
|
|
815
966
|
test('Marker with draggable:false does not move to new position in response to a mouse-triggered drag', () => {
|
|
816
967
|
const map = createMap();
|
|
817
968
|
const marker = new Marker({})
|
package/src/ui/marker.ts
CHANGED
|
@@ -18,6 +18,19 @@ import type {PointLike} from './camera.ts';
|
|
|
18
18
|
*/
|
|
19
19
|
export type Alignment = 'map' | 'viewport' | 'auto';
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Screen-pixel deltas applied when a focused draggable marker is moved with the arrow keys.
|
|
23
|
+
*/
|
|
24
|
+
const ARROW_KEY_DELTAS: Partial<Record<KeyboardEvent['key'], [number, number]>> = {
|
|
25
|
+
ArrowLeft: [-1, 0],
|
|
26
|
+
ArrowRight: [1, 0],
|
|
27
|
+
ArrowUp: [0, -1],
|
|
28
|
+
ArrowDown: [0, 1]
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const KEYBOARD_DRAG_SMALL_STEP = 1;
|
|
32
|
+
const KEYBOARD_DRAG_LARGE_STEP = 10;
|
|
33
|
+
|
|
21
34
|
/**
|
|
22
35
|
* The {@link Marker} options object
|
|
23
36
|
*/
|
|
@@ -52,6 +65,10 @@ export type MarkerOptions = {
|
|
|
52
65
|
scale?: number;
|
|
53
66
|
/**
|
|
54
67
|
* A boolean indicating whether or not a marker is able to be dragged to a new position on the map.
|
|
68
|
+
* A draggable default marker also becomes keyboard focusable and, while focused, moves by
|
|
69
|
+
* 1 screen pixel per arrow-key press (10 with Shift), firing the same `dragstart`/`drag`/`dragend`
|
|
70
|
+
* events as pointer dragging. Markers with a custom `element` keep their focusability and
|
|
71
|
+
* keyboard behavior application-owned.
|
|
55
72
|
* @defaultValue false
|
|
56
73
|
*/
|
|
57
74
|
draggable?: boolean;
|
|
@@ -222,12 +239,13 @@ export class Marker extends Evented<MarkerEventType> {
|
|
|
222
239
|
_rotation: number;
|
|
223
240
|
_pitchAlignment: Alignment;
|
|
224
241
|
_rotationAlignment: Alignment;
|
|
225
|
-
_originalTabIndex: string; // original tabindex of _element
|
|
226
242
|
_opacity: string;
|
|
227
243
|
_opacityWhenCovered: string;
|
|
228
244
|
_opacityTimeout: ReturnType<typeof setTimeout>;
|
|
229
245
|
_subpixelPositioning: boolean;
|
|
230
246
|
_roleManaged: boolean;
|
|
247
|
+
_tabIndexManaged: boolean;
|
|
248
|
+
_keyboardDragActive: boolean;
|
|
231
249
|
|
|
232
250
|
/**
|
|
233
251
|
* @param options - the options
|
|
@@ -243,6 +261,8 @@ export class Marker extends Evented<MarkerEventType> {
|
|
|
243
261
|
this._subpixelPositioning = options?.subpixelPositioning || false;
|
|
244
262
|
this._isDragging = false;
|
|
245
263
|
this._roleManaged = false;
|
|
264
|
+
this._tabIndexManaged = false;
|
|
265
|
+
this._keyboardDragActive = false;
|
|
246
266
|
this._state = 'inactive';
|
|
247
267
|
this._rotation = options?.rotation || 0;
|
|
248
268
|
this._rotationAlignment = options?.rotationAlignment || 'auto';
|
|
@@ -447,6 +467,12 @@ export class Marker extends Evented<MarkerEventType> {
|
|
|
447
467
|
delete this._map;
|
|
448
468
|
}
|
|
449
469
|
this._element.removeEventListener('click', this._onClick);
|
|
470
|
+
this._element.removeEventListener('keydown', this._onKeyDown);
|
|
471
|
+
this._element.removeEventListener('keyup', this._onKeyUp);
|
|
472
|
+
this._element.removeEventListener('blur', this._onBlur);
|
|
473
|
+
this._element.removeEventListener('keypress', this._onKeyPress);
|
|
474
|
+
// Drop any in-flight keyboard drag silently, like a pointer drag interrupted by remove().
|
|
475
|
+
this._keyboardDragActive = false;
|
|
450
476
|
this._element.remove();
|
|
451
477
|
if (this._popup) this._popup.remove();
|
|
452
478
|
return this;
|
|
@@ -520,10 +546,6 @@ export class Marker extends Evented<MarkerEventType> {
|
|
|
520
546
|
this._popup.remove();
|
|
521
547
|
this._popup = null;
|
|
522
548
|
this._element.removeEventListener('keypress', this._onKeyPress);
|
|
523
|
-
|
|
524
|
-
if (!this._originalTabIndex) {
|
|
525
|
-
this._element.removeAttribute('tabindex');
|
|
526
|
-
}
|
|
527
549
|
}
|
|
528
550
|
|
|
529
551
|
if (popup) {
|
|
@@ -544,13 +566,10 @@ export class Marker extends Evented<MarkerEventType> {
|
|
|
544
566
|
}
|
|
545
567
|
this._popup = popup;
|
|
546
568
|
|
|
547
|
-
this._originalTabIndex = this._element.getAttribute('tabindex');
|
|
548
|
-
if (!this._originalTabIndex) {
|
|
549
|
-
this._element.setAttribute('tabindex', '0');
|
|
550
|
-
}
|
|
551
569
|
this._element.addEventListener('keypress', this._onKeyPress);
|
|
552
570
|
}
|
|
553
571
|
|
|
572
|
+
this._updateTabIndex();
|
|
554
573
|
this._updateAccessibilityRole();
|
|
555
574
|
return this;
|
|
556
575
|
}
|
|
@@ -581,6 +600,57 @@ export class Marker extends Evented<MarkerEventType> {
|
|
|
581
600
|
}
|
|
582
601
|
};
|
|
583
602
|
|
|
603
|
+
/**
|
|
604
|
+
* Move a focused draggable default marker with the arrow keys
|
|
605
|
+
* (1 screen pixel per keydown; 10 with Shift).
|
|
606
|
+
* Mirrors the pointer drag gesture: the position updates before `dragstart`
|
|
607
|
+
* fires on the first movement, every movement fires `drag`, and releasing
|
|
608
|
+
* the arrow key (or losing focus) fires `dragend`. Holding a key down
|
|
609
|
+
* produces repeated `drag` events within a single gesture.
|
|
610
|
+
*/
|
|
611
|
+
_onKeyDown = (e: KeyboardEvent): void => {
|
|
612
|
+
// Custom marker elements own their keyboard behavior (#7790).
|
|
613
|
+
if (!this._defaultMarker || !this._draggable || !this._map || !this._lngLat) return;
|
|
614
|
+
// Leave keys originating from elements nested inside the marker to the application.
|
|
615
|
+
if (e.composedPath()[0] !== this._element) return;
|
|
616
|
+
// Leave Alt/Ctrl/Meta shortcuts to the browser and the application.
|
|
617
|
+
if (e.altKey || e.ctrlKey || e.metaKey) return;
|
|
618
|
+
|
|
619
|
+
const delta = ARROW_KEY_DELTAS[e.key];
|
|
620
|
+
if (!delta) return;
|
|
621
|
+
|
|
622
|
+
e.preventDefault();
|
|
623
|
+
// The marker element lives in the canvas container, so without this the
|
|
624
|
+
// map's KeyboardHandler would also pan (or rotate/pitch with Shift) the camera.
|
|
625
|
+
e.stopPropagation();
|
|
626
|
+
|
|
627
|
+
const step = e.shiftKey ? KEYBOARD_DRAG_LARGE_STEP : KEYBOARD_DRAG_SMALL_STEP;
|
|
628
|
+
const pos = this._map.project(this._lngLat);
|
|
629
|
+
this.setLngLat(this._map.unproject(new Point(pos.x + delta[0] * step, pos.y + delta[1] * step)));
|
|
630
|
+
|
|
631
|
+
if (!this._keyboardDragActive) {
|
|
632
|
+
this._keyboardDragActive = true;
|
|
633
|
+
this.fire(new MarkerDragEvent('dragstart'));
|
|
634
|
+
}
|
|
635
|
+
this.fire(new MarkerDragEvent('drag'));
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
_onKeyUp = (e: KeyboardEvent): void => {
|
|
639
|
+
if (!ARROW_KEY_DELTAS[e.key]) return;
|
|
640
|
+
this._endKeyboardDrag();
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
_onBlur = (): void => {
|
|
644
|
+
this._endKeyboardDrag();
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
_endKeyboardDrag(): void {
|
|
648
|
+
if (this._keyboardDragActive) {
|
|
649
|
+
this._keyboardDragActive = false;
|
|
650
|
+
this.fire(new MarkerDragEvent('dragend'));
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
584
654
|
_onMapClick = (e: MapMouseEvent): void => {
|
|
585
655
|
const targetElement = e.originalEvent.target;
|
|
586
656
|
const element = this._element;
|
|
@@ -851,7 +921,10 @@ export class Marker extends Evented<MarkerEventType> {
|
|
|
851
921
|
};
|
|
852
922
|
|
|
853
923
|
/**
|
|
854
|
-
* Sets the `draggable` property and functionality of the marker
|
|
924
|
+
* Sets the `draggable` property and functionality of the marker.
|
|
925
|
+
* A draggable default marker is also keyboard focusable and movable with the
|
|
926
|
+
* arrow keys (see {@link MarkerOptions.draggable}); custom marker elements keep
|
|
927
|
+
* their focusability and keyboard behavior application-owned.
|
|
855
928
|
* @param shouldBeDraggable - Turns drag functionality on/off
|
|
856
929
|
*/
|
|
857
930
|
setDraggable(shouldBeDraggable?: boolean): this {
|
|
@@ -870,6 +943,20 @@ export class Marker extends Evented<MarkerEventType> {
|
|
|
870
943
|
}
|
|
871
944
|
}
|
|
872
945
|
|
|
946
|
+
if (this._defaultMarker) {
|
|
947
|
+
if (this._draggable) {
|
|
948
|
+
this._element.addEventListener('keydown', this._onKeyDown);
|
|
949
|
+
this._element.addEventListener('keyup', this._onKeyUp);
|
|
950
|
+
this._element.addEventListener('blur', this._onBlur);
|
|
951
|
+
} else {
|
|
952
|
+
this._element.removeEventListener('keydown', this._onKeyDown);
|
|
953
|
+
this._element.removeEventListener('keyup', this._onKeyUp);
|
|
954
|
+
this._element.removeEventListener('blur', this._onBlur);
|
|
955
|
+
this._endKeyboardDrag();
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
this._updateTabIndex();
|
|
873
960
|
this._updateAccessibilityRole();
|
|
874
961
|
return this;
|
|
875
962
|
}
|
|
@@ -882,6 +969,29 @@ export class Marker extends Evented<MarkerEventType> {
|
|
|
882
969
|
return this._draggable;
|
|
883
970
|
}
|
|
884
971
|
|
|
972
|
+
/**
|
|
973
|
+
* Keep the marker element focusable while it has built-in keyboard behavior.
|
|
974
|
+
* A popup makes any marker element interactive; dragging only manages
|
|
975
|
+
* focusability for the default marker so custom elements stay
|
|
976
|
+
* application-owned (#7790). A tabindex supplied by the application is
|
|
977
|
+
* never added, changed, or removed here.
|
|
978
|
+
*/
|
|
979
|
+
_updateTabIndex(): void {
|
|
980
|
+
const needsTabIndex = !!this._popup || (this._defaultMarker && this._draggable);
|
|
981
|
+
if (needsTabIndex) {
|
|
982
|
+
if (!this._element.hasAttribute('tabindex')) {
|
|
983
|
+
this._element.setAttribute('tabindex', '0');
|
|
984
|
+
this._tabIndexManaged = true;
|
|
985
|
+
}
|
|
986
|
+
} else if (this._tabIndexManaged) {
|
|
987
|
+
// Only remove the value we set; the application may have overridden it since.
|
|
988
|
+
if (this._element.getAttribute('tabindex') === '0') {
|
|
989
|
+
this._element.removeAttribute('tabindex');
|
|
990
|
+
}
|
|
991
|
+
this._tabIndexManaged = false;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
885
995
|
/**
|
|
886
996
|
* Keep the default marker role aligned with interactivity.
|
|
887
997
|
* Default markers need a role because `aria-label` is set in {@link Marker.addTo}.
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {type RequestParameters, makeRequest, sameOrigin, type GetResourceResponse} from './ajax.ts';
|
|
2
2
|
import {arrayBufferToImageBitmap, arrayBufferToImage, ensureError, extend, isWorker, isImageBitmap} from './util.ts';
|
|
3
3
|
import {config} from './config.ts';
|
|
4
|
-
import {AbortError} from './abort_error.ts';
|
|
4
|
+
import {AbortError, throwIfAborted} from './abort_error.ts';
|
|
5
5
|
import {getProtocol} from '../source/protocol_crud.ts';
|
|
6
6
|
|
|
7
|
+
import type {RequestManager, ResourceType} from './request_manager.ts';
|
|
8
|
+
|
|
7
9
|
type ImageQueueThrottleControlCallback = () => boolean;
|
|
8
10
|
|
|
9
11
|
export type ImageRequestQueueItem = {
|
|
@@ -97,11 +99,39 @@ export namespace ImageRequest {
|
|
|
97
99
|
return false;
|
|
98
100
|
};
|
|
99
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Transform `url` through {@link RequestManager.transformRequest} and load the image.
|
|
104
|
+
* The transform may be async; if `abortController` aborts while it is pending, the
|
|
105
|
+
* request is never issued and the returned promise rejects with an `AbortError`.
|
|
106
|
+
* @param requestManager - The request manager whose transform to apply.
|
|
107
|
+
* @param url - The image URL, before transformation.
|
|
108
|
+
* @param resourceType - The resource type to pass to the transform.
|
|
109
|
+
* @param abortController - allows to abort the request.
|
|
110
|
+
* @param supportImageRefresh - `true`, if the image request need to support refresh based on cache headers.
|
|
111
|
+
* @param imageBitmapOptions - Options for `createImageBitmap`. Pass `{colorSpaceConversion: 'none'}` for images
|
|
112
|
+
* whose pixels hold data rather than color, such as raster-DEM tiles, so the browser does not color manage them.
|
|
113
|
+
* @returns - A promise resolved when the image is loaded.
|
|
114
|
+
*/
|
|
115
|
+
export async function transformAndGetImage(
|
|
116
|
+
requestManager: RequestManager,
|
|
117
|
+
url: string,
|
|
118
|
+
resourceType: ResourceType,
|
|
119
|
+
abortController: AbortController,
|
|
120
|
+
supportImageRefresh: boolean = true,
|
|
121
|
+
imageBitmapOptions?: ImageBitmapOptions
|
|
122
|
+
): Promise<GetResourceResponse<HTMLImageElement | ImageBitmap | null>> {
|
|
123
|
+
const requestParameters = await requestManager.transformRequest(url, resourceType);
|
|
124
|
+
throwIfAborted(abortController.signal);
|
|
125
|
+
return ImageRequest.getImage(requestParameters, abortController, supportImageRefresh, imageBitmapOptions);
|
|
126
|
+
}
|
|
127
|
+
|
|
100
128
|
/**
|
|
101
129
|
* Request to load an image.
|
|
102
130
|
* @param requestParameters - Request parameters.
|
|
103
131
|
* @param abortController - allows to abort the request.
|
|
104
132
|
* @param supportImageRefresh - `true`, if the image request need to support refresh based on cache headers.
|
|
133
|
+
* @param imageBitmapOptions - Options for `createImageBitmap`. Pass `{colorSpaceConversion: 'none'}` for images
|
|
134
|
+
* whose pixels hold data rather than color, such as raster-DEM tiles, so the browser does not color manage them.
|
|
105
135
|
* @returns - A promise resolved when the image is loaded.
|
|
106
136
|
*/
|
|
107
137
|
export const getImage = (
|
|
@@ -39,7 +39,7 @@ export function drawBackground(painter: Painter, tileManager: TileManager, layer
|
|
|
39
39
|
|
|
40
40
|
if (image) {
|
|
41
41
|
context.activeTexture.set(gl.TEXTURE0);
|
|
42
|
-
painter.
|
|
42
|
+
painter.patternAtlas.bind(painter.context);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
const crossfade = layer.getCrossfadeParameters();
|
|
@@ -71,9 +71,9 @@ function bgPatternUniformValues(
|
|
|
71
71
|
tileSize: number;
|
|
72
72
|
}
|
|
73
73
|
): UniformValues<BackgroundPatternUniformsType> {
|
|
74
|
-
const imagePosA = painter.
|
|
75
|
-
const imagePosB = painter.
|
|
76
|
-
const {width, height} = painter.
|
|
74
|
+
const imagePosA = painter.patternAtlas.getPattern(image.from.toString());
|
|
75
|
+
const imagePosB = painter.patternAtlas.getPattern(image.to.toString());
|
|
76
|
+
const {width, height} = painter.patternAtlas.getPixelSize();
|
|
77
77
|
|
|
78
78
|
const numTiles = Math.pow(2, tile.tileID.overscaledZ);
|
|
79
79
|
const tileSizeAtNearestZoom = tile.tileSize * Math.pow(2, painter.transform.tileZoom) / numTiles;
|