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

Commit

Permalink
MINOR: Fix input handling in dynamic markers example. (#2153)
Browse files Browse the repository at this point in the history
* MINOR: Fix input handling in dynamic markers example.

Add touch screen support.

Signed-off-by: Andres Mandado <andres.mandado-almajano@here.com>

* MINOR: Adapt LongPressHandler to support touch events.

Signed-off-by: Andres Mandado <andres.mandado-almajano@here.com>

* MINOR: Move instructions out of GUI.

Signed-off-by: Andres Mandado <andres.mandado-almajano@here.com>
  • Loading branch information
atomicsulfate committed Mar 30, 2021
1 parent 5fe2612 commit 79c12d4
Show file tree
Hide file tree
Showing 3 changed files with 308 additions and 101 deletions.
157 changes: 88 additions & 69 deletions @here/harp-examples/src/markers_dynamic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@

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,
AuthenticationMethod,
GeoJsonDataProvider,
VectorTileDataSource
} from "@here/harp-vectortile-datasource";
import { GUI } from "dat.gui";

import { apikey, copyrightInfo } from "../config";

Expand Down Expand Up @@ -70,7 +71,8 @@ export namespace DynamicMarkersExample {
}
},
target: new GeoCoordinates(52.52, 13.4),
zoomLevel: 12
zoomLevel: 12,
delayLabelsUntilMovementFinished: false
});

CopyrightElementHandler.install("copyrightNotice").attach(map);
Expand All @@ -84,18 +86,30 @@ export namespace DynamicMarkersExample {
window.addEventListener("resize", () => {
map.resize(window.innerWidth, window.innerHeight);
});
const omvDataSource = new VectorTileDataSource({
baseUrl: "https://vector.hereapi.com/v2/vectortiles/base/mc",
apiFormat: APIFormat.XYZOMV,
styleSetName: "tilezen",
maxDataLevel: 17,
authenticationCode: apikey,
authenticationMethod: {
method: AuthenticationMethod.QueryString,
name: "apikey"
},
copyrightInfo
});
map.addDataSource(omvDataSource);

// Register the icon image referenced in the style.
for (const { name, url } of icons) {
map.userImageCache.addImage(name, url);
}
map.update();

return map;
}

function handlePick(
mapView: MapView,
markersDataSource: FeaturesDataSource,
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 @@ -115,78 +129,83 @@ export namespace DynamicMarkersExample {
}

let markerId = 0;
function attachClickEvents(mapView: MapView, markersDataSource: FeaturesDataSource) {
mapView.canvas.addEventListener("click", event => {
if (event.shiftKey) {
const geo = mapView.getGeoCoordinatesAt(event.clientX, event.clientY);
if (geo) {
// Add a new marker to the data source at the click coordinates.
markersDataSource.add(
new MapViewPointFeature(geo.toGeoPoint() as number[], {
text: markerId.toString(),
id: markerId,
icon: icons[markerId % icons.length].name,
renderOrder: markerId
})
);
markerId++;
}
} else if (event.ctrlKey) {
handlePick(mapView, markersDataSource, event.pageX, event.pageY);
}
});

function addMarker(x: number, y: number) {
const geo = mapView.getGeoCoordinatesAt(x, y);
if (geo) {
// Add a new marker to the data source at the click coordinates.
markersDataSource.add(
new MapViewPointFeature(geo.toGeoPoint() as number[], {
text: markerId.toString(),
id: markerId,
icon: icons[markerId % icons.length].name,
renderOrder: markerId
})
);
markerId++;
}
}

function clearMarkers() {
markersDataSource.clear();
markerId = 0;
}
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;
new LongPressHandler(
canvas,
event => {
const canvasPos = getCanvasPosition(event);
removeMarker(canvasPos.x, canvasPos.y);
},
event => {
const canvasPos = getCanvasPosition(event);
addMarker(canvasPos.x, canvasPos.y);
}
);
window.addEventListener("keypress", event => {
if (event.key === "c") {
markersDataSource.clear();
markerId = 0;
clearMarkers();
}
});
}

const instructions = `
Shift+Left Click to add a marker<br/>Ctrl+Left Click to remove it<br/>
Press 'c' to clear the map.<br/>`;
function addUI() {
const gui = new GUI();
gui.width = 250;
gui.add(
{
clear: clearMarkers
},
"clear"
).name("(C)lear markers");
}

function addInstructions() {
const message = document.createElement("div");
message.style.position = "absolute";
message.style.cssFloat = "right";
message.style.top = "10px";
message.style.right = "10px";
message.style.backgroundColor = "grey";
message.innerHTML = instructions;
message.innerHTML = `Tap map to add a marker, long press on a marker to remove it`;
message.style.position = "relative";
message.style.top = "60px";
document.body.appendChild(message);
}

function attachDataSources(mapView: MapView) {
const omvDataSource = new VectorTileDataSource({
baseUrl: "https://vector.hereapi.com/v2/vectortiles/base/mc",
apiFormat: APIFormat.XYZOMV,
styleSetName: "tilezen",
maxDataLevel: 17,
authenticationCode: apikey,
authenticationMethod: {
method: AuthenticationMethod.QueryString,
name: "apikey"
},
copyrightInfo
});
mapView.addDataSource(omvDataSource);

// Register the icon image referenced in the style.
for (const { name, url } of icons) {
mapView.userImageCache.addImage(name, url);
}
addInstructions();

// Create a [[FeaturesDataSource]] for the markers.
const markersDataSource = new FeaturesDataSource({
name: "geojson",
styleSetName: "geojson",
gatherFeatureAttributes: true
});
mapView.addDataSource(markersDataSource);
const mapView = initializeMapView("mapCanvas");

attachClickEvents(mapView, markersDataSource);
}
// Create a [[FeaturesDataSource]] for the markers.
const markersDataSource = new FeaturesDataSource({
name: "geojson",
styleSetName: "geojson",
gatherFeatureAttributes: true
});
mapView.addDataSource(markersDataSource);

const mapView = initializeMapView("mapCanvas");
attachDataSources(mapView);
attachInputEvents();
addUI();
}
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

0 comments on commit 79c12d4

Please sign in to comment.