vitessce
Version:
Vitessce app and React component library
254 lines (237 loc) • 7.52 kB
JavaScript
import React, { PureComponent } from 'react';
import DeckGL, { OrthographicView, OrbitView } from 'deck.gl';
import ToolMenu from './ToolMenu';
import { DEFAULT_GL_OPTIONS } from '../utils';
import { getCursor, getCursorWithTool } from './cursor';
/**
* Abstract class component intended to be inherited by
* the Spatial and Scatterplot class components.
* Contains a common constructor, common DeckGL callbacks,
* and common render function.
*/
export default class AbstractSpatialOrScatterplot extends PureComponent {
constructor(props) {
super(props);
this.state = {
gl: null,
tool: null,
};
this.viewport = null;
this.onViewStateChange = this.onViewStateChange.bind(this);
this.onInitializeViewInfo = this.onInitializeViewInfo.bind(this);
this.onWebGLInitialized = this.onWebGLInitialized.bind(this);
this.onToolChange = this.onToolChange.bind(this);
this.onHover = this.onHover.bind(this);
}
/**
* Called by DeckGL upon a viewState change,
* for example zoom or pan interaction.
* Emit the new viewState to the `setViewState`
* handler prop.
* @param {object} params
* @param {object} params.viewState The next deck.gl viewState.
*/
onViewStateChange({ viewState: nextViewState }) {
const {
setViewState, viewState, layers, spatialAxisFixed,
} = this.props;
const use3d = layers?.some(l => l.use3d);
setViewState({
...nextViewState,
// If the axis is fixed, just use the current target in state i.e don't change target.
target: spatialAxisFixed && use3d ? viewState.target : nextViewState.target,
});
}
/**
* Called by DeckGL upon viewport
* initialization.
* @param {object} viewState
* @param {object} viewState.viewport
*/
onInitializeViewInfo({ viewport }) {
this.viewport = viewport;
}
/**
* Called by DeckGL upon initialization,
* helps to understand when to pass layers
* to the DeckGL component.
* @param {object} gl The WebGL context object.
*/
onWebGLInitialized(gl) {
this.setState({ gl });
}
/**
* Called by the ToolMenu buttons.
* Emits the new tool value to the
* `onToolChange` prop.
* @param {string} tool Name of tool.
*/
onToolChange(tool) {
const { onToolChange: onToolChangeProp } = this.props;
this.setState({ tool });
if (onToolChangeProp) {
onToolChangeProp(tool);
}
}
/**
* Create the DeckGL layers.
* @returns {object[]} Array of
* DeckGL layer objects.
* Intended to be overriden by descendants.
*/
// eslint-disable-next-line class-methods-use-this
getLayers() {
return [];
}
// eslint-disable-next-line consistent-return
onHover(info) {
const {
coordinate, sourceLayer: layer, tile,
} = info;
const {
setCellHighlight, cellHighlight, setComponentHover, layers,
} = this.props;
const hasBitmask = (layers || []).some(l => l.type === 'bitmask');
if (!setCellHighlight || !tile) {
return null;
}
if (!layer || !coordinate) {
if (cellHighlight && hasBitmask) {
setCellHighlight(null);
}
return null;
}
const { content, bbox, z } = tile;
if (!content) {
if (cellHighlight && hasBitmask) {
setCellHighlight(null);
}
return null;
}
const { data, width, height } = content;
const {
left, right, top, bottom,
} = bbox;
const bounds = [
left,
data.height < layer.tileSize ? height : bottom,
data.width < layer.tileSize ? width : right,
top,
];
if (!data) {
if (cellHighlight && hasBitmask) {
setCellHighlight(null);
}
return null;
}
// Tiled layer needs a custom layerZoomScale.
if (layer.id.includes('bitmask')) {
// The zoomed out layer needs to use the fixed zoom at which it is rendered.
const layerZoomScale = Math.max(
1,
2 ** Math.round(-z),
);
const dataCoords = [
Math.floor((coordinate[0] - bounds[0]) / layerZoomScale),
Math.floor((coordinate[1] - bounds[3]) / layerZoomScale),
];
const coords = dataCoords[1] * width + dataCoords[0];
const hoverData = data.map(d => d[coords]);
const cellId = hoverData.find(i => i > 0);
if (cellId !== Number(cellHighlight)) {
if (setComponentHover) {
setComponentHover();
}
// eslint-disable-next-line no-unused-expressions
setCellHighlight(cellId ? String(cellId) : null);
}
}
}
/**
* Emits a function to project from the
* cell ID space to the scatterplot or
* spatial coordinate space, via the
* `updateViewInfo` prop.
*/
viewInfoDidUpdate(getCellCoords) {
const { updateViewInfo, cells, uuid } = this.props;
const { viewport } = this;
if (updateViewInfo && viewport) {
updateViewInfo({
uuid,
project: (cellId) => {
const cell = cells[cellId];
try {
const [positionX, positionY] = getCellCoords(cell);
return viewport.project([positionX, positionY]);
} catch (e) {
return [null, null];
}
},
});
}
}
/**
* Intended to be overriden by descendants.
*/
componentDidUpdate() {
}
/**
* A common render function for both Spatial
* and Scatterplot components.
*/
render() {
const {
deckRef, viewState, uuid, layers: layerProps, hideTools,
} = this.props;
const { gl, tool } = this.state;
const layers = this.getLayers();
const use3d = (layerProps || []).some(l => l.use3d);
const showCellSelectionTools = this.cellsLayer !== null
|| (this.cellsEntries.length && this.cellsEntries[0][1].xy);
const showPanTool = this.cellsLayer !== null || layerProps.findIndex(l => l.type === 'bitmask' || l.type === 'raster') >= 0;
// For large datasets or ray casting, the visual quality takes only a small
// hit in exchange for much better performance by setting this to false:
// https://deck.gl/docs/api-reference/core/deck#usedevicepixels
const useDevicePixels = this.cellsEntries.length < 100000 && !use3d;
return (
<>
<ToolMenu
activeTool={tool}
setActiveTool={this.onToolChange}
visibleTools={{
pan: showPanTool && !hideTools,
selectRectangle: showCellSelectionTools && !hideTools,
selectLasso: showCellSelectionTools && !hideTools,
}}
/>
<DeckGL
id={`deckgl-overlay-${uuid}`}
ref={deckRef}
views={[
use3d
? new OrbitView({ id: 'orbit', controller: true, orbitAxis: 'Y' })
: new OrthographicView({
id: 'ortho',
}),
]} // id is a fix for https://github.com/uber/deck.gl/issues/3259
layers={
gl && viewState.target.slice(0, 2).every(i => typeof i === 'number')
? layers
: []
}
glOptions={DEFAULT_GL_OPTIONS}
onWebGLInitialized={this.onWebGLInitialized}
onViewStateChange={this.onViewStateChange}
viewState={viewState}
useDevicePixels={useDevicePixels}
controller={tool ? { dragPan: false } : true}
getCursor={tool ? getCursorWithTool : getCursor}
onHover={this.onHover}
>
{this.onInitializeViewInfo}
</DeckGL>
</>
);
}
}