diff --git a/README.md b/README.md index b76780a2a..6a4c0fbc3 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ peer depenedencies from this package into your own: "@luma.gl/core": "8.0.3", "@luma.gl/shadertools": "8.0.3", "deck.gl": "^8.1.0-alpha.1", +"geotiff": "^1.0.0-beta.6", "math.gl": "^3.1.3", "nebula.gl": "^0.17.1", "zarr": "^0.1.4" @@ -44,7 +45,7 @@ This component can be used with an already existing `DeckGL` setup. ##### `getTileData` (Function) **POTENIAL FUTURE BREAKING CHANGES WITH NEW FEATURES** -`getTileData` given x, y, z indices of the tile, returns the tile data or a Promise that resolves to the tile data. Alternatively, pass in `useZarr` as true to use `zarr` and our funcionality. Soon, there will be a `useGeoTIFF` as well. Look +`getTileData` given x, y, z indices of the tile, returns the tile data or a Promise that resolves to the tile data. Alternatively, pass in `useZarr` as true to use `zarr` and our functionality. Otherwise, you can use `useTiff` to make range requests directly against a pyramid/tiled tiff. Look at [this](IMAGE_RENDERING.md) for how the zarr should be laid out. Receives arguments: diff --git a/demo/src/App.js b/demo/src/App.js index 0aba63409..284b4398c 100644 --- a/demo/src/App.js +++ b/demo/src/App.js @@ -26,8 +26,6 @@ const OrangeSlider = withStyles({ } })(Slider) - - export default class App extends PureComponent { constructor(props){ @@ -91,17 +89,17 @@ export default class App extends PureComponent { ], } const propSettings = { + useTiff: true, imageHeight: source.height * source.tileSize, imageWidth: source.width * source.tileSize, tileSize: source.tileSize, sourceChannels: source.channels, minZoom: Math.floor( -1 * Math.log2(Math.max(source.height * source.tileSize, source.width * source.tileSize)), - ) + ), + maxZoom: -9 } - const useZarr = true const props = { - useZarr, initialViewState, ...propSettings, ...this.state diff --git a/demo/src/source-info.js b/demo/src/source-info.js index 32bd11357..7eb6659fd 100644 --- a/demo/src/source-info.js +++ b/demo/src/source-info.js @@ -2,10 +2,17 @@ export const source = { height: 141, width: 206, tileSize: 256, + // channels: { + // channel_0: "https://vitessce-vanderbilt-data.storage.googleapis.com/test-data/vanderbilt-data/single_channel_pyramid/img_pyramid/channel_0", + // channel_1: "https://vitessce-vanderbilt-data.storage.googleapis.com/test-data/vanderbilt-data/single_channel_pyramid/img_pyramid/channel_1", + // channel_2: "https://vitessce-vanderbilt-data.storage.googleapis.com/test-data/vanderbilt-data/single_channel_pyramid/img_pyramid/channel_2", + // channel_3: "https://vitessce-vanderbilt-data.storage.googleapis.com/test-data/vanderbilt-data/single_channel_pyramid/img_pyramid/channel_3", + // }, channels: { - channel_0: "https://vitessce-vanderbilt-data.storage.googleapis.com/test-data/vanderbilt-data/single_channel_pyramid/img_pyramid/channel_0", - channel_1: "https://vitessce-vanderbilt-data.storage.googleapis.com/test-data/vanderbilt-data/single_channel_pyramid/img_pyramid/channel_1", - channel_2: "https://vitessce-vanderbilt-data.storage.googleapis.com/test-data/vanderbilt-data/single_channel_pyramid/img_pyramid/channel_2", - channel_3: "https://vitessce-vanderbilt-data.storage.googleapis.com/test-data/vanderbilt-data/single_channel_pyramid/img_pyramid/channel_3", + channel_0: "https://vitessce-vanderbilt-data.storage.googleapis.com/test-data/vanderbilt-data/tiff-pyramid/vanderbilt_test_0.ome.tiff", + channel_1: "https://vitessce-vanderbilt-data.storage.googleapis.com/test-data/vanderbilt-data/tiff-pyramid/vanderbilt_test_1.ome.tiff", + channel_2: "https://vitessce-vanderbilt-data.storage.googleapis.com/test-data/vanderbilt-data/tiff-pyramid/vanderbilt_test_2.ome.tiff", + channel_3: "https://vitessce-vanderbilt-data.storage.googleapis.com/test-data/vanderbilt-data/tiff-pyramid/vanderbilt_test_3.ome.tiff", } + }; diff --git a/package-lock.json b/package-lock.json index 31126d00d..e33b93137 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5752,6 +5752,15 @@ "integrity": "sha1-kQQGSbetObKQRkO9mtUeXobVJOM=", "dev": true }, + "geotiff": { + "version": "1.0.0-beta.6", + "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-1.0.0-beta.6.tgz", + "integrity": "sha512-xdZ/MLcnrv1+6wQlQZQIs11zNJywylnV1pXqDw7Ao7bmLRpM421a39dXP5e6SG+vio0mnDUZkL2XknKbqppFzw==", + "requires": { + "pako": "^1.0.3", + "xmldom": "0.1.*" + } + }, "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", @@ -7710,8 +7719,7 @@ "pako": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", - "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", - "dev": true + "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==" }, "parallel-transform": { "version": "1.2.0", @@ -10425,6 +10433,11 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "xmldom": { + "version": "0.1.31", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz", + "integrity": "sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index bc6d1e2e3..90e0c71e0 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "babel-preset-stage-0": "^6.24.1", "css-loader": "^3.4.2", "deck.gl": "^8.1.0-alpha.1", + "geotiff": "^1.0.0-beta.6", "html-webpack-plugin": "^3.2.0", "math.gl": "^3.1.3", "mini-css-extract-plugin": "^0.9.0", diff --git a/src/layers/microscopy-viewer-layer/data-utils/index.js b/src/layers/microscopy-viewer-layer/data-utils/index.js index 3e65805a4..2e743bbed 100644 --- a/src/layers/microscopy-viewer-layer/data-utils/index.js +++ b/src/layers/microscopy-viewer-layer/data-utils/index.js @@ -1,3 +1,4 @@ -import loadZarr from './zarr-utils'; +import {loadZarr, getZarrConnections} from './zarr-utils'; +import {loadTiff, getTiffConnections} from './tiff-utils'; -export {loadZarr} +export {loadZarr, loadTiff, getTiffConnections, getZarrConnections} diff --git a/src/layers/microscopy-viewer-layer/data-utils/tiff-utils.js b/src/layers/microscopy-viewer-layer/data-utils/tiff-utils.js new file mode 100644 index 000000000..c35e59b03 --- /dev/null +++ b/src/layers/microscopy-viewer-layer/data-utils/tiff-utils.js @@ -0,0 +1,46 @@ +import {fromUrl, Pool, getDecoder } from 'geotiff/dist/geotiff.bundle.min.js'; + +async function loadTile({image, channel, x, y, pool}) { + var tile = await image.getTileOrStrip(x,y, 0, pool) + var dataObj = {} + const is8Bits = image.fileDirectory.BitsPerSample[0]===8 + const is16Bits = image.fileDirectory.BitsPerSample[0]===16 + const is32Bits = image.fileDirectory.BitsPerSample[0]===32 + dataObj[channel] = (is8Bits && new Uint8Array(tile.data)) || + (is16Bits && new Uint16Array(tile.data)) || + (is32Bits && new Uint32Array(tile.data)) + return dataObj +} + +export function loadTiff({connections, x, y, z, pool}){ + const configListPromises = Object.keys(connections).map((channel) => { + var image = connections[channel][z] + return loadTile({image, channel, x, y, pool}) + }) + return Promise.all(configListPromises).then((dataList) => { + const orderedList = [] + const dataObj = Object.assign({}, ...dataList) + Object.keys(dataObj).sort().forEach((key) => { + orderedList.push(dataObj[key]); + }) + return orderedList; + }); +} + +export function getTiffConnections({sourceChannels, maxZoom}){ + const tiffConnections = Object.keys(sourceChannels).map(async (channel) => { + var tiff = await fromUrl(sourceChannels[channel]) + var imageObj = {} + for(var i = 0; i < -maxZoom; i++) { + var image = await tiff.getImage(i) + if(!imageObj[channel]) { + imageObj[channel] = [image] + } else { + imageObj[channel].push(image) + } + + } + return imageObj + }) + return Promise.all(tiffConnections) +} diff --git a/src/layers/microscopy-viewer-layer/data-utils/zarr-utils.js b/src/layers/microscopy-viewer-layer/data-utils/zarr-utils.js index d22debdf8..8afff7fd7 100644 --- a/src/layers/microscopy-viewer-layer/data-utils/zarr-utils.js +++ b/src/layers/microscopy-viewer-layer/data-utils/zarr-utils.js @@ -1,41 +1,23 @@ import { slice, openArray } from 'zarr'; -var zarrArrays = {} - -async function getData({ config, tileSize, x, y, stride, tilingWidth }) { +async function getData({ arr, channel, tileSize, x, y, stride, tilingWidth }) { const arrSlice = slice(stride * tilingWidth * y + stride * x, stride * tilingWidth * y + stride * (x + 1)); - const zarrKey = config.zarrConfig.store + config.zarrConfig.path; - if (!zarrArrays[zarrKey]) { - zarrArrays[zarrKey] = await openArray(config.zarrConfig); - } - const arr = zarrArrays[zarrKey]; const dataSlice = await arr.get([arrSlice]); const { data } = dataSlice; - const { channelName } = config; const dataObj = {} - dataObj[channelName] = data + dataObj[channel] = data return dataObj; } -export default function loadZarr({ sourceChannels, tileSize, x, y, z, imageWidth }) { +export function loadZarr({ connections, tileSize, x, y, z, imageWidth }) { const tilingWidth = Math.ceil(imageWidth / (tileSize * (2 ** z))); - const dataNames = ['redData', 'greenData', 'blueData']; // denotes order rgb - const configList = Object.keys(sourceChannels).map((channel, i) => ({ - channelName: channel, - channelType: dataNames[i], - zarrConfig: { - store: `${sourceChannels[channel]}/`, - path: `pyramid_${z}.zarr`, - mode: 'r', - }, - })); const stride = tileSize * tileSize; - const configListPromises = configList.map((config) => { + const channelPromises = Object.keys(connections).map((channel) => { return getData({ - config, tileSize, x, y, stride, tilingWidth, + arr: connections[channel][z], channel, tileSize, x, y, stride, tilingWidth, }); }); - return Promise.all(configListPromises).then((dataList) => { + return Promise.all(channelPromises).then((dataList) => { const orderedList = [] const dataObj = Object.assign({}, ...dataList) Object.keys(dataObj).sort().forEach(function(key) { @@ -44,3 +26,23 @@ export default function loadZarr({ sourceChannels, tileSize, x, y, z, imageWidth return orderedList; }); } + +export function getZarrConnections({sourceChannels, maxZoom}) { + const zarrConnections = Object.keys(sourceChannels).map(async (channel) => { + var zarrObj = {} + for(var i = 0; i < -maxZoom; i++) { + var zarr = await openArray({ + store: `${sourceChannels[channel]}/`, + path: `pyramid_${i}.zarr`, + mode: 'r', + }) + if(!zarrObj[channel]) { + zarrObj[channel] = [zarr] + } else { + zarrObj[channel].push(zarr) + } + } + return zarrObj + }) + return Promise.all(zarrConnections) +} diff --git a/src/layers/microscopy-viewer-layer/microscopy-viewer-layer-base.js b/src/layers/microscopy-viewer-layer/microscopy-viewer-layer-base.js new file mode 100644 index 000000000..9b3e6c6be --- /dev/null +++ b/src/layers/microscopy-viewer-layer/microscopy-viewer-layer-base.js @@ -0,0 +1,97 @@ +import {BaseTileLayer} from '@deck.gl/layers'; +import {Texture2D} from '@luma.gl/webgl' +import GL from '@luma.gl/constants'; +import { COORDINATE_SYSTEM } from 'deck.gl'; +import { XRLayer } from '../xr-layer'; +import {tileToBoundingBox} from './tiling-utils'; +import {getTileIndices} from './tiling-utils'; +import { loadZarr, loadTiff } from './data-utils'; + +const defaultProps = Object.assign({}, BaseTileLayer.defaultProps, { + id: `microscopy-tile-layer`, + pickable: false, + coordinateSystem: COORDINATE_SYSTEM.CARTESIAN, + maxZoom: 0, + onViewportLoad: false, + renderSubLayers: (props) => { + const { + bbox: { + west, south, east, north, + }, + } = props.tile; + const { sliderValues, data, colorValues } = props; + const xrl = new XRLayer(props, { + id: `XR-Layer-${west}-${south}-${east}-${north}`, + pickable: false, + coordinateSystem: COORDINATE_SYSTEM.CARTESIAN, + rgbData: data, + sliderValues, + colorValues, + bounds: [west, south, east, north], + visible: true, + }); + return xrl; + }, +}); + +export class MicroscopyViewerLayerBase extends BaseTileLayer { + + constructor(props) { + const minZoom = Math.floor(-1 * Math.log2(Math.max(props.imageHeight, props.imageWidth))); + const {sliderValues, colorValues} = props + var orderedSliderValues = [] + var orderedColorValues = [] + Object.keys(sliderValues).sort().forEach(function(key) { + orderedSliderValues.push(sliderValues[key]); + }) + Object.keys(colorValues).sort().forEach(function(key) { + orderedColorValues.push(colorValues[key]); + }) + var diff = 6 - orderedSliderValues.length + for (var i = 0; i < diff; i++) { + orderedSliderValues.push(65535); + } + var diff = 6 - orderedColorValues.length + for (var j = 0; j < diff; j++) { + orderedColorValues.push([0,0,0]); + } + orderedColorValues = orderedColorValues.map(color => color.map(ch => ch / 255)) + const getZarr = ({ x, y, z }) => { + return loadZarr({ + x, y, z: -1 * z, ...props, + }); + } + const getTiff = ({ x, y, z }) => { + return loadTiff({ + x, y, z: -1 * z, ...props, + }); + } + const getTileData = (props.useZarr && getZarr) || + (props.useTiff && getTiff) || + props.getTileData + const overrideValuesProps = Object.assign( + {}, props, { + sliderValues: orderedSliderValues, + colorValues: orderedColorValues, + minZoom, + getTileData, + getTileIndices: (viewport, maxZoom, minZoom) => { + return getTileIndices({ + viewport, maxZoom, minZoom, ...props, + }); + }, + tileToBoundingBox: (x, y, z) => { + return tileToBoundingBox({ + x, y, z, ...props, + }); + }, + } + ) + const layerProps = Object.assign({}, defaultProps, overrideValuesProps) + super(layerProps) + } + +} + +MicroscopyViewerLayerBase.layerName = 'MicroscopyViewerLayerBase'; +MicroscopyViewerLayerBase.defaultProps = defaultProps; diff --git a/src/layers/microscopy-viewer-layer/microscopy-viewer-layer.js b/src/layers/microscopy-viewer-layer/microscopy-viewer-layer.js index bf6e82772..f36f9eb82 100644 --- a/src/layers/microscopy-viewer-layer/microscopy-viewer-layer.js +++ b/src/layers/microscopy-viewer-layer/microscopy-viewer-layer.js @@ -1,94 +1,38 @@ -import {BaseTileLayer} from '@deck.gl/layers'; -import {Texture2D} from '@luma.gl/webgl' -import GL from '@luma.gl/constants'; -import { COORDINATE_SYSTEM } from 'deck.gl'; -import { XRLayer } from '../xr-layer'; -import {tileToBoundingBox} from './tiling-utils'; -import {getTileIndices} from './tiling-utils'; -import { loadZarr } from './data-utils'; +import {MicroscopyViewerLayerBase} from './microscopy-viewer-layer-Base' +import {getTiffConnections,getZarrConnections} from './data-utils' +import {CompositeLayer} from '@deck.gl/core'; +import {fromUrl, Pool, getDecoder } from 'geotiff/dist/geotiff.bundle.min.js'; -const defaultProps = Object.assign({}, BaseTileLayer.defaultProps, { - id: `microscopy-tile-layer`, - pickable: false, - coordinateSystem: COORDINATE_SYSTEM.CARTESIAN, - maxZoom: 0, - onViewportLoad: false, - renderSubLayers: (props) => { - const { - bbox: { - west, south, east, north, - }, - } = props.tile; - const { sliderValues, data, colorValues } = props; - const xrl = new XRLayer(props, { - id: `XR-Layer-${west}-${south}-${east}-${north}`, - pickable: false, - coordinateSystem: COORDINATE_SYSTEM.CARTESIAN, - rgbData: data, - sliderValues, - colorValues, - bounds: [west, south, east, north], - visible: true, - }); - return xrl; - }, -}); +export class MicroscopyViewerLayer extends CompositeLayer { -export class MicroscopyViewerLayer extends BaseTileLayer { + initializeState(){ + this.state = { + connections: null, + pool: null + } + } + + shouldUpdateState({changeFlags}) { + return changeFlags.somethingChanged; + } - constructor(props) { - const minZoom = Math.floor(-1 * Math.log2(Math.max(props.imageHeight, props.imageWidth))); - const {sliderValues, colorValues} = props - var orderedSliderValues = [] - var orderedColorValues = [] - Object.keys(sliderValues).sort().forEach(function(key) { - orderedSliderValues.push(sliderValues[key]); + updateState(){ + this.props.useTiff + ? !this.state.connections && getTiffConnections({...this.props}).then((connections) => { + this.setState({connections, pool: new Pool()}) }) - Object.keys(colorValues).sort().forEach(function(key) { - orderedColorValues.push(colorValues[key]); + : !this.state.connections && getZarrConnections({...this.props}).then((connections) => { + this.setState({connections}) }) - var diff = 6 - orderedSliderValues.length - for (var i = 0; i < diff; i++) { - orderedSliderValues.push([0,65535]); - } - var diff = 6 - orderedColorValues.length - for (var j = 0; j < diff; j++) { - orderedColorValues.push([0,0,0]); - } - // flatten for use on shaders - var flatSliderValues = [].concat.apply([], orderedSliderValues) - orderedColorValues = orderedColorValues.map(color => color.map(ch => ch / 255)) - const getZarr = ({ x, y, z }) => { - return loadZarr({ - x, y, z: -1 * z, ...props, - }); - } - const getTileData = props.useZarr - ? getZarr - : props.getTileData - const overrideValuesProps = Object.assign( - {}, props, { - sliderValues: flatSliderValues, - colorValues: orderedColorValues, - minZoom, - getTileData, - getTileIndices: (viewport, maxZoom, minZoom) => { - return getTileIndices({ - viewport, maxZoom, minZoom, ...props, - }); - }, - tileToBoundingBox: (x, y, z) => { - return tileToBoundingBox({ - x, y, z, ...props, - }); - }, - } - ) - const layerProps = Object.assign({}, defaultProps, overrideValuesProps) - super(layerProps) + + } + + renderLayers() { + const layers = this.state.connections ? new MicroscopyViewerLayerBase({connections: Object.assign({},...this.state.connections), pool: this.state.pool, ...this.props}) : [] + return layers } } -MicroscopyViewerLayer.layerName = 'MicrsocopyViewerLayer'; -MicroscopyViewerLayer.defaultProps = defaultProps; + +MicroscopyViewerLayer.layerName = 'MicroscopyViewerLayer'; diff --git a/webpack.config.js b/webpack.config.js index a941cbcd6..1d7e0422c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -56,6 +56,11 @@ module.exports = { } ] }, + node: { + fs: 'empty', + buffer: 'empty', + http: 'empty', + }, plugins: [ new HtmlWebPackPlugin({ hash: true,