Skip to main content

Managing user interactions

User interactions via mouse, keyboard and touch devices trigger many different behaviors in the application. Depending on the currently active tools or modes, these events can overlap each other and cause unwanted behavior and side effects.

The UserInteractionManager is the central place where event listeners are registered and reassignment of mouse/keyboard control between different components and tools is managed.

UserInteractionManager can manage the following kind of events:

  • Mouse primary click, alternate click and double click
  • Mouse movement
  • Mouse wheel
  • Mouse wheel buttons
  • Any mouse action in combination with modifier keys
  • More complex interactions consisting of simpler events like openlayers draw or modify
  • Keyboard events including modifier keys

The UserInteractionManager is responsible for:

  • registering event listeners in the state
  • reassigning event listening depending on active tool or mode
  • allowing parallel or exclusive listening to events
  • restoring defaults once tools deactivate or close

Guidelines

For user interaction management to work, these guidelines have to be followed when creating listeners for interaction events:

  1. Register the listener with the UserInteractionManager
  2. Whenever the event triggers, ask for permission before reacting to the event
  3. Unregister listeners once the tool deactivates or closes

Unit tests enforce these guidelines by searching and comparing event listeners and listener registrations in the code base.

1. Register listener

Register listeners when the tool activates or becomes visible. When working with a class that extendsGirafeHTMLElement, the helper function registerInteraction() can be used.

Decide if the listener should be exclusively listening for the event (isExclusive = true), or if other tools can react as well.

// In GirafeHTMLElement
this.registerInteraction('map.mouseclick', true);

// Outside GirafeHTMLElement
UserInteractionManager.getInstance().registerListener('map.mouseclick', true, 'nameOfTool');
Exclusive listeners

If registering a listener as exklusive, any other listener for this event will be disabled until the tool is closed again

2. Ask for permission when event triggers

Before the event can be processed, each listener must ask for permission with the UserInteractionManager.

// In GirafeHTMLElement
document.addEventListener('click', (e) => {
if (!this.canExecute('map.mouseclick')) {
return;
}
// React to event
});

// Outside GirafeHTMLElement
document.addEventListener('click', (e) => {
if (!UserInteractionManager.getInstance().canExecute('map.mouseclick', 'nameOfTool')) {
return;
}
// React to event
});

Many openlayers interactions (draw, modify, etc.) provide a condition option that can be used to make the permission check:

this.modifyInteraction = new Modify({
source: this.linestringSource,
condition: (e) => primaryAction(e) && this.canExecute('map.modify')
});

3. Unregister on close

When working in a GirafeHTMLElement class, unregistering isn't necessary because the component does it automatically in its hide() function.

Anywhere else, use:

UserInteractionManager.getInstance().unregister('map.mouseclick', 'nameOfTool');

Event names: GgUserInteractionEvent

To help define and identify events across components and tools of GeoGirafe, the type GgUserInteractionEvent is provided. It is a list of strings that describe common mouse and keyboard events including optional modifier keys.

Examples:

  • 'map.mouseclick'
  • 'map.mouseclick.ctrl'
  • 'keydown.P'
  • 'keypress.ctrl.X'
  • 'map.draw'
  • 'map.modify'
  • 'globe.draw'

GgUserInteractionEvent provides code suggestions and auto complete in IDEs and helps to reduce the chance for typos. It is easily expandable if new events need to be registered and managed.

The type also contains some more complex interactions that trigger many different mouse events while the user interacts with the map, like 'map.draw' or 'map.modify'. The UserInteractionManager handles this by defining gGEventDependencies: When 'map.draw' is registered exclusively, the manager disables 'map.mouseclick' and 'map.mousedoubleclick' as well.

Interaction management in the state

The UserInteractionManager stores listeners in the state by creating and saving GgUserInteractionListener objects. These objects identify the listener and describe its behaviour fully. Every GgUserInteractionListener object in the state must be unique.

  • eventName: String of type GgUserInteractionEvent identifying the event
  • isExclusive: Decides if this listener is the only one that can react to the event (true). If false, multiple tools can listen for the event in parallel
  • toolName: Name of component, manager or tool. When working with a component extending GirafeHTMLElement, this.name is used

Typical use cases

Disable map feature selection

If feature selection on the map is interfering with other interactions of your component, it can be deactivated by registering 'map.select' as an exclusive listener. Even if the component does not need feature selection functionality, the exclusive registration will block the map from reacting to mouse clicks and therefore deactivating feature selection temporarily.

In the component add:

// GirafeHTMLElement
this.registerInteraction('map.select', true);

When the component closes, the GirafeHTMLElement will automatically unregister the 'map.select' listener, which will in turn reactivate feature selection on the map.

Drawing and editing features on the map

First, the events are registered when the component is rendered visible. Usually, drawing and modifying interactions should be registered as exclusive, since having multiple draw interactions active at the same time will lead to unwanted side effects.

registerEvents() {
this.registerInteraction('map.draw', true);
this.registerInteraction('map.modify', true);
}

::: tip Note that openlayers interactions are complex events that consist of multiple simpler events like mouse click or double click. The UserInteractionManager knows about this and will deactivate these simpler events as well. :::

Then, create your openlayers interactions as usual but add a condition that checks for execution permission. primaryAction(e) refers to the default condition to trigger the drawing interaction.

this.draw = new Draw({
source: this.drawingSource,
type: olTool as Type,
geometryFunction: geomFunction,
condition: (e) => primaryAction(e) && this.canExecute('map.draw'),
});

Don't forget to deactivate or remove the openlayers interaction from the map once the component closes.

// deactivate
this.draw.setActive(false);
// and/or remove
this.map.removeInteraction(this.draw);