UNPKG

8.03 kBJavaScriptView Raw
1import React, { PureComponent } from 'react'; // eslint-disable-line import/no-unresolved
2import DeckGL from '@deck.gl/react';
3import { getVivId } from '../views/utils';
4
5/**
6 * This component handles rendering the various views within the DeckGL contenxt.
7 * @param {Object} props
8 * @param {Array} props.layerProps Props for the layers in each view.
9 * @param {Array} props.randomize Whether or not to randomize which view goes first (for dynamic rendering).
10 * @param {VivView} props.views Various VivViews to render.
11 * */
12export default class VivViewer extends PureComponent {
13 constructor(props) {
14 super(props);
15 this.state = {
16 viewStates: {},
17 initialViewStates: {}
18 };
19 const { viewStates, initialViewStates } = this.state;
20 const { views } = this.props;
21 views.forEach(view => {
22 viewStates[view.id] = view.filterViewState({
23 viewState: view.initialViewState
24 });
25 initialViewStates[view.id] = view.filterViewState({
26 viewState: view.initialViewState
27 });
28 });
29 this._onViewStateChange = this._onViewStateChange.bind(this);
30 this.layerFilter = this.layerFilter.bind(this);
31 this.onHover = this.onHover.bind(this);
32 }
33
34 /**
35 * This prevents only the `draw` call of a layer from firing,
36 * but not other layer lifecycle methods. Nonetheless, it is
37 * still useful.
38 * @param {Layer} layer Layer being updated.
39 * @param {Viewport} viewport Viewport being updated.
40 * @returns {boolean} Whether or not this layer should be drawn in this viewport.
41 */
42 // eslint-disable-next-line class-methods-use-this
43 layerFilter({ layer, viewport }) {
44 return layer.id.includes(getVivId(viewport.id));
45 }
46
47 /**
48 * This updates the viewState as a callback to the viewport changing in DeckGL
49 * (hence the need for storing viewState in state).
50 */
51 _onViewStateChange({ viewId, viewState, oldViewState }) {
52 // Save the view state and trigger rerender.
53 const { views } = this.props;
54 this.setState(prevState => {
55 const viewStates = {};
56 views.forEach(view => {
57 const currentViewState = prevState.viewStates[view.id];
58 viewStates[view.id] = view.filterViewState({
59 viewState: { ...viewState, id: viewId },
60 oldViewState,
61 currentViewState
62 });
63 });
64 return { viewStates };
65 });
66 }
67
68 /**
69 * This updates the viewStates' height and width with the newest height and
70 * width on any call where the viewStates changes (i.e resize events),
71 * using the previous state (falling back on the view's initial state) for target x and y, zoom level etc.
72 */
73 static getDerivedStateFromProps(props, prevState) {
74 const { views } = props;
75 // Update internal viewState on view changes as well as height and width changes.
76 // Maybe we should add x/y too?
77 if (
78 views.some(
79 view =>
80 !prevState.viewStates[view.id] ||
81 view.initialViewState.height !==
82 prevState.viewStates[view.id].height ||
83 view.initialViewState.width !== prevState.viewStates[view.id].width
84 )
85 ) {
86 const viewStates = {};
87 views.forEach(view => {
88 const { height, width } = view.initialViewState;
89 const currentViewState = prevState.viewStates[view.id];
90 viewStates[view.id] = view.filterViewState({
91 viewState: {
92 ...(currentViewState || view.initialViewState),
93 height,
94 width,
95 id: view.id
96 }
97 });
98 });
99 return { viewStates };
100 }
101 if (
102 views.some(
103 view =>
104 view.initialViewState.target !==
105 prevState.initialViewStates[view.id].target ||
106 view.initialViewState.zoom !==
107 prevState.initialViewStates[view.id].zoom
108 )
109 ) {
110 const initialViewStates = {};
111 const viewStates = {};
112 views.forEach(view => {
113 viewStates[view.id] = view.filterViewState({
114 viewState: view.initialViewState
115 });
116 initialViewStates[view.id] = view.filterViewState({
117 viewState: view.initialViewState
118 });
119 });
120 return { initialViewStates, viewStates };
121 }
122 return prevState;
123 }
124
125 // eslint-disable-next-line consistent-return
126 onHover({ sourceLayer, coordinate, layer }) {
127 if (!coordinate) {
128 return null;
129 }
130 const { hoverHooks } = this.props;
131 if (!hoverHooks) {
132 return null;
133 }
134 const { handleValue } = hoverHooks;
135 if (!handleValue) {
136 return null;
137 }
138 const { channelData, bounds } = sourceLayer.props;
139 if (!channelData) {
140 return null;
141 }
142 const { data, width } = channelData;
143 if (!data) {
144 return null;
145 }
146 let dataCoords;
147 // Tiled layer needs a custom layerZoomScale.
148 if (sourceLayer.id.includes('Tiled')) {
149 const {
150 loader: { tileSize }
151 } = layer.props;
152 const {
153 tileId: { z }
154 } = sourceLayer.props;
155 // The zoomed out layer needs to use the fixed zoom at which it is rendered.
156 // See: https://github.com/visgl/deck.gl/blob/2b15bc459c6534ea38ce1153f254ce0901f51d6f/modules/geo-layers/src/tile-layer/utils.js#L130.
157 const layerZoomScale = Math.max(
158 1,
159 2 ** Math.round(-z + Math.log2(512 / tileSize))
160 );
161 dataCoords = [
162 Math.floor((coordinate[0] - bounds[0]) / layerZoomScale),
163 Math.floor((coordinate[1] - bounds[3]) / layerZoomScale)
164 ];
165 } else {
166 // Using floor means that as we zoom out, we are scaling by the zoom just passed, not the one coming.
167 const { zoom } = layer.context.viewport;
168 const layerZoomScale = Math.max(1, 2 ** Math.floor(-zoom));
169 dataCoords = [
170 Math.floor((coordinate[0] - bounds[0]) / layerZoomScale),
171 Math.floor((coordinate[1] - bounds[3]) / layerZoomScale)
172 ];
173 }
174 const coords = dataCoords[1] * width + dataCoords[0];
175 const hoverData = data.map(d => d[coords]);
176 handleValue(hoverData);
177 }
178
179 /**
180 * This renders the layers in the DeckGL context.
181 */
182 _renderLayers() {
183 const { onHover } = this;
184 const { viewStates } = this.state;
185 const { views, layerProps } = this.props;
186 return views.map((view, i) =>
187 view.getLayers({
188 viewStates,
189 props: {
190 ...layerProps[i],
191 onHover
192 }
193 })
194 );
195 }
196
197 render() {
198 /* eslint-disable react/destructuring-assignment */
199 const { views, randomize } = this.props;
200 const { viewStates } = this.state;
201 const deckGLViews = views.map(view => view.getDeckGlView());
202 // DeckGL seems to use the first view more than the second for updates
203 // so this forces it to use the others more evenly. This isn't perfect,
204 // but I am not sure what else to do. The DeckGL render hooks don't help,
205 // but maybe useEffect() would help? I couldn't work it out as
206 // The issue is that I'm not sure how React would distinguish between forced updates
207 // from permuting the views array and "real" updates like zoom/pan.
208 // I tried keeping a counter but I couldn't figure out resetting it
209 // without triggering a re-render.
210 if (randomize) {
211 const random = Math.random();
212 const holdFirstElement = deckGLViews[0];
213 // weight has to go to 1.5 because we use Math.round().
214 const randomWieghted = random * 1.49;
215 const randomizedIndex = Math.round(randomWieghted * (views.length - 1));
216 deckGLViews[0] = deckGLViews[randomizedIndex];
217 deckGLViews[randomizedIndex] = holdFirstElement;
218 }
219 return (
220 <DeckGL
221 glOptions={{ webgl2: true }}
222 layerFilter={this.layerFilter}
223 layers={this._renderLayers()}
224 onViewStateChange={this._onViewStateChange}
225 views={deckGLViews}
226 viewState={viewStates}
227 getCursor={({ isDragging }) => {
228 return isDragging ? 'grabbing' : 'crosshair';
229 }}
230 />
231 );
232
233 /* eslint-disable react/destructuring-assignment */
234 }
235}