@vitessce/gl 3.1.3 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/{deflate-c9746f58.js → deflate-69c280fb.js} +1 -1
  2. package/dist/{index-72755d2f.js → index-2a7f1ae0.js} +21935 -5890
  3. package/dist/index.js +14 -13
  4. package/dist/{jpeg-cb74431a.js → jpeg-95f78026.js} +1 -1
  5. package/dist/{lerc-5dffa929.js → lerc-5320d7fa.js} +1 -1
  6. package/dist/{lzw-85fa08a9.js → lzw-8d3691e0.js} +1 -1
  7. package/dist/{packbits-6b8314bf.js → packbits-2846ed6b.js} +1 -1
  8. package/dist/{raw-f12c17bf.js → raw-b0ec4406.js} +1 -1
  9. package/dist/{webimage-99c1b585.js → webimage-a17e81f9.js} +1 -1
  10. package/dist-tsc/BitmaskLayerBeta.d.ts +152 -0
  11. package/dist-tsc/BitmaskLayerBeta.d.ts.map +1 -0
  12. package/dist-tsc/BitmaskLayerBeta.js +253 -0
  13. package/dist-tsc/SelectionLayer.d.ts +1 -0
  14. package/dist-tsc/SelectionLayer.d.ts.map +1 -1
  15. package/dist-tsc/SelectionLayer.js +45 -31
  16. package/dist-tsc/bitmask-layer-beta-shaders.d.ts +3 -0
  17. package/dist-tsc/bitmask-layer-beta-shaders.d.ts.map +1 -0
  18. package/dist-tsc/bitmask-layer-beta-shaders.js +212 -0
  19. package/dist-tsc/bitmask-utils.d.ts +12 -0
  20. package/dist-tsc/bitmask-utils.d.ts.map +1 -0
  21. package/dist-tsc/bitmask-utils.js +89 -0
  22. package/dist-tsc/bitmask-utils.test.d.ts +2 -0
  23. package/dist-tsc/bitmask-utils.test.d.ts.map +1 -0
  24. package/dist-tsc/bitmask-utils.test.js +72 -0
  25. package/dist-tsc/index.d.ts +2 -1
  26. package/dist-tsc/index.js +2 -1
  27. package/dist-tsc/selection-utils.d.ts +3 -3
  28. package/dist-tsc/selection-utils.d.ts.map +1 -1
  29. package/dist-tsc/selection-utils.js +26 -28
  30. package/dist-tsc/viv.d.ts +1 -1
  31. package/dist-tsc/viv.js +1 -1
  32. package/package.json +3 -2
  33. package/src/BitmaskLayerBeta.js +360 -0
  34. package/src/SelectionLayer.js +60 -42
  35. package/src/bitmask-layer-beta-shaders.js +214 -0
  36. package/src/bitmask-utils.js +99 -0
  37. package/src/bitmask-utils.test.js +91 -0
  38. package/src/index.js +3 -1
  39. package/src/selection-utils.js +13 -17
  40. package/src/viv.js +1 -0
