Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scale Bar #158

Merged
merged 14 commits into from
Apr 22, 2020
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Added

- OMETiffLoader for reading ome-tiff files directly
- Add scale bar (only for OMEXML for now)

### Changed

Expand Down
7 changes: 0 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,6 @@ over the hood and WebGL under the hood. To learn more about the "theory" behind
this, look at [this](image_rendering_docs/IMAGE_RENDERING.md). To really make this sing, you need to
use an http2 server in production (s3 is passable, though).

### Using this in your project

In the interest of keeping this app as lightweight and extensible as possible,
there are no dependencies except for peer dependencies, which you will need to specify in your project.
The reason for this is primarily to support export external DeckGL setups so that
you might combine our layer with your own.

### Build

To build the component alone via `webpack` use `npm run-script build-component`.
Expand Down
2 changes: 1 addition & 1 deletion demo/src/constants.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const MAX_CHANNELS = 6;
export const DEFAULT_VIEW_STATE = { zoom: -5.5, target: [30000, 10000, 0] };
export const DEFAULT_VIEW_STATE = { zoom: -6, target: [25000, 10000, 0] };
export const DEFAULT_OVERVIEW = {
margin: 25,
scale: 0.15,
Expand Down
1 change: 1 addition & 0 deletions documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ toc:
- name: Layers
- VivViewerLayer
- StaticImageLayer
- ScaleBarLayer
- name: Loaders
- ZarrLoader
- OMETiffLoader
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,14 @@
"@deck.gl/layers": "^8.1.0",
"@deck.gl/mesh-layers": "^8.1.0",
"@deck.gl/react": "^8.1.0",
"geotiff": "git+https://github.com/ilan-gold/geotiff.js.git#master",
"@loaders.gl/core": "^2.0.2",
"@loaders.gl/loader-utils": "^2.0.2",
"@luma.gl/constants": "^8.1.0",
"@luma.gl/core": "^8.1.0",
"@luma.gl/shadertools": "^8.1.0",
"@mapbox/vector-tile": "^1.3.1",
"fast-xml-parser": "^3.16.0",
"geotiff": "git+https://github.com/ilan-gold/geotiff.js.git#master",
"math.gl": "^3.1.3",
"nebula.gl": "^0.17.1",
"pbf": "^3.2.1",
Expand Down
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { VivViewerLayer, StaticImageLayer } from './layers';
import { VivViewerLayer, StaticImageLayer, ScaleBarLayer } from './layers';
import { VivViewer, PictureInPictureViewer, SideBySideViewer } from './viewers';
import { VivView, OverviewView, DetailView, SideBySideView } from './views';
import {
Expand All @@ -9,6 +9,7 @@ import {
} from './loaders';

export {
ScaleBarLayer,
VivViewerLayer,
VivViewer,
VivView,
Expand Down
147 changes: 147 additions & 0 deletions src/layers/ScaleBarLayer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { CompositeLayer, COORDINATE_SYSTEM } from '@deck.gl/core';
import { LineLayer, TextLayer } from '@deck.gl/layers';
import { range } from './utils';
import { makeBoundingBox } from '../views/utils';

function getPosition(boundingBox, position) {
const viewLength = boundingBox[2][0] - boundingBox[0][0];
switch (position) {
case 'bottom-right': {
const yCoord =
boundingBox[2][1] - (boundingBox[2][1] - boundingBox[0][1]) * 0.085;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the 0.085 doing? maybe add this as an arg in getPosition with this default.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure.

const xLeftCoord = boundingBox[2][0] - viewLength * 0.085;
return [yCoord, xLeftCoord];
}
case 'top-right': {
const yCoord = (boundingBox[2][1] - boundingBox[0][1]) * 0.085;
const xLeftCoord = boundingBox[2][0] - viewLength * 0.085;
return [yCoord, xLeftCoord];
}
case 'top-left': {
const yCoord = (boundingBox[2][1] - boundingBox[0][1]) * 0.085;
const xLeftCoord = viewLength * 0.085;
return [yCoord, xLeftCoord];
}
case 'bottom-left': {
const yCoord =
boundingBox[2][1] - (boundingBox[2][1] - boundingBox[0][1]) * 0.085;
const xLeftCoord = viewLength * 0.085;
return [yCoord, xLeftCoord];
}
default: {
throw new Error(`Position ${position} not found`);
}
}
}

const defaultProps = {
pickable: false,
viewState: {
type: 'object',
value: { zoom: 0, target: [0, 0, 0] },
compare: true
},
PhysicalSizeXUnit: { type: 'string', value: '', compare: true },
PhysicalSizeX: { type: 'number', value: 1, compare: true },
position: { type: 'string', value: 'bottom-right', compare: true }
};

/**
* This layer creates a scale bar using three LineLayers and a TextLayer.
* Looks like: |--------| made up of three LineLayers (left tick, right tick, center length bar) and a bottom TextLayer
* @param {Object} props
* @param {String} props.PhysicalSizeXUnit Physical unit size per pixel at full resolution.
* @param {Number} props.PhysicalSizeX Physical size of a pixel.
* @param {Array} props.boundingBox Boudning box of the view in which this should render.
* @param {id} props.id Id from the parent layer.
* @param {ViewState} props.viewState The current viewState for the desired view. We cannot internally use this.context.viewport because it is one frame behind:
* https://github.com/visgl/deck.gl/issues/4504
*/
export default class ScaleBarLayer extends CompositeLayer {
renderLayers() {
const {
id,
PhysicalSizeXUnit,
PhysicalSizeX,
position,
viewState
} = this.props;
const boundingBox = makeBoundingBox(viewState);
const { zoom } = viewState;
const viewLength = boundingBox[2][0] - boundingBox[0][0];
const barLength = viewLength * 0.05;
// This is a good heuristic for stopping the bar tick marks from getting too small
// and/or the text squishing up into the bar.
const barHeight = Math.max(
2 ** (-zoom + 1.5),
(boundingBox[2][1] - boundingBox[0][1]) * 0.007
);
const numUnits = barLength * PhysicalSizeX;
const [yCoord, xLeftCoord] = getPosition(boundingBox, position);
const lengthBar = new LineLayer({
id: `scale-bar-length-${id}`,
coordinateSystem: COORDINATE_SYSTEM.CARTESIAN,
data: [
[
[xLeftCoord, yCoord],
[xLeftCoord + barLength, yCoord]
]
],
getSourcePosition: d => d[0],
getTargetPosition: d => d[1],
getWidth: 2,
getColor: [220, 220, 220]
});
const tickBoundsLeft = new LineLayer({
id: `scale-bar-height-left-${id}`,
coordinateSystem: COORDINATE_SYSTEM.CARTESIAN,
data: [
[
[xLeftCoord, yCoord - barHeight],
[xLeftCoord, yCoord + barHeight]
]
],
getSourcePosition: d => d[0],
getTargetPosition: d => d[1],
getWidth: 2,
getColor: [220, 220, 220]
});
const tickBoundsRight = new LineLayer({
id: `scale-bar-height-right-${id}`,
coordinateSystem: COORDINATE_SYSTEM.CARTESIAN,
data: [
[
[xLeftCoord + barLength, yCoord - barHeight],
[xLeftCoord + barLength, yCoord + barHeight]
]
],
getSourcePosition: d => d[0],
getTargetPosition: d => d[1],
getWidth: 2,
getColor: [220, 220, 220]
});
const textLayer = new TextLayer({
id: `units-label-layer-${id}`,
coordinateSystem: COORDINATE_SYSTEM.CARTESIAN,
data: [
{
text: String(numUnits).slice(0, 5) + PhysicalSizeXUnit,
position: [xLeftCoord + barLength * 0.5, yCoord + barHeight * 4]
}
],
getColor: [220, 220, 220, 255],
getSize: 11,
sizeUnits: 'meters',
sizeScale: 2 ** -zoom,
characterSet: [
...PhysicalSizeXUnit.split(''),
...range(10).map(i => String(i)),
'.'
]
});
return [lengthBar, tickBoundsLeft, tickBoundsRight, textLayer];
}
}

ScaleBarLayer.layerName = 'ScaleBarLayer';
ScaleBarLayer.defaultProps = defaultProps;
1 change: 1 addition & 0 deletions src/layers/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as VivViewerLayer } from './VivViewerLayer';
export { default as StaticImageLayer } from './StaticImageLayer';
export { default as OverviewLayer } from './OverviewLayer';
export { default as ScaleBarLayer } from './ScaleBarLayer';
4 changes: 4 additions & 0 deletions src/loaders/OMETiffLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ export default class OMETiffLoader {
this.type = 'ome-tiff';
// get first image's description, which contains OMEXML
this.omexml = new OMEXML(omexmlString);
this.PhysicalSizeX = this.omexml.PhysicalSizeX;
this.PhysicalSizeXUnit = this.omexml.PhysicalSizeXUnit;
this.PhysicalSizeY = this.omexml.PhysicalSizeY;
this.PhysicalSizeYUnit = this.omexml.PhysicalSizeYUnit;
this.offsets = offsets || [];
this.channelNames = this.omexml.getChannelNames();
this.width = this.omexml.SizeX;
Expand Down
13 changes: 11 additions & 2 deletions src/loaders/omeXML.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,17 @@ export default class OMEXML {
this.SizeY = Number.parseInt(Pixels['@_SizeY']);
this.DimensionOrder = Pixels['@_DimensionOrder'];
this.Type = Pixels['@_Type'];
this.PhysicalSizeYUnit = Pixels['@_PhysicalSizeYUnit'];
this.PhysicalSizeXUnit = Pixels['@_PhysicalSizeXUnit'];
const PhysicalSizeYUnit = Pixels['@_PhysicalSizeYUnit'];
// This µ character is not well handled - I got odd behavior but this solves it.
this.PhysicalSizeYUnit =
PhysicalSizeYUnit && PhysicalSizeYUnit.includes('µm')
? 'µm'
: this.PhysicalSizeXUnit;
const PhysicalSizeXUnit = Pixels['@_PhysicalSizeXUnit'];
this.PhysicalSizeXUnit =
PhysicalSizeXUnit && PhysicalSizeXUnit.includes('µm')
? 'µm'
: this.PhysicalSizeXUnit;
Comment on lines +34 to +36
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this field often missing?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you describe what the 'odd behavior' is?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave a clearer note.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a prepended character. Could have been an error but the µm is kind of wonky so it could be a python-javascript difference.

this.PhysicalSizeY = Pixels['@_PhysicalSizeY'];
this.PhysicalSizeX = Pixels['@_PhysicalSizeX'];
}
Expand Down
18 changes: 15 additions & 3 deletions src/views/DetailView.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { VivViewerLayer, StaticImageLayer } from '../layers';
import { VivViewerLayer, StaticImageLayer, ScaleBarLayer } from '../layers';
import VivView from './VivView';
import { getVivId } from './utils';

/**
* This class generates a VivViewerLayer and a view for use in the VivViewer as a detailed view.
* */
export default class DetailView extends VivView {
getLayers({ props }) {
getLayers({ props, viewStates }) {
const { loader } = props;
const { id } = this;
const thisViewState = viewStates[id];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps layerViewState or viewStateUpdate or viewState? Confusing to prefix with this in javascript because it is a keyword...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense

const layer = loader.isPyramid
? new VivViewerLayer(props, {
id: `${loader.type}${getVivId(id)}`,
Expand All @@ -18,6 +19,17 @@ export default class DetailView extends VivView {
id: `${loader.type}${getVivId(id)}`,
viewportId: id
});
return [layer];
const { PhysicalSizeXUnit, PhysicalSizeX } = loader;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer that if these are on the loaders we use camelcase like other properties, rather than copying the omexml keys directly. Similar to tileSize, we assume that the PhysicalSizeX and PhysicalSizeY are the same for the image, so maybe we could just have the property physicalSize which is an object:

const { unit, value } = loader.physicalSize;

or if we want to add all sizes and scale bar just assumes x:

const { x, y, z } = loader.physicalSizes;
const { unit, value } = x;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will make it easy to implement on the zarr loader.

Copy link
Member

@manzt manzt Apr 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could even check size units in x and y and console.warn if they are different.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this. This was the thorniest point so I'm glad to have the feedback.

const scaleBarLayer =
PhysicalSizeXUnit && PhysicalSizeX
? new ScaleBarLayer({
id: getVivId(id),
loader,
PhysicalSizeXUnit,
PhysicalSizeX,
viewState: thisViewState
})
: null;
return [layer, scaleBarLayer];
}
}
16 changes: 13 additions & 3 deletions src/views/SideBySideView.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { PolygonLayer } from '@deck.gl/layers';
import { COORDINATE_SYSTEM } from '@deck.gl/core';

import { VivViewerLayer, StaticImageLayer } from '../layers';
import { VivViewerLayer, StaticImageLayer, ScaleBarLayer } from '../layers';
import VivView from './VivView';
import { getVivId, makeBoundingBox } from './utils';

/**
* This class generates a VivViewerLayer and a view for use in the SideBySideViewer.
* It is linked with its other views as controlled by `linkedIds`, `zoomLock`, and `panLock` parameters.
Expand Down Expand Up @@ -119,6 +118,17 @@ export default class SideBySideView extends VivView {
getLineColor: viewportOutlineColor,
getLineWidth: viewportOutlineWidth * 2 ** -thisViewState.zoom
});
return [detailLayer, border];
const { PhysicalSizeXUnit, PhysicalSizeX } = loader;
const scaleBarLayer =
PhysicalSizeXUnit && PhysicalSizeX
? new ScaleBarLayer({
id: getVivId(id),
loader,
PhysicalSizeXUnit,
PhysicalSizeX,
viewState: thisViewState
})
: null;
return [detailLayer, border, scaleBarLayer];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be clearer to me what's going on if we just pushed layers to an empty array, rather than adding a null here.

const layers = [];
// ...
layers.push(detailLayer);
// ...
layers.push(border);
// ... 
if (PhysicalSizeXUnit && PhysicalSizeX) {
  // ...
  layers.push(scaleBarLayer);
}

}
}
38 changes: 38 additions & 0 deletions tests/layers_views/layers/ScaleBarLayer.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* eslint-disable import/no-extraneous-dependencies, no-unused-expressions */
import test from 'tape-catch';
import { generateLayerTests, testLayer } from '@deck.gl/test-utils';
import { OrthographicView } from '@deck.gl/core';
import ScaleBarLayer from '../../../src/layers/ScaleBarLayer';

test('ScaleBarLayer', t => {
const view = new OrthographicView({
id: 'ortho',
controller: true,
height: 4,
width: 4,
target: [2, 2, 0],
zoom: 0
});
const testCases = generateLayerTests({
Layer: ScaleBarLayer,
assert: t.ok,
sampleProps: {
viewState: { target: [2, 2, 0], zoom: 0, width: 4, height: 4 },
PhysicalSizeXUnit: 'cm',
PhysicalSizeX: 1,
position: 'bottom-left'
},
onBeforeUpdate: ({ testCase }) => t.comment(testCase.title)
});
testLayer({
Layer: ScaleBarLayer,
testCases,
onError: t.notOkimport,
viewport: view.makeViewport({
height: 4,
width: 4,
viewState: { target: [2, 2, 0], zoom: 0, width: 4, height: 4 }
})
});
t.end();
});
1 change: 1 addition & 0 deletions tests/layers_views/layers/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require('./StaticImageLayer.spec');
require('./XRLayer.spec');
require('./VivViewerLayerBase.spec');
require('./ScaleBarLayer.spec');
require('./utils.spec');
18 changes: 16 additions & 2 deletions tests/layers_views/views/DetailView.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import test from 'tape-catch';
import { DetailView } from '../../../src/views';
import { generateViewTests, defaultArguments } from './VivView.spec';
import { VivViewerLayer } from '../../../src/layers';
import { VivViewerLayer, ScaleBarLayer } from '../../../src/layers';

const id = 'detail';
const detailViewArguments = { ...defaultArguments };
Expand All @@ -14,11 +14,25 @@ generateViewTests(DetailView, detailViewArguments);
test(`DetailView layer type and props check`, t => {
const view = new DetailView(detailViewArguments);
const loader = { type: 'loads', isPyramid: true };
const layers = view.getLayers({ props: { loader } });
const layers = view.getLayers({
props: {
loader: { ...loader, PhysicalSizeXUnit: 'cm', PhysicalSizeX: 0.5 }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just define these on the loader above?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The base test just checks for id-passing. This one checks that proper layers are rendered, for which this is needed and in the former case it is not. The OverviewView, for example, doesn't technically need this information.

},
viewStates: {
detail: {
target: [0, 0, 0],
zoom: 0
}
}
});
t.ok(
layers[0] instanceof VivViewerLayer,
'DetailView layer should be VivViewerLayer.'
);
t.ok(
layers[1] instanceof ScaleBarLayer,
'DetailView layer should be VivViewerLayer.'
);
t.equal(
layers[0].props.viewportId,
view.id,
Expand Down
Loading