initializeOpenLayerSlideViewer()

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();
  }