@@ -0,0 +1,360 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import GL from '@luma.gl/constants'; // eslint-disable-line import/no-extraneous-dependencies
3
+ import { project32, picking } from '@deck.gl/core'; // eslint-disable-line import/no-extraneous-dependencies
4
+ import { Texture2D, isWebGL2 } from '@luma.gl/core';
5
+ import { XRLayer } from '@hms-dbmi/viv';
6
+ import { fromEntries } from '@vitessce/utils';
7
+ import { range } from 'lodash-es';
8
+ import { fs, vs } from './bitmask-layer-beta-shaders.js';
9
+ import {
10
+ GLSL_COLORMAPS,
11
+ GLSL_COLORMAP_DEFAULT,
12
+ COLORMAP_SHADER_PLACEHOLDER,
13
+ } from './constants.js';
14
+ import { multiSetsToTextureData } from './bitmask-utils.js';
15
+
16
+
17
+ const MAX_CHANNELS = 7;
18
+ const MULTI_FEATURE_TEX_SIZE = 2048;
19
+
20
+ function padWithDefault(arr, defaultValue, padWidth) {
21
+ const newArr = [...arr];
22
+ for (let i = 0; i < padWidth; i += 1) {
23
+ newArr.push(defaultValue);
24
+ }
25
+ return newArr;
26
+ }
27
+
28
+ function getColor(arr) {
29
+ return arr ? arr.map(v => v / 255) : [0, 0, 0];
30
+ }
31
+
32
+
33
+ const defaultProps = {
34
+ channelStrokeWidths: { type: 'array', value: null, compare: true },
35
+ channelsFilled: { type: 'array', value: null, compare: true },
36
+ channelOpacities: { type: 'array', value: null, compare: true },
37
+ channelColors: { type: 'array', value: null, compare: true },
38
+ hoveredCell: { type: 'number', value: null, compare: true },
39
+ colormap: { type: 'string', value: GLSL_COLORMAP_DEFAULT, compare: true },
40
+ expressionData: { type: 'object', value: null, compare: true },
41
+ multiFeatureValues: { type: 'array', value: null, compare: true },
42
+ setColorValues: { type: 'array', value: null, compare: true },
43
+ channelFeatureValueColormaps: { type: 'array', value: null, compare: true },
44
+ channelFeatureValueColormapRanges: { type: 'array', value: null, compare: true },
45
+ channelIsStaticColorMode: { type: 'array', value: null, compare: true },
46
+ channelIsSetColorMode: { type: 'array', value: null, compare: true },
47
+ };
48
+
49
+ /**
50
+ * A BitmapLayer that performs aggregation in the fragment shader,
51
+ * and renders its texture from a Uint8Array rather than an ImageData.
52
+ */
53
+ export default class BitmaskLayer extends XRLayer {
54
+ // eslint-disable-next-line class-methods-use-this
55
+ getShaders() {
56
+ const { colormap } = this.props;
57
+ return {
58
+ fs,
59
+ vs,
60
+ modules: [project32, picking],
61
+ defines: {
62
+ [COLORMAP_SHADER_PLACEHOLDER]: GLSL_COLORMAPS.includes(colormap)
63
+ ? colormap
64
+ : GLSL_COLORMAP_DEFAULT,
65
+ },
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Override the parent loadChannelTextures to enable
71
+ * up to eight channels (rather than six).
72
+ * Reference: https://github.com/hms-dbmi/viv/blob/v0.13.3/packages/layers/src/xr-layer/xr-layer.js#L316
73
+ */
74
+ loadChannelTextures(channelData) {
75
+ const textures = {
76
+ channel0: null,
77
+ channel1: null,
78
+ channel2: null,
79
+ channel3: null,
80
+ channel4: null,
81
+ channel5: null,
82
+ channel6: null,
83
+ };
84
+ if (this.state.textures) {
85
+ Object.values(this.state.textures).forEach(tex => tex && tex.delete());
86
+ }
87
+ if (channelData
88
+ && Object.keys(channelData).length > 0
89
+ && channelData.data
90
+ ) {
91
+ channelData.data.forEach((d, i) => {
92
+ textures[`channel${i}`] = this.dataToTexture(
93
+ d,
94
+ channelData.width,
95
+ channelData.height,
96
+ );
97
+ }, this);
98
+ this.setState({ textures });
99
+ }
100
+ }
101
+
102
+ updateState({ props, oldProps, changeFlags }) {
103
+ super.updateState({ props, oldProps, changeFlags });
104
+ if (
105
+ props.multiFeatureValues !== oldProps.multiFeatureValues
106
+ || props.setColorValues !== oldProps.setColorValues
107
+ || props.channelIsSetColorMode !== oldProps.channelIsSetColorMode
108
+ ) {
109
+ const { multiFeatureValues, setColorValues, channelIsSetColorMode } = this.props;
110
+ // Use one expressionTex for all channels,
111
+ // using an offset mechanism.
112
+ const [
113
+ valueTex,
114
+ colorTex,
115
+ valueTexOffsets,
116
+ colorTexOffsets,
117
+ valueTexHeight,
118
+ colorTexHeight,
119
+ ] = this.multiSetsToTexture(
120
+ multiFeatureValues,
121
+ setColorValues,
122
+ channelIsSetColorMode,
123
+ );
124
+ this.setState({
125
+ valueTex,
126
+ colorTex,
127
+ valueTexOffsets,
128
+ colorTexOffsets,
129
+ valueTexHeight,
130
+ colorTexHeight,
131
+ });
132
+ }
133
+ if (props.colormap !== oldProps.colormap) {
134
+ const { gl } = this.context;
135
+ if (this.state.model) {
136
+ this.state.model.delete();
137
+ }
138
+ // eslint-disable-next-line no-underscore-dangle
139
+ this.setState({ model: this._getModel(gl) });
140
+
141
+ this.getAttributeManager().invalidateAll();
142
+ }
143
+ }
144
+
145
+ draw(opts) {
146
+ const { uniforms } = opts;
147
+ const {
148
+ channelStrokeWidths,
149
+ channelsFilled,
150
+ channelOpacities,
151
+ channelColors,
152
+ channelsVisible,
153
+ // TODO: use `channelFeatureValueColormaps` in shader,
154
+ // figure out how to call multiple GLSL colormap functions
155
+ channelFeatureValueColormaps,
156
+ channelFeatureValueColormapRanges,
157
+ channelIsStaticColorMode,
158
+ channelIsSetColorMode,
159
+ hoveredCell,
160
+ colorScaleLo,
161
+ colorScaleHi,
162
+ isExpressionMode,
163
+ zoom,
164
+ minZoom,
165
+ maxZoom,
166
+ zoomOffset, // TODO: figure out if this needs to be used or not
167
+ } = this.props;
168
+ const {
169
+ textures,
170
+ model,
171
+ // Expression and set index (and colors) textures with offsets
172
+ valueTex,
173
+ colorTex,
174
+ valueTexOffsets,
175
+ colorTexOffsets,
176
+ valueTexHeight,
177
+ colorTexHeight,
178
+ } = this.state;
179
+ // Render the image
180
+ if (textures && model) {
181
+ const scaleFactor = 1 / (2 ** (maxZoom - zoom));
182
+ const colors = fromEntries(range(MAX_CHANNELS).map(i => ([`color${i}`, getColor(channelColors[i])])));
183
+ model
184
+ .setUniforms(
185
+ Object.assign({}, uniforms, {
186
+ ...colors,
187
+ // Bitmask image channel data textures
188
+ ...textures,
189
+ multiFeatureTexSize: MULTI_FEATURE_TEX_SIZE,
190
+ // Expression textures with offsets
191
+ valueTex,
192
+ valueTexOffsets: padWithDefault(
193
+ valueTexOffsets,
194
+ 0,
195
+ MAX_CHANNELS - valueTexOffsets.length,
196
+ ),
197
+ valueTexHeight,
198
+ // Set indices and colors textures with offsets
199
+ colorTex,
200
+ colorTexOffsets: padWithDefault(
201
+ colorTexOffsets,
202
+ 0,
203
+ MAX_CHANNELS - colorTexOffsets.length,
204
+ ),
205
+ colorTexHeight,
206
+ // Visualization properties
207
+ channelsFilled: padWithDefault(
208
+ channelsFilled,
209
+ true,
210
+ // There are six texture entries on the shaders
211
+ MAX_CHANNELS - channelsFilled.length,
212
+ ),
213
+ channelOpacities: padWithDefault(
214
+ channelOpacities,
215
+ 0.0,
216
+ // There are six texture entries on the shaders
217
+ MAX_CHANNELS - channelOpacities.length,
218
+ ),
219
+ channelStrokeWidths: padWithDefault(
220
+ channelStrokeWidths,
221
+ 1.0,
222
+ // There are six texture entries on the shaders
223
+ MAX_CHANNELS - channelStrokeWidths.length,
224
+ ),
225
+ channelColormapRangeStarts: padWithDefault(
226
+ channelFeatureValueColormapRanges.map(r => r?.[0] || 0.0),
227
+ 0.0,
228
+ // There are six texture entries on the shaders
229
+ MAX_CHANNELS - channelFeatureValueColormapRanges.length,
230
+ ),
231
+ channelColormapRangeEnds: padWithDefault(
232
+ channelFeatureValueColormapRanges.map(r => r?.[1] || 1.0),
233
+ 1.0,
234
+ // There are six texture entries on the shaders
235
+ MAX_CHANNELS - channelFeatureValueColormapRanges.length,
236
+ ),
237
+ channelIsStaticColorMode: padWithDefault(
238
+ channelIsStaticColorMode,
239
+ true,
240
+ // There are six texture entries on the shaders
241
+ MAX_CHANNELS - channelIsStaticColorMode.length,
242
+ ),
243
+ channelIsSetColorMode: padWithDefault(
244
+ channelIsSetColorMode,
245
+ false,
246
+ // There are six texture entries on the shaders
247
+ MAX_CHANNELS - channelIsSetColorMode.length,
248
+ ),
249
+ hovered: hoveredCell || 0,
250
+ channelsVisible: padWithDefault(
251
+ channelsVisible,
252
+ false,
253
+ // There are six texture entries on the shaders
254
+ MAX_CHANNELS - channelsVisible.length,
255
+ ),
256
+ // uColorScaleRange: [colorScaleLo, colorScaleHi],
257
+ // uIsExpressionMode: isExpressionMode,
258
+ // uIsOutlined: false,
259
+ scaleFactor,
260
+ }),
261
+ )
262
+ .draw();
263
+ }
264
+ }
265
+
266
+ /**
267
+ * This function creates textures from the data
268
+ */
269
+ dataToTexture(data, width, height) {
270
+ const isWebGL2On = isWebGL2(this.context.gl);
271
+ return new Texture2D(this.context.gl, {
272
+ width,
273
+ height,
274
+ // Only use Float32 so we don't have to write two shaders
275
+ data: new Float32Array(data),
276
+ // we don't want or need mimaps
277
+ mipmaps: false,
278
+ parameters: {
279
+ // NEAREST for integer data
280
+ [GL.TEXTURE_MIN_FILTER]: GL.NEAREST,
281
+ [GL.TEXTURE_MAG_FILTER]: GL.NEAREST,
282
+ // CLAMP_TO_EDGE to remove tile artifacts
283
+ [GL.TEXTURE_WRAP_S]: GL.CLAMP_TO_EDGE,
284
+ [GL.TEXTURE_WRAP_T]: GL.CLAMP_TO_EDGE,
285
+ },
286
+ format: isWebGL2On ? GL.R32F : GL.LUMINANCE,
287
+ dataFormat: isWebGL2On ? GL.RED : GL.LUMINANCE,
288
+ type: GL.FLOAT,
289
+ });
290
+ }
291
+
292
+ multiSetsToTexture(multiFeatureValues, setColorValues, channelIsSetColorMode) {
293
+ const isWebGL2On = isWebGL2(this.context.gl);
294
+
295
+ const [
296
+ totalData,
297
+ valueTexHeight,
298
+ indicesOffsets,
299
+ totalColors,
300
+ colorTexHeight,
301
+ colorsOffsets,
302
+ ] = multiSetsToTextureData(
303
+ multiFeatureValues,
304
+ setColorValues,
305
+ channelIsSetColorMode,
306
+ MULTI_FEATURE_TEX_SIZE,
307
+ );
308
+
309
+ return [
310
+ // Color indices texture
311
+ new Texture2D(this.context.gl, {
312
+ width: MULTI_FEATURE_TEX_SIZE,
313
+ height: valueTexHeight,
314
+ // Only use Float32 so we don't have to write two shaders
315
+ data: new Float32Array(totalData),
316
+ // we don't want or need mimaps
317
+ mipmaps: false,
318
+ parameters: {
319
+ // NEAREST for integer data
320
+ [GL.TEXTURE_MIN_FILTER]: GL.NEAREST,
321
+ [GL.TEXTURE_MAG_FILTER]: GL.NEAREST,
322
+ // CLAMP_TO_EDGE to remove tile artifacts
323
+ [GL.TEXTURE_WRAP_S]: GL.CLAMP_TO_EDGE,
324
+ [GL.TEXTURE_WRAP_T]: GL.CLAMP_TO_EDGE,
325
+ },
326
+ format: isWebGL2On ? GL.R32F : GL.LUMINANCE,
327
+ dataFormat: isWebGL2On ? GL.RED : GL.LUMINANCE,
328
+ type: GL.FLOAT,
329
+ }),
330
+ // Colors texture
331
+ new Texture2D(this.context.gl, {
332
+ width: MULTI_FEATURE_TEX_SIZE,
333
+ height: colorTexHeight,
334
+ // Only use Float32 so we don't have to write two shaders
335
+ data: new Float32Array(totalColors),
336
+ // we don't want or need mimaps
337
+ mipmaps: false,
338
+ parameters: {
339
+ // NEAREST for integer data
340
+ [GL.TEXTURE_MIN_FILTER]: GL.NEAREST,
341
+ [GL.TEXTURE_MAG_FILTER]: GL.NEAREST,
342
+ // CLAMP_TO_EDGE to remove tile artifacts
343
+ [GL.TEXTURE_WRAP_S]: GL.CLAMP_TO_EDGE,
344
+ [GL.TEXTURE_WRAP_T]: GL.CLAMP_TO_EDGE,
345
+ },
346
+ format: isWebGL2On ? GL.R32F : GL.LUMINANCE,
347
+ dataFormat: isWebGL2On ? GL.RED : GL.LUMINANCE,
348
+ type: GL.FLOAT,
349
+ }),
350
+ // Offsets
351
+ indicesOffsets,
352
+ colorsOffsets,
353
+ // Texture heights
354
+ valueTexHeight,
355
+ colorTexHeight,
356
+ ];
357
+ }
358
+ }
359
+ BitmaskLayer.layerName = 'BitmaskLayer';
360
+ BitmaskLayer.defaultProps = defaultProps;
@@ -70,10 +70,8 @@ const PASS_THROUGH_PROPS = [
70
70
  export default class SelectionLayer extends CompositeLayer {
71
71
  _selectPolygonObjects(coordinates) {
72
72
  const {
73
- onSelect,
74
- getCellCoords,
75
- cellsQuadTree,
76
73
  flipY,
74
+ obsLayers,
77
75
  } = this.props;
78
76
 
79
77
  const flippedCoordinates = (flipY
@@ -83,53 +81,73 @@ export default class SelectionLayer extends CompositeLayer {
83
81
  // Convert the selection to a turf polygon object.
84
82
  const selectedPolygon = turfPolygon(flippedCoordinates);
85
83
 
86
- // Create an array to store the results.
87
- const pickingInfos = [];
88
-
89
84
  // quadtree.visit() takes a callback that returns a boolean:
90
85
  // If true returned, then the children of the node are _not_ visited.
91
86
  // If false returned, then the children of the node are visited.
92
87
  // Reference: https://github.com/d3/d3-quadtree#quadtree_visit
93
- cellsQuadTree.visit((node, x0, y0, x1, y1) => {
94
- const nodePoints = [[[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]];
95
- const nodePolygon = turfPolygon(nodePoints);
96
-
97
- const nodePolygonContainsSelectedPolygon = booleanContains(nodePolygon, selectedPolygon);
98
- const nodePolygonWithinSelectedPolygon = booleanWithin(nodePolygon, selectedPolygon);
99
- const nodePolygonOverlapsSelectedPolgyon = booleanOverlap(nodePolygon, selectedPolygon);
100
-
101
- if (!nodePolygonContainsSelectedPolygon
102
- && !nodePolygonWithinSelectedPolygon
103
- && !nodePolygonOverlapsSelectedPolgyon) {
104
- // We are not interested in anything below this node,
105
- // so return true because we are done with this node.
106
- return true;
107
- }
108
-
109
- // This node made it past the above return statement, so it must either
110
- // contain, be within, or overlap with the selected polygon.
111
-
112
- // Check if this is a leaf node.
113
- if (node.data
114
- && booleanPointInPolygon(
115
- turfPoint([].slice.call(getCellCoords(node.data))), selectedPolygon,
116
- )
117
- ) {
118
- // This node has data, so it is a leaf node representing one data point,
119
- // and we have verified that the point is in the selected polygon.
120
- pickingInfos.push(node.data);
121
- }
122
-
123
- // Return false because we are not done.
124
- // We want to visit the children of this node.
125
- return false;
88
+ obsLayers.forEach((obsLayer) => {
89
+ const {
90
+ getObsCoords,
91
+ obsQuadTree,
92
+ obsIndex,
93
+ onSelect: layerOnSelect,
94
+ } = obsLayer;
95
+
96
+ // Create an array to store the results.
97
+ // Clear the array before checking each new layer.
98
+ const pickingInfos = [];
99
+
100
+ // It is possible for a layer to not have an obsQuadTree,
101
+ // for example if the layer is a segmentation bitmask without associated
102
+ // obsLocations.
103
+ obsQuadTree?.visit((node, x0, y0, x1, y1) => {
104
+ const nodePoints = [[[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]];
105
+ const nodePolygon = turfPolygon(nodePoints);
106
+
107
+ const nodePolygonContainsSelectedPolygon = booleanContains(nodePolygon, selectedPolygon);
108
+ const nodePolygonWithinSelectedPolygon = booleanWithin(nodePolygon, selectedPolygon);
109
+ const nodePolygonOverlapsSelectedPolgyon = booleanOverlap(nodePolygon, selectedPolygon);
110
+
111
+ if (!nodePolygonContainsSelectedPolygon
112
+ && !nodePolygonWithinSelectedPolygon
113
+ && !nodePolygonOverlapsSelectedPolgyon) {
114
+ // We are not interested in anything below this node,
115
+ // so return true because we are done with this node.
116
+ return true;
117
+ }
118
+
119
+ // This node made it past the above return statement, so it must either
120
+ // contain, be within, or overlap with the selected polygon.
121
+
122
+ // Check if this is a leaf node.
123
+ if (node.data
124
+ && booleanPointInPolygon(
125
+ turfPoint([].slice.call(getObsCoords(node.data))), selectedPolygon,
126
+ )
127
+ ) {
128
+ // This node has data, so it is a leaf node representing one data point,
129
+ // and we have verified that the point is in the selected polygon.
130
+ pickingInfos.push(node.data);
131
+ }
132
+
133
+ // Return false because we are not done.
134
+ // We want to visit the children of this node.
135
+ return false;
136
+ });
137
+ const pickingIds = pickingInfos.map(obsI => obsIndex[obsI]);
138
+ layerOnSelect(pickingIds);
126
139
  });
140
+ }
127
141
 
128
- onSelect({ pickingInfos });
142
+ _selectEmpty() {
143
+ const { obsLayers } = this.props;
144
+ obsLayers.forEach((obsLayer) => {
145
+ const { onSelect: layerOnSelect } = obsLayer;
146
+ layerOnSelect([]);
147
+ });
129
148
  }
130
149
 
131
150
  renderLayers() {
132
- const { onSelect } = this.props;
133
151
  const mode = MODE_MAP[this.props.selectionType] || ViewMode;
134
152
 
135
153
  const inheritedProps = {};
@@ -153,7 +171,7 @@ export default class SelectionLayer extends CompositeLayer {
153
171
  this._selectPolygonObjects(coordinates);
154
172
  } else if (editType === EDIT_TYPE_CLEAR) {
155
173
  // We want to select an empty array to clear any previous selection.
156
- onSelect({ pickingInfos: [] });
174
+ this._selectEmpty();
157
175
  }
158
176
  },
159
177
  _subLayerProps: {