Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,11 @@ class WSIResolvedView extends ResolvedViewportView<WSIResolvedViewState> {
resolution: number;
}): [number, number] {
const { canvasPoint, indexPoint, resolution } = args;
const pixelRatio =
typeof window === 'undefined' ? 1 : window.devicePixelRatio || 1;
// CSS pixels on both sides, as in canvasToIndexForWSI
const halfCanvasX = Math.max(this.state.canvasWidth, 1) / 2;
const halfCanvasY = Math.max(this.state.canvasHeight, 1) / 2;
const deltaCanvasX = canvasPoint[0] * pixelRatio - halfCanvasX;
const deltaCanvasY = canvasPoint[1] * pixelRatio - halfCanvasY;
const deltaCanvasX = canvasPoint[0] - halfCanvasX;
const deltaCanvasY = canvasPoint[1] - halfCanvasY;
const rotation =
this.state.view.getRotation?.() || this.state.viewState.rotation || 0;
const cos = Math.cos(rotation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,47 +140,46 @@ export function buildWSIColorTransform(
</svg>#colour')`;
}

/**
* Converts a canvas point to a WSI index point. Canvas points, canvasWidth and
* canvasHeight are all CSS pixels, the space OpenLayers' view resolution is
* expressed in, so the device pixel ratio does not enter the transform (as in
* the legacy WSIViewport since #2769).
*/
export function canvasToIndexForWSI(args: {
canvasPos: Point2;
canvasWidth: number;
canvasHeight: number;
view: WSIMapViewLike;
devicePixelRatio?: number;
}): Point3 {
const transform = getWSICanvasTransform(args);

transform.invert();

const indexPoint = transform.transformPoint(
args.canvasPos.map(
(value) => value * (args.devicePixelRatio || window.devicePixelRatio || 1)
) as Point2
);
const indexPoint = transform.transformPoint(args.canvasPos);

return [indexPoint[0], indexPoint[1], 0];
}

/**
* Converts a WSI index point to a canvas point in CSS pixels, the inverse of
* canvasToIndexForWSI.
*/
export function indexToCanvasForWSI(args: {
indexPos: Point3;
canvasWidth: number;
canvasHeight: number;
view: WSIMapViewLike;
devicePixelRatio?: number;
}): Point2 {
const transform = getWSICanvasTransform(args);

return transform
.transformPoint([args.indexPos[0], args.indexPos[1]])
.map(
(value) => value / (args.devicePixelRatio || window.devicePixelRatio || 1)
) as Point2;
return transform.transformPoint([args.indexPos[0], args.indexPos[1]]);
}

function getWSICanvasTransform(args: {
canvasWidth: number;
canvasHeight: number;
view: WSIMapViewLike;
devicePixelRatio?: number;
}): Transform {
const resolution = args.view.getResolution();
const rotation = args.view.getRotation();
Expand Down
110 changes: 84 additions & 26 deletions packages/core/test/wsiTransformUtils.jest.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
canvasToIndexForWSI,
indexToCanvasForWSI,
} from '../src/RenderingEngine/GenericViewport/WSI/wsiTransformUtils';
import WSIResolvedView from '../src/RenderingEngine/GenericViewport/WSI/WSIResolvedView';

// Identity-oriented WSI: row cosines [1,0,0], column cosines [0,1,0],
// scan-axis normal (cross product) [0,0,1]. Origin offset from slide corner
Expand Down Expand Up @@ -384,37 +385,94 @@ describe('wsiTransformUtils', () => {
expect(canvasPos[1]).toBeLessThan(canvasHeight / 2);
});

it('divides the whole transformed point by an explicit devicePixelRatio argument rather than window.devicePixelRatio', () => {
Object.defineProperty(window, 'devicePixelRatio', {
configurable: true,
value: 3, // should be ignored because an explicit value is passed
// Canvas points are CSS pixels, and so are canvasWidth/canvasHeight (the
// viewport passes the element's clientWidth/clientHeight): the device
// pixel ratio must not scale either side of the transform.
describe.each([1, 2, 3])('at devicePixelRatio %s', (dpr) => {
beforeEach(() => {
Object.defineProperty(window, 'devicePixelRatio', {
configurable: true,
value: dpr,
});
});
const view = makeView({ resolution: 1, center: [0, 0] });
const canvasWidth = 200;
const canvasHeight = 200;

const canvasPosDpr1 = indexToCanvasForWSI({
indexPos: [20, 0, 0],
canvasWidth,
canvasHeight,
view,
devicePixelRatio: 1,
it('maps the canvas center to the view center and back', () => {
const view = makeView({
resolution: 2,
rotation: 0.4,
center: [30, 70],
});

const indexPos = canvasToIndexForWSI({
canvasPos: [150, 75],
canvasWidth: 300,
canvasHeight: 150,
view,
});
const canvasPos = indexToCanvasForWSI({
indexPos: [30, 70, 0],
canvasWidth: 300,
canvasHeight: 150,
view,
});

expect(indexPos[0]).toBeCloseTo(30, 6);
expect(indexPos[1]).toBeCloseTo(70, 6);
expect(canvasPos[0]).toBeCloseTo(150, 6);
expect(canvasPos[1]).toBeCloseTo(75, 6);
});
const canvasPosDpr2 = indexToCanvasForWSI({
indexPos: [20, 0, 0],
canvasWidth,
canvasHeight,
view,
devicePixelRatio: 2,

it('moves one resolution step of index per CSS pixel', () => {
const view = makeView({ resolution: 2, center: [30, 70] });

const indexPos = canvasToIndexForWSI({
canvasPos: [160, 75],
canvasWidth: 300,
canvasHeight: 150,
view,
});

expect(indexPos[0]).toBeCloseTo(50, 6);
expect(indexPos[1]).toBeCloseTo(70, 6);
});

// getWSICanvasTransform builds its translation from the raw canvasWidth/
// canvasHeight (independent of devicePixelRatio); indexToCanvasForWSI then
// divides the entire resulting point -- center offset included -- by
// devicePixelRatio. So the whole point scales by 1/dpr, not just the
// displacement from the canvas center.
expect(canvasPosDpr2[0]).toBeCloseTo(canvasPosDpr1[0] / 2, 6);
expect(canvasPosDpr2[1]).toBeCloseTo(canvasPosDpr1[1] / 2, 6);
it('keeps the index under the cursor when zooming at a canvas point', () => {
const canvasPoint = [200, 100];
const canvasSize = { canvasWidth: 300, canvasHeight: 150 };
const view = {
...makeView({ resolution: 2, center: [30, 70] }),
getZoom: () => 1,
};
const resolvedView = new WSIResolvedView({
...canvasSize,
view,
viewState: {
centerIndex: [30, 70],
resolution: 2,
rotation: 0,
zoom: 1,
},
});
const indexUnderCursor = canvasToIndexForWSI({
canvasPos: canvasPoint,
...canvasSize,
view,
});

const { centerIndex, resolution } = resolvedView.withZoom(
2,
canvasPoint
).state.viewState;
const indexAfterZoom = canvasToIndexForWSI({
canvasPos: canvasPoint,
...canvasSize,
view: makeView({ resolution, center: centerIndex }),
});

expect(resolution).toBeCloseTo(1, 6);
expect(indexAfterZoom[0]).toBeCloseTo(indexUnderCursor[0], 6);
expect(indexAfterZoom[1]).toBeCloseTo(indexUnderCursor[1], 6);
});
});
});
});