Skip to content

Commit

Permalink
First pass at new EventManager.
Browse files Browse the repository at this point in the history
Provides single API for both raw and gestural events; leverages hammer.js for both.
Replaces use of luma.gl's addEvents.
Does not yet reimplement support for dragging (i.e. dragging is broken as of this commit).
  • Loading branch information
ericsoco committed May 8, 2017
1 parent 3f4e102 commit 4b08366
Show file tree
Hide file tree
Showing 7 changed files with 1,089 additions and 567 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"d3-hexbin": "^0.2.1",
"earcut": "^2.0.6",
"gl-matrix": "^2.3.2",
"hammerjs": "^2.0.8",
"lodash.flattendeep": "^4.4.0",
"prop-types": "^15.5.8",
"seer": "^0.0.4"
Expand Down
17 changes: 16 additions & 1 deletion src/react/deckgl.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ import autobind from './autobind';
import WebGLRenderer from './webgl-renderer';
import {LayerManager, Layer} from '../lib';
import {EffectManager, Effect} from '../experimental';
import {GL, addEvents} from 'luma.gl';
import {GL} from 'luma.gl';
import {Viewport, WebMercatorViewport} from '../lib/viewports';
import {log} from '../lib/utils';
import {createEventManager} from '../utils/event-manager';

function noop() {}

Expand Down Expand Up @@ -69,6 +70,7 @@ export default class DeckGL extends React.Component {
super(props);
this.state = {};
this.needsRedraw = true;
this.eventManager = null;
this.layerManager = null;
this.effectManager = null;
autobind(this);
Expand Down Expand Up @@ -126,6 +128,18 @@ export default class DeckGL extends React.Component {
);
}

// TODO: add handlers on demand at runtime, not all at once on init
this.eventManager = createEventManager(canvas, {})
.on({
click: this._onClick,
mousemove: this._onMouseMove,
dragstart: this._onDragEvent,
dragmove: this._onDragEvent,
dragend: this._onDragEvent,
dragcancel: this._onDragCancel
});

/*
this.events = addEvents(canvas, {
cacheSize: false,
cachePosition: false,
Expand All @@ -137,6 +151,7 @@ export default class DeckGL extends React.Component {
onDragEnd: this._onDragEvent,
onDragCancel: this._onDragCancel
});
*/
}

// Route events to layers
Expand Down
137 changes: 137 additions & 0 deletions src/utils/event-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import {createGestureManager, EVENT_RECOGNIZER_MAP} from './gesture-manager';

const EVENT_HANDLER_MAP = {
pointerdown: 'mousedown',
pointermove: 'mousemove',
pointerup: 'mouseup',
mousedown: 'mousedown',
mousemove: 'mousemove',
mouseup: 'mouseup'
};
const GESTURE_ALIASES = {
click: 'tap'
};

const DEFAULT_OPTIONS = {
handleRawEventsWithGestureManager: true
};

/**
* Single API for subscribing to events about both "raw" input (e.g. 'mousemove', 'click')
* and gestural input (e.g. 'panstart', 'tap').
*/
class EventManager {
constructor(element, options) {
this.element = element;
this.handlers = {};
this.gestureManager = createGestureManager(element, options);

// If specified, use GestureManager for raw event handling
// as well as for gesture recognition.
if (options.handleRawEventsWithGestureManager) {
this._onRawInput = this._onRawInput.bind(this);
this.gestureManager.startHandlingRawEvents(this._onRawInput);
}
}

on(event, handler) {
if (typeof event === 'string') {
// wrap and store handler
if (!this.handlers[event]) {
this.handlers[event] = [];
}
const handlersForEvent = this.handlers[event];
let handlerForEvent = handlersForEvent.find(h => h.handler === handler);
if (!handlerForEvent) {
handlerForEvent = {
handler,
wrapper: e => handler(this._wrapEvent(e, 'srcEvent'))
};
handlersForEvent.push(handlerForEvent);
}

// Handle as a gestural event if appropriate
const aliasedEvent = GESTURE_ALIASES[event] || event;
if (EVENT_RECOGNIZER_MAP[aliasedEvent]) {
this.gestureManager.on(aliasedEvent, handlerForEvent.wrapper);
}
} else {
for (const eventName in event) {
this.on(eventName, event[eventName]);
}
}
}

off(event, handler) {
if (typeof event === 'string') {
// find wrapped handler
const handlersForEvent = this.handlers[event];
if (handlersForEvent) {
const i = handlersForEvent.findIndex(h => h.handler === handler);
let wrapper;
if (i !== -1) {
wrapper = handlersForEvent.splice(i, 1)[0].wrapper;
}
if (!handlersForEvent.length) {
delete this.handlers[event];
}

// Handle as a gestural event if appropriate
const aliasedEvent = GESTURE_ALIASES[event] || event;
if (wrapper && EVENT_RECOGNIZER_MAP[aliasedEvent]) {
this.gestureManager.off(aliasedEvent, wrapper);
}
}
} else {
for (const eventName in event) {
this.off(eventName, event[eventName]);
}
}
}

_onRawInput(event) {
if (EVENT_RECOGNIZER_MAP[event.type]) {
// let GestureManager handle these events
return;
}

const {srcEvent} = event;
const normalizedEventType = srcEvent && EVENT_HANDLER_MAP[srcEvent.type];
if (!normalizedEventType) {
// not a recognized event type
return;
}

const handlersForEvent = this.handlers[normalizedEventType];
if (handlersForEvent) {
handlersForEvent.forEach(handler => handler.wrapper(event));
}
}

_wrapEvent(event, srcEventPropName) {
// TODO: decide on how best to wrap for cross-browser compatibility
// possible options:
// - luma.gl's `EventsProxy.eventInfo()` wrapper
// (https://github.com/uber/luma.gl/blob/master/src/addons/event.js#L171)
// - npm `synthetic-dom-event`
// (https://www.npmjs.com/package/synthetic-dom-events))
// - hammer.js `compute-input-data` wrapper + additional info
// (https://github.com/hammerjs/hammer.js/blob/master/src/inputjs/compute-input-data.js)

// TODO: consider refactoring deck.gl to look for `srcEvent` instead of `event`
// for original/source event object;
// `event.srcEvent` is more descriptive than `event.event`

return {
event: event[srcEventPropName]
};
}
}

