Skip to content

Commit

Permalink
#2498: new CooperativeGestureControl
Browse files Browse the repository at this point in the history
  • Loading branch information
arekgotfryd committed Oct 25, 2023
1 parent e732569 commit f02798b
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 82 deletions.
111 changes: 111 additions & 0 deletions src/ui/control/cooperative_gesture_contol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {DOM} from '../../util/dom';

import type {Map} from '../map';
import type {ControlPosition, IControl} from './control';

/**
* The {@link CooperativeGestureControl} options object
*/
/**
* An options object for the gesture settings
* @example
* ```ts
* let options = {
* windowsHelpText: "Use Ctrl + scroll to zoom the map",
* macHelpText: "Use ⌘ + scroll to zoom the map",
* mobileHelpText: "Use two fingers to move the map",
* }
* ```
*/
export type GestureOptions = {
windowsHelpText?: string;
macHelpText?: string;
mobileHelpText?: string;
};

/**
* A `CooperativeGestureControl` is a control that adds cooperative gesture info when user tries to zoom in/out.
*
* @group Markers and Controls
*
* @example
* ```ts
* map.addControl(new maplibregl.CooperativeGestureControl({
* windowsHelpText: "Use Ctrl + scroll to zoom the map",
* macHelpText: "Use ⌘ + scroll to zoom the map",
* mobileHelpText: "Use two fingers to move the map",
* }));
* ```
**/
export class CooperativeGestureControl implements IControl {
options: boolean | GestureOptions;
_map: Map;
_container: HTMLElement;
_metaKey: keyof MouseEvent = navigator.userAgent.indexOf('Mac') !== -1 ? 'metaKey' : 'ctrlKey';

constructor(options: boolean | GestureOptions = {}) {
this.options = options;
}

getDefaultPosition(): ControlPosition {
return 'top-left';
}

/** {@inheritDoc IControl.onAdd} */
onAdd(map: Map) {
this._map = map;
const mapCanvasContainer = this._map.getCanvasContainer();
const cooperativeGestures = this._map.getCooperativeGestures();
this._container = DOM.create('div', 'maplibregl-cooperative-gesture-screen', this._map.getContainer());
let desktopMessage = typeof cooperativeGestures !== 'boolean' && cooperativeGestures.windowsHelpText ? cooperativeGestures.windowsHelpText : 'Use Ctrl + scroll to zoom the map';
if (this._metaKey === 'metaKey') {
desktopMessage = typeof cooperativeGestures !== 'boolean' && cooperativeGestures.macHelpText ? cooperativeGestures.macHelpText : 'Use ⌘ + scroll to zoom the map';
}
const mobileMessage = typeof cooperativeGestures !== 'boolean' && cooperativeGestures.mobileHelpText ? cooperativeGestures.mobileHelpText : 'Use two fingers to move the map';
this._container.innerHTML = `
<div class="maplibregl-desktop-message">${desktopMessage}</div>

Check warning

Code scanning / CodeQL

Unsafe HTML constructed from library input Medium

This HTML construction which depends on
library input
might later allow
cross-site scripting
.
This HTML construction which depends on
library input
might later allow
cross-site scripting
.
This HTML construction which depends on
library input
might later allow
cross-site scripting
.
<div class="maplibregl-mobile-message">${mobileMessage}</div>

Check warning

Code scanning / CodeQL

Unsafe HTML constructed from library input Medium

This HTML construction which depends on
library input
might later allow
cross-site scripting
.
This HTML construction which depends on
library input
might later allow
cross-site scripting
.
This HTML construction which depends on
library input
might later allow
cross-site scripting
.
`;
// Remove cooperative gesture screen from the accessibility tree since screenreaders cannot interact with the map using gestures
this._container.setAttribute('aria-hidden', 'true');
// Add event to canvas container since gesture container is pointer-events: none
this._map.on('wheel', this._cooperativeGesturesOnWheel);
this._map.on('touchmove', this._cooperativeGesturesOnTouch);
// Add a cooperative gestures class (enable touch-action: pan-x pan-y;)
mapCanvasContainer.classList.add('maplibregl-cooperative-gestures');

return this._container;
}

/** {@inheritDoc IControl.onRemove} */
onRemove() {
DOM.remove(this._container);
if (this._map) {
const mapCanvasContainer = this._map.getCanvasContainer();
this._map.off('wheel', this._cooperativeGesturesOnWheel);
this._map.off('touchmove', this._cooperativeGesturesOnTouch);
mapCanvasContainer.classList.remove('maplibregl-cooperative-gestures');
this._map = undefined;
}
}

_cooperativeGesturesOnTouch = (event: TouchEvent) => {
this._onCooperativeGesture(event, false);
};

_cooperativeGesturesOnWheel = (event: WheelEvent) => {
this._onCooperativeGesture(event, event[this._metaKey]);
};

_onCooperativeGesture(event: any, metaPress) {
if (!metaPress) {
// Alert user how to scroll/pan
this._container.classList.add('maplibregl-show');
setTimeout(() => {
this._container.classList.remove('maplibregl-show');
}, 100);
}
return false;
}

}
3 changes: 2 additions & 1 deletion src/ui/control/fullscreen_control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import {DOM} from '../../util/dom';
import {warnOnce} from '../../util/util';

import {Event, Evented} from '../../util/evented';
import type {Map, GestureOptions} from '../map';
import type {Map} from '../map';
import type {IControl} from './control';
import {GestureOptions} from './cooperative_gesture_contol';

/**
* The {@link FullscreenControl} options
Expand Down
47 changes: 3 additions & 44 deletions src/ui/handler/scroll_zoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ const maxScalePerFrame = 2;
*/
export class ScrollZoomHandler implements Handler {
_map: Map;
_cooperativeGesturesScreen: HTMLElement;
_tr: TransformProvider;
_metaKey: keyof MouseEvent = navigator.userAgent.indexOf('Mac') !== -1 ? 'metaKey' : 'ctrlKey';
_el: HTMLElement;
Expand Down Expand Up @@ -138,7 +137,8 @@ export class ScrollZoomHandler implements Handler {
if (this.isEnabled()) return;
this._enabled = true;
this._aroundCenter = !!options && (options as AroundCenterOptions).around === 'center';
if (this._map.getCooperativeGestures()) this.setupCooperativeGestures();
const cooperativeGestures = this._map.getCooperativeGestures();
if (cooperativeGestures) this._map.setCooperativeGestures(cooperativeGestures);
}

/**
Expand All @@ -152,7 +152,7 @@ export class ScrollZoomHandler implements Handler {
disable() {
if (!this.isEnabled()) return;
this._enabled = false;
if (this._map.getCooperativeGestures()) this.destroyCooperativeGestures();
this._map.setCooperativeGestures(null);
}

wheel(e: WheelEvent) {
Expand Down Expand Up @@ -362,45 +362,4 @@ export class ScrollZoomHandler implements Handler {
delete this._finishTimeout;
}
}

setupCooperativeGestures() {
const cooperativeGestures = this._map.getCooperativeGestures();
this._cooperativeGesturesScreen = DOM.create('div', 'maplibregl-cooperative-gesture-screen', this._map.getContainer());
let desktopMessage = typeof cooperativeGestures !== 'boolean' && cooperativeGestures.windowsHelpText ? cooperativeGestures.windowsHelpText : 'Use Ctrl + scroll to zoom the map';
if (navigator.platform.indexOf('Mac') === 0) {
desktopMessage = typeof cooperativeGestures !== 'boolean' && cooperativeGestures.macHelpText ? cooperativeGestures.macHelpText : 'Use ⌘ + scroll to zoom the map';
}
const mobileMessage = typeof cooperativeGestures !== 'boolean' && cooperativeGestures.mobileHelpText ? cooperativeGestures.mobileHelpText : 'Use two fingers to move the map';
this._cooperativeGesturesScreen.innerHTML = `
<div class="maplibregl-desktop-message">${desktopMessage}</div>
<div class="maplibregl-mobile-message">${mobileMessage}</div>
`;
// Remove cooperative gesture screen from the accessibility tree since screenreaders cannot interact with the map using gestures
this._cooperativeGesturesScreen.setAttribute('aria-hidden', 'true');
// Add event to canvas container since gesture container is pointer-events: none
this._el.addEventListener('wheel', this._cooperativeGesturesOnWheel, false);
// Add a cooperative gestures class (enable touch-action: pan-x pan-y;)
this._el.classList.add('maplibregl-cooperative-gestures');
}

destroyCooperativeGestures() {
DOM.remove(this._cooperativeGesturesScreen);
this._el.removeEventListener('wheel', this._cooperativeGesturesOnWheel, false);
this._el.classList.remove('maplibregl-cooperative-gestures');
}

_cooperativeGesturesOnWheel = (event: WheelEvent) => {
this._onCooperativeGesture(event, event[this._metaKey], 1);
};

_onCooperativeGesture(event: any, metaPress, touches) {
if (!metaPress && touches < 2) {
// Alert user how to scroll/pan
this._cooperativeGesturesScreen.classList.add('maplibregl-show');
setTimeout(() => {
this._cooperativeGesturesScreen.classList.remove('maplibregl-show');
}, 100);
}
return false;
}
}
18 changes: 2 additions & 16 deletions src/ui/handler/touch_pan.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import Point from '@mapbox/point-geometry';
import {indexTouches} from './handler_util';
import type {Map} from '../map';
import {GestureOptions} from '../map';

import {Handler} from '../handler_manager';
import {GestureOptions} from '../control/cooperative_gesture_contol';

export class TouchPanHandler implements Handler {

Expand All @@ -15,7 +16,6 @@ export class TouchPanHandler implements Handler {
_clickTolerance: number;
_sum: Point;
_map: Map;
_cancelCooperativeMessage: boolean;

constructor(options: {
clickTolerance: number;
Expand All @@ -31,27 +31,13 @@ export class TouchPanHandler implements Handler {
this._active = false;
this._touches = {};
this._sum = new Point(0, 0);

// Put a delay on the cooperative gesture message so it's less twitchy
setTimeout(() => {
this._cancelCooperativeMessage = false;
}, 200);
}

touchstart(e: TouchEvent, points: Array<Point>, mapTouches: Array<Touch>) {
return this._calculateTransform(e, points, mapTouches);
}

touchmove(e: TouchEvent, points: Array<Point>, mapTouches: Array<Touch>) {
if (this._map._cooperativeGestures) {
if (this._minTouches === 2 && mapTouches.length < 2 && !this._cancelCooperativeMessage) {
// If coop gesture enabled, show panning info to user
this._map.scrollZoom._onCooperativeGesture(e, false, mapTouches.length);
} else if (!this._cancelCooperativeMessage) {
// If user is successfully navigating, we don't need this warning until the touch resets
this._cancelCooperativeMessage = true;
}
}
if (!this._active || mapTouches.length < this._minTouches) return;
e.preventDefault();
return this._calculateTransform(e, points, mapTouches);
Expand Down
34 changes: 13 additions & 21 deletions src/ui/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {Terrain} from '../render/terrain';
import {RenderToTexture} from '../render/render_to_texture';
import {config} from '../util/config';
import type {QueryRenderedFeaturesOptions, QuerySourceFeatureOptions} from '../source/query_features';
import {CooperativeGestureControl, GestureOptions} from './control/cooperative_gesture_contol';

const version = packageJSON.version;

Expand Down Expand Up @@ -328,23 +329,6 @@ export type MapOptions = {
maxCanvasSize?: [number, number];
};

/**
* An options object for the gesture settings
* @example
* ```ts
* let options = {
* windowsHelpText: "Use Ctrl + scroll to zoom the map",
* macHelpText: "Use ⌘ + scroll to zoom the map",
* mobileHelpText: "Use two fingers to move the map",
* }
* ```
*/
export type GestureOptions = {
windowsHelpText?: string;
macHelpText?: string;
mobileHelpText?: string;
};

export type AddImageOptions = {

}
Expand Down Expand Up @@ -456,6 +440,7 @@ export class Map extends Camera {
_controlPositions: {[_: string]: HTMLElement};
_interactive: boolean;
_cooperativeGestures: boolean | GestureOptions;
_cooperativeGesturesControl: CooperativeGestureControl;
_showTileBoundaries: boolean;
_showCollisionBoxes: boolean;
_showPadding: boolean;
Expand Down Expand Up @@ -652,7 +637,9 @@ export class Map extends Camera {

this.handlers = new HandlerManager(this, options as CompleteMapOptions);

if (this._cooperativeGestures) this.scrollZoom.setupCooperativeGestures();
if (options.cooperativeGestures) {
this.addControl(new CooperativeGestureControl(options.cooperativeGestures));
}

const hashName = (typeof options.hash === 'string' && options.hash) || undefined;
this._hash = options.hash && (new Hash(hashName)).addTo(this);
Expand Down Expand Up @@ -1170,9 +1157,14 @@ export class Map extends Camera {
setCooperativeGestures(gestureOptions?: GestureOptions | boolean | null): Map {
this._cooperativeGestures = gestureOptions;
if (this._cooperativeGestures) {
this.scrollZoom.setupCooperativeGestures();
if (this._cooperativeGesturesControl) {
//remove existing control
this.removeControl(this._cooperativeGesturesControl);
}
this._cooperativeGesturesControl = new CooperativeGestureControl(this._cooperativeGestures);
this.addControl(this._cooperativeGesturesControl);
} else {
this.scrollZoom.destroyCooperativeGestures();
this.removeControl(this._cooperativeGesturesControl);
}

return this;
Expand Down Expand Up @@ -3235,7 +3227,7 @@ export class Map extends Camera {
this._canvas.removeEventListener('webglcontextlost', this._contextLost, false);
DOM.remove(this._canvasContainer);
DOM.remove(this._controlContainer);
if (this._cooperativeGestures) this.scrollZoom.destroyCooperativeGestures();
if (this._cooperativeGestures) this.removeControl(this._cooperativeGesturesControl);
this._container.classList.remove('maplibregl-map');

PerformanceUtils.clearMetrics();
Expand Down

0 comments on commit f02798b

Please sign in to comment.