Skip to content

Commit 93f0ba7

Browse files
feat(mapbox): Replace shadow transform with proxied approach (#2514)
1 parent 3673af7 commit 93f0ba7

File tree

7 files changed

+322
-169
lines changed

7 files changed

+322
-169
lines changed

docs/api-reference/mapbox/types.md

+1
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ An object with the following fields:
168168
- `zoom`: number - The zoom level.
169169
- `pitch`: number - The pitch (tilt) of the map, in degrees.
170170
- `bearing`: number - The bearing (rotation) of the map, in degrees.
171+
- `elevation`: number|undefined - The map center elevation from sea leavel on terrain surface, if any
171172

172173

173174
## Events

modules/react-mapbox/src/mapbox/mapbox.ts

+51-98
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
import {
2-
transformToViewState,
3-
applyViewStateToTransform,
4-
cloneTransform,
5-
syncProjection
6-
} from '../utils/transform';
1+
import {transformToViewState, compareViewStateWithTransform} from '../utils/transform';
2+
import {ProxyTransform, createProxyTransform} from './proxy-transform';
73
import {normalizeStyle} from '../utils/style-utils';
84
import {deepEqual} from '../utils/deep-equal';
95

@@ -162,23 +158,27 @@ const handlerNames = [
162158
*/
163159
export default class Mapbox {
164160
private _MapClass: {new (options: any): MapInstance};
165-
// mapboxgl.Map instance
161+
/** mapboxgl.Map instance */
166162
private _map: MapInstance = null;
167-
// User-supplied props
163+
/** User-supplied props */
168164
props: MapboxProps;
169165

170-
// Mapbox map is stateful.
171-
// During method calls/user interactions, map.transform is mutated and
172-
// deviate from user-supplied props.
173-
// In order to control the map reactively, we shadow the transform
174-
// with the one below, which reflects the view state resolved from
175-
// both user-supplied props and the underlying state
176-
private _renderTransform: Transform;
166+
/** The transform that replaces native map.transform to resolve changes vs. React props
167+
* See proxy-transform.ts
168+
*/
169+
private _proxyTransform: ProxyTransform;
177170

178171
// Internal states
172+
/** Making updates driven by React props. Do not trigger React callbacks to avoid infinite loop */
179173
private _internalUpdate: boolean = false;
174+
/** Map is currently rendering */
180175
private _inRender: boolean = false;
176+
/** Map features under the pointer */
181177
private _hoveredFeatures: MapGeoJSONFeature[] = null;
178+
/** View state changes driven by React props
179+
* They still need to fire move/etc. events because controls such as marker/popup
180+
* subscribe to the move event internally to update their position
181+
* React callbacks like onMove are not called for these */
182182
private _deferredEvents: {
183183
move: boolean;
184184
zoom: boolean;
@@ -208,7 +208,7 @@ export default class Mapbox {
208208
}
209209

210210
get transform(): Transform {
211-
return this._renderTransform;
211+
return this._map.transform;
212212
}
213213

214214
setProps(props: MapboxProps) {
@@ -217,7 +217,7 @@ export default class Mapbox {
217217

218218
const settingsChanged = this._updateSettings(props, oldProps);
219219
if (settingsChanged) {
220-
this._createShadowTransform(this._map);
220+
this._createProxyTransform(this._map);
221221
}
222222
const sizeChanged = this._updateSize(props);
223223
const viewStateChanged = this._updateViewState(props, true);
@@ -318,39 +318,40 @@ export default class Mapbox {
318318
if (props.cursor) {
319319
map.getCanvas().style.cursor = props.cursor;
320320
}
321-
this._createShadowTransform(map);
321+
this._createProxyTransform(map);
322322

323323
// Hack
324324
// Insert code into map's render cycle
325325
// eslint-disable-next-line @typescript-eslint/unbound-method
326326
const renderMap = map._render;
327327
map._render = (arg: number) => {
328+
// Hijacked to set this state flag
328329
this._inRender = true;
329330
renderMap.call(map, arg);
330331
this._inRender = false;
331332
};
332333
// eslint-disable-next-line @typescript-eslint/unbound-method
333334
const runRenderTaskQueue = map._renderTaskQueue.run;
334335
map._renderTaskQueue.run = (arg: number) => {
336+
// This is where camera updates from input handler/animation happens
337+
// And where all view state change events are fired
338+
this._proxyTransform.$internalUpdate = true;
335339
runRenderTaskQueue.call(map._renderTaskQueue, arg);
336-
this._onBeforeRepaint();
340+
this._proxyTransform.$internalUpdate = false;
341+
this._fireDefferedEvents();
337342
};
338-
map.on('render', () => this._onAfterRepaint());
339343
// Insert code into map's event pipeline
340344
// eslint-disable-next-line @typescript-eslint/unbound-method
341345
const fireEvent = map.fire;
342346
map.fire = this._fireEvent.bind(this, fireEvent);
343347

344348
// add listeners
345-
map.on('resize', () => {
346-
this._renderTransform.resize(map.transform.width, map.transform.height);
347-
});
348349
map.on('styledata', () => {
349350
this._updateStyleComponents(this.props, {});
350-
// Projection can be set in stylesheet
351-
syncProjection(map.transform, this._renderTransform);
352351
});
353-
map.on('sourcedata', () => this._updateStyleComponents(this.props, {}));
352+
map.on('sourcedata', () => {
353+
this._updateStyleComponents(this.props, {});
354+
});
354355
for (const eventName in pointerEvents) {
355356
map.on(eventName, this._onPointerEvent);
356357
}
@@ -396,11 +397,11 @@ export default class Mapbox {
396397
}
397398
}
398399

399-
_createShadowTransform(map: any) {
400-
const renderTransform = cloneTransform(map.transform);
401-
map.painter.transform = renderTransform;
402-
403-
this._renderTransform = renderTransform;
400+
_createProxyTransform(map: any) {
401+
const proxyTransform = createProxyTransform(map.transform);
402+
map.transform = proxyTransform;
403+
map.painter.transform = proxyTransform;
404+
this._proxyTransform = proxyTransform;
404405
}
405406

406407
/* Trigger map resize if size is controlled
@@ -427,28 +428,11 @@ export default class Mapbox {
427428
@returns {bool} true if anything is changed
428429
*/
429430
_updateViewState(nextProps: MapboxProps, triggerEvents: boolean): boolean {
430-
if (this._internalUpdate) {
431-
return false;
432-
}
433-
const map = this._map;
434-
435-
const tr = this._renderTransform;
436-
// Take a snapshot of the transform before mutation
431+
const viewState: Partial<ViewState> = nextProps.viewState || nextProps;
432+
const tr = this._proxyTransform;
437433
const {zoom, pitch, bearing} = tr;
438-
const isMoving = map.isMoving();
439-
440-
if (isMoving) {
441-
// All movement of the camera is done relative to the sea level
442-
tr.cameraElevationReference = 'sea';
443-
}
444-
const changed = applyViewStateToTransform(tr, {
445-
...transformToViewState(map.transform),
446-
...nextProps
447-
});
448-
if (isMoving) {
449-
// Reset camera reference
450-
tr.cameraElevationReference = 'ground';
451-
}
434+
const changed = compareViewStateWithTransform(this._proxyTransform, viewState);
435+
tr.$reactViewState = viewState;
452436

453437
if (changed && triggerEvents) {
454438
const deferredEvents = this._deferredEvents;
@@ -459,12 +443,6 @@ export default class Mapbox {
459443
deferredEvents.pitch ||= pitch !== tr.pitch;
460444
}
461445

462-
// Avoid manipulating the real transform when interaction/animation is ongoing
463-
// as it would interfere with Mapbox's handlers
464-
if (!isMoving) {
465-
applyViewStateToTransform(map.transform, nextProps);
466-
}
467-
468446
return changed;
469447
}
470448

@@ -576,18 +554,14 @@ export default class Mapbox {
576554

577555
private _queryRenderedFeatures(point: Point) {
578556
const map = this._map;
579-
const tr = map.transform;
580557
const {interactiveLayerIds = []} = this.props;
581558
try {
582-
map.transform = this._renderTransform;
583559
return map.queryRenderedFeatures(point, {
584560
layers: interactiveLayerIds.filter(map.getLayer.bind(map))
585561
});
586562
} catch {
587563
// May fail if style is not loaded
588564
return [];
589-
} finally {
590-
map.transform = tr;
591565
}
592566
}
593567

@@ -637,9 +611,14 @@ export default class Mapbox {
637611
if (!this._internalUpdate) {
638612
// @ts-ignore
639613
const cb = this.props[cameraEvents[e.type]];
614+
const tr = this._proxyTransform;
640615
if (cb) {
616+
e.viewState = transformToViewState(tr.$proposedTransform ?? tr);
641617
cb(e);
642618
}
619+
if (e.type === 'moveend') {
620+
tr.$proposedTransform = null;
621+
}
643622
}
644623
if (e.type in this._deferredEvents) {
645624
this._deferredEvents[e.type] = false;
@@ -648,57 +627,31 @@ export default class Mapbox {
648627

649628
_fireEvent(baseFire: Function, event: string | MapEvent, properties?: object) {
650629
const map = this._map;
651-
const tr = map.transform;
630+
const tr = this._proxyTransform;
652631

653-
const eventType = typeof event === 'string' ? event : event.type;
654-
if (eventType === 'move') {
655-
this._updateViewState(this.props, false);
656-
}
657-
if (eventType in cameraEvents) {
658-
if (typeof event === 'object') {
659-
(event as unknown as ViewStateChangeEvent).viewState = transformToViewState(tr);
660-
}
661-
if (this._map.isMoving()) {
662-
// Replace map.transform with ours during the callbacks
663-
map.transform = this._renderTransform;
664-
baseFire.call(map, event, properties);
665-
map.transform = tr;
666-
667-
return map;
668-
}
632+
// Always expose the controlled transform to controls/end user
633+
const internal = tr.$internalUpdate;
634+
try {
635+
tr.$internalUpdate = false;
636+
baseFire.call(map, event, properties);
637+
} finally {
638+
tr.$internalUpdate = internal;
669639
}
670-
baseFire.call(map, event, properties);
671640

672641
return map;
673642
}
674643

675-
// All camera manipulations are complete, ready to repaint
676-
_onBeforeRepaint() {
644+
// If there are camera changes driven by props, invoke camera events so that DOM controls are synced
645+
_fireDefferedEvents() {
677646
const map = this._map;
678-
679-
// If there are camera changes driven by props, invoke camera events so that DOM controls are synced
680647
this._internalUpdate = true;
681648
for (const eventType in this._deferredEvents) {
682649
if (this._deferredEvents[eventType]) {
683650
map.fire(eventType);
684651
}
685652
}
686653
this._internalUpdate = false;
687-
688-
const tr = this._map.transform;
689-
// Make sure camera matches the current props
690-
map.transform = this._renderTransform;
691-
692-
this._onAfterRepaint = () => {
693-
// Mapbox transitions between non-mercator projection and mercator during render time
694-
// Copy it back to the other
695-
syncProjection(this._renderTransform, tr);
696-
// Restores camera state before render/load events are fired
697-
map.transform = tr;
698-
};
699654
}
700-
701-
_onAfterRepaint: () => void;
702655
}
703656

704657
/**

0 commit comments

Comments
 (0)