-
Notifications
You must be signed in to change notification settings - Fork 42
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
Scale Bar #158
Changes from 12 commits
61297a4
fc9aa5b
4718a46
c4e77d4
c61a16e
8b23eb1
d5e1e7b
7f6333e
54c8f12
7314ed7
9ff6b96
b10cf51
f2a7957
460a267
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||
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; |
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'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this field often missing? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you describe what the 'odd behavior' is? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll leave a clearer note. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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']; | ||
} | ||
|
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]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. perhaps There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)}`, | ||
|
@@ -18,6 +19,17 @@ export default class DetailView extends VivView { | |
id: `${loader.type}${getVivId(id)}`, | ||
viewportId: id | ||
}); | ||
return [layer]; | ||
const { PhysicalSizeXUnit, PhysicalSizeX } = loader; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this will make it easy to implement on the zarr loader. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could even check size units in There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]; | ||
} | ||
} |
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. | ||
|
@@ -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]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 const layers = [];
// ...
layers.push(detailLayer);
// ...
layers.push(border);
// ...
if (PhysicalSizeXUnit && PhysicalSizeX) {
// ...
layers.push(scaleBarLayer);
} |
||
} | ||
} |
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(); | ||
}); |
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'); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 }; | ||
|
@@ -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 } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not just define these on the loader above? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
There was a problem hiding this comment.
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 ingetPosition
with this default.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure.