in pathology/viewer/src/components/ol-tile-viewer/ol-tile-viewer.component.ts [376:636]
initializeOpenLayerSlideViewer(slideInfo: SlideInfo) {
// Calculate image dimensions and resolutions for each level
let resolutions: number[] = [];
let tileSizes: number[] = [];
slideInfo.levelMap.forEach(level => {
const levelWidthMeters = level.width * level.pixelWidth! * 1e-3;
resolutions.push(levelWidthMeters / level.width);
tileSizes.push(level.tileSize);
});
resolutions = resolutions.reverse();
tileSizes = tileSizes.reverse();
// Calculate the extent based on the highest resolution level (zoom level 0)
const baseLevel = slideInfo.levelMap[slideInfo.levelMap.length - 1];
const size = {
x: baseLevel.width * baseLevel.pixelWidth! * 1e-3,
y: baseLevel.height * baseLevel.pixelHeight! * 1e-3,
};
const extent = [0, -size.y, size.x, 0];
// Create the TileGrid
const tileGrid =
new TileGrid({extent, resolutions, tileSizes, origin: [0, 0]});
const tileUrlFunction = () => {
return '';
};
const tileLoadFunction = (imageTile: Tile) => {
const [z, x, y] = imageTile.tileCoord;
const slideLevelsByZoom = [...slideInfo.levelMap];
// OpenLayers most zoomed out layer is 0. Reversed of how levelMap
// stores it.
const slideLevel: Level = slideLevelsByZoom.reverse()[z];
const {tileSize} = slideLevel;
const tilesPerWidth = Math.ceil(Number(slideLevel?.width) / tileSize);
const tilesPerHeight = Math.ceil(Number(slideLevel?.height) / tileSize);
// Frame number is computed by location at X (x+1 for offset), plus number
// of tiles in previous rows (y*tilesPerWidth).
const frame = (x + 1) + (tilesPerWidth * y);
// Out of bound frames
if (frame > tilesPerWidth * tilesPerHeight) {
imageTile.setState(TileState.ERROR);
}
const instanceUid = slideLevel?.properties[0].instanceUid ?? '';
const downSampleMultiplier = slideLevel.downSampleMultiplier ?? 0;
const path = (this.slideDescriptor?.id as string) ?? '';
const isYOutOfBound = (y + 1) === tilesPerHeight &&
slideLevel.height % slideLevel.tileSize !== 0;
const isXOutOfBound = (x + 1) === tilesPerWidth &&
slideLevel.width % slideLevel.tileSize !== 0;
this.dicomwebService
.getEncodedImageTile(
path, instanceUid, frame, downSampleMultiplier, this.iccProfile)
.pipe(
tap((imageData: string) => {
const imageUrl = this.imageDataToImageUrl(imageData);
if ((imageTile instanceof ImageTile)) {
if (!isYOutOfBound && !isXOutOfBound) {
(imageTile.getImage() as HTMLImageElement).src = imageUrl;
} else {
this.cropTile(
slideLevel, imageUrl, imageTile, isXOutOfBound,
isYOutOfBound);
}
}
}),
catchError((e) => {
imageTile.setState(TileState.ERROR);
return e;
}),
)
.subscribe();
};
const projection = new Projection({
code: 'custom-image',
units: 'm',
extent,
});
const scalelineControl = new ScaleLine({
units: 'metric',
bar: true,
text: true,
minWidth: 140,
});
const slideSource = new XYZ({
tileGrid,
tileLoadFunction,
tileUrlFunction,
projection,
});
// Layer to show the tiles from DICOM Proxy for the whole slide.
const slideLayer = new TileLayer({
preload: 3,
source: slideSource,
properties: {
name: 'slide-layer',
title: this.slideInfo?.slideName ?? '',
}
});
const slideOverviewControl = new OverviewMap({
collapsed: false,
view: new ol.View({
resolutions: [tileGrid.getResolution(0)],
extent,
projection,
maxZoom: 0,
minZoom: 0,
zoom: 0,
}),
layers: [
new TileLayer({
source: slideSource,
}),
],
});
// Layer to show grid around the tiles, helpful in debugging.
const debugLayer = new TileLayer({
source: new TileDebug({
tileGrid,
projection,
}),
properties: {name: 'debug-layer'}
});
// Layer to show drawings
const drawSource = new Vector<Feature<Geometry>>({wrapX: false});
const drawLayer = new VectorLayer({
source: drawSource,
style: [DRAW_STROKE_STYLE],
properties: {
name: 'draw-layer',
title: 'Annotations',
}
});
const measureSource = new Vector({
wrapX: false,
});
const measureLayer = new VectorLayer({
source: measureSource,
style: [
MEASURE_STROKE_STYLE,
],
properties: {
name: 'measure-layer',
}
});
const initialZoom = this.getInitialZoomByContainer(slideInfo.levelMap);
let olMap: ol.Map|undefined = undefined;
if (this.isThumbnail) {
const thumbnailInitialZoom = 0;
olMap = new ol.Map({
target: this.olMapContainer.nativeElement,
layers: [
slideLayer,
],
controls: [],
interactions: [],
view: new ol.View({
resolutions,
extent,
constrainOnlyCenter: true,
center: getCenter(extent),
zoom: thumbnailInitialZoom,
minZoom: thumbnailInitialZoom,
maxZoom: thumbnailInitialZoom,
projection,
})
});
return;
} else {
olMap = new ol.Map({
target: this.olMapContainer.nativeElement,
controls:
controlDefaults().extend([slideOverviewControl, scalelineControl]),
layers: [
slideLayer,
debugLayer,
drawLayer,
measureLayer,
],
view: new ol.View({
resolutions,
extent,
constrainOnlyCenter: true,
center: getCenter(extent),
zoom: initialZoom,
minZoom: 0,
projection,
})
});
}
const mousePositionControl = new MousePosition({
coordinateFormat: (coordinate) => {
if (!coordinate) return '';
// Convert meteres to millimetre.
coordinate[0] = coordinate[0] * 1000;
coordinate[1] = coordinate[1] * 1000;
return coordinateFormat(coordinate, '{x}mm, {y}mm', 2);
},
projection,
});
const linkInteraction =
new Link({replace: true, params: ['x', 'y', 'z', 'r']});
olMap.addControl(mousePositionControl);
olMap.addInteraction(linkInteraction);
olMap.addInteraction(new DragRotateAndZoom());
olMap.addControl(new FullScreen());
this.olMap = olMap;
this.olMap?.once('postrender', (event) => {
this.olMapLoaded.emit(this.olMap!);
const overviewMap = slideOverviewControl.getOverviewMap();
const loadedChangeListener = overviewMap.on('loadend', (e) => {
if (loadedChangeListener) {
unByKey(loadedChangeListener);
}
this.setOverviewAspectRatio(
`${slideInfo.levelMap[0].width}/${slideInfo.levelMap[0].height}`);
this.handleOverviewMapExpanding(overviewMap);
});
let initialZoom = this.olMap?.getView().getResolution();
if (initialZoom) {
this.magnificationLevel =
this.convertDecimalToFraction(metersToMagnification(initialZoom));
this.cdRef.detectChanges();
}
this.olMap?.on('moveend', e => {
const finalZoom = this.olMap?.getView().getResolution() ?? 0;
if (initialZoom !== finalZoom || !this.magnificationLevel) {
// this event has to do with a zoom in - zoom out
initialZoom = finalZoom;
this.magnificationLevel =
this.convertDecimalToFraction(metersToMagnification(finalZoom));
this.cdRef.detectChanges();
}
});
});
this.setupDebugLayerVisibilty();
}