export function createEventManager(element, options = {}) {
// TODO: should this be a Singleton?
// conversely, is actually a good reason to use the Factory pattern here,
// or should we allow direct instantiation?
options = Object.assign({}, DEFAULT_OPTIONS, options);
return new EventManager(element, options);
}
120 changes: 120 additions & 0 deletions src/utils/gesture-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import {
Manager,
Tap,
Press,
Pinch,
Rotate,
Pan,
Swipe
} from 'hammerjs';
import WheelInput from './wheel-input';
import log from '../lib/utils/log';

export const EVENT_RECOGNIZER_MAP = {
tap: 'tap',
doubletap: 'tap',
press: 'press',
pinch: 'pinch',
pinchin: 'pinch',
pinchout: 'pinch',
pinchstart: 'pinch',
pinchmove: 'pinch',
pinchend: 'pinch',
pinchcancel: 'pinch',
rotate: 'rotate',
rotatestart: 'rotate',
rotatemove: 'rotate',
rotateend: 'rotate',
rotatecancel: 'rotate',
pan: 'pan',
panstart: 'pan',
panmove: 'pan',
panup: 'pan',
pandown: 'pan',
panleft: 'pan',
panright: 'pan',
panend: 'pan',
pancancel: 'pan',
swipe: 'swipe',
swipeleft: 'swipe',
swiperight: 'swipe',
swipeup: 'swipe',
swipedown: 'swipe'
};

const RECOGNIZERS = {
doubletap: new Tap({
event: 'doubletap',
taps: 2
}),
pan: new Pan({
threshold: 10
}),
pinch: new Pinch(),
press: new Press(),
rotate: new Rotate(),
swipe: new Swipe(),
tap: new Tap()
};

class GestureManager {

constructor(element, options) {
// TODO: support overriding default RECOGNIZERS by passing
// recognizers / configs, keyed to event name.

// how to get inputClass from createInputInstance without a Manager instance?
// mostly just need to run logic from createInputInstance...
// Issue filed: https://github.com/hammerjs/hammer.js/issues/1106
const inputClass = WheelInput;

this.manager = new Manager(element, {
inputClass
});
}

on(event, handler) {
const recognizerEvent = this._getRecognizerEvent(event);
if (!recognizerEvent) {
return;
}

// add recognizer for this event if not already added.
if (!this.manager.get(recognizerEvent)) {
this.manager.add(RECOGNIZERS[recognizerEvent]);
}

// TODO: verify event+handler not already registered.
// https://github.com/hammerjs/hammer.js/issues/1107
this.manager.on(event, handler);
}

off(event, handler) {
const recognizerEvent = this._getRecognizerEvent(event);
if (!recognizerEvent) {
return;
}

this.manager.off(event, handler);
}

startHandlingRawEvents(handler) {
this.manager.on('hammer.input', handler);
}

stopHandlingRawEvents(handler) {
this.manager.off('hammer.input', handler);
}

_getRecognizerEvent(event) {
const recognizerEvent = EVENT_RECOGNIZER_MAP[event];
if (!recognizerEvent) {
log(1, 'no gesture recognizer available for event', event);
}
return recognizerEvent;
}
}

export function createGestureManager(element, options = {}) {
return new GestureManager(element, options);
}
40 changes: 40 additions & 0 deletions src/utils/pointer-move-event-input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {PointerEventInput} from 'hammerjs';

const POINTER_MOVE = 'pointermove';

// Copied from Hammer.js' pointerevent.js
const IE10_POINTER_TYPE_ENUM = {
2: 'touch',
3: 'pen',
4: 'mouse',
5: 'kinect' // see https://twitter.com/jacobrossi/status/480596438489890816
};

export default class PointerMoveEventInput extends PointerEventInput {

constructor(...opts) {
super(...opts);
}

handler(event) {
// let 'pointermove' events through when the pointer is not down.
// all other cases (including 'pointermove' while pointer is down)
// are handled by PointerEventInput.
const {store} = this;
if (event.type === POINTER_MOVE) {
const storeIndex = store.findIndex(i =>
i.pointerId == event.pointerId); // eslint-disable-line eqeqeq
if (storeIndex < 0) {
this.callback(this.manager, POINTER_MOVE, {
pointers: store,
changedPointers: [event],
pointerType: IE10_POINTER_TYPE_ENUM[event.pointerType] || event.pointerType,
srcEvent: event
});
return;
}
}

super.handler(event);
}
}

0 comments on commit 4b08366

Please sign in to comment.