What is a controller?
Using a controller, you can interact with the Map independently of the UI toolkit. Examples of these interactions are:
-
Map navigation
-
Selection
-
Editing the geometry of a feature
The main controller classes
IController
IController
IController
-
Interface to implement. An
IController
IController
IController
implementation interprets received events and triggers an action on the map. It receives toolkit-independent events —IInputEvent
IInputEvent
IInputEvent
— and has aLayerList
LayerList
LayerList
for controller-specific visualization. IInputEvent
IInputEvent
IInputEvent
-
Toolkit-independent events that represent interactions of the user with the view. These events can represent both low-level input events — mouse move, press, or release for example — and more high-level gestures, such as click and drag. The API offers common event types:
-
Dragging
-
Scrolling
-
Key pressed and released
-
Some events represent mouse interactions:
-
Mouse pressed and released
-
Mouse clicks: sequence of a mouse press and a mouse release.
-
Mouse moves
-
Mouse drags: sequence of a mouse press, one or more mouse moves and a final mouse released.
Some events can represent touch gestures:
-
A tap
-
A long press
-
A two-pointers gesture: aggregates information related to pinch and rotation in the same event
-
A touch-drag event: handles all types of single-point and multiple-point movements on the map, such as a swipe gesture with 2 pointers
If you want to use input in an IController
IController
IController
that isn’t available out-of-the-box, you can create a custom IInputEvent
IInputEvent
IInputEvent
implementation.
- Event Handlers
-
Reusable building blocks that use
IInputEvent
IInputEvent
IInputEvent
input to perform an action. The API offers basic handlers that you can use for the implementation of a controller. For example:-
HoverEventHandler
HoverEventHandler
HoverEventHandler
: modifies the hover state of the feature under the cursor. -
PanEventHandler
PanEventHandler
PanEventHandler
: uses amapNavigator
to pan the map. -
RotateEventHandler
RotateEventHandler
RotateEventHandler
: uses amapNavigator
to rotate the map. -
SelectEventHandler
SelectEventHandler
SelectEventHandler
: modifies the selected state of feature under the cursor. -
ZoomEventHandler
ZoomEventHandler
ZoomEventHandler
: uses amapNavigator
to zoom the map in both directions.
-
- Map
-
The LuciadCPillar map can have at most one controller active at any time. You activate the controller using the
setController
setController
setController
method. When you activate a controller, any controller that was already active gets de-activated first.
How to implement or customize an IController
When you implement an IController
IController
IController
, you receive events as parameters of the onEvent
onEvent
onEvent
method.
You can then choose an action to carry out, based on the event type and properties.
A convenient way to do that is to pass on the event to an existing event handler or a new one.
The following code snippets from the DefaultController
class in the samples show you how you can do that:
// MouseDragEvent: left mouse button drag to pan the map, right mouse button drag to rotate the map
auto mouseDragEvent = std::dynamic_pointer_cast<MouseDragEvent>(inputEvent);
if (mouseDragEvent) {
auto eventResult = EventResult::NotConsumed;
if (mouseDragEvent->getModifierKeys() == ModifierKeys::none()) {
if (mouseDragEvent->getMouseButton() == MouseButton::left()) {
eventResult = _panEventHandler.onDragEvent(mouseDragEvent, map);
} else if (mouseDragEvent->getMouseButton() == MouseButton::right()) {
_rotateEventHandler.setYawEnabled(true);
eventResult = _rotateEventHandler.onDragEvent(mouseDragEvent, map);
}
}
if (eventResult == EventResult::Consumed) {
return;
}
}
// TouchDragEvent: single touch point drag to pan the map, double touch point drag to change the pitch (for 3D maps)
auto touchDragEvent = std::dynamic_pointer_cast<TouchDragEvent>(inputEvent);
if (touchDragEvent) {
auto eventResult = EventResult::NotConsumed;
if (touchDragEvent->getTouchPointCount() == 1) { // drag
eventResult = _panEventHandler.onDragEvent(touchDragEvent, map);
} else if (touchDragEvent->getTouchPointCount() == 2) {
// pitch
if (map->is3D()) {
_rotateEventHandler.setYawEnabled(false);
eventResult = _rotateEventHandler.onDragEvent(touchDragEvent, map);
}
}
if (eventResult == EventResult::Consumed) {
return;
}
}
// MouseDragEvent: left mouse button drag to pan the map, right mouse button drag to rotate the map
if (inputEvent is MouseDragEvent mouseDragEvent)
{
EventResult result = EventResult.NotConsumed;
if (mouseDragEvent.ModifierKeys == ModifierKeys.None)
{
if (mouseDragEvent.MouseButton == MouseButton.Left)
{
result = _panEventHandler.OnDragEvent(mouseDragEvent, _map);
}
else if (mouseDragEvent.MouseButton == MouseButton.Right)
{
_rotateEventHandler.IsYawEnabled = true;
result = _rotateEventHandler.OnDragEvent(mouseDragEvent, _map);
}
}
if (result == EventResult.Consumed)
{
return;
}
}
// TouchDragEvent: single touch point drag to pan the map, double touch point drag to change the pitch (for 3D maps)
if (inputEvent is TouchDragEvent touchDragEvent)
{
eventResult = EventResult.NotConsumed;
if (touchDragEvent.TouchPointCount == 1)
{
// drag
eventResult = _panEventHandler.OnDragEvent(touchDragEvent, _map);
}
else if (touchDragEvent.TouchPointCount == 2)
{
// pitch
if (_map.Is3D)
{
_rotateEventHandler.IsYawEnabled = false;
eventResult = _rotateEventHandler.OnDragEvent(touchDragEvent, _map);
}
}
if (eventResult == EventResult.Consumed)
{
return;
}
}
// TouchDragEvent: single touch point drag to pan the map, double touch point drag to change the pitch (for 3D maps)
if (inputEvent is TouchDragEvent) {
eventResult = EventResult.NotConsumed
if (inputEvent.touchPointCount == 1L) {
eventResult = panEventHandler.onDragEvent(inputEvent, map)
} else if (inputEvent.touchPointCount == 2L) {
if (map.is3D) {
rotateEventHandler.isYawEnabled = false
eventResult = rotateEventHandler.onDragEvent(inputEvent, map)
}
}
if (eventResult == EventResult.Consumed) {
return
}
}
auto scrollEvent = std::dynamic_pointer_cast<ScrollEvent>(inputEvent);
if (scrollEvent && (scrollEvent->getModifierKeys() == ModifierKeys::none() || scrollEvent->getModifierKeys() == ModifierKeys::ctrl() ||
scrollEvent->getModifierKeys() == ModifierKeys::shift())) {
double zoomFactor = scrollEvent->getModifierKeys() == ModifierKeys::shift() ? 0.1 : (scrollEvent->getModifierKeys() == ModifierKeys::ctrl() ? 2.0 : 0.5);
ZoomEventHandler{}.onScrollEvent(scrollEvent, map, zoomFactor);
}
if (_currentTouchGesture == TouchGesture::Pinch) {
auto currentDistance = pinchRotateEvent->getCurrentDistance();
if (currentDistance > 0.0 && _currentZoomAction.has_value()) {
auto zoomFactor = pinchRotateEvent->getInitialDistance() / currentDistance;
_currentZoomAction->factor(zoomFactor).zoom();
}
}
if (inputEvent is ScrollEvent scrollEvent && (scrollEvent.ModifierKeys == ModifierKeys.None ||
scrollEvent.ModifierKeys == ModifierKeys.Ctrl ||
scrollEvent.ModifierKeys == ModifierKeys.Shift))
{
var zoomFactor = scrollEvent.ModifierKeys == ModifierKeys.Shift ? 0.1 : scrollEvent.ModifierKeys == ModifierKeys.Ctrl ? 2.0 : 0.5;
_zoomEventHandler.OnScrollEvent(scrollEvent, _map, zoomFactor);
}
if (_currentTouchGesture == TouchGesture.Pinch && _currentZoomAction != null)
{
var currentDistance = pinchRotateEvent.CurrentDistance;
if (currentDistance > 0.0)
{
var zoomFactor = pinchRotateEvent.InitialDistance / currentDistance;
_currentZoomAction.Factor(zoomFactor).Animate(false).Zoom();
}
}
if (currentTouchGesture == TouchGesture.Pinch && currentZoomAction != null) {
val currentDistance = inputEvent.currentDistance
if (currentDistance > 0.0) {
val zoomFactor = inputEvent.initialDistance / currentDistance
currentZoomAction!!.factor(zoomFactor).zoom()
}
}
How to handle mouse and touch input using the same handler.
The fact that MouseClickEvent
MouseClickEvent
MouseClickEvent
and TouchTapEvent
TouchTapEvent
TouchTapEvent
are both sub classes of the same superclass (ClickEvent
ClickEvent
ClickEvent
) allows the event consumer to support both mouse and touch inputs the same way.
The same remark goes for the MouseDragEvent
MouseDragEvent
MouseDragEvent
and the TouchDragEvent
TouchDragEvent
TouchDragEvent
that share the same super class: DragEvent
DragEvent
DragEvent
.
As an example, the following code snippet allows the user to zoom using a double click or a double tap (used in the DefaultController
in the code samples).
// Using the base class to allow zooming using both double-clicks and double-taps.
const auto clickEvent = std::dynamic_pointer_cast<ClickEvent>(inputEvent);
if (clickEvent && clickEvent->getClickCount() % 2 == 0) {
ZoomEventHandler{}.onClickEvent(clickEvent, map, true, 0.5);
return;
}
if (inputEvent is ClickEvent clickEvent && clickEvent.ClickCount % 2 == 0)
{
_zoomEventHandler.OnClickEvent(clickEvent, _map, true, 0.5);
return;
}
if (inputEvent is TouchTapEvent && inputEvent.clickCount == 2L) {
ZoomEventHandler().onClickEvent(inputEvent, map, true, 0.5)
return
}
How to add visualization to an IController
To respond to some user interactions, you may need to add visual components to the map. For instance, if a user interacts with a map feature to edit it, you need to display editing handles on the feature. Users can then change the feature by manipulating those handles.
If you need to add visual components to the map to respond to user interactions, your controller can implement the getLayerList
getLayerList
getLayerList
method.
You use it to return a LayerList
LayerList
LayerList
to visualize controller-specific information such as handles.
If the controller’s layer list is a dedicated one, the map retrieves it when the controller is attached, and appends this list to its own layer list with the controller list on top.
Both options have their advantages:
-
If you use a
LayerList
LayerList
LayerList
, you can easily hide theIController
IController
IController
LayerList
LayerList
LayerList
from a layer control UI element. -
If the controller adds a layer to the
Map
Map
Map
LayerList
LayerList
LayerList
, you can more easily control the painting order. LuciadCPillar always paints the layers of anIController
IController
IController
LayerList
LayerList
LayerList
after it paints theMap
Map
Map
layers.
How to integrate IController in a UI toolkit
If you want to use your own UI Toolkit, you must map the toolkit events to IInputEvent
IInputEvent
IInputEvent
instances. Once you have those IInputEvent
IInputEvent
IInputEvent
instances, you can send them to the active IController
IController
IController
using the onEvent
onEvent
onEvent
method.
The API offers utility classes to make this easier: MouseGestureRecognizer
MouseGestureRecognizer
MouseGestureRecognizer
and TouchGestureRecognizer
TouchGestureRecognizer
TouchGestureRecognizer
. These classes transform low-level UI events, such as move, press, or release, into more high-level IInputEvent
IInputEvent
IInputEvent
objects, such as drag, click or pinch.
The sample integration code also has utility classes that forward Qt or WPF events to gesture recognizers and map the resulting
IInputEvent
IInputEvent
IInputEvent
to the active controller:
-
Qt (QML) :
QQuickMapObject
-
Qt (QWidget) :
QMapWidget
-
WPF :
WpfMapControl
.
See this article for more information about how to pass Qt events to the gesture recognizers.