Skip to content
This repository has been archived by the owner on Mar 8, 2023. It is now read-only.

Commit

Permalink
MINOR: Adapt LongPressHandler to support touch events.
Browse files Browse the repository at this point in the history
Signed-off-by: Andres Mandado <[email protected]>
  • Loading branch information
atomicsulfate committed Mar 25, 2021
1 parent 81cba28 commit 38212d1
Show file tree
Hide file tree
Showing 3 changed files with 252 additions and 95 deletions.
95 changes: 32 additions & 63 deletions @here/harp-examples/src/markers_dynamic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { FeaturesDataSource, MapViewPointFeature } from "@here/harp-features-datasource";
import { GeoCoordinates } from "@here/harp-geoutils";
import { MapControls, MapControlsUI } from "@here/harp-map-controls";
import { LongPressHandler, MapControls, MapControlsUI } from "@here/harp-map-controls";
import { CopyrightElementHandler, MapView } from "@here/harp-mapview";
import {
APIFormat,
Expand Down Expand Up @@ -109,7 +109,7 @@ export namespace DynamicMarkersExample {
return map;
}

function removeMarkerAt(x: number, y: number): void {
function removeMarker(x: number, y: number): void {
// Intersection test filtering the results by layer name to get only markers.
const layerName = (markersDataSource.dataProvider() as GeoJsonDataProvider).name;
const results = mapView.intersectMapObjects(x, y).filter(result => {
Expand All @@ -129,19 +129,7 @@ export namespace DynamicMarkersExample {
}

let markerId = 0;
let longTapTimer: number | undefined;
let isLongTap = false;
const longTapTimeMs = 500;
let lastCanvasPosition: { x: number; y: number } | undefined;

function cancelLongTapTimer() {
clearTimeout(longTapTimer);
longTapTimer = undefined;
}
function handleLongTap() {
cancelLongTapTimer();
isLongTap = true;
}

function addMarker(x: number, y: number) {
const geo = mapView.getGeoCoordinatesAt(x, y);
if (geo) {
Expand All @@ -162,52 +150,24 @@ export namespace DynamicMarkersExample {
markersDataSource.clear();
markerId = 0;
}
function getCanvasPosition(
event: MouseEvent | TouchEvent,
canvas: HTMLCanvasElement
): { x: number; y: number } {
const ev = event instanceof MouseEvent ? event : event.changedTouches[0];
const { left, top } = canvas.getBoundingClientRect();
return { x: ev.clientX - Math.floor(left), y: ev.clientY - Math.floor(top) };
}

function handleTapStart(event: MouseEvent | TouchEvent) {
lastCanvasPosition = getCanvasPosition(event, mapView.canvas);
longTapTimer = setTimeout(handleLongTap, longTapTimeMs) as any;
}

function handleTapEnd(event: MouseEvent | TouchEvent) {
cancelLongTapTimer();
const canvasPos = getCanvasPosition(event, mapView.canvas);
// It's a tap only if there's (almost) no dragging.
const MAX_MOVE = 10;
const wasDragged =
lastCanvasPosition &&
(Math.abs(lastCanvasPosition.x - canvasPos.x) > MAX_MOVE ||
Math.abs(lastCanvasPosition.y - canvasPos.y) > MAX_MOVE);
if (wasDragged) {
return;
}
if (isLongTap) {
removeMarkerAt(canvasPos.x, canvasPos.y);
isLongTap = false;
} else {
addMarker(canvasPos.x, canvasPos.y);
}
function getCanvasPosition(event: MouseEvent | Touch): { x: number; y: number } {
const { left, top } = mapView.canvas.getBoundingClientRect();
return { x: event.clientX - Math.floor(left), y: event.clientY - Math.floor(top) };
}

function attachInputEvents() {
const canvas = mapView.canvas;
canvas.addEventListener("mousedown", handleTapStart);
canvas.addEventListener("touchstart", event => {
if (event.changedTouches.length === 1) {
handleTapStart(event);
new LongPressHandler(
canvas,
event => {
const canvasPos = getCanvasPosition(event);
removeMarker(canvasPos.x, canvasPos.y);
},
event => {
const canvasPos = getCanvasPosition(event);
addMarker(canvasPos.x, canvasPos.y);
}
});

canvas.addEventListener("mouseup", handleTapEnd);
canvas.addEventListener("touchend", handleTapEnd);

);
window.addEventListener("keypress", event => {
if (event.key === "c") {
clearMarkers();
Expand All @@ -217,19 +177,28 @@ export namespace DynamicMarkersExample {

function addUI() {
const gui = new GUI();
gui.width = 300;
const instructions = "Short/long tap to add/remove a marker";
gui.width = 250;
const instructions1 = "Tap map to add a marker";
const instructions2 = "Long press on marker to remove it";
const options = {
instructions,
instructions1,
instructions2,
clear: clearMarkers
};
const instrField = gui
.add(options, "instructions")
const instrField1 = gui
.add(options, "instructions1")
.name("")
.onChange(() => {
options.instructions1 = instructions1;
});
(instrField1.domElement.style as any) = "width:90%";
const instrField2 = gui
.add(options, "instructions2")
.name("")
.onChange(() => {
options.instructions = instructions;
options.instructions2 = instructions2;
});
(instrField.domElement.style as any) = "width:90%";
(instrField2.domElement.style as any) = "width:90%";
gui.add(options, "clear").name("(C)lear markers");
}

Expand Down
103 changes: 71 additions & 32 deletions @here/harp-map-controls/lib/LongPressHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

/**
* Class that can be used to track long presses on an HTML Element. A long press is a press that
* lasts a minimum duration (see the [[timeout]] member) while the mouse is not moved more than a
* lasts a minimum duration (see the [[timeout]] member) while the pointer is not moved more than a
* certain threshold (see the [[moveThreshold]] member).
*/
export class LongPressHandler {
Expand All @@ -21,31 +21,41 @@ export class LongPressHandler {
moveThreshold: number = 5;

/**
* Button id that should be handled by this event.
* Mouse button id that should be handled by this event.
*/
buttonId: number = 0;

private m_mouseDownEvent?: MouseEvent = undefined;
private m_pressEvent?: MouseEvent | Touch = undefined;
private m_timerId?: number = undefined;
private m_moveHandlerRegistered: boolean = false;
private readonly m_boundMouseMoveHandler: any;
private readonly m_boundPointerMoveHandler: any;
private readonly m_boundMouseDownHandler: any;
private readonly m_boundTouchStartHandler: any;
private readonly m_boundMouseUpHandler: any;
private readonly m_boundTouchEndHandler: any;

/**
* Default constructor.
*
* @param element - The HTML element to track.
* @param onLongPress - The callback to call when a long press occurred.
* @param onTap - Optional callback to call on a tap, i.e. when the press ends before the
* specified timeout.
*/
constructor(readonly element: HTMLElement, public onLongPress: (event: MouseEvent) => void) {
// workaround - need to bind 'this' for our dynamic mouse move handler
this.m_boundMouseMoveHandler = this.onMouseMove.bind(this);
this.m_boundMouseDownHandler = this.onMousedown.bind(this);
this.m_boundMouseUpHandler = this.onMouseup.bind(this);

constructor(
readonly element: HTMLElement,
public onLongPress: (event: MouseEvent | Touch) => void,
public onTap?: (event: MouseEvent | Touch) => void
) {
this.m_boundPointerMoveHandler = this.onPointerMove.bind(this);
this.m_boundMouseDownHandler = this.onMouseDown.bind(this);
this.m_boundTouchStartHandler = this.onTouchStart.bind(this);
this.m_boundMouseUpHandler = this.onMouseUp.bind(this);
this.m_boundTouchEndHandler = this.onTouchEnd.bind(this);
this.element.addEventListener("mousedown", this.m_boundMouseDownHandler);
this.element.addEventListener("touchstart", this.m_boundTouchStartHandler);
this.element.addEventListener("mouseup", this.m_boundMouseUpHandler);
this.element.addEventListener("touchend", this.m_boundTouchEndHandler);
}

/**
Expand All @@ -54,45 +64,72 @@ export class LongPressHandler {
dispose() {
this.cancel();
this.element.removeEventListener("mousedown", this.m_boundMouseDownHandler);
this.element.removeEventListener("touchstart", this.m_boundTouchStartHandler);
this.element.removeEventListener("mouseup", this.m_boundMouseUpHandler);
this.element.removeEventListener("touchend", this.m_boundTouchEndHandler);
}

private onMousedown(event: MouseEvent) {
if (event.button !== this.buttonId) {
return;
}
private startPress(event: MouseEvent | TouchEvent) {
this.cancelTimer();

this.m_mouseDownEvent = event;
this.m_pressEvent = (event as any).changedTouches?.[0] ?? event;
this.m_timerId = setTimeout(() => this.onTimeout(), this.timeout) as any;
this.addMouseMoveHandler();
this.addPointerMoveHandler();
}

private onMouseDown(event: MouseEvent) {
if (event.button === this.buttonId) {
this.startPress(event);
}
}

private onMouseup(event: MouseEvent) {
if (event.button !== this.buttonId) {
private onTouchStart(event: TouchEvent) {
if (this.m_pressEvent) {
// Cancel long press if a second touch starts while holding the first one.
this.cancel();
return;
}
this.cancel();

if (event.changedTouches.length === 1) {
this.startPress(event);
}
}

private onMouseMove(event: MouseEvent) {
if (this.m_mouseDownEvent === undefined) {
return; // Must not happen
private onMouseUp(event: MouseEvent) {
if (this.m_pressEvent && event.button === this.buttonId) {
this.cancel();
this.onTap?.(event);
}
}

private onTouchEnd(event: TouchEvent) {
if (
event.changedTouches.length === 1 &&
event.changedTouches[0].identifier === (this.m_pressEvent as any).identifier
) {
this.cancel();
this.onTap?.(event.changedTouches[0]);
}
}

private onPointerMove(event: MouseEvent | TouchEvent) {
if (this.m_pressEvent === undefined) {
return; // Must not happen
}
const { clientX, clientY } = (event as any).changedTouches?.[0] ?? event;
const manhattanLength =
Math.abs(event.clientX - this.m_mouseDownEvent.clientX) +
Math.abs(event.clientY - this.m_mouseDownEvent.clientY);
Math.abs(clientX - this.m_pressEvent.clientX) +
Math.abs(clientY - this.m_pressEvent.clientY);

if (manhattanLength >= this.moveThreshold) {
if (manhattanLength > this.moveThreshold) {
this.cancel();
}
}

private cancel() {
this.m_mouseDownEvent = undefined;
this.m_pressEvent = undefined;
this.cancelTimer();
this.removeMouseMoveHandler();
this.removePointerMoveHandler();
}

private cancelTimer() {
Expand All @@ -104,26 +141,28 @@ export class LongPressHandler {
this.m_timerId = undefined;
}

private addMouseMoveHandler() {
private addPointerMoveHandler() {
if (this.m_moveHandlerRegistered) {
return;
}

this.element.addEventListener("mousemove", this.m_boundMouseMoveHandler);
this.element.addEventListener("mousemove", this.m_boundPointerMoveHandler);
this.element.addEventListener("touchmove", this.m_boundPointerMoveHandler);
this.m_moveHandlerRegistered = true;
}

private removeMouseMoveHandler() {
private removePointerMoveHandler() {
if (!this.m_moveHandlerRegistered) {
return;
}

this.element.removeEventListener("mousemove", this.m_boundMouseMoveHandler);
this.element.removeEventListener("mousemove", this.m_boundPointerMoveHandler);
this.element.removeEventListener("touchmove", this.m_boundPointerMoveHandler);
this.m_moveHandlerRegistered = false;
}

private onTimeout() {
const event = this.m_mouseDownEvent;
const event = this.m_pressEvent;

this.m_timerId = undefined;
this.cancel();
Expand Down
Loading

0 comments on commit 38212d1

Please sign in to comment.