• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / react-map-gl / 14139551773

28 Mar 2025 11:20PM UTC coverage: 85.063% (-0.3%) from 85.368%
14139551773

push

github

web-flow
feat(mapbox): Replace shadow transform with proxied approach (#2514)

945 of 1169 branches covered (80.84%)

Branch coverage included in aggregate %.

231 of 278 new or added lines in 3 files covered. (83.09%)

5564 of 6483 relevant lines covered (85.82%)

70.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.9
/modules/react-mapbox/src/mapbox/proxy-transform.ts
1
import type {Transform} from '../types/internal';
1✔
2
import type {ViewState, LngLat} from '../types/common';
1✔
3
import {applyViewStateToTransform, isViewStateControlled} from '../utils/transform';
1✔
4

1✔
5
/**
1✔
6
 * Mapbox map is stateful.
1✔
7
 * During method calls/user interactions, map.transform is mutated and deviate from user-supplied props.
1✔
8
 * In order to control the map reactively, we trap the transform mutations with a proxy,
1✔
9
 * which reflects the view state resolved from both user-supplied props and the underlying state
1✔
10
 */
1✔
11
export type ProxyTransform = Transform & {
1✔
12
  $internalUpdate: boolean;
1✔
13
  $proposedTransform: Transform | null;
1✔
14
  $reactViewState: Partial<ViewState>;
1✔
15
};
1✔
16

1✔
17
// These are Transform class methods that:
1✔
18
// + do not mutate any view state properties
1✔
19
// + populate private members derived from view state properties
1✔
20
// They should always reflect the state of their owning instance and NOT trigger any proxied getter/setter
1✔
21
const unproxiedMethods = new Set([
1✔
22
  '_calcMatrices',
1✔
23
  '_calcFogMatrices',
1✔
24
  '_updateCameraState',
1✔
25
  '_updateSeaLevelZoom'
1✔
26
]);
1✔
27

1✔
28
export function createProxyTransform(tr: Transform): ProxyTransform {
1✔
29
  let internalUpdate = false;
13✔
30
  let reactViewState: Partial<ViewState> = {};
13✔
31
  /**
13✔
32
   * Reflects view state set by react props
13✔
33
   * This is the transform seen by painter, style etc.
13✔
34
   */
13✔
35
  const controlledTransform: Transform = tr;
13✔
36
  /** Populated during camera move (handler/easeTo) if there is a discrepency between react props and proposed view state
13✔
37
   * This is the transform seen by Mapbox's input handlers
13✔
38
   */
13✔
39
  let proposedTransform: Transform | null = null;
13✔
40

13✔
41
  const handlers: ProxyHandler<Transform> = {
13✔
42
    get(target: Transform, prop: string) {
13✔
43
      // Props added by us
13,285✔
44
      if (prop === '$reactViewState') {
13,285!
NEW
45
        return reactViewState;
×
NEW
46
      }
×
47
      if (prop === '$proposedTransform') {
13,285✔
48
        return proposedTransform;
18✔
49
      }
18✔
50
      if (prop === '$internalUpdate') {
13,285✔
51
        return internalUpdate;
358✔
52
      }
358✔
53

12,909✔
54
      // Ugly - this method is called from HandlerManager bypassing zoom setter
12,909✔
55
      if (prop === '_setZoom') {
13,285!
NEW
56
        return (z: number) => {
×
NEW
57
          if (internalUpdate) {
×
NEW
58
            proposedTransform?.[prop](z);
×
NEW
59
          }
×
NEW
60
          if (!Number.isFinite(reactViewState.zoom)) {
×
NEW
61
            controlledTransform[prop](z);
×
NEW
62
          }
×
NEW
63
        };
×
NEW
64
      }
✔
65

12,909✔
66
      // Ugly - this method is called from HandlerManager and mutates transform._camera
12,909✔
67
      if (
12,909✔
68
        internalUpdate &&
12,909✔
69
        prop === '_translateCameraConstrained' &&
1,496!
NEW
70
        isViewStateControlled(reactViewState)
×
71
      ) {
13,285!
NEW
72
        proposedTransform = proposedTransform || controlledTransform.clone();
×
NEW
73
      }
✔
74

12,909✔
75
      if (unproxiedMethods.has(prop)) {
13,285!
NEW
76
        // When this function is executed, it updates both transforms respectively
×
NEW
77
        return function (...parms: unknown[]) {
×
NEW
78
          proposedTransform?.[prop](...parms);
×
NEW
79
          controlledTransform[prop](...parms);
×
NEW
80
        };
×
NEW
81
      }
✔
82

12,909✔
83
      // Expose the proposed transform to input handlers
12,909✔
84
      if (internalUpdate && proposedTransform) {
13,285✔
85
        return proposedTransform[prop];
1,122✔
86
      }
1,122✔
87

11,787✔
88
      // Expose the controlled transform to renderer, markers, and event listeners
11,787✔
89
      return controlledTransform[prop];
11,787✔
90
    },
13,285✔
91

13✔
92
    set(target: Transform, prop: string, value: unknown) {
13✔
93
      // Props added by us
1,074✔
94
      if (prop === '$reactViewState') {
1,074✔
95
        reactViewState = value as Partial<ViewState>;
44✔
96
        applyViewStateToTransform(controlledTransform, reactViewState);
44✔
97
        return true;
44✔
98
      }
44✔
99
      if (prop === '$proposedTransform') {
1,074✔
100
        proposedTransform = value as Transform;
4✔
101
        return true;
4✔
102
      }
4✔
103
      if (prop === '$internalUpdate') {
1,074✔
104
        internalUpdate = value as boolean;
848✔
105
        return true;
848✔
106
      }
848✔
107

178✔
108
      // Controlled props
178✔
109
      let controlledValue = value;
178✔
110
      if (prop === 'center' || prop === '_center') {
1,074✔
111
        if (Number.isFinite(reactViewState.longitude) || Number.isFinite(reactViewState.latitude)) {
56✔
112
          // @ts-expect-error LngLat constructor is not typed
42✔
113
          controlledValue = new value.constructor(
42✔
114
            reactViewState.longitude ?? (value as LngLat).lng,
42!
115
            reactViewState.latitude ?? (value as LngLat).lat
42!
116
          );
42✔
117
        }
42✔
118
      } else if (prop === 'zoom' || prop === '_zoom' || prop === '_seaLevelZoom') {
1,074✔
119
        if (Number.isFinite(reactViewState.zoom)) {
28✔
120
          controlledValue = controlledTransform[prop];
21✔
121
        }
21✔
122
      } else if (prop === '_centerAltitude') {
122!
NEW
123
        if (Number.isFinite(reactViewState.elevation)) {
×
NEW
124
          controlledValue = controlledTransform[prop];
×
NEW
125
        }
×
126
      } else if (prop === 'pitch' || prop === '_pitch') {
94!
NEW
127
        if (Number.isFinite(reactViewState.pitch)) {
×
NEW
128
          controlledValue = controlledTransform[prop];
×
NEW
129
        }
×
130
      } else if (prop === 'bearing' || prop === 'rotation' || prop === 'angle') {
94!
NEW
131
        if (Number.isFinite(reactViewState.bearing)) {
×
NEW
132
          controlledValue = controlledTransform[prop];
×
NEW
133
        }
×
NEW
134
      }
✔
135

178✔
136
      // During camera update, we save view states that are overriden by controlled values in proposedTransform
178✔
137
      if (internalUpdate && controlledValue !== value) {
1,074✔
138
        proposedTransform = proposedTransform || controlledTransform.clone();
63✔
139
      }
63✔
140
      if (internalUpdate && proposedTransform) {
1,074✔
141
        proposedTransform[prop] = value;
66✔
142
      }
66✔
143

178✔
144
      // controlledTransform is not exposed to view state mutation
178✔
145
      controlledTransform[prop] = controlledValue;
178✔
146
      return true;
178✔
147
    }
1,074✔
148
  };
13✔
149
  return new Proxy(tr, handlers) as ProxyTransform;
13✔
150
}
13✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc