-
-
Notifications
You must be signed in to change notification settings - Fork 995
Description
Motivation
Most modern web applications use a reactive/MVC UI framework, such as React, Angular and Vue. When building a complex application that contains a map component, it is very rare that the map library itself provides all the necessary functionalities. Some typical use cases include:
- Synchronize multiple Map instances, side-by-side, minimap, etc.
- Synchronize the Map camera with an external overlay, e.g. deck.gl or custom UI that moves with the map
- Match the Map with non-geospatial UI, e.g. a list of visible points of interests.
These scenarios all concern two-way state sharing between Map instances and other UI components. In the reactive programming paradigm, an exemplary implementation manages the camera state at the app's top level with a state store. Any UI component can request a change to that state. When the state does change, a rerender is triggered that flows down to the map and other components.
The design that maplibre-gl inherited from mapbox-gl does not make this easy. When the map receives user input, it immediately modifies the camera (transform) and rerenders. A move event is fired after the fact. Say the application updates its state based on a move event listener. When the other React components get rendered, they are always one animation frame behind the map.
I have maintained react-map-gl for over five years. There had been different attempts to address this issue, all concerning a) blocking the map from updating its own camera b) forcing the map to redraw when React rerenders. Prior to v6, the wrapper sets interactive: false and implements its own input handlers. In v7, the wrapper swaps out map.transform with a "shadow transform" that matches the React props just before repaint. Both approaches have painful shortcomings including performance and a creeping amount of hacks.
mapbox-gl issue regarding the stateful nature of input handling
mapbox-gl issue regarding the state dependency of transform
Proposal
Whenever map.transform is about to be modified (any operation that leads to a move event), fire a premove event with the proposed changes: zoom, pitch, bearing, center. The event listeners are allowed to mutate the proposed camera parameters. map.transform is then updated to the "approved" values. In pseudo code:
/// Current
const tr = map.transform;
tr.bearing = newBearing;
tr.pitch = newPitch;
tr.zoom = newZoom;
tr.setLocationAtPoint(around, aroundPoint);
fireMoveEvents();/// Proposed
const tr = map.transform;
const tr2 = tr.clone(); // this will resolve any incoming change and apply constraints
tr2.bearing = newBearing;
tr2.pitch = newPitch;
tr2.zoom = newZoom;
tr2.setLocationAtPoint(around, aroundPoint);
const proposedChanges = {
center: tr2.center,
bearing: tr2.bearing,
pitch: tr2.pitch,
zoom: tr2.zoom,
};
map.fire(new Event('premove'), {proposedChanges});
tr.bearing = proposedChanges.bearing;
tr.pitch = proposedChanges.pitch;
tr.zoom = proposedChanges.zoom;
tr.center = proposedChanges.center;
fireMoveEvents();The current usage pattern and code path will stay the same if the event is not handled.
In an application (or wrapper library) where the Map instance is used as a stateless component, premove is used to block the camera from updating immediately. Rather, the transform will be modified when the state change propogates.
Some of the handler classes will need to be updated. They currently produce panDelta, pitchDelta, zoomDelta etc. for each input event and assume that map.transform holds the accumulated change. Once the transform state and user intention are decoupled, the handlers will need to track the accumulated changes themselves, so that interaction is still responsive and not dependent on when rerender happens.
Here is a very rough proof-of-concept, only enough changes for pointer inputs to work correctly:
https://github.com/maplibre/maplibre-gl-js/compare/x/premove-poc
I'm open to alternative ideas.
Concerns
- It's breaking this library's API convention to have event handlers pass data back.
- Changes to input handlers have rippling effects throughout the code base.
- Is it possible to write test cases that block future commits that assume immediate state change?