From 654eb8b7c1204294d91ba4db404b94cd17383592 Mon Sep 17 00:00:00 2001 From: hackermd Date: Wed, 5 May 2021 11:47:47 -0400 Subject: [PATCH] Update documentation --- docs/annotations__AnnotationManager.js.html | 226 + docs/annotations_markers_arrow.js.html | 220 + ...annotations_markups__MarkupManager.js.html | 473 ++ docs/annotations_markups_measurement.js.html | 126 + ...annotations_markups_textEvaluation.js.html | 184 + ...ortestLineBetweenOverlayAndFeature.js.html | 95 + docs/api.html | 6 +- docs/dicom-microscopy-viewer.js.html | 23 +- docs/events.html | 6 +- docs/events.js.html | 4 +- docs/global.html | 5502 ++++++++++++++++- docs/index.html | 11 +- docs/metadata.Comprehensive3DSR.html | 171 + .../metadata.VLWholeSlideMicroscopyImage.html | 9 +- docs/metadata.html | 9 +- docs/metadata.js.html | 48 +- docs/roi.ROI.html | 20 +- docs/roi.html | 6 +- docs/roi.js.html | 53 +- docs/scoord3d.Ellipse.html | 4 +- docs/scoord3d.Ellipsoid.html | 4 +- docs/scoord3d.Multipoint.html | 4 +- docs/scoord3d.Point.html | 4 +- docs/scoord3d.Polygon.html | 4 +- docs/scoord3d.Polyline.html | 4 +- docs/scoord3d.html | 6 +- docs/scoord3d.js.html | 4 +- docs/utils.html | 38 +- docs/utils.js.html | 550 +- docs/viewer.LabelImageViewer.html | 6 +- docs/viewer.OverviewImageViewer.html | 6 +- docs/viewer.VolumeImageViewer.html | 4205 +++++++++++-- docs/viewer.html | 6 +- docs/viewer.js.html | 1880 ++++-- 34 files changed, 12380 insertions(+), 1537 deletions(-) create mode 100644 docs/annotations__AnnotationManager.js.html create mode 100644 docs/annotations_markers_arrow.js.html create mode 100644 docs/annotations_markups__MarkupManager.js.html create mode 100644 docs/annotations_markups_measurement.js.html create mode 100644 docs/annotations_markups_textEvaluation.js.html create mode 100644 docs/annotations_markups_utils_getShortestLineBetweenOverlayAndFeature.js.html create mode 100644 docs/metadata.Comprehensive3DSR.html diff --git a/docs/annotations__AnnotationManager.js.html b/docs/annotations__AnnotationManager.js.html new file mode 100644 index 00000000..f5bf85ab --- /dev/null +++ b/docs/annotations__AnnotationManager.js.html @@ -0,0 +1,226 @@ + + + + + JSDoc: Source: annotations/_AnnotationManager.js + + + + + + + + + + +
+ +

Source: annotations/_AnnotationManager.js

+ + + + + + +
+
+
import dcmjs from "dcmjs";
+
+import _MarkupManager from "./markups/_MarkupManager";
+
+/** Enums */
+import Enums from "../enums";
+
+/** Markers */
+import ArrowMarker, { format as arrowFormat } from "./markers/arrow";
+
+/** Markups */
+import MeasurementMarkup, {
+  format as measurementFormat,
+} from "./markups/measurement";
+import TextEvaluationMarkup, {
+  format as textFormat,
+} from "./markups/textEvaluation";
+
+/** Utils */
+import { areCodedConceptsEqual, getContentItemNameCodedConcept } from "../utils";
+
+const { Marker, Markup } = Enums;
+
+class _AnnotationManager {
+  constructor({ map, pyramid } = {}) {
+    const markupManager = new _MarkupManager({
+      map,
+      pyramid,
+      formatters: {
+        [Marker.Arrow]: arrowFormat,
+        [Markup.Measurement]: measurementFormat,
+        [Markup.TextEvaluation]: textFormat,
+      },
+    });
+
+    this.props = {
+      map,
+      pyramid,
+      markupManager,
+    };
+
+    /** Markups */
+    this[Markup.Measurement] = MeasurementMarkup(this.props);
+    this[Markup.TextEvaluation] = TextEvaluationMarkup(this.props);
+
+    /** Markers */
+    this[Marker.Arrow] = ArrowMarker(this.props);
+  }
+
+  /**
+   * Add markup properties based on ROI
+   * measurements and evaluations.
+   *
+   * @param {Feature} feature The feature
+   */
+  _addMeasurementsAndEvaluationsProperties(feature) {
+    const { measurements, evaluations } = feature.getProperties();
+
+    if (measurements && measurements.length) {
+      return measurements.some((measurement) => {
+        const SUPPORTED_MEASUREMENTS_CODED_CONCEPTS = [
+          new dcmjs.sr.coding.CodedConcept({
+            meaning: "Area",
+            value: "42798000",
+            schemeDesignator: "SCT",
+          }),
+          new dcmjs.sr.coding.CodedConcept({
+            meaning: "Length",
+            value: "410668003",
+            schemeDesignator: "SCT",
+          }),
+        ];
+        const measurementCodedConcept = getContentItemNameCodedConcept(
+          measurement
+        );
+        if (
+          SUPPORTED_MEASUREMENTS_CODED_CONCEPTS.some((codedConcept) =>
+          areCodedConceptsEqual(measurementCodedConcept, codedConcept)
+          )
+        ) {
+          feature.set(
+            Enums.InternalProperties.Markup,
+            Enums.Markup.Measurement
+          );
+        }
+      });
+    }
+
+    if (evaluations && evaluations.length) {
+      return evaluations.some((evaluation) => {
+        const SUPPORTED_EVALUATIONS_CODED_CONCEPTS = [
+          new dcmjs.sr.coding.CodedConcept({
+            value: "112039",
+            meaning: "Tracking Identifier",
+            schemeDesignator: "DCM",
+          }),
+        ];
+        const evaluationCodedConcept = getContentItemNameCodedConcept(
+          evaluation
+        );
+        if (
+          SUPPORTED_EVALUATIONS_CODED_CONCEPTS.some((codedConcept) =>
+          areCodedConceptsEqual(codedConcept, evaluationCodedConcept)
+          )
+        ) {
+          feature.set(
+            Enums.InternalProperties.Markup,
+            Enums.Markup.TextEvaluation
+          );
+        }
+      });
+    }
+  }
+
+  /**
+   * Sets annotations visibility.
+   * 
+   * @param {boolean} isVisible 
+   */
+  setVisible(isVisible) {
+    this.props.markupManager.setVisible(isVisible);
+  }
+
+  onAdd(feature) {
+    /**
+     * Add properties to ROI feature before triggering
+     * markup and markers callbacks to keep UI in sync.
+     */
+    this._addMeasurementsAndEvaluationsProperties(feature);
+
+    this[Marker.Arrow].onAdd(feature);
+    this[Markup.Measurement].onAdd(feature);
+    this[Markup.TextEvaluation].onAdd(feature);
+  }
+
+  onFailure(uid) {
+    this[Marker.Arrow].onFailure(uid);
+    this[Markup.Measurement].onFailure(uid);
+    this[Markup.TextEvaluation].onFailure(uid);
+  }
+
+  onRemove(feature) {
+    this[Marker.Arrow].onRemove(feature);
+    this[Markup.Measurement].onRemove(feature);
+    this[Markup.TextEvaluation].onRemove(feature);
+  }
+
+  onUpdate(feature) {
+    this[Marker.Arrow].onUpdate(feature);
+    this[Markup.Measurement].onUpdate(feature);
+    this[Markup.TextEvaluation].onUpdate(feature);
+  }
+
+  onDrawStart(event) {
+    this[Marker.Arrow].onDrawStart(event);
+    this[Markup.Measurement].onDrawStart(event);
+    this[Markup.TextEvaluation].onDrawStart(event);
+  }
+
+  onDrawEnd(event) {
+    this[Marker.Arrow].onDrawEnd(event);
+    this[Markup.Measurement].onDrawEnd(event);
+    this[Markup.TextEvaluation].onDrawEnd(event);
+    this.props.markupManager.onDrawEnd(event);
+  }
+
+  onDrawAbort(event) {
+    this[Marker.Arrow].onDrawAbort(event);
+    this[Markup.Measurement].onDrawAbort(event);
+    this[Markup.TextEvaluation].onDrawAbort(event);
+    this.props.markupManager.onDrawAbort(event);
+  }
+}
+
+export default _AnnotationManager;
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/docs/annotations_markers_arrow.js.html b/docs/annotations_markers_arrow.js.html new file mode 100644 index 00000000..c66121cd --- /dev/null +++ b/docs/annotations_markers_arrow.js.html @@ -0,0 +1,220 @@ + + + + + JSDoc: Source: annotations/markers/arrow.js + + + + + + + + + + +
+ +

Source: annotations/markers/arrow.js

+ + + + + + +
+
+
import Style from "ol/style/Style";
+import Stroke from "ol/style/Stroke";
+import Point from "ol/geom/Point";
+import LineString from "ol/geom/LineString";
+import Icon from "ol/style/Icon";
+
+import Enums from "../../enums";
+import defaultStyles from "../styles";
+
+/**
+ * Format arrow output.
+ *
+ * @param {LineString} arrow geometry
+ * @return {string} The formatted output
+ */
+export const format = (feature) =>
+  feature.get(Enums.InternalProperties.Label) || "";
+
+/**
+ * Builds arrow styles.
+ *
+ * @param {object} feature The feature instance
+ * @param {object} map The viewer map instance
+ * @returns {object} Style instance
+ */
+const _applyStyles = (feature, map) => {
+  const geometry = feature.getGeometry();
+  if (geometry instanceof Point || geometry instanceof LineString) {
+    const anchor = [0, 0.5];
+    const rotation = 120;
+    const point = geometry.getCoordinates();
+    const styleOptions = feature.get(Enums.InternalProperties.StyleOptions);
+    const color =
+      styleOptions && styleOptions.stroke && styleOptions.stroke.color
+        ? styleOptions.stroke.color
+        : defaultStyles.stroke.color;
+
+    feature.setStyle((feature, resolution) => {
+      const view = map.getView();
+      const currentZoomLevel = view.getZoom();
+      const zoomResolution = view.getResolutionForZoom(currentZoomLevel);
+      const newScale = zoomResolution / resolution;
+
+      const pointIcon = `
+          <svg version="1.1" width="70px" height="70px" viewBox="0 -7.101 760.428 415.101" style="enable-background:new 0 0 408 408;" xmlns="http://www.w3.org/2000/svg">
+            <g>
+              <path style="fill:${encodeURIComponent(
+                color
+              )};" d="M 736.978 175.952 L 96.9 178.5 L 239.7 35.7 L 204 0 L 0 204 L 204 408 L 239.7 372.3 L 96.9 229.5 L 737.197 224.191 L 736.978 175.952 Z"/>
+            </g>
+          </svg>
+        `;
+
+      const icon = `
+        <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="70px" height="70px" viewBox="0 0 407.436 407.436" style="enable-background:new 0 0 407.436 407.436;">
+          <polygon style="fill:${encodeURIComponent(
+            color
+          )};" points="315.869,21.178 294.621,0 91.566,203.718 294.621,407.436 315.869,386.258 133.924,203.718 "/>
+        </svg>
+      `;
+
+      const styles = [];
+
+      if (geometry instanceof LineString) {
+        geometry.forEachSegment((start, end) => {
+          const dx = end[0] - start[0];
+          const dy = end[1] - start[1];
+          const rotation = Math.atan2(dy, dx);
+
+          const arrowStyle = new Style({
+            geometry: new Point(start),
+            image: new Icon({
+              opacity: 1,
+              src: `data:image/svg+xml;utf8,${icon}`,
+              scale: newScale /** Absolute-sized icon */,
+              anchor: [0.3, 0.5],
+              rotateWithView: true,
+              rotation: -rotation,
+            }),
+          });
+
+          styles.push(
+            new Style({
+              stroke: new Stroke({
+                color,
+                width: 5 * newScale /** Keep scale sync with icon */,
+              }),
+            })
+          );
+
+          /** Arrow */
+          styles.push(arrowStyle);
+        });
+
+        return styles;
+      }
+
+      const iconStyle = new Style({
+        geometry: new Point(point),
+        image: new Icon({
+          opacity: 1,
+          src: `data:image/svg+xml;utf8,${pointIcon}`,
+          scale: newScale /** Absolute-sized icon */,
+          anchor,
+          rotateWithView: true,
+          rotation: -rotation,
+        }),
+      });
+
+      return iconStyle;
+    });
+  }
+};
+
+const _isArrow = (feature) =>
+  Enums.Marker.Arrow === feature.get(Enums.InternalProperties.Marker);
+
+/**
+ * Arrow marker definition.
+ *
+ * @param {object} dependencies Shared dependencies
+ * @param {object} dependencies.map Map shared instance
+ * @param {object} dependencies.markupManager Markup manager shared instance
+ */
+const ArrowMarker = ({ map, markupManager }) => {
+  return {
+    onAdd: (feature) => {
+      if (_isArrow(feature)) {
+        _applyStyles(feature, map);
+
+        /** Keep arrow style after external style changes */
+        feature.on(
+          Enums.FeatureEvents.PROPERTY_CHANGE,
+          ({ key: property, target: feature }) => {
+            if (property === Enums.InternalProperties.StyleOptions) {
+              _applyStyles(feature, map);
+            }
+          }
+        );
+
+        /** Update arrow icon position on feature geometry change */
+        feature.getGeometry().on(Enums.FeatureGeometryEvents.CHANGE, () => {
+          _applyStyles(feature, map);
+        });
+      }
+    },
+    onDrawStart: ({ feature }) => {
+      if (_isArrow(feature)) {
+        _applyStyles(feature, map);
+      }
+    },
+    onRemove: (feature) => {
+      if (_isArrow(feature)) {
+        const featureId = feature.getId();
+        markupManager.remove(featureId);
+      }
+    },
+    onFailure: (uid) => {
+      if (uid) {
+        markupManager.remove(uid);
+      }
+    },
+    onUpdate: (feature) => {},
+    onDrawEnd: ({ feature }) => {},
+    onDrawAbort: ({ feature }) => {},
+  };
+};
+
+export default ArrowMarker;
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/docs/annotations_markups__MarkupManager.js.html b/docs/annotations_markups__MarkupManager.js.html new file mode 100644 index 00000000..407a02e7 --- /dev/null +++ b/docs/annotations_markups__MarkupManager.js.html @@ -0,0 +1,473 @@ + + + + + JSDoc: Source: annotations/markups/_MarkupManager.js + + + + + + + + + + +
+ +

Source: annotations/markups/_MarkupManager.js

+ + + + + + +
+
+
import DragPan from "ol/interaction/DragPan";
+import Overlay from "ol/Overlay";
+import VectorLayer from "ol/layer/Vector";
+import "ol/ol.css";
+import VectorSource from "ol/source/Vector";
+import Style from "ol/style/Style";
+import Stroke from "ol/style/Stroke";
+import Collection from "ol/Collection";
+import Feature from "ol/Feature";
+
+import Enums from "../../enums";
+import { getShortestLineBetweenOverlayAndFeature } from "./utils";
+import { getUnitSuffix } from "../../utils";
+import { coordinateWithOffset } from "../../scoord3dUtils";
+import defaultStyles from "../styles";
+
+class _MarkupManager {
+  constructor({ map, pyramid, formatters, onClick, onStyle } = {}) {
+    this._map = map;
+    this._pyramid = pyramid;
+    this._formatters = formatters;
+
+    this.onClick = onClick;
+    this.onStyle = onStyle;
+
+    this._markups = new Map();
+    this._listeners = new Map();
+    this._links = new Collection([], { unique: true });
+
+    const defaultColor = defaultStyles.stroke.color;
+    this._styleTag = document.createElement("style");
+    this._styleTag.innerHTML = this._getTooltipStyles(defaultColor);
+
+    this._linksVector = new VectorLayer({
+      source: new VectorSource({ features: this._links }),
+    });
+
+    this._markupsOverlay = new Overlay({ element: this._styleTag });
+    this._map.addOverlay(this._markupsOverlay);
+    this._map.addLayer(this._linksVector);
+  }
+
+  /**
+   * Set markups visibility.
+   * 
+   * @param {boolean} isVisible 
+   * @returns {void}
+   */
+  setVisible(isVisible) {
+    this._linksVector.setVisible(isVisible);
+
+    if (isVisible) {
+      this._markups.forEach((markup) => {
+        this._map.removeOverlay(markup.overlay);
+        this._map.addOverlay(markup.overlay);
+      });
+      return;
+    }
+    this._markups.forEach((markup) => {
+      this._map.addOverlay(markup.overlay);
+      this._map.removeOverlay(markup.overlay);
+    });
+  }
+
+  /**
+   * Checks whether a markup exists.
+   *
+   * @param {string} id The markup id
+   * @return {boolean}
+   */
+  has(id) {
+    return this._markups.has(id);
+  }
+
+  /**
+   * Returns a markup
+   *
+   * @param {string} id The markup id
+   * @return {object} The markup
+   */
+  get(id) {
+    return this._markups.get(id);
+  }
+
+  /**
+   * Removes a markup and its link.
+   *
+   * @param {string} id The markup id
+   * @return {string} The markup id
+   */
+  remove(id) {
+    const markup = this.get(id);
+    if (!markup) {
+      return id;
+    }
+
+    const links = this._links.getArray();
+    const link = links.find((feature) => feature.getId() === id);
+    if (link) {
+      this._links.remove(link);
+    }
+
+    this._map.removeOverlay(markup.overlay);
+    this._markups.delete(id);
+
+    if (this._listeners.get(id)) {
+      this._listeners.delete(id);
+    }
+
+    return id;
+  }
+
+  /**
+   * Creates a new markup
+   *
+   * @param {object} options The options
+   * @param {Feature} options.feature The feature to plug the measure markup
+   * @param {string} options.value The inner content of element
+   * @param {boolean} options.isLinkable Create a link between feature and markup
+   * @param {boolean} options.isDraggable Allow markup to be dragged
+   * @param {array} offset Markup offset
+   * @return {object} The markup object
+   */
+  create({ feature, value = "", isLinkable = true, isDraggable = true }) {
+    const id = feature.getId();
+    if (!id) {
+      console.warn("Failed to create markup, feature id not found");
+      return;
+    }
+
+    if (this.has(id)) {
+      console.warn("Markup for feature already exists", id);
+      return this.get(id);
+    }
+
+    const markup = { id, isLinkable, isDraggable };
+
+    const element = document.createElement("div");
+    element.id = markup.isDraggable ? Enums.InternalProperties.Markup : "";
+    element.className = "ol-tooltip ol-tooltip-measure";
+    element.innerText = value;
+
+    const spacedCoordinate = coordinateWithOffset(feature);
+
+    markup.element = element;
+    markup.overlay = new Overlay({
+      className: "markup-container",
+      positioning: "center-center",
+      stopEvent: false,
+      dragging: false,
+      position: spacedCoordinate,
+      element: markup.element,
+    });
+
+    this._map.addOverlay(markup.overlay);
+    this._markups.set(id, markup);
+
+    this._drawLink(feature);
+    this._wireInternalEvents(feature);
+
+    return markup;
+  }
+
+  /**
+   * Wire internal events to markup feature.
+   *
+   * @param {object} feature
+   * @returns {void}
+   */
+  _wireInternalEvents(feature) {
+    const id = feature.getId();
+    const markup = this.get(id);
+    const geometry = feature.getGeometry();
+    const listener = geometry.on(
+      Enums.FeatureGeometryEvents.CHANGE,
+      ({ target: geometry }) => {
+        if (this.has(id)) {
+          const view = this._map.getView();
+          const unitSuffix = getUnitSuffix(view);
+          const format = this._getFormatter(feature);
+          const output = format(feature, unitSuffix, this._pyramid);
+          this.update({
+            feature,
+            value: output,
+            coordinate: geometry.getLastCoordinate(),
+          });
+          this._drawLink(feature);
+        }
+      }
+    );
+    this._listeners.set(id, listener);
+
+    this._styleTooltip(feature);
+
+    /** Keep markup style after external style changes */
+    feature.on(
+      Enums.FeatureEvents.PROPERTY_CHANGE,
+      ({ key: property, target: feature }) => {
+        if (property === Enums.InternalProperties.StyleOptions) {
+          this._styleTooltip(feature);
+        }
+      }
+    );
+
+    /** Update markup style on feature geometry change */
+    feature.getGeometry().on(Enums.FeatureGeometryEvents.CHANGE, () => {
+      this._styleTooltip(feature);
+    });
+
+    let dragPan;
+    let dragProperty = "dragging";
+    this._map.getInteractions().forEach((interaction) => {
+      if (interaction instanceof DragPan) {
+        dragPan = interaction;
+      }
+    });
+
+    markup.element.addEventListener(Enums.HTMLElementEvents.MOUSE_DOWN, () => {
+      const markup = this.get(id);
+      if (markup) {
+        dragPan.setActive(false);
+        markup.overlay.set(dragProperty, true);
+      }
+    });
+
+    this._map.on(Enums.MapEvents.POINTER_MOVE, (event) => {
+      const markup = this.get(id);
+      if (
+        markup &&
+        markup.overlay.get(dragProperty) === true &&
+        markup.isDraggable
+      ) {
+        /** Doesn't need to have the offset */
+        markup.overlay.setPosition(event.coordinate);
+        this._drawLink(feature);
+      }
+    });
+
+    this._map.on(Enums.MapEvents.POINTER_UP, () => {
+      const markup = this.get(id);
+      if (
+        markup &&
+        markup.overlay.get(dragProperty) === true &&
+        markup.isDraggable
+      ) {
+        dragPan.setActive(true);
+        markup.overlay.set(dragProperty, false);
+      }
+    });
+  }
+
+  onDrawAbort({ feature }) {
+    this.remove(feature.getId());
+  }
+
+  /**
+   * Updates the feature's markup tooltip style.
+   *
+   * @param {object} feature
+   * @returns {void}
+   */
+  _styleTooltip(feature) {
+    const styleOptions = feature.get(Enums.InternalProperties.StyleOptions);
+    if (styleOptions && styleOptions.stroke) {
+      const { color } = styleOptions.stroke;
+      const tooltipColor = color || defaultStyles.stroke.color;
+      const links = this._links.getArray();
+      const link = links.find((link) => link.getId() === feature.getId());
+      if (link) {
+        const styles = link.getStyle();
+        const stroke = styles.getStroke();
+        stroke.setColor(tooltipColor);
+        styles.setStroke(stroke);
+        link.setStyle(styles);
+      }
+      const marker = this.get(feature.getId());
+      if (marker) {
+        marker.element.style.color = tooltipColor;
+      }
+    }
+  }
+
+  /**
+   * Returns tooltip styles.
+   *
+   * @param {string} color
+   */
+  _getTooltipStyles(color) {
+    return `
+      .ol-tooltip {
+        color: ${color};
+        white-space: nowrap;
+        font-size: 17px;
+        font-weight: bold;
+      }
+      .ol-tooltip-measure { opacity: 1; }
+      .ol-tooltip-static { color: ${color}; }
+      .ol-tooltip-measure:before,
+      .ol-tooltip-static:before {
+        content: '',
+      }
+
+      #markup { cursor: move; }
+      .markup-container { display: block !important; }
+    `;
+  }
+
+  /**
+   * Checks if feature has the correct markup.
+   *
+   * @param {Feature} feature The feature
+   */
+  _isValidFeature(feature) {
+    return Object.values(Enums.Markup).includes(
+      feature.get(Enums.InternalProperties.Markup)
+    );
+  }
+
+  /**
+   * Update markup content.
+   *
+   * @param {object} markup The markup properties
+   * @param {Feature} markup.feature The markup feature
+   * @param {string} markup.value The markup content
+   * @param {string} markup.coordinate The markup coordinate
+   */
+  update({ feature, value, coordinate }) {
+    const id = feature.getId();
+
+    if (!id) {
+      console.warn("Failed attempt to update markup, feature with empty id");
+      return;
+    }
+
+    const markup = this.get(id);
+    if (!markup) {
+      console.warn("No markup found for given feature");
+      return;
+    }
+
+    if (value) {
+      markup.element.innerText = value;
+    }
+
+    if (coordinate) {
+      markup.overlay.setPosition(coordinateWithOffset(feature));
+    }
+
+    this._markups.set(id, markup);
+  }
+
+  /**
+   * This event is responsible assign markup classes on drawEnd event
+   *
+   * @param {object} event The event
+   */
+  onDrawEnd(event) {
+    const feature = event.feature;
+    if (this._isValidFeature(feature)) {
+      const featureId = feature.getId();
+      const markup = this.get(featureId);
+      if (markup) {
+        markup.element.className = "ol-tooltip ol-tooltip-static";
+        this._markups.set(featureId, markup);
+      }
+    }
+  }
+
+  /**
+   * Returns the string format function for a given markup.
+   *
+   * @param {object} feature The feature
+   * @returns {function} format function
+   */
+  _getFormatter(feature) {
+    const markup = feature.get(Enums.InternalProperties.Markup);
+    const formatter = this._formatters[markup];
+    if (!formatter) return () => "";
+    return formatter;
+  }
+
+  /**
+   * Draws a link between the feature and the markup.
+   *
+   * @param {object} feature The feature
+   */
+  _drawLink(feature) {
+    const markup = this.get(feature.getId());
+    if (!markup || !markup.isLinkable) {
+      return;
+    }
+
+    const line = getShortestLineBetweenOverlayAndFeature(
+      feature,
+      markup.overlay
+    );
+
+    const updated = this._links.getArray().some((feature) => {
+      if (feature.getId() === markup.id) {
+        feature.setGeometry(line);
+        return true;
+      }
+    });
+
+    if (!updated) {
+      const feature = new Feature({ geometry: line, name: "Line" });
+      feature.setId(markup.id);
+      feature.setStyle(
+        new Style({
+          stroke: new Stroke({
+            color: defaultStyles.stroke.color,
+            lineDash: [0.3, 7],
+            width: 3,
+          }),
+        })
+      );
+      this._links.push(feature);
+    }
+  }
+}
+
+export default _MarkupManager;
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/docs/annotations_markups_measurement.js.html b/docs/annotations_markups_measurement.js.html new file mode 100644 index 00000000..b808e2da --- /dev/null +++ b/docs/annotations_markups_measurement.js.html @@ -0,0 +1,126 @@ + + + + + JSDoc: Source: annotations/markups/measurement.js + + + + + + + + + + +
+ +

Source: annotations/markups/measurement.js

+ + + + + + +
+
+
import Enums from "../../enums";
+import { getUnitSuffix } from "../../utils";
+import {
+  getFeatureScoord3dArea,
+  getFeatureScoord3dLength,
+} from "../../scoord3dUtils.js";
+
+/**
+ * Format measure output.
+ *
+ * @param {Feature} feature feature
+ * @param {string} units units
+ * @return {string} The formatted measure of this feature
+ */
+export const format = (feature, units, pyramid) => {
+  const length = getFeatureScoord3dLength(feature, pyramid);
+  const area = getFeatureScoord3dArea(feature, pyramid);
+  let value = length || area;
+  return length
+    ? `${value.toFixed(2)} ${units}`
+    : `${value.toFixed(2)} ${units}²`;
+};
+
+/**
+ * Checks if feature has measurement markup properties.
+ *
+ * @param {object} feature
+ * @returns {boolean} true if feature has measurement markup properties
+ */
+const _isMeasurement = (feature) =>
+  Enums.Markup.Measurement === feature.get(Enums.InternalProperties.Markup);
+
+/**
+ * Measurement markup definition.
+ *
+ * @param {object} dependencies Shared dependencies
+ * @param {object} dependencies.map Viewer's map instance
+ * @param {object} dependencies.pyramid Pyramid metadata
+ * @param {object} dependencies.markupManager MarkupManager shared instance
+ */
+const MeasurementMarkup = ({ map, pyramid, markupManager }) => {
+  return {
+    onAdd: (feature) => {
+      if (_isMeasurement(feature)) {
+        const view = map.getView();
+        const unitSuffix = getUnitSuffix(view);
+        markupManager.create({
+          feature,
+          value: format(feature, unitSuffix, pyramid),
+        });
+      }
+    },
+    onFailure: (uid) => {
+      if (uid) {
+        markupManager.remove(uid);
+      }
+    },
+    onRemove: (feature) => {
+      if (_isMeasurement(feature)) {
+        const featureId = feature.getId();
+        markupManager.remove(featureId);
+      }
+    },
+    onDrawStart: ({ feature }) => {
+      if (_isMeasurement(feature)) {
+        markupManager.create({ feature });
+      }
+    },
+    onUpdate: (feature) => {},
+    onDrawEnd: ({ feature }) => {},
+    onDrawAbort: ({ feature }) => {},
+  };
+};
+
+export default MeasurementMarkup;
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/docs/annotations_markups_textEvaluation.js.html b/docs/annotations_markups_textEvaluation.js.html new file mode 100644 index 00000000..aea0ba69 --- /dev/null +++ b/docs/annotations_markups_textEvaluation.js.html @@ -0,0 +1,184 @@ + + + + + JSDoc: Source: annotations/markups/textEvaluation.js + + + + + + + + + + +
+ +

Source: annotations/markups/textEvaluation.js

+ + + + + + +
+
+
import Fill from "ol/style/Fill";
+import Stroke from "ol/style/Stroke";
+import Style from "ol/style/Style";
+import Circle from "ol/style/Circle";
+
+import Enums from "../../enums";
+
+/**
+ * Format free text output.
+ *
+ * @param {Feature} feature feature
+ * @return {string} The formatted output
+ */
+export const format = (feature) =>
+  feature.get(Enums.InternalProperties.Label) || "";
+
+/**
+ * Builds the text evaluation style.
+ *
+ * @param {object} feature
+ * @returns {object} Style instance
+ */
+const _applyStyle = (feature) => {
+  if (_hasMarker(feature)) {
+    return;
+  }
+
+  const style = new Style({
+    image: new Circle({
+      fill: new Fill({
+        color: "rgba(255,255,255,0.0)",
+      }),
+      stroke: new Stroke({
+        color: "rgba(255,255,255,0.0)",
+        width: 0,
+      }),
+      radius: 5,
+    }),
+  });
+
+  feature.setStyle(style);
+};
+
+/**
+ * Checks if feature has text evaluation properties.
+ *
+ * @param {object} feature
+ * @returns {boolean} true if feature has text evaluation properties
+ */
+const _isTextEvaluation = (feature) =>
+  Enums.Markup.TextEvaluation === feature.get(Enums.InternalProperties.Markup);
+
+/**
+ * Checks if feature has marker properties.
+ *
+ * @param {object} feature
+ * @returns {boolean} true if feature has marker properties
+ */
+const _hasMarker = (feature) => !!feature.get(Enums.InternalProperties.Marker);
+
+/**
+ * Handler to create markups based on feature properties
+ * and apply text evaluation styles.
+ *
+ * @param {object} feature
+ * @param {object} markupManager MarkupManager instance
+ * @returns {void}
+ */
+const _onInteractionEventHandler = ({ feature, markupManager }) => {
+  const featureHasMarker = _hasMarker(feature);
+  markupManager.create({
+    feature,
+    value: format(feature),
+    isLinkable: featureHasMarker,
+    isDraggable: featureHasMarker,
+  });
+  _applyStyle(feature);
+};
+
+/**
+ * Text evaluation markup definition.
+ *
+ * @param {object} dependencies Shared dependencies
+ * @param {object} dependencies.markupManager MarkupManager shared instance
+ */
+const TextEvaluationMarkup = ({ markupManager }) => {
+  return {
+    onAdd: (feature) => {
+      if (_isTextEvaluation(feature)) {
+        _onInteractionEventHandler({ feature, markupManager });
+
+        /** Keep text style after external style changes */
+        feature.on(
+          Enums.FeatureEvents.PROPERTY_CHANGE,
+          ({ key: property, target: feature }) => {
+            if (property === Enums.InternalProperties.StyleOptions) {
+              _applyStyle(feature);
+            }
+          }
+        );
+      }
+    },
+    onFailure: (uid) => {
+      if (uid) {
+        markupManager.remove(uid);
+      }
+    },
+    onRemove: (feature) => {
+      if (_isTextEvaluation(feature)) {
+        const featureId = feature.getId();
+        markupManager.remove(featureId);
+      }
+    },
+    onUpdate: (feature) => {
+      if (_isTextEvaluation(feature)) {
+        markupManager.update({ feature, value: format(feature) });
+      }
+    },
+    onDrawStart: ({ feature }) => {
+      if (_isTextEvaluation(feature)) {
+        _onInteractionEventHandler({ feature, markupManager });
+      }
+    },
+    onDrawEnd: ({ feature }) => {
+      if (_isTextEvaluation(feature)) {
+        _onInteractionEventHandler({ feature, markupManager });
+      }
+    },
+    onDrawAbort: ({ feature }) => {},
+  };
+};
+
+export default TextEvaluationMarkup;
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/docs/annotations_markups_utils_getShortestLineBetweenOverlayAndFeature.js.html b/docs/annotations_markups_utils_getShortestLineBetweenOverlayAndFeature.js.html new file mode 100644 index 00000000..190bb7d8 --- /dev/null +++ b/docs/annotations_markups_utils_getShortestLineBetweenOverlayAndFeature.js.html @@ -0,0 +1,95 @@ + + + + + JSDoc: Source: annotations/markups/utils/getShortestLineBetweenOverlayAndFeature.js + + + + + + + + + + +
+ +

Source: annotations/markups/utils/getShortestLineBetweenOverlayAndFeature.js

+ + + + + + +
+
+
import LineString from 'ol/geom/LineString';
+import Circle from 'ol/geom/Circle';
+import { fromCircle } from "ol/geom/Polygon";
+
+/**
+ * Builds a new LineString instance with the shortest
+ * distance between a given overlay and a feature.
+ * 
+ * @param {object} feature The feature
+ * @param {object} overlay The overlay instance
+ * @returns {LineString} The smallest line between the overlay and feature
+ */
+const getShortestLineBetweenOverlayAndFeature = (feature, overlay) => {
+  let result;
+  let distanceSq = Infinity;
+
+  let featureGeometry = feature.getGeometry();
+
+  if (featureGeometry instanceof Circle) {
+    featureGeometry = fromCircle(featureGeometry).clone();
+  }
+
+  let geometry = featureGeometry.getLinearRing ? featureGeometry.getLinearRing(0) : featureGeometry;
+
+  (geometry.getCoordinates() || geometry.getExtent()).forEach(coordinates => {
+    const closest = overlay.getPosition();
+    const distanceNew = Math.pow(closest[0] - coordinates[0], 2) + Math.pow(closest[1] - coordinates[1], 2);
+    if (distanceNew < distanceSq) {
+      distanceSq = distanceNew;
+      result = [coordinates, closest];
+    }
+  });
+
+  const coordinates = overlay.getPosition();
+  const closest = geometry.getClosestPoint(coordinates);
+  const distanceNew = Math.pow(closest[0] - coordinates[0], 2) + Math.pow(closest[1] - coordinates[1], 2);
+  if (distanceNew < distanceSq) {
+    distanceSq = distanceNew;
+    result = [closest, coordinates];
+  }
+
+  return new LineString(result);
+};
+
+export default getShortestLineBetweenOverlayAndFeature;
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/docs/api.html b/docs/api.html index 9b347084..8ecea364 100644 --- a/docs/api.html +++ b/docs/api.html @@ -74,7 +74,7 @@

api

Source:
@@ -119,13 +119,13 @@

api


diff --git a/docs/dicom-microscopy-viewer.js.html b/docs/dicom-microscopy-viewer.js.html index 78866308..fc8f13ec 100644 --- a/docs/dicom-microscopy-viewer.js.html +++ b/docs/dicom-microscopy-viewer.js.html @@ -27,7 +27,11 @@

Source: dicom-microscopy-viewer.js

import EVENTS from './events.js';
-import { VLWholeSlideMicroscopyImage, formatMetadata } from './metadata.js';
+import {
+  Comprehensive3DSR,
+  VLWholeSlideMicroscopyImage,
+  formatMetadata,
+} from './metadata.js';
 import { ROI } from './roi.js';
 import {
   Point,
@@ -38,6 +42,11 @@ 

Source: dicom-microscopy-viewer.js

Ellipse, } from './scoord3d.js'; import { + applyInverseTransform, + applyTransform, + buildInverseTransform, + buildTransform, + computeRotation, mapSlideCoordToPixelCoord, mapPixelCoordToSlideCoord, } from './utils.js'; @@ -74,6 +83,7 @@

Source: dicom-microscopy-viewer.js

const metadata = { formatMetadata, VLWholeSlideMicroscopyImage, + Comprehensive3DSR, }; /** Namespace for 3-dimensional spatial coordinates (SCOORD3D). @@ -86,7 +96,7 @@

Source: dicom-microscopy-viewer.js

Polyline, Polygon, Ellipsoid, - Ellipse + Ellipse, }; /** Namespace for regions of interest (ROI). @@ -110,6 +120,11 @@

Source: dicom-microscopy-viewer.js

* @namespace utils */ const utils = { + applyInverseTransform, + applyTransform, + buildInverseTransform, + buildTransform, + computeRotation, mapSlideCoordToPixelCoord, mapPixelCoordToSlideCoord, }; @@ -125,13 +140,13 @@

Source: dicom-microscopy-viewer.js


- Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/events.html b/docs/events.html index 57eb6abc..0d776ae3 100644 --- a/docs/events.html +++ b/docs/events.html @@ -72,7 +72,7 @@

events

Source:
@@ -117,13 +117,13 @@

events


- Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/events.js.html b/docs/events.js.html index 2d2a898b..2f587d0e 100644 --- a/docs/events.js.html +++ b/docs/events.js.html @@ -58,13 +58,13 @@

Source: events.js


- Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/global.html b/docs/global.html index 050ebaa3..1df733bb 100644 --- a/docs/global.html +++ b/docs/global.html @@ -98,26 +98,78 @@

Members

-

(constant) keywordToTag :Object

+

anchor

- Maps DICOM Attribute Keyword to Tag. + Absolute-sized icon
-
Type:
-
    -
  • + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Source:
    +
    + + + + + + + +
    + + + + + + + -Object +

    anchor

    + + + + +
    + Absolute-sized icon +
    -
  • -
@@ -152,7 +204,7 @@
Type:
Source:
@@ -170,26 +222,140 @@
Type:
-

(constant) tagToKeyword :Object

+

(constant) format

- Maps DICOM Attribute Tag to Keyword. + Format arrow output.
-
Type:
-
    -
  • + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Source:
    +
    + + + + + + + +
    + + + + + + + -Object +

    (constant) format

    + + + + +
    + Format measure output. +
    + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Source:
    +
    + + + + + + + +
    + + + + + + + + +

    (constant) format

    + + + + +
    + Format free text output. +
    -
  • -
@@ -224,7 +390,7 @@
Type:
Source:
@@ -243,6 +409,5302 @@
Type:
+ +

Methods

+ + + + + + + +

_addROIPropertiesToFeature(feature, properties, opt_silent)

+ + + + + + +
+ Add ROI properties to feature in a safe way +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
feature + + +object + + + + The feature instance that represents the ROI
properties + + +object + + + + Valid ROI properties +
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
measurements + + +object + + + + ROI measurements
evaluations + + +object + + + + ROI evaluations
label + + +object + + + + ROI label
marker + + +object + + + + ROI marker (this is used while we don't have presentation states)
+ +
opt_silent + + +boolean + + + + Opt silent update
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

_applyStyle(feature) → {object}

+ + + + + + +
+ Builds the text evaluation style. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
feature + + +object + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Style instance +
+ + + +
+
+ Type +
+
+ +object + + +
+
+ + + + + + + + + + + + + +

_applyStyles(feature, map) → {object}

+ + + + + + +
+ Builds arrow styles. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
feature + + +object + + + + The feature instance
map + + +object + + + + The viewer map instance
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Style instance +
+ + + +
+
+ Type +
+
+ +object + + +
+
+ + + + + + + + + + + + + +

_getOpenLayersStyle(styleOptions) → {Style}

+ + + + + + +
+ Map style options to OpenLayers style. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
styleOptions + + +object + + + + Style options +
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
stroke + + +object + + + + Style options for the outline of the geometry +
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
color + + +Array.<number> + + + + RGBA color of the outline
width + + +number + + + + Width of the outline
+ +
fill + + +object + + + + Style options for body the geometry +
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
color + + +Array.<number> + + + + RGBA color of the body
+ +
image + + +object + + + + Style options for image
+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ OpenLayers style +
+ + + +
+
+ Type +
+
+ +Style + + +
+
+ + + + + + + + + + + + + +

_hasMarker(feature) → {boolean}

+ + + + + + +
+ Checks if feature has marker properties. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
feature + + +object + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ true if feature has marker properties +
+ + + +
+
+ Type +
+
+ +boolean + + +
+
+ + + + + + + + + + + + + +

_isMeasurement(feature) → {boolean}

+ + + + + + +
+ Checks if feature has measurement markup properties. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
feature + + +object + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ true if feature has measurement markup properties +
+ + + +
+
+ Type +
+
+ +boolean + + +
+
+ + + + + + + + + + + + + +

_isTextEvaluation(feature) → {boolean}

+ + + + + + +
+ Checks if feature has text evaluation properties. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
feature + + +object + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ true if feature has text evaluation properties +
+ + + +
+
+ Type +
+
+ +boolean + + +
+
+ + + + + + + + + + + + + +

_onInteractionEventHandler(feature, markupManager) → {void}

+ + + + + + +
+ Handler to create markups based on feature properties +and apply text evaluation styles. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
feature + + +object + + + +
markupManager + + +object + + + + MarkupManager instance
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +void + + +
+
+ + + + + + + + + + + + + +

_setFeatureStyle(styleOptions)

+ + + + + + +
+ Updates the style of a feature. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
styleOptions + + +object + + + + Style options +
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
stroke + + +object + + + + Style options for the outline of the geometry +
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
color + + +Array.<number> + + + + RGBA color of the outline
width + + +number + + + + Width of the outline
+ +
fill + + +object + + + + Style options for body the geometry +
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
color + + +Array.<number> + + + + RGBA color of the body
+ +
image + + +object + + + + Style options for image
+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

_updateFeatureEvaluations(feature) → {void}

+ + + + + + +
+ Update feature evaluations from its properties +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
feature + + +Feature + + + + The feature
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +void + + +
+
+ + + + + + + + + + + + + +

_updateFeatureMeasurements(map, feature, pyramid) → {void}

+ + + + + + +
+ Generate feature measurements from its measurement properties +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
map + + +object + + + + The map instance
feature + + +object + + + + The feature instance
pyramid + + +object + + + + The pyramid metadata
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +void + + +
+
+ + + + + + + + + + + + + +

_wireMeasurementsAndQualitativeEvaluationsEvents(map, feature, pyramid) → {void}

+ + + + + + +
+ Wire measurements and qualitative events to generate content items +based on feature properties and geometry changes +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
map + + +object + + + + The map instance
feature + + +object + + + + The feature instance
pyramid + + +object + + + + The pyramid metadata
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +void + + +
+
+ + + + + + + + + + + + + +

applyInverseTransform(options) → {Array.<number>}

+ + + + + + +
+ Applies an affine transformation to a coordinate in the slide coordinate +system to map it into the Total Pixel Matrix. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
options + + +Object + + + + Options
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ (Row, Column) position in the Total Pixel Matrix +
+ + + +
+
+ Type +
+
+ +Array.<number> + + +
+
+ + + + + + + + + + + + + +

applyTransform(options) → {Array.<number>}

+ + + + + + +
+ Applies an affine transformation to a coordinate in the Total Pixel Matrix +to map it into the slide coordinate system. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
options + + +Object + + + + Options
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ (X, Y) position in the slide coordinate system +
+ + + +
+
+ Type +
+
+ +Array.<number> + + +
+
+ + + + + + + + + + + + + +

areCodedConceptsEqual(codedConcept1, codedConcept2) → {boolean}

+ + + + + + + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
codedConcept1 + + +object + + + +
codedConcept2 + + +object + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ true if content items equal and false otherwise +
+ + + +
+
+ Type +
+
+ +boolean + + +
+
+ + + + + + + + + + + + + +

ArrowMarker(dependencies)

+ + + + + + +
+ Arrow marker definition. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
dependencies + + +object + + + + Shared dependencies +
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
map + + +object + + + + Map shared instance
markupManager + + +object + + + + Markup manager shared instance
+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

buildInverseTransform() → {Array.<Array.<number>>}

+ + + + + + +
+ Builds an affine transformation matrix to map coordinates in the slide +coordinate system into the Total Pixel Matrix. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
options.offset + + +Array.<number> + + + + X and Y offset of the image in the slide coordinate system
options.orientation + + +Array.<number> + + + + Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system
options.spacing + + +Array.<number> + + + + Spacing between pixel rows and columns of the Total Pixel Matrix
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ 3x3 affine transformation matrix +
+ + + +
+
+ Type +
+
+ +Array.<Array.<number>> + + +
+
+ + + + + + + + + + + + + +

buildTransform(options) → {Array.<Array.<number>>}

+ + + + + + +
+ Builds an affine transformation matrix to map coordinates in the Total +Pixel Matrix into the slide coordinate system. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
options + + +Object + + + + Options +
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
offset + + +Array.<number> + + + + X and Y offset of the image in the slide coordinate system
orientation + + +Array.<number> + + + + Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system
spacing + + +Array.<number> + + + + Spacing between pixel rows and columns of the Total Pixel Matrix
+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ 3x3 affine transformation matrix +
+ + + +
+
+ Type +
+
+ +Array.<Array.<number>> + + +
+
+ + + + + + + + + + + + + +

computeRotation(options) → {number}

+ + + + + + +
+ Computes the rotation of the image with respect to the frame of reference. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
options + + +Object + + + + Options +
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
orientation + + +Array.<number> + + + + Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system
inDegrees + + +boolean + + + + Whether angle should be returned in degrees instead of radians
+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ Angle +
+ + + +
+
+ Type +
+
+ +number + + +
+
+ + + + + + + + + + + + + +

createRotationMatrix(options) → {Array.<Array.<number>>}

+ + + + + + +
+ Creates a rotation matrix. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
options + + +Object + + + + Options +
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
orientation + + +Array.<number> + + + + Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system
+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ 2x2 rotation matrix +
+ + + +
+
+ Type +
+
+ +Array.<Array.<number>> + + +
+
+ + + + + + + + + + + + + +

doContentItemsMatch(contentItem1, contentItem2) → {boolean}

+ + + + + + +
+ Check wether two content items match. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
contentItem1 + + +object + + + +
contentItem2 + + +object + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ true if content items match and false otherwise +
+ + + +
+
+ Type +
+
+ +boolean + + +
+
+ + + + + + + + + + + + + +

getContentItemNameCodedConcept(contentItem) → {object}

+ + + + + + +
+ Get name coded concept from content item. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
contentItem + + +object + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ The concept name coded concept +
+ + + +
+
+ Type +
+
+ +object + + +
+
+ + + + + + + + + + + + + +

getShortestLineBetweenOverlayAndFeature(feature, overlay) → {LineString}

+ + + + + + +
+ Builds a new LineString instance with the shortest +distance between a given overlay and a feature. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
feature + + +object + + + + The feature
overlay + + +object + + + + The overlay instance
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ The smallest line between the overlay and feature +
+ + + +
+
+ Type +
+
+ +LineString + + +
+
+ + + + + + + + + + + + + +

getUnitSuffix(view) → {string}

+ + + + + + +
+ Get view unit suffix. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
view + + +object + + + + Map view
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ unit suffix +
+ + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + + +

MeasurementMarkup(dependencies)

+ + + + + + +
+ Measurement markup definition. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
dependencies + + +object + + + + Shared dependencies +
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
map + + +object + + + + Viewer's map instance
pyramid + + +object + + + + Pyramid metadata
markupManager + + +object + + + + MarkupManager shared instance
+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

TextEvaluationMarkup(dependencies)

+ + + + + + +
+ Text evaluation markup definition. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
dependencies + + +object + + + + Shared dependencies +
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
markupManager + + +object + + + + MarkupManager shared instance
+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + @@ -258,13 +5720,13 @@
Type:

- Documentation generated by JSDoc 3.6.5 on Sun Sep 06 2020 11:07:53 GMT-0400 (Eastern Daylight Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/index.html b/docs/index.html index 41104322..307e9229 100644 --- a/docs/index.html +++ b/docs/index.html @@ -166,7 +166,12 @@

Support

  • The Neuroimage Analysis Center
  • The National Center for Image Guided Therapy
  • The MGH & BWH Center for Clinical Data Science
  • -
    + +

    Troubleshooting

    +

    Overview map not responsive:

    +

    Some interactions could cause side effects on the viewer's instance of OverviewMap since it also uses the same interactions in the map. +If you enable the 'draw' interaction, the overview map will lost its ability to instantly update +the map when changed.

    @@ -177,13 +182,13 @@

    Support


    - Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
    diff --git a/docs/metadata.Comprehensive3DSR.html b/docs/metadata.Comprehensive3DSR.html new file mode 100644 index 00000000..67ec83a0 --- /dev/null +++ b/docs/metadata.Comprehensive3DSR.html @@ -0,0 +1,171 @@ + + + + + JSDoc: Class: Comprehensive3DSR + + + + + + + + + + +
    + +

    Class: Comprehensive3DSR

    + + + + + + +
    + +
    + +

    + metadata~Comprehensive3DSR()

    + +
    DICOM Comprehensive 3D SR instance.
    + + +
    + +
    +
    + + + + +

    Constructor

    + + + +

    new Comprehensive3DSR()

    + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Source:
    +
    + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + +
    + + + +
    + +
    + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time) +
    + + + + + \ No newline at end of file diff --git a/docs/metadata.VLWholeSlideMicroscopyImage.html b/docs/metadata.VLWholeSlideMicroscopyImage.html index 45898ca2..c2172d55 100644 --- a/docs/metadata.VLWholeSlideMicroscopyImage.html +++ b/docs/metadata.VLWholeSlideMicroscopyImage.html @@ -31,7 +31,8 @@

    Class: VLWholeSlideMicroscopyImage

    metadata~VLWholeSlideMicroscopyImage()

    -
    DICOM VL Whole Slide Microscopy Image instance.
    +
    DICOM VL Whole Slide Microscopy Image instance +(without Pixel Data or any other bulk data).
    @@ -94,7 +95,7 @@

    <
    Source:
    @@ -156,13 +157,13 @@

    <
    - Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
    diff --git a/docs/metadata.html b/docs/metadata.html index ad026d71..bd5fc8e6 100644 --- a/docs/metadata.html +++ b/docs/metadata.html @@ -72,7 +72,7 @@

    metadata

    Source:
    @@ -96,6 +96,9 @@

    metadata

    Classes

    +
    Comprehensive3DSR
    +
    +
    VLWholeSlideMicroscopyImage
    @@ -290,13 +293,13 @@
    Returns:

    - Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
    diff --git a/docs/metadata.js.html b/docs/metadata.js.html index 48e2b3f2..6ac27d7d 100644 --- a/docs/metadata.js.html +++ b/docs/metadata.js.html @@ -134,7 +134,10 @@

    Source: metadata.js

    // The top level (lowest resolution) image may be a single frame image in // which case the "NumberOfFrames" attribute is optional. We include it for // consistency. - if (!('NumberOfFrames' in dataset)) { + if (dataset === undefined) { + throw new Error('Could not format metadata: ', metadata) + } + if (!('NumberOfFrames' in dataset) && (dataset.Modality === 'SM')) { dataset.NumberOfFrames = 1; } @@ -142,7 +145,8 @@

    Source: metadata.js

    } -/** DICOM VL Whole Slide Microscopy Image instance. +/** DICOM VL Whole Slide Microscopy Image instance + * (without Pixel Data or any other bulk data). * * @class * @memberof metadata @@ -154,20 +158,48 @@

    Source: metadata.js

    * @params {Object} options.metadata - Metadata in DICOM JSON format */ constructor(options) { - const sopClassUID = options.metadata['00080016']['Value'][0]; - if (sopClassUID !== '1.2.840.10008.5.1.4.1.1.77.1.6') { + const dataset = formatMetadata(options.metadata); + if (dataset.SOPClassUID !== '1.2.840.10008.5.1.4.1.1.77.1.6') { throw new Error( 'Cannot construct VL Whole Slide Microscopy Image instance ' + - `given dataset with SOP Class UID "${sopClassUID}"` + `given dataset with SOP Class UID "${dataset.SOPClassUID}"` ); } + Object.assign(this, dataset); + } +} + +/** DICOM Comprehensive 3D SR instance. + * + * @class + * @memberof metadata + */ +class Comprehensive3DSR { + + /** + * @params {Object} options + * @params {Object} options.metadata - Metadata in DICOM JSON format + */ + constructor(options) { const dataset = formatMetadata(options.metadata); + if (dataset.SOPClassUID !== '1.2.840.10008.5.1.4.1.1.88.34') { + throw new Error( + 'Cannot construct Comprehensive 3D SR instance ' + + `given dataset with SOP Class UID "${dataset.SOPClassUID}"` + ); + } + Object.assign(this, dataset); } } -export { VLWholeSlideMicroscopyImage, formatMetadata, getFrameMapping }; +export { + Comprehensive3DSR, + formatMetadata, + getFrameMapping, + VLWholeSlideMicroscopyImage, +};
    @@ -178,13 +210,13 @@

    Source: metadata.js


    - Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
    diff --git a/docs/roi.ROI.html b/docs/roi.ROI.html index f8d4015c..beee1fad 100644 --- a/docs/roi.ROI.html +++ b/docs/roi.ROI.html @@ -94,7 +94,7 @@

    new ROISource:
    @@ -188,7 +188,7 @@

    evaluation
    Source:
    @@ -250,7 +250,7 @@

    measureme
    Source:
    @@ -312,7 +312,7 @@

    properties<
    Source:
    @@ -374,7 +374,7 @@

    scoord3dSource:
    @@ -436,7 +436,7 @@

    uidSource:
    @@ -516,7 +516,7 @@

    addEvalu
    Source:
    @@ -604,7 +604,7 @@

    addMeas
    Source:
    @@ -650,13 +650,13 @@

    addMeas
    - Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
    diff --git a/docs/roi.html b/docs/roi.html index 2821af2e..040267db 100644 --- a/docs/roi.html +++ b/docs/roi.html @@ -72,7 +72,7 @@

    roi

    Source:
    @@ -124,13 +124,13 @@

    Classes


    - Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
    diff --git a/docs/roi.js.html b/docs/roi.js.html index d4bac934..0a75fc38 100644 --- a/docs/roi.js.html +++ b/docs/roi.js.html @@ -26,15 +26,15 @@

    Source: roi.js

    -
    import { generateUID } from './utils.js';
    +            
    import { generateUID } from "./utils.js";
    +import Enums from "./enums";
     
    -const _uid = Symbol('uid');
    -const _scoord3d = Symbol('scoord3d');
    -const _properties = Symbol('properties');
    -
    -const _name = Symbol('name');
    -const _value = Symbol('value');
    +const _uid = Symbol("uid");
    +const _scoord3d = Symbol("scoord3d");
    +const _properties = Symbol("properties");
     
    +const _name = Symbol("name");
    +const _value = Symbol("value");
     
     /** A region of interest (ROI)
      *
    @@ -42,7 +42,6 @@ 

    Source: roi.js

    * @memberof roi */ class ROI { - /* Creates a new ROI object. * * @param {Object} options - Options for construction of ROI @@ -51,38 +50,37 @@

    Source: roi.js

    * @param {Object} options.properties - Properties (name-value pairs) */ constructor(options) { - if (!('scoord3d' in options)) { - throw new Error('spatial coordinates are required for ROI') + if (!("scoord3d" in options)) { + throw new Error("spatial coordinates are required for ROI"); } - if (!(typeof(options.scoord3d) === 'object' || options.scoord3d !== null)) { - throw new Error('scoord3d of ROI must be a Scoord3D object') + if (!(typeof options.scoord3d === "object" || options.scoord3d !== null)) { + throw new Error("scoord3d of ROI must be a Scoord3D object"); } - if (!('uid' in options)) { + if (!("uid" in options)) { this[_uid] = generateUID(); } else { - if (!(typeof(options.uid) === 'string' || options.uid instanceof String)) { - throw new Error('uid of ROI must be a string') + if (!(typeof options.uid === "string" || options.uid instanceof String)) { + throw new Error("uid of ROI must be a string"); } this[_uid] = options.uid; } this[_scoord3d] = options.scoord3d; - if ('properties' in options) { - if (!(typeof(options.properties) === 'object')) { - throw new Error('properties of ROI must be an object') + if ("properties" in options) { + if (!(typeof options.properties === "object")) { + throw new Error("properties of ROI must be an object"); } this[_properties] = options.properties; if (this[_properties].evaluations === undefined) { - this[_properties]['evaluations'] = [] + this[_properties][Enums.InternalProperties.Evaluations] = []; } if (this[_properties].measurements === undefined) { - this[_properties]['measurements'] = [] + this[_properties][Enums.InternalProperties.Measurements] = []; } } else { this[_properties] = {}; - this[_properties]['evaluations'] = [] - this[_properties]['measurements'] = [] + this[_properties][Enums.InternalProperties.Evaluations] = []; + this[_properties][Enums.InternalProperties.Measurements] = []; } - console.log(this[_properties]) } /** Gets unique identifier of region of interest. @@ -130,7 +128,7 @@

    Source: roi.js

    * @params {Object} item - NUM content item representing a measurement */ addMeasurement(item) { - this[_properties]['measurements'].push(item); + this[_properties][Enums.InternalProperties.Measurements].push(item); } /** Adds a qualitative evaluation. @@ -138,9 +136,8 @@

    Source: roi.js

    * @params {Object} item - CODE content item representing a qualitative evaluation */ addEvaluation(item) { - this[_properties]['evaluations'].push(item); + this[_properties][Enums.InternalProperties.Evaluations].push(item); } - } export { ROI }; @@ -154,13 +151,13 @@

    Source: roi.js


    - Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
    diff --git a/docs/scoord3d.Ellipse.html b/docs/scoord3d.Ellipse.html index 92e3c21f..67dce5a2 100644 --- a/docs/scoord3d.Ellipse.html +++ b/docs/scoord3d.Ellipse.html @@ -658,13 +658,13 @@
    Type:

    - Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
    diff --git a/docs/scoord3d.Ellipsoid.html b/docs/scoord3d.Ellipsoid.html index 3d151957..3ba6a784 100644 --- a/docs/scoord3d.Ellipsoid.html +++ b/docs/scoord3d.Ellipsoid.html @@ -659,13 +659,13 @@
    Type:

    - Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
    diff --git a/docs/scoord3d.Multipoint.html b/docs/scoord3d.Multipoint.html index 77cf2168..e5d87f2a 100644 --- a/docs/scoord3d.Multipoint.html +++ b/docs/scoord3d.Multipoint.html @@ -656,13 +656,13 @@
    Type:

    - Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
    diff --git a/docs/scoord3d.Point.html b/docs/scoord3d.Point.html index 310f0e44..f6390fe1 100644 --- a/docs/scoord3d.Point.html +++ b/docs/scoord3d.Point.html @@ -655,13 +655,13 @@
    Type:

    - Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
    diff --git a/docs/scoord3d.Polygon.html b/docs/scoord3d.Polygon.html index 3d8d5ae2..4748bb24 100644 --- a/docs/scoord3d.Polygon.html +++ b/docs/scoord3d.Polygon.html @@ -657,13 +657,13 @@
    Type:

    - Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
    diff --git a/docs/scoord3d.Polyline.html b/docs/scoord3d.Polyline.html index e24d3740..6f69d2f7 100644 --- a/docs/scoord3d.Polyline.html +++ b/docs/scoord3d.Polyline.html @@ -656,13 +656,13 @@
    Type:

    - Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
    diff --git a/docs/scoord3d.html b/docs/scoord3d.html index d0cae1c4..86b99eec 100644 --- a/docs/scoord3d.html +++ b/docs/scoord3d.html @@ -72,7 +72,7 @@

    scoord3d

    Source:
    @@ -139,13 +139,13 @@

    Classes


    - Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
    diff --git a/docs/scoord3d.js.html b/docs/scoord3d.js.html index 045339b4..ec083d57 100644 --- a/docs/scoord3d.js.html +++ b/docs/scoord3d.js.html @@ -391,13 +391,13 @@

    Source: scoord3d.js


    - Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
    diff --git a/docs/utils.html b/docs/utils.html index 29f57b04..57ca069e 100644 --- a/docs/utils.html +++ b/docs/utils.html @@ -72,7 +72,7 @@

    utils

    Source:
    @@ -111,7 +111,7 @@

    Methods

    -

    (static) mapPixelCoordToSlideCoord(options) → {Array}

    +

    (static) mapPixelCoordToSlideCoord(options) → {Array.<number>}

    @@ -204,7 +204,7 @@
    Properties
    -Array +Array.<number> @@ -227,7 +227,7 @@
    Properties
    -Array +Array.<number> @@ -250,7 +250,7 @@
    Properties
    -Array +Array.<number> @@ -267,13 +267,13 @@
    Properties
    - piont + point -Array +Array.<number> @@ -331,7 +331,7 @@
    Properties
    Source:
    @@ -371,7 +371,7 @@
    Returns:
    -Array +Array.<number>
    @@ -389,7 +389,7 @@
    Returns:
    -

    (static) mapSlideCoordToPixelCoord(options) → {Array}

    +

    (static) mapSlideCoordToPixelCoord(options) → {Array.<number>}

    @@ -482,7 +482,7 @@
    Properties
    -Array +Array.<number> @@ -505,7 +505,7 @@
    Properties
    -Array +Array.<number> @@ -528,7 +528,7 @@
    Properties
    -Array +Array.<number> @@ -545,13 +545,13 @@
    Properties
    - piont + point -Array +Array.<number> @@ -609,7 +609,7 @@
    Properties
    Source:
    @@ -649,7 +649,7 @@
    Returns:
    -Array +Array.<number>
    @@ -677,13 +677,13 @@
    Returns:

    - Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
    diff --git a/docs/utils.js.html b/docs/utils.js.html index d29ee546..0d97c739 100644 --- a/docs/utils.js.html +++ b/docs/utils.js.html @@ -26,10 +26,11 @@

    Source: utils.js

    -
    import { cross, inv, multiply } from 'mathjs'
    +            
    import { inv, multiply } from "mathjs";
    +import { getPointResolution } from "ol/proj";
     
    -
    -/** Generates a UUID-derived DICOM UID with root `2.25`.
    +/** 
    + * Generates a UUID-derived DICOM UID with root `2.25`.
      *
      * @returns {string} Unique identifier
      * @private
    @@ -47,175 +48,438 @@ 

    Source: utils.js

    * of the sixteen octets (octet 0). */ // FIXME: This is not a valid UUID! - let uid = '2.25.' + Math.floor(1 + Math.random() * 9); + let uid = "2.25." + Math.floor(1 + Math.random() * 9); while (uid.length < 44) { uid += Math.floor(1 + Math.random() * 10); } return uid; } +/** + * Creates a rotation matrix. + * + * @param {Object} options - Options + * @param {number[]} options.orientation - Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system + * @returns {number[][]} 2x2 rotation matrix + */ +function createRotationMatrix(options) { + if (!("orientation" in options)) { + throw new Error('Option "orientation" is required.'); + } + const orientation = options.orientation; + const row_direction = orientation.slice(0, 3); + const column_direction = orientation.slice(3, 6); + return [ + [row_direction[0], column_direction[0]], + [row_direction[1], column_direction[1]], + [row_direction[2], column_direction[3]], + ]; +} -/** Maps 2D (Column, Row) image coordinate in the Total Pixel Matrix - * to 3D (X, Y, Z) slide coordinates in the Frame of Reference. +/** + * Computes the rotation of the image with respect to the frame of reference. * * @param {Object} options - Options - * @param {Array} options.offset - X and Y offset in the slide coordinate system - * @param {Array} options.orientation - Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system - * @param {Array} options.spacing - Spacing between pixels along the Column and Row direction of the Total Pixel Matrix - * @param {Array} options.piont - Column and Row position of the point in the Total Pixel Matrix - * @returns {Array} X, Y and Z position of the point in the slide coordinate system - * @memberof utils + * @param {number[]} options.orientation - Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system + * @param {boolean} options.inDegrees - Whether angle should be returned in degrees instead of radians + * @returns {number} Angle */ -function mapPixelCoordToSlideCoord(options) { - // X and Y Offset in Slide Coordinate System - if (!('offset' in options) ) { - throw new Error('Option "offset" is required.'); - } - if (!(Array.isArray(options.offset))) { - throw new Error('Option "offset" must be an array.'); - } - if (options.offset.length !== 2) { - throw new Error('Option "offset" must be an array with 2 elements.'); - } - const offset = options.offset; +function computeRotation(options) { + const rot = createRotationMatrix({ orientation: options.orientation }); + const angle = Math.atan2(-rot[0][1], rot[0][0]); + var inDegrees = false; + if ("inDegrees" in options) { + inDegrees = true; + } + if (inDegrees) { + return angle / (Math.PI / 180); + } else { + return angle; + } +} - // Image Orientation Slide with direction cosines for Row and Column direction - if (!('orientation' in options) ) { - throw new Error('Option "orientation" is required.'); - } - if (!(Array.isArray(options.orientation))) { - throw new Error('Option "orientation" must be an array.'); - } - if (options.orientation.length !== 6) { - throw new Error('Option "orientation" must be an array with 6 elements.'); - } - const orientation = options.orientation; +/** + * Builds an affine transformation matrix to map coordinates in the Total + * Pixel Matrix into the slide coordinate system. + * + * @param {Object} options - Options + * @param {number[]} options.offset - X and Y offset of the image in the slide coordinate system + * @param {number[]} options.orientation - Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system + * @param {number[]} options.spacing - Spacing between pixel rows and columns of the Total Pixel Matrix + * @returns {number[][]} 3x3 affine transformation matrix + */ +function buildTransform(options) { + // X and Y Offset in Slide Coordinate System + if (!("offset" in options)) { + throw new Error('Option "offset" is required.'); + } + if (!Array.isArray(options.offset)) { + throw new Error('Option "offset" must be an array.'); + } + if (options.offset.length !== 2) { + throw new Error('Option "offset" must be an array with 2 elements.'); + } - // Pixel Spacing along the Row and Column direction - if (!('spacing' in options) ) { - throw new Error('Option "spacing" is required.'); - } - if (!(Array.isArray(options.spacing))) { - throw new Error('Option "spacing" must be an array.'); - } - if (options.spacing.length !== 2) { - throw new Error('Option "spacing" must be an array with 2 elements.'); - } - const spacing = options.spacing; + // Image Orientation Slide with direction cosines for Row and Column direction + if (!("orientation" in options)) { + throw new Error('Option "orientation" is required.'); + } + if (!Array.isArray(options.orientation)) { + throw new Error('Option "orientation" must be an array.'); + } + if (options.orientation.length !== 6) { + throw new Error('Option "orientation" must be an array with 6 elements.'); + } - // Row and Column position in the Total Pixel Matrix - if (!('point' in options) ) { - throw new Error('Option "point" is required.'); - } - if (!(Array.isArray(options.point))) { - throw new Error('Option "point" must be an array.'); - } - if (options.point.length !== 2) { - throw new Error('Option "point" must be an array with 2 elements.'); - } - const point = options.point; + // Pixel Spacing along the Row and Column direction + if (!("spacing" in options)) { + throw new Error('Option "spacing" is required.'); + } + if (!Array.isArray(options.spacing)) { + throw new Error('Option "spacing" must be an array.'); + } + if (options.spacing.length !== 2) { + throw new Error('Option "spacing" must be an array with 2 elements.'); + } + + const orientation = options.orientation; + const offset = options.offset; + const spacing = options.spacing; + return [ + [orientation[0] * spacing[1], orientation[3] * spacing[0], offset[0]], + [orientation[1] * spacing[1], orientation[4] * spacing[0], offset[1]], + [0, 0, 1], + ]; +} + +/** + * Applies an affine transformation to a coordinate in the Total Pixel Matrix + * to map it into the slide coordinate system. + * + * @param {Object} options - Options + * @params {number[]} options.coordinate - (Row, Column) position in the Total Pixel Matrix + * @params {number[][]} options.affine - 3x3 affine transformation matrix + * @returns {number[]} (X, Y) position in the slide coordinate system + */ +function applyTransform(options) { + if (!("coordinate" in options)) { + throw new Error('Option "coordinate" is required.'); + } + if (!Array.isArray(options.coordinate)) { + throw new Error('Option "coordinate" must be an array.'); + } + if (options.coordinate.length !== 2) { + throw new Error('Option "coordinate" must be an array with 2 elements.'); + } + + if (!("affine" in options)) { + throw new Error('Option "affine" is required.'); + } + if (!Array.isArray(options.affine)) { + throw new Error('Option "affine" must be an array.'); + } + if (options.affine.length !== 3) { + throw new Error('Option "affine" must be a 3x3 array.'); + } + if (!Array.isArray(options.affine[0])) { + throw new Error('Option "affine" must be a 3x3 array.'); + } + if (options.affine[0].length !== 3 || options.affine[1].length !== 3) { + throw new Error('Option "affine" must be a 3x3 array.'); + } + + const coordinate = options.coordinate; + const affine = options.affine; + const imageCoordinate = [[coordinate[0]], [coordinate[1]], [1]]; + + const slideCoordinate = multiply(affine, imageCoordinate); + + const x = Number(slideCoordinate[0][0].toFixed(4)); + const y = Number(slideCoordinate[1][0].toFixed(4)); + return [x, y]; +} + +/** + * Builds an affine transformation matrix to map coordinates in the slide + * coordinate system into the Total Pixel Matrix. + * + * @param {number[]} options.offset - X and Y offset of the image in the slide coordinate system + * @param {number[]} options.orientation - Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system + * @param {number[]} options.spacing - Spacing between pixel rows and columns of the Total Pixel Matrix + * @returns {number[][]} 3x3 affine transformation matrix + */ +function buildInverseTransform(options) { + // X and Y Offset in Slide Coordinate System + if (!("offset" in options)) { + throw new Error('Option "offset" is required.'); + } + if (!Array.isArray(options.offset)) { + throw new Error('Option "offset" must be an array.'); + } + if (options.offset.length !== 2) { + throw new Error('Option "offset" must be an array with 2 elements.'); + } - const m = [ - [orientation[0] * spacing[1], orientation[3] * spacing[0], offset[0]], - [orientation[1] * spacing[1], orientation[4] * spacing[0], offset[1]], - [0, 0, 1] - ]; + // Image Orientation Slide with direction cosines for Row and Column direction + if (!("orientation" in options)) { + throw new Error('Option "orientation" is required.'); + } + if (!Array.isArray(options.orientation)) { + throw new Error('Option "orientation" must be an array.'); + } + if (options.orientation.length !== 6) { + throw new Error('Option "orientation" must be an array with 6 elements.'); + } + + // Pixel Spacing along the Row and Column direction + if (!("spacing" in options)) { + throw new Error('Option "spacing" is required.'); + } + if (!Array.isArray(options.spacing)) { + throw new Error('Option "spacing" must be an array.'); + } + if (options.spacing.length !== 2) { + throw new Error('Option "spacing" must be an array with 2 elements.'); + } - const vImage = [ - [point[0]], - [point[1]], - [1] - ]; + const orientation = options.orientation; + const offset = options.offset; + const spacing = options.spacing; + const m = [ + [orientation[0] * spacing[1], orientation[3] * spacing[0], offset[0]], + [orientation[1] * spacing[1], orientation[4] * spacing[0], offset[1]], + [0, 0, 1], + ]; + return inv(m); +} - const vSlide = multiply(m, vImage); +/** + * Applies an affine transformation to a coordinate in the slide coordinate + * system to map it into the Total Pixel Matrix. + * + * @param {Object} options - Options + * @params {number[]} options.coordinate - (X, Y) position in the slide coordinate system + * @params {number[][]} options.affine - 3x3 affine transformation matrix + * @returns {number[]} (Row, Column) position in the Total Pixel Matrix + */ +function applyInverseTransform(options) { + if (!("coordinate" in options)) { + throw new Error('Option "coordinate" is required.'); + } + if (!Array.isArray(options.coordinate)) { + throw new Error('Option "coordinate" must be an array.'); + } + if (options.coordinate.length !== 2) { + throw new Error('Option "coordinate" must be an array with 2 elements.'); + } - const x = Number(vSlide[0][0].toFixed(4)); - const y = Number(vSlide[1][0].toFixed(4)); - return [x, y]; + if (!("affine" in options)) { + throw new Error('Option "affine" is required.'); + } + if (!Array.isArray(options.affine)) { + throw new Error('Option "affine" must be an array.'); + } + if (options.affine.length !== 3) { + throw new Error('Option "affine" must be a 3x3 array.'); + } + if (!Array.isArray(options.affine[0])) { + throw new Error('Option "affine" must be a 3x3 array.'); + } + if (options.affine[0].length !== 3 || options.affine[1].length !== 3) { + throw new Error('Option "affine" must be a 3x3 array.'); + } + + const coordinate = options.coordinate; + const affine = options.affine; + + const slideCoordinate = [[coordinate[0]], [coordinate[1]], [1]]; + + const pixelCoordinate = multiply(affine, slideCoordinate); + + const row = Number(pixelCoordinate[1][0].toFixed(4)); + const col = Number(pixelCoordinate[0][0].toFixed(4)); + return [col, row]; } +/** + * Maps 2D (Column, Row) image coordinate in the Total Pixel Matrix + * to 3D (X, Y, Z) slide coordinates in the Frame of Reference. + * + * @param {Object} options - Options + * @param {number[]} options.offset - X and Y offset in the slide coordinate system + * @param {number[]} options.orientation - Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system + * @param {number[]} options.spacing - Spacing between pixels along the Column and Row direction of the Total Pixel Matrix + * @param {number[]} options.point - Column and Row position of the point in the Total Pixel Matrix + * @returns {number[]} X, Y and Z position of the point in the slide coordinate system + * @memberof utils + */ +function mapPixelCoordToSlideCoord(options) { + if (!("point" in options)) { + throw new Error('Option "point" is required.'); + } + if (!Array.isArray(options.point)) { + throw new Error('Option "point" must be an array.'); + } + if (options.point.length !== 2) { + throw new Error('Option "point" must be an array with 2 elements.'); + } + const point = options.point; + + const affine = buildTransform({ + orientation: options.orientation, + offset: options.offset, + spacing: options.spacing, + }); + return applyTransform({ coordinate: point, affine: affine }); +} -/** Maps 3D (X, Y, Z) slide coordinate in to the Frame of Reference to +/** + * Maps 3D (X, Y, Z) slide coordinate in to the Frame of Reference to * 2D (Column, Row) image coordinate in the Total Pixel Matrix. * * @param {Object} options - Options - * @param {Array} options.offset - X and Y offset in the slide coordinate system - * @param {Array} options.orientation - Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system - * @param {Array} options.spacing - Spacing between pixels along the Column and Row direction of the Total Pixel Matrix - * @param {Array} options.piont - X, Y and Z position of the point in the slide coordinate system - * @returns {Array} Column and Row position of the point in the Total Pixel Matrix + * @param {number[]} options.offset - X and Y offset in the slide coordinate system + * @param {number[]} options.orientation - Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system + * @param {number[]} options.spacing - Spacing between pixels along the Column and Row direction of the Total Pixel Matrix + * @param {number[]} options.point - X, Y and Z position of the point in the slide coordinate system + * @returns {number[]} Column and Row position of the point in the Total Pixel Matrix * @memberof utils */ function mapSlideCoordToPixelCoord(options) { - // X and Y Offset in Slide Coordinate System - if (!('offset' in options) ) { - throw new Error('Option "offset" is required.'); - } - if (!(Array.isArray(options.offset))) { - throw new Error('Option "offset" must be an array.'); - } - if (options.offset.length !== 2) { - throw new Error('Option "offset" must be an array with 2 elements.'); - } - const offset = options.offset; + if (!("point" in options)) { + throw new Error('Option "point" is required.'); + } + if (!Array.isArray(options.point)) { + throw new Error('Option "point" must be an array.'); + } + if (options.point.length !== 2) { + throw new Error('Option "point" must be an array with 2 elements.'); + } + const point = options.point; + const affine = buildInverseTransform({ + orientation: options.orientation, + offset: options.offset, + spacing: options.spacing, + }); + + return applyInverseTransform({ coordinate: point, affine: affine }); +} - // Image Orientation Slide with direction cosines for Row and Column direction - if (!('orientation' in options) ) { - throw new Error('Option "orientation" is required.'); - } - if (!(Array.isArray(options.orientation))) { - throw new Error('Option "orientation" must be an array.'); - } - if (options.orientation.length !== 6) { - throw new Error('Option "orientation" must be an array with 6 elements.'); - } - const orientation = options.orientation; +/** + * Get view unit suffix. + * + * @param {object} view Map view + * @returns {string} unit suffix + */ +function getUnitSuffix(view) { + const UnitsEnum = { METERS: "m" }; + const DEFAULT_DPI = 25.4 / 0.28; + + const center = view.getCenter(); + const projection = view.getProjection(); + const resolution = view.getResolution(); + + const pointResolutionUnits = UnitsEnum.METERS; + + let pointResolution = getPointResolution( + projection, + resolution, + center, + pointResolutionUnits + ); + + const DEFAULT_MIN_WIDTH = 65; + const minWidth = (DEFAULT_MIN_WIDTH * DEFAULT_DPI) / DEFAULT_DPI; + + let nominalCount = minWidth * pointResolution; + let suffix = ""; + + if (nominalCount < 0.001) { + suffix = "μm"; + pointResolution *= 1000000; + } else if (nominalCount < 1) { + suffix = "mm"; + pointResolution *= 1000; + } else if (nominalCount < 1000) { + suffix = "m"; + } else { + suffix = "km"; + pointResolution /= 1000; + } - // Pixel Spacing along the Row and Column direction - if (!('spacing' in options) ) { - throw new Error('Option "spacing" is required.'); - } - if (!(Array.isArray(options.spacing))) { - throw new Error('Option "spacing" must be an array.'); - } - if (options.spacing.length !== 2) { - throw new Error('Option "spacing" must be an array with 2 elements.'); - } - const spacing = options.spacing; + return suffix; +} - // X and Y coordinate in the Slide Coordinate System - if (!('point' in options) ) { - throw new Error('Option "point" is required.'); - } - if (!(Array.isArray(options.point))) { - throw new Error('Option "point" must be an array.'); - } - if (options.point.length !== 2) { - throw new Error('Option "point" must be an array with 2 elements.'); +/** + * Get name coded concept from content item. + * + * @param {object} contentItem + * @returns {object} The concept name coded concept + */ +const getContentItemNameCodedConcept = (contentItem) => + contentItem.ConceptNameCodeSequence[0]; + +/** + * + * @param {object} codedConcept1 + * @param {object} codedConcept2 + * @returns {boolean} true if content items equal and false otherwise + */ +const areCodedConceptsEqual = (codedConcept1, codedConcept2) => { + if ( + codedConcept2.CodeValue === codedConcept1.CodeValue && + codedConcept2.CodingSchemeDesignator === + codedConcept1.CodingSchemeDesignator + ) { + if ( + codedConcept2.CodingSchemeVersion && + codedConcept1.CodingSchemeVersion + ) { + return ( + codedConcept2.CodingSchemeVersion === codedConcept1.CodingSchemeVersion + ); } - const point = options.point; - - const m = [ - [orientation[0] * spacing[1], orientation[3] * spacing[0], offset[0]], - [orientation[1] * spacing[1], orientation[4] * spacing[0], offset[1]], - [0, 0, 1] - ]; - const mInverted = inv(m); - - const vSlide = [ - [point[0]], - [point[1]], - [1] - ]; - - const vImage = multiply(mInverted, vSlide); - - const row = Number(vImage[1][0].toFixed(4)); - const col = Number(vImage[0][0].toFixed(4)); - return [col, row]; -} + return true; + } + return false; +}; -export { generateUID, mapPixelCoordToSlideCoord, mapSlideCoordToPixelCoord }; +/** + * Check wether two content items match. + * + * @param {object} contentItem1 + * @param {object} contentItem2 + * @returns {boolean} true if content items match and false otherwise + */ +const doContentItemsMatch = (contentItem1, contentItem2) => { + const contentItem1NameCodedConcept = getContentItemNameCodedConcept( + contentItem1 + ); + const contentItem2NameCodedConcept = getContentItemNameCodedConcept( + contentItem2 + ); + return contentItem1NameCodedConcept.equals + ? contentItem1NameCodedConcept.equals(contentItem2NameCodedConcept) + : areCodedConceptsEqual( + contentItem1NameCodedConcept, + contentItem2NameCodedConcept + ); +}; + +export { + getUnitSuffix, + applyInverseTransform, + applyTransform, + buildInverseTransform, + buildTransform, + computeRotation, + generateUID, + mapPixelCoordToSlideCoord, + mapSlideCoordToPixelCoord, + doContentItemsMatch, + areCodedConceptsEqual, + getContentItemNameCodedConcept, +};
    @@ -226,13 +490,13 @@

    Source: utils.js


    - Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
    diff --git a/docs/viewer.LabelImageViewer.html b/docs/viewer.LabelImageViewer.html index c598244f..ba67c780 100644 --- a/docs/viewer.LabelImageViewer.html +++ b/docs/viewer.LabelImageViewer.html @@ -365,7 +365,7 @@
    Properties
    Source:
    @@ -427,13 +427,13 @@
    Properties

    - Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
    diff --git a/docs/viewer.OverviewImageViewer.html b/docs/viewer.OverviewImageViewer.html index 73a7388d..00df16b7 100644 --- a/docs/viewer.OverviewImageViewer.html +++ b/docs/viewer.OverviewImageViewer.html @@ -365,7 +365,7 @@
    Properties
    Source:
    @@ -427,13 +427,13 @@
    Properties

    - Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
    diff --git a/docs/viewer.VolumeImageViewer.html b/docs/viewer.VolumeImageViewer.html index 2500709e..36312f24 100644 --- a/docs/viewer.VolumeImageViewer.html +++ b/docs/viewer.VolumeImageViewer.html @@ -205,6 +205,41 @@
    Properties
    + + + styleOptions + + + + + +object + + + + + + + + + + + + + + + + + + + + + + Default style options for annotations + + + + controls @@ -404,7 +439,7 @@
    Properties
    Source:
    @@ -498,7 +533,7 @@

    [undef
    Source:
    @@ -570,7 +605,7 @@

    Type:
    Source:
    @@ -642,7 +677,7 @@
    Type:
    Source:
    @@ -660,13 +695,13 @@
    Type:
    -

    isDrawInteractionActive :boolean

    +

    isDragPanInteractionActive :boolean

    - Whether draw interaction is active + Whether dragPan interaction is active.
    @@ -714,7 +749,7 @@
    Type:
    Source:
    @@ -732,13 +767,13 @@
    Type:
    -

    isModifyInteractionActive :boolean

    +

    isDragZoomInteractionActive :boolean

    - Whether modify interaction is active. + Whether dragZoom interaction is active.
    @@ -786,7 +821,7 @@
    Type:
    Source:
    @@ -804,13 +839,13 @@
    Type:
    -

    isSelectInteractionActive :boolean

    +

    isDrawInteractionActive :boolean

    - Whether select interaction is active. + Whether draw interaction is active
    @@ -858,7 +893,7 @@
    Type:
    Source:
    @@ -876,13 +911,13 @@
    Type:
    -

    numberOfROIs :number

    +

    isModifyInteractionActive :boolean

    - Number of annotated regions of interest. + Whether modify interaction is active.
    @@ -891,7 +926,7 @@
    Type:
    • -number +boolean
    • @@ -930,7 +965,7 @@
      Type:
      Source:
      @@ -948,13 +983,13 @@
      Type:
      -

      size :Array.<number>

      +

      isSelectInteractionActive :boolean

      - Gets the size of the viewport. + Whether select interaction is active.
      @@ -963,7 +998,7 @@
      Type:
      • -Array.<number> +boolean
      • @@ -1002,7 +1037,7 @@
        Type:
        Source:
        @@ -1019,133 +1054,99 @@
        Type:
        - - - -

        Methods

        - - +

        isTranslateInteractionActive :boolean

        - - - -

        activateDrawInteraction(options)

        - - -
        - Activates the draw interaction for graphic annotation of regions of interest. + Whether translate interaction is active.
        +
        Type:
        +
          +
        • + +boolean + + +
        • +
        +
        -
        Parameters:
        - - - - - - + - + - + - + - - - + - - - - - + - + - + - + +
        Source:
        +
        + - - +
        Type:
        +
          +
        • + +number - -
        -
        NameTypeDescription
        options - - -object + + - - Drawing options. -
        Properties
        - + - - - - - - + - + + - - - - - - - - - - - - - - +
        + Number of annotated regions of interest. +
        - - - - -
        NameTypeDescription
        geometryType + - -string +

        numberOfROIs :number

        - -
        Name of the geometry type (point, circle, box, polygon, freehandPolygon, line, freehandLine)
        -
        + +
      @@ -1180,7 +1181,7 @@
      Properties
      Source:
      @@ -1196,19 +1197,83 @@
      Properties
      + + +

      size :Array.<number>

      + + + + +
      + Gets the size of the viewport. +
      + + + +
      Type:
      +
        +
      • + +Array.<number> + + +
      • +
      + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + +
      Source:
      +
      + + + + +
      + + + +

      Methods

      @@ -1216,7 +1281,7 @@
      Properties
      -

      activateModifyInteraction(options)

      +

      activateDragPanInteraction(options)

      @@ -1224,7 +1289,7 @@

      - Activates modify interaction. + Activates dragpan interaction. @@ -1266,7 +1331,7 @@
      Parameters:
      -object +Object @@ -1276,7 +1341,7 @@
      Parameters:
      - Modification options. + DragPan options. @@ -1317,7 +1382,7 @@
      Parameters:
      Source:
      @@ -1353,7 +1418,7 @@
      Parameters:
      -

      addROI(Regions)

      +

      activateDragZoomInteraction(options)

      @@ -1361,7 +1426,7 @@

      addROI - Adds a regions of interest. + Activates dragZoom interaction. @@ -1397,13 +1462,13 @@
      Parameters:
      - Regions + options -ROI +object @@ -1413,7 +1478,7 @@
      Parameters:
      - of interest. + DragZoom options. @@ -1454,7 +1519,7 @@
      Parameters:
      Source:
      @@ -1490,7 +1555,7 @@
      Parameters:
      -

      addROIEvaluation(uid, item)

      +

      activateDrawInteraction(options)

      @@ -1498,7 +1563,7 @@

      addRO
      - Adds a qualitative evaluation to a region of interest. + Activates the draw interaction for graphic annotation of regions of interest.
      @@ -1534,13 +1599,13 @@

      Parameters:
      - uid + options -string +object @@ -1550,35 +1615,2847 @@
      Parameters:
      - Unique identifier of the region of interest - - + Drawing options. +
      Properties
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeDescription
      geometryType + + +string + + + + Name of the geometry type (point, circle, box, polygon, freehandPolygon, line, freehandLine)
      marker + + +string + + + + Marker
      markup + + +string + + + + Markup
      maxPoints + + +number + + + + Geometry max points
      minPoints + + +number + + + + Geometry min points
      vertexEnabled + + +boolean + + + + Enable vertex
      styleOptions + + +object + + + + Style options +
      Properties
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeDescription
      stroke + + +object + + + + Style options for the outline of the geometry +
      Properties
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeDescription
      color + + +Array.<number> + + + + RGBA color of the outline
      width + + +number + + + + Width of the outline
      + +
      fill + + +object + + + + Style options for body the geometry +
      Properties
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeDescription
      color + + +Array.<number> + + + + RGBA color of the body
      + +
      image + + +object + + + + Style options for image
      + +
      + + + + + + + + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Source:
      +
      + + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +

      activateModifyInteraction(options)

      + + + + + + +
      + Activates modify interaction. +
      + + + + + + + + + +
      Parameters:
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeDescription
      options + + +object + + + + Modification options.
      + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Source:
      +
      + + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +

      activateSelectInteraction(options)

      + + + + + + +
      + Activates select interaction. +
      + + + + + + + + + +
      Parameters:
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeDescription
      options + + +object + + + + selection options.
      + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Source:
      +
      + + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +

      activateSnapInteraction(options)

      + + + + + + +
      + Activates snap interaction. +
      + + + + + + + + + +
      Parameters:
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeDescription
      options + + +Object + + + + Snap options.
      + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Source:
      +
      + + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +

      activateTranslateInteraction(options)

      + + + + + + +
      + Activates translate interaction. +
      + + + + + + + + + +
      Parameters:
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeDescription
      options + + +Object + + + + Translation options.
      + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Source:
      +
      + + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +

      addROI(roi, styleOptions)

      + + + + + + +
      + Adds a regions of interest. +
      + + + + + + + + + +
      Parameters:
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeDescription
      roi + + +ROI + + + + Regions of interest +
      Properties
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeDescription
      properties + + +object + + + + ROI properties +
      Properties
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeDescription
      measurements + + +object + + + + ROI measurements
      evaluations + + +object + + + + ROI evaluations
      label + + +object + + + + ROI label
      marker + + +object + + + + ROI marker (this is used while we don't have presentation states)
      + +
      + +
      styleOptions + + +object + + + + Style options +
      Properties
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeDescription
      stroke + + +object + + + + Style options for the outline of the geometry +
      Properties
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeDescription
      color + + +Array.<number> + + + + RGBA color of the outline
      width + + +number + + + + Width of the outline
      + +
      fill + + +object + + + + Style options for body the geometry +
      Properties
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeDescription
      color + + +Array.<number> + + + + RGBA color of the body
      + +
      image + + +object + + + + Style options for image
      + +
      + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Source:
      +
      + + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +

      addROIEvaluation(uid, item)

      + + + + + + +
      + Adds a qualitative evaluation to a region of interest. +
      + + + + + + + + + +
      Parameters:
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeDescription
      uid + + +string + + + + Unique identifier of the region of interest
      item + + +Object + + + + CODE content item representing a qualitative evaluation
      + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Source:
      +
      + + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +

      addROIMeasurement(uid, item)

      + + + + + + +
      + Adds a measurement to a region of interest. +
      + + + + + + + + + +
      Parameters:
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeDescription
      uid + + +string + + + + Unique identifier of the region of interest
      item + + +Object + + + + NUM content item representing a measurement
      + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Source:
      +
      + + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +

      addViewportOverlay(options)

      + + + + + + +
      + Adds a new viewport overlay. +
      + + + + + + + + + +
      Parameters:
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeDescription
      options + + +object + + + + Overlay options +
      Properties
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeDescription
      element + + +object + + + + The custom overlay html element
      className + + +object + + + + Class to style the OpenLayer's overlay container
      + +
      + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Source:
      +
      + + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +

      deactivateDragPanInteraction() → {void}

      + + + + + + +
      + Deactivate dragpan interaction. +
      + + + + + + + + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Source:
      +
      + + + + + + + +
      + + + + + + + + + + + + + + + +
      Returns:
      + + + + +
      +
      + Type +
      +
      + +void + + +
      +
      + + + + + + + + + + + + + +

      deactivateDragZoomInteraction() → {void}

      + + + + + + +
      + Deactivates dragZoom interaction. +
      + + + + + + + + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Source:
      +
      + + + + + + + +
      + + + + + + + + + + + + + + + +
      Returns:
      + + + + +
      +
      + Type +
      +
      + +void + + +
      +
      + + + + + + + + + + + + + +

      deactivateDrawInteraction() → {void}

      + + + + + + +
      + Deactivates draw interaction. +
      + + + + + + + + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Source:
      +
      + + + + + + + +
      + + + + + + + + + + + + + + + +
      Returns:
      + + + + +
      +
      + Type +
      +
      + +void + + +
      +
      + + + + + + + + + + + + + +

      deactivateModifyInteraction()

      + + + + + + +
      + Deactivates modify interaction. +
      + + + + + + + + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Source:
      +
      + + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +

      deactivateSelectInteraction() → {void}

      + + + + + + +
      + Deactivates select interaction. +
      + + + + + + + + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Source:
      +
      + + + + + + + +
      + + + + + + + + + + + + + + + +
      Returns:
      + + + + +
      +
      + Type +
      +
      + +void + + +
      +
      + + + + + + + + + + + + + +

      deactivateSnapInteraction() → {void}

      + + + + + + +
      + Deactivates snap interaction. +
      + + + + + + + + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Source:
      +
      + + + + + + + +
      + + + + + + + + + + + + + + + +
      Returns:
      + + + + +
      +
      + Type +
      +
      + +void + + +
      +
      + + + + + + + + + + + + + +

      deactivateTranslateInteraction() → {void}

      + + + + + + +
      + Deactivates translate interaction. +
      + + + + + + + + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Source:
      +
      + + + + + + + +
      + + + + + + + + + + + + + + + +
      Returns:
      + + + + +
      +
      + Type +
      +
      + +void + + +
      +
      + - - - item - - - - -Object - - + - + - CODE content item representing a qualitative evaluation - + +

      getAllROIs() → {Array.<ROI>}

      + - - + + + +
      + Gets all annotated regions of interest. +
      + + + + + + + @@ -1614,7 +4491,7 @@
      Parameters:
      Source:
      @@ -1639,6 +4516,28 @@
      Parameters:
      +
      Returns:
      + + +
      + Array of regions of interest. +
      + + + +
      +
      + Type +
      +
      + +Array.<ROI> + + +
      +
      + + @@ -1650,7 +4549,7 @@
      Parameters:
      -

      addROIMeasurement(uid, item)

      +

      getROI(uid) → {ROI}

      @@ -1658,7 +4557,7 @@

      addR
      - Adds a measurement to a region of interest. + Gets an individual annotated regions of interest.
      @@ -1714,29 +4613,6 @@

      Parameters:
      - - - - item - - - - - -Object - - - - - - - - - - NUM content item representing a measurement - - - @@ -1774,7 +4650,7 @@
      Parameters:
      Source:
      @@ -1799,6 +4675,28 @@
      Parameters:
      +
      Returns:
      + + +
      + Regions of interest. +
      + + + +
      +
      + Type +
      +
      + +ROI + + +
      +
      + + @@ -1810,7 +4708,7 @@
      Parameters:
      -

      addViewportOverlay(options)

      +

      hideROIs()

      @@ -1818,7 +4716,8 @@

      add
      - Adds a new viewport overlay. + Hides annotated regions of interest such that they are no longer + visible on the viewport.
      @@ -1829,125 +4728,92 @@

      add -

      Parameters:
      - - - - - - - - - - +
      -
      - - + - - - - - + - + - + - + - - + +

      popROI() → {ROI}

      + - -
      NameTypeDescription
      options - - -object + + - - Overlay options -
      Properties
      - + - - - - - - + - + - + - + +
      Source:
      +
      + - - - + - - - - - + + - - - - - - - - - - - - - - - + + + + + + + + - -
      NameTypeDescription
      element - - -object - - The custom overlay html element
      className - - -object - - Class to style the OpenLayer's overlay container
      -
      + + + +
      + Pops the most recently annotated regions of interest. +
      + + + + + + + @@ -1983,7 +4849,7 @@
      Properties
      Source:
      @@ -2008,6 +4874,28 @@
      Properties
      +
      Returns:
      + + +
      + Regions of interest. +
      + + + +
      +
      + Type +
      +
      + +ROI + + +
      +
      + + @@ -2019,7 +4907,7 @@
      Properties
      -

      deactivateDrawInteraction()

      +

      removeAllROIs() → {void}

      @@ -2027,7 +4915,7 @@

      - Deactivates draw interaction. + Removes all annotated regions of interest. @@ -2071,7 +4959,7 @@

      Source:
      @@ -2096,6 +4984,24 @@

      Returns:

      + + + + +
      +
      + Type +
      +
      + +void + + +
      +
      + + @@ -2107,7 +5013,7 @@

      deactivateModifyInteraction()

      +

      removeROI(uid)

      @@ -2115,7 +5021,7 @@

      <
      - Deactivates modify interaction. + Removes an individual regions of interest.
      @@ -2126,6 +5032,55 @@

      < +
      Parameters:
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeDescription
      uid + + +string + + + + Unique identifier of the region of interest
      + + @@ -2159,7 +5114,7 @@

      <
      Source:
      @@ -2195,7 +5150,7 @@

      < -

      deactivateSelectInteraction()

      +

      render(options)

      @@ -2203,7 +5158,7 @@

      <
      - Deactivates select interaction. + Renders the images in the specified viewport container.
      @@ -2214,6 +5169,107 @@

      < +
      Parameters:
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeDescription
      options + + +object + + + + Rendering options. +
      Properties
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeDescription
      container + + +string +| + +HTMLElement + + + + HTML Element in which the viewer should be injected.
      + +
      + + @@ -2247,7 +5303,7 @@

      <
      Source:
      @@ -2283,7 +5339,7 @@

      < -

      getAllROIs() → {Array.<ROI>}

      +

      resize() → {void}

      @@ -2291,7 +5347,7 @@

      getAllROIs<
      - Gets all annotated regions of interest. + Resizes the viewer to fit the viewport.
      @@ -2335,7 +5391,7 @@

      getAllROIs<
      Source:
      @@ -2363,10 +5419,6 @@

      getAllROIs<

      Returns:
      -
      - Array of regions of interest. -
      -
      @@ -2375,7 +5427,7 @@
      Returns:
      -Array.<ROI> +void
      @@ -2393,7 +5445,7 @@
      Returns:
      -

      getROI(uid) → {ROI}

      +

      setROIStyle(uid, styleOptions)

      @@ -2401,7 +5453,7 @@

      getROI - Gets an individual annotated regions of interest. + Sets the style of a region of interest. @@ -2453,211 +5505,251 @@
      Parameters:
      - Unique identifier of the region of interest - - - - - - - - - - - -
      - - - - - - - - - - - - - - - - - - - - - - - - - - -
      Source:
      -
      - - - - - - - -
      - - - - - - - - - - - - - - - -
      Returns:
      - - -
      - Regions of interest. -
      - + Unique identifier of the regions of interest. + + -
      -
      - Type -
      -
      - -ROI + + + styleOptions + + + + + +object -
      -
      + + - + + + Style options +
      Properties
      + + + + + + + + - - + - -

      hideROIs()

      - + + + + + + + + - -
      - Hides annotated regions of interest such that they are no longer - visible on the viewport. -
      + + + + + - - + + + + + + + + + + + + + + + + + + + + +
      NameTypeDescription
      stroke + + +object + + Style options for the outline of the geometry +
      Properties
      + + + + + + + + + + + + + + + + + + + -
      +
      - + - + - + + - + + + + - + - + - + - -
      Source:
      -
      - + + + +
      NameTypeDescription
      color + + +Array.<number> - - + + RGBA color of the outline
      width + + +number - - + + Width of the outline
      - +
      fill + + +object + + Style options for body the geometry +
      Properties
      + + + + + + + + + + + + + + + + + + + + + - - + + -

      popROI() → {ROI}

      - + +
      NameTypeDescription
      color + + +Array.<number> + + RGBA color of the body
      + +
      image + + +object -
      - Pops the most recently annotated regions of interest. -
      + +
      Style options for image
      + + + + + @@ -2693,7 +5785,7 @@

      popROISource:
      @@ -2718,28 +5810,6 @@

      popROIReturns:

      - - -
      - Regions of interest. -
      - - - -
      -
      - Type -
      -
      - -ROI - - -
      -
      - - @@ -2751,7 +5821,7 @@
      Returns:
      -

      removeAllROIs()

      +

      showROIs()

      @@ -2759,7 +5829,8 @@

      removeAl
      - Removes all annotated regions of interest. + Shows annotated regions of interest such that they become visible + on the viewport ontop of the images.
      @@ -2803,7 +5874,7 @@

      removeAl
      Source:
      @@ -2839,72 +5910,23 @@

      removeAl -

      removeROI(uid)

      - - +

      toggleOverviewMap() → {void}

      - - -
      - Removes an individual regions of interest. -
      - - - - - - - - - -
      Parameters:
      - - - - - - - - - - - - - - - - - - - - - - +
      + Toggles overview map. +
      - - - - - - - -
      NameTypeDescription
      uid - - -string - - Unique identifier of the region of interest
      @@ -2940,7 +5962,7 @@
      Parameters:
      Source:
      @@ -2965,6 +5987,24 @@
      Parameters:
      +
      Returns:
      + + + + +
      +
      + Type +
      +
      + +void + + +
      +
      + + @@ -2976,7 +6016,7 @@
      Parameters:
      -

      render(options)

      +

      updateROI(roi)

      @@ -2984,7 +6024,7 @@

      render - Renders the images in the specified viewport container. + Update properties of regions of interest. @@ -3020,7 +6060,7 @@
      Parameters:
      - options + roi @@ -3036,7 +6076,7 @@
      Parameters:
      - Rendering options. + ROI to be updated
      Properties
      @@ -3062,16 +6102,13 @@
      Properties
      - container + uid string -| - -HTMLElement @@ -3081,196 +6118,160 @@
      Properties
      - HTML Element in which the viewer should be injected. - - - - - - - + Unique identifier of the region of interest - - - + + + properties + + + + +object + + -
      + - + - + ROI properties +
      Properties
      + - + + + + + + - + - + - + - + + + + - + + + + - + - -
      Source:
      -
      - + - + - + + - - - - - - - - - - - - - - - - + + + + + - - + - -

      resize()

      - + + + + + + + - + - + - + + - + + + + - + - -
      Source:
      -
      - + - + - + + - - - - - - - - - - - - - - - - - - - - + +
      NameTypeDescription
      measurements + + +object - - + + ROI measurements
      evaluations + + +object + + ROI evaluations
      label + + +object -
      - Resizes the viewer to fit the viewport. -
      - - - - - - - - - - - - - -
      - - - - - - - + +
      ROI label
      marker + + +object - - + + ROI marker (this is used while we don't have presentation states)
      - - + + + + - -

      showROIs()

      - + + - - - -
      - Shows annotated regions of interest such that they become visible - on the viewport ontop of the images. -
      - - - - - - - + + @@ -3306,7 +6307,7 @@

      showROIsSource:
      @@ -3352,13 +6353,13 @@

      showROIs
      - Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
      diff --git a/docs/viewer.html b/docs/viewer.html index d8d74037..4270577e 100644 --- a/docs/viewer.html +++ b/docs/viewer.html @@ -72,7 +72,7 @@

      viewer

      Source:
      @@ -130,13 +130,13 @@

      Classes


      - Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)
      diff --git a/docs/viewer.js.html b/docs/viewer.js.html index a5ee13de..86636a70 100644 --- a/docs/viewer.js.html +++ b/docs/viewer.js.html @@ -26,70 +26,126 @@

      Source: viewer.js

      -
      import 'ol/ol.css';
      -import Collection from 'ol/Collection';
      -import Draw, { createRegularPolygon, createBox } from 'ol/interaction/Draw';
      +            
      import "ol/ol.css";
      +import Collection from "ol/Collection";
      +import Draw, { createRegularPolygon } from "ol/interaction/Draw";
       import EVENT from "./events";
      -import Feature from 'ol/Feature';
      -import FullScreen from 'ol/control/FullScreen';
      -import ImageLayer from 'ol/layer/Image';
      -import Map from 'ol/Map';
      -import Modify from 'ol/interaction/Modify';
      -import MouseWheelZoom from 'ol/interaction/MouseWheelZoom';
      -import OverviewMap from 'ol/control/OverviewMap';
      -import Projection from 'ol/proj/Projection';
      +import Feature from "ol/Feature";
      +import Fill from "ol/style/Fill";
      +import FullScreen from "ol/control/FullScreen";
      +import ImageLayer from "ol/layer/Image";
      +import Map from "ol/Map";
      +import Modify from "ol/interaction/Modify";
      +import MouseWheelZoom from "ol/interaction/MouseWheelZoom";
      +import OverviewMap from "ol/control/OverviewMap";
      +import Projection from "ol/proj/Projection";
       import publish from "./eventPublisher";
      -import ScaleLine from 'ol/control/ScaleLine';
      -import Select from 'ol/interaction/Select';
      -import Static from 'ol/source/ImageStatic';
      -import Overlay from 'ol/Overlay';
      -import TileLayer from 'ol/layer/Tile';
      -import TileImage from 'ol/source/TileImage';
      -import TileGrid from 'ol/tilegrid/TileGrid';
      -import VectorSource from 'ol/source/Vector';
      -import VectorLayer from 'ol/layer/Vector';
      -import View from 'ol/View';
      -import { default as PolygonGeometry } from 'ol/geom/Polygon';
      -import { default as PointGeometry } from 'ol/geom/Point';
      -import { default as LineStringGeometry } from 'ol/geom/LineString';
      -import { default as CircleGeometry } from 'ol/geom/Circle';
      +import ScaleLine from "ol/control/ScaleLine";
      +import Select from "ol/interaction/Select";
      +import Snap from "ol/interaction/Snap";
      +import Translate from "ol/interaction/Translate";
      +import Style from "ol/style/Style";
      +import Stroke from "ol/style/Stroke";
      +import Circle from "ol/style/Circle";
      +import Static from "ol/source/ImageStatic";
      +import Overlay from "ol/Overlay";
      +import TileLayer from "ol/layer/Tile";
      +import TileImage from "ol/source/TileImage";
      +import TileGrid from "ol/tilegrid/TileGrid";
      +import VectorSource from "ol/source/Vector";
      +import VectorLayer from "ol/layer/Vector";
      +import View from "ol/View";
      +import DragPan from "ol/interaction/DragPan";
      +import DragZoom from "ol/interaction/DragZoom";
      +
       import { default as VectorEventType } from "ol/source/VectorEventType";
      -import { default as MapEventType } from "ol/MapEventType";
      -import { defaults as defaultInteractions } from 'ol/interaction';
       
      -import { getCenter } from 'ol/extent';
      -import { toStringXY, rotate } from 'ol/coordinate';
      +import { getCenter } from "ol/extent";
      +
      +import * as DICOMwebClient from "dicomweb-client";
      +import dcmjs from "dcmjs";
       
      -import { VLWholeSlideMicroscopyImage, getFrameMapping } from './metadata.js';
      -import { ROI } from './roi.js';
      +import { VLWholeSlideMicroscopyImage, getFrameMapping } from "./metadata.js";
      +import { ROI } from "./roi.js";
       import {
      +  computeRotation,
         generateUID,
      -  mapPixelCoordToSlideCoord,
      -  mapSlideCoordToPixelCoord
      -} from './utils.js';
      +  getUnitSuffix,
      +  doContentItemsMatch,
      +} from "./utils.js";
       import {
      -  Point,
      -  Multipoint,
      -  Polyline,
      -  Polygon,
      -  Ellipsoid,
      -  Ellipse
      -} from './scoord3d.js';
      +  getPixelSpacing,
      +  geometry2Scoord3d,
      +  scoord3d2Geometry,
      +  getFeatureScoord3dLength,
      +  getFeatureScoord3dArea,
      +} from "./scoord3dUtils";
      +import Enums from "./enums";
      +import _AnnotationManager from "./annotations/_AnnotationManager";
      +import Icon from "ol/style/Icon";
      +
      +function _getInteractionBindingCondition(bindings) {
      +  const BUTTONS = {
      +    left: 1,
      +    middle: 4,
      +    right: 2,
      +  };
      +
      +  const { mouseButtons, modifierKey } = bindings;
      +
      +  const _mouseButtonCondition = (event) => {
      +    /** No mouse button condition set. */
      +    if (!mouseButtons || !mouseButtons.length) {
      +      return true;
      +    }
       
      -import * as DICOMwebClient from 'dicomweb-client';
      +    const button = event.pointerEvent
      +      ? event.pointerEvent.buttons
      +      : event.originalEvent.buttons;
       
      -/** Extracts value of Pixel Spacing attribute from metadata.
      - *
      - * @param {object} metadata - Metadata of a DICOM VL Whole Slide Microscopy Image instance
      - * @returns {number[]} Spacing between pixel columns and rows in millimeter
      - * @private
      - */
      -function _getPixelSpacing(metadata) {
      -  const functionalGroup = metadata.SharedFunctionalGroupsSequence[0];
      -  const pixelMeasures = functionalGroup.PixelMeasuresSequence[0];
      -  return pixelMeasures.PixelSpacing;
      -}
      +    return mouseButtons.some((mb) => BUTTONS[mb] === button);
      +  };
      +
      +  const _modifierKeyCondition = (event) => {
      +    const pointerEvent = event.pointerEvent
      +      ? event.pointerEvent
      +      : event.originalEvent;
      +
      +    if (!modifierKey) {
      +      /**
      +       * No modifier key, don't pass if key pressed as other
      +       * tool may be using this tool.
      +       */
      +      return (
      +        !pointerEvent.altKey &&
      +        !pointerEvent.metaKey &&
      +        !pointerEvent.shiftKey &&
      +        !pointerEvent.ctrlKey
      +      );
      +    }
       
      +    switch (modifierKey) {
      +      case "alt":
      +        return pointerEvent.altKey === true || pointerEvent.metaKey === true;
      +      case "shift":
      +        return pointerEvent.shiftKey === true;
      +      case "ctrl":
      +        return pointerEvent.ctrlKey === true;
      +      default:
      +        /** Invalid modifier key set (ignore requirement as if key not pressed). */
      +        return (
      +          !pointerEvent.altKey &&
      +          !pointerEvent.metaKey &&
      +          !pointerEvent.shiftKey &&
      +          !pointerEvent.ctrlKey
      +        );
      +    }
      +  };
      +
      +  return (event) => {
      +    return _mouseButtonCondition(event) && _modifierKeyCondition(event);
      +  };
      +}
       
       /** Determines whether image needs to be rotated relative to slide
        * coordinate system based on direction cosines.
      @@ -118,360 +174,337 @@ 

      Source: viewer.js

      * @param {object} metadata - Metadata of a DICOM VL Whole Slide Microscopy Image instance * @returns {number} Rotation in radians * @private -*/ + */ function _getRotation(metadata) { - var degrees; - if ( - (metadata.ImageOrientationSlide[0] === 0) && - (metadata.ImageOrientationSlide[1] === -1) && - (metadata.ImageOrientationSlide[2] === 0) && - (metadata.ImageOrientationSlide[3] === -1) && - (metadata.ImageOrientationSlide[4] === 0) && - (metadata.ImageOrientationSlide[5] === 0) - ) { - /* - * The Total Pixel Matrix is rotated with respect to the slide coordinate - * system by 180 degrees, such that an increase along the row direction - * (left to right) leads to lower Y coordinate values and an increase - * along the column direction (top to bottom) leads to lower X coordinate - * values. - */ - degrees = 180; - } else if ( - (metadata.ImageOrientationSlide[0] === 1) && - (metadata.ImageOrientationSlide[1] === 0) && - (metadata.ImageOrientationSlide[2] === 0) && - (metadata.ImageOrientationSlide[3] === 0) && - (metadata.ImageOrientationSlide[4] === -1) && - (metadata.ImageOrientationSlide[5] === 0) - ) { - /* - * The Total Pixel Matrix is rotated with respect to the slide coordinate - * system by 90 degrees, such that an increase along the row direction - * (left to right) leads to higher X coordinate values and an increase - * along the column direction (top to bottom) leads to lower Y coordinate - * values. - */ - degrees = 90; - } else if ( - (metadata.ImageOrientationSlide[0] === -1) && - (metadata.ImageOrientationSlide[1] === 0) && - (metadata.ImageOrientationSlide[2] === 0) && - (metadata.ImageOrientationSlide[3] === 0) && - (metadata.ImageOrientationSlide[4] === 1) && - (metadata.ImageOrientationSlide[5] === 0) - ) { - /* - * The Total Pixel Matrix is rotated with respect to the slide coordinate - * system by 270 degrees, such that an increase along the row direction - * (left to right) leads to lower X coordinate values and an increase - * along the column direction (top to bottom) leads to higher Y coordinate - * values. - */ - degrees = 270; - } else if ( - (metadata.ImageOrientationSlide[0] === 0) && - (metadata.ImageOrientationSlide[1] === 1) && - (metadata.ImageOrientationSlide[2] === 0) && - (metadata.ImageOrientationSlide[3] === 1) && - (metadata.ImageOrientationSlide[4] === 0) && - (metadata.ImageOrientationSlide[5] === 0) - ) { - /* - * The Total Pixel Matrix is aligned with the slide coordinate system - * such that an increase along the row direction (left to right) leads to - * higher Y coordinate values and an increase along the column direction - * (top to bottom) leads to higher X coordinate values. - */ - degrees = 0; - } else { - throw new Error(`Unexpected image orientation ${metadata.ImageOrientationSlide}`); - } - return degrees * (Math.PI / 180); + // Angle with respect to the reference orientation + const angle = computeRotation({ + orientation: metadata.ImageOrientationSlide, + }); + // We want the slide oriented horizontally with the label on the right side + const correction = 90 * (Math.PI / 180); + return angle + correction; } - -/** Converts a vector graphic from an Openlayers Geometry into a DICOM SCOORD3D - * representation. +/** Map style options to OpenLayers style. * - * @param {object} geometry - Openlayers Geometry - * @param {Object[]} pyramid - Metadata for resolution levels of image pyramid - * @returns {Scoord3D} DICOM Microscopy Viewer Scoord3D - * @private + * @param {object} styleOptions - Style options + * @param {object} styleOptions.stroke - Style options for the outline of the geometry + * @param {number[]} styleOptions.stroke.color - RGBA color of the outline + * @param {number} styleOptions.stroke.width - Width of the outline + * @param {object} styleOptions.fill - Style options for body the geometry + * @param {number[]} styleOptions.fill.color - RGBA color of the body + * @param {object} styleOptions.image - Style options for image + * @return {Style} OpenLayers style */ -function _geometry2Scoord3d(geometry, pyramid) { - console.info('map coordinates from pixel matrix to slide coordinate system') - const frameOfReferenceUID = pyramid[pyramid.length - 1].FrameOfReferenceUID; - const type = geometry.getType(); - if (type === 'Point') { - let coordinates = geometry.getCoordinates(); - coordinates = _geometryCoordinates2scoord3dCoordinates(coordinates, pyramid); - return new Point({ - coordinates, - frameOfReferenceUID: frameOfReferenceUID - }); - } else if (type === 'Polygon') { - /* - * The first linear ring of the array defines the outer-boundary (surface). - * Each subsequent linear ring defines a hole in the surface. - */ - let coordinates = geometry.getCoordinates()[0].map(c => { - return _geometryCoordinates2scoord3dCoordinates(c, pyramid); - }); - return new Polygon({ - coordinates, - frameOfReferenceUID: frameOfReferenceUID - }); - } else if (type === 'LineString') { - let coordinates = geometry.getCoordinates().map(c => { - return _geometryCoordinates2scoord3dCoordinates(c, pyramid); - }); - return new Polyline({ - coordinates, - frameOfReferenceUID: frameOfReferenceUID - }); - } else if (type === 'Circle') { - const center = geometry.getCenter(); - const radius = geometry.getRadius(); - // Endpoints of major and minor axis of the ellipse. - let coordinates = [ - [center[0] - radius, center[1], 0], - [center[0] + radius, center[1], 0], - [center[0], center[1] - radius, 0], - [center[0], center[1] + radius, 0], - ]; - coordinates = coordinates.map(c => { - return _geometryCoordinates2scoord3dCoordinates(c, pyramid); - }) +function _getOpenLayersStyle(styleOptions) { + const style = new Style(); + + if ("stroke" in styleOptions) { + const strokeOptions = { + color: styleOptions.stroke.color, + width: styleOptions.stroke.width, + }; + const stroke = new Stroke(strokeOptions); + style.setStroke(stroke); + } - // const metadata = pyramid[pyramid.length-1]; - // const pixelSpacing = _getPixelSpacing(metadata); + if ("fill" in styleOptions) { + const fillOptions = { + color: styleOptions.fill.color, + }; + const fill = new Fill(fillOptions); + style.setFill(fill); + } - return new Ellipse({ - coordinates, - frameOfReferenceUID: frameOfReferenceUID - }); - } else { - // TODO: Combine multiple points into MULTIPOINT. - console.error(`unknown geometry type "${type}"`) + if ("image" in styleOptions) { + const { image } = styleOptions; + + if (image.circle) { + const options = { + radius: image.circle.radius, + stroke: new Stroke(image.circle.stroke), + fill: new Fill(image.circle.fill), + }; + const circle = new Circle(options); + style.setImage(circle); + } + + if (image.icon) { + const icon = new Icon(image.icon); + style.setImage(icon); + } } + + return style; } -/** Converts a vector graphic from a DICOM SCOORD3D into an Openlayers Geometry - * representation. +/** + * Add ROI properties to feature in a safe way * - * @param {Scoord3D} scoord3d - DICOM Microscopy Viewer Scoord3D - * @param {Object[]} pyramid - Metadata for resolution levels of image pyramid - * @returns {object} Openlayers Geometry - * @private + * @param {object} feature The feature instance that represents the ROI + * @param {object} properties Valid ROI properties + * @param {object} properties.measurements - ROI measurements + * @param {object} properties.evaluations - ROI evaluations + * @param {object} properties.label - ROI label + * @param {object} properties.marker - ROI marker (this is used while we don't have presentation states) + * @param {boolean} opt_silent Opt silent update */ -function _scoord3d2Geometry(scoord3d, pyramid) { - console.info('map coordinates from slide coordinate system to pixel matrix') - const type = scoord3d.graphicType; - const data = scoord3d.graphicData; - - if (type === 'POINT') { - let coordinates = _scoord3dCoordinates2geometryCoordinates(data, pyramid); - return new PointGeometry(coordinates); - } else if (type === 'POLYLINE') { - const coordinates = data.map(d => { - return _scoord3dCoordinates2geometryCoordinates(d, pyramid); - }); - return new LineStringGeometry(coordinates); - } else if (type === 'POLYGON') { - const coordinates = data.map(d => { - return _scoord3dCoordinates2geometryCoordinates(d, pyramid); - }); - return new PolygonGeometry([coordinates]); - } else if (type === 'ELLIPSE') { - // TODO: ensure that the ellipse represents a circle, i.e. that - // major and minor axis form a right angle and have the same length - const majorAxisCoordinates = data.slice(0, 2); - const minorAxisCoordinates = data.slice(2, 4); - // Circle is defined by two points: the center point and a point on the - // circumference. - const point1 = majorAxisCoordinates[0]; - const point2 = majorAxisCoordinates[1]; - let coordinates = [ - [ - (point1[0] + point2[0]) / parseFloat(2), - (point1[1] + point2[1]) / parseFloat(2), - 0 - ], - point2 - ]; - coordinates = coordinates.map(d => { - return _scoord3dCoordinates2geometryCoordinates(d, pyramid); - }); - // to flat coordinates - coordinates = [...coordinates[0].slice(0, 2), ...coordinates[1].slice(0, 2)]; +function _addROIPropertiesToFeature(feature, properties, opt_silent) { + const { Label, Measurements, Evaluations, Marker } = Enums.InternalProperties; - // flat coordinates in combination with opt_layout and no opt_radius are also accepted - // and internaly it calculates the Radius - return new CircleGeometry(coordinates, null, "XY"); - } else { - console.error(`unsupported graphic type "${type}"`) + if (properties[Label]) { + feature.set(Label, properties[Label], opt_silent); + } + + if (properties[Measurements]) { + feature.set(Measurements, properties[Measurements], opt_silent); } -} -function _geometryCoordinates2scoord3dCoordinates(coordinates, pyramid) { - return _coordinateFormatGeometry2Scoord3d([coordinates[0], coordinates[1], coordinates[2]], pyramid); + if (properties[Evaluations]) { + feature.set(Evaluations, properties[Evaluations], opt_silent); + } + + if (properties[Marker]) { + feature.set(Marker, properties[Marker], opt_silent); + } } -function _scoord3dCoordinates2geometryCoordinates(coordinates, pyramid) { - return _coordinateFormatScoord3d2Geometry([coordinates[0], coordinates[1], coordinates[2]], pyramid) +/** + * Wire measurements and qualitative events to generate content items + * based on feature properties and geometry changes + * + * @param {object} map The map instance + * @param {object} feature The feature instance + * @param {object} pyramid The pyramid metadata + * @returns {void} + */ +function _wireMeasurementsAndQualitativeEvaluationsEvents( + map, + feature, + pyramid +) { + /** + * Update feature measurement properties first and then measurements + */ + _updateFeatureMeasurements(map, feature, pyramid); + feature.getGeometry().on(Enums.FeatureGeometryEvents.CHANGE, () => { + _updateFeatureMeasurements(map, feature, pyramid); + }); + + /** + * Update feature evaluations + */ + _updateFeatureEvaluations(feature); + feature.on(Enums.FeatureEvents.PROPERTY_CHANGE, () => + _updateFeatureEvaluations(feature) + ); } -/** Translates coordinates of Total Pixel Matrix in pixel unit into coordinates - * in Frame of Reference (slide coordinate system) in millimeter unit. +/** + * Update feature evaluations from its properties * - * @param {number[]|number[][]} coordinates - Coordinates in Total Pixel Matrix - * @param {Object[]} pyramid - Metadata for resolution levels of image pyramid - * @returns {number[]|number[][]} Coordinates in Frame of Reference - * @private + * @param {Feature} feature The feature + * @returns {void} */ -function _coordinateFormatGeometry2Scoord3d(coordinates, pyramid) { - let transform = false; - if (!Array.isArray(coordinates[0])) { - coordinates = [coordinates]; - transform = true; - } - const metadata = pyramid[pyramid.length - 1]; - const origin = metadata.TotalPixelMatrixOriginSequence[0]; - const orientation = metadata.ImageOrientationSlide; - const spacing = _getPixelSpacing(metadata); - const offset = [ - Number(origin.XOffsetInSlideCoordinateSystem), - Number(origin.YOffsetInSlideCoordinateSystem), - ]; - - coordinates = coordinates.map(c => { - const pixelCoord = [c[0], -(c[1] + 1)]; - const slideCoord = mapPixelCoordToSlideCoord({ - orientation, - spacing, - offset, - point: pixelCoord - }); - return [slideCoord[0], slideCoord[1], 0]; +function _updateFeatureEvaluations(feature) { + const evaluations = feature.get(Enums.InternalProperties.Evaluations) || []; + const label = feature.get(Enums.InternalProperties.Label); + + if (!label) return; + + const evaluation = new dcmjs.sr.valueTypes.TextContentItem({ + name: new dcmjs.sr.coding.CodedConcept({ + value: "112039", + meaning: "Tracking Identifier", + schemeDesignator: "DCM", + }), + value: label, + relationshipType: Enums.RelationshipTypes.HAS_OBS_CONTEXT, }); - if (transform) { - return coordinates[0]; + + const index = evaluations.findIndex((e) => + doContentItemsMatch(e, evaluation) + ); + + if (index > -1) { + evaluations[index] = evaluation; + } else { + evaluations.push(evaluation); } - return coordinates; + + feature.set(Enums.InternalProperties.Evaluations, evaluations); + console.debug(`Evaluations of feature (${feature.getId()}):`, evaluations); } -/** Translates coordinates of coordinates in Frame of Reference - * (slide coordinate system) in millimeter unit into coordinates in - * Total Pixel Matrix in pixel unit. +/** + * Generate feature measurements from its measurement properties * - * @param {number[]|number[][]} coordinates - Coordinates in Frame of Reference - * @param {Object[]} pyramid - Metadata for resolution levels of image pyramid - * @returns {number[]|number[][]} Coordinates in Total Pixel Matrix - * @private + * @param {object} map The map instance + * @param {object} feature The feature instance + * @param {object} pyramid The pyramid metadata + * @returns {void} */ -function _coordinateFormatScoord3d2Geometry(coordinates, pyramid) { - let transform = false; - if (!Array.isArray(coordinates[0])) { - coordinates = [coordinates]; - transform = true; - } - const metadata = pyramid[pyramid.length - 1]; - const orientation = metadata.ImageOrientationSlide; - const spacing = _getPixelSpacing(metadata); - const origin = metadata.TotalPixelMatrixOriginSequence[0]; - const offset = [ - Number(origin.XOffsetInSlideCoordinateSystem), - Number(origin.YOffsetInSlideCoordinateSystem), - ]; - - coordinates = coordinates.map(c => { - const slideCoord = [c[0], c[1]]; - const pixelCoord = mapSlideCoordToPixelCoord({ - offset, - orientation, - spacing, - point: slideCoord +function _updateFeatureMeasurements(map, feature, pyramid) { + if ( + Enums.Markup.Measurement !== feature.get(Enums.InternalProperties.Markup) + ) { + return; + } + + const measurements = feature.get(Enums.InternalProperties.Measurements) || []; + const area = getFeatureScoord3dArea(feature, pyramid); + const length = getFeatureScoord3dLength(feature, pyramid); + + if (!area && !length) return; + + const unitSuffixToMeaningMap = { + μm: "micrometer", + mm: "millimeter", + m: "meters", + km: "kilometers", + }; + + let measurement; + const view = map.getView(); + const unitSuffix = getUnitSuffix(view); + const unitCodedConceptValue = unitSuffix; + const unitCodedConceptMeaning = unitSuffixToMeaningMap[unitSuffix]; + + if (area) { + measurement = new dcmjs.sr.valueTypes.NumContentItem({ + name: new dcmjs.sr.coding.CodedConcept({ + meaning: "Area", + value: "42798000", + schemeDesignator: "SCT", + }), + value: area, + unit: [ + new dcmjs.sr.coding.CodedConcept({ + value: unitCodedConceptValue, + meaning: unitCodedConceptMeaning, + schemeDesignator: "SCT", + }), + ], }); - return [pixelCoord[0], -(pixelCoord[1] + 1), 0]; - }); - if (transform) { - return coordinates[0]; } - return coordinates; + + if (length) { + measurement = new dcmjs.sr.valueTypes.NumContentItem({ + name: new dcmjs.sr.coding.CodedConcept({ + meaning: "Length", + value: "410668003", + schemeDesignator: "SCT", + }), + value: length, + unit: [ + new dcmjs.sr.coding.CodedConcept({ + value: unitCodedConceptValue, + meaning: unitCodedConceptMeaning, + schemeDesignator: "SCT", + }), + ], + }); + } + + const index = measurements.findIndex((m) => + doContentItemsMatch(m, measurement) + ); + + if (index > -1) { + measurements[index] = measurement; + } else { + measurements.push(measurement); + } + + feature.set(Enums.InternalProperties.Measurements, measurements); + console.debug(`Measurements of feature (${feature.getId()}):`, measurements); } -/** Extracts and transforms the region of interest (ROI) from an Openlayers - * Feature. +/** + * Updates the style of a feature. * - * @param {object} feature - Openlayers Feature - * @param {Object[]} pyramid - Metadata for resolution levels of image pyramid - * @returns {ROI} Region of interest - * @private + * @param {object} styleOptions - Style options + * @param {object} styleOptions.stroke - Style options for the outline of the geometry + * @param {number[]} styleOptions.stroke.color - RGBA color of the outline + * @param {number} styleOptions.stroke.width - Width of the outline + * @param {object} styleOptions.fill - Style options for body the geometry + * @param {number[]} styleOptions.fill.color - RGBA color of the body + * @param {object} styleOptions.image - Style options for image */ -function _getROIFromFeature(feature, pyramid) { - let roi = {} - if (feature !== undefined) { - const geometry = feature.getGeometry(); - const scoord3d = _geometry2Scoord3d(geometry, pyramid); - const properties = feature.getProperties(); - // Remove geometry from properties mapping - const geometryName = feature.getGeometryName(); - delete properties[geometryName]; - const uid = feature.getId(); - roi = new ROI({ scoord3d, properties, uid }); - } - return roi; +function _setFeatureStyle(feature, styleOptions) { + if (styleOptions !== undefined) { + const style = _getOpenLayersStyle(styleOptions); + feature.setStyle(style); + + /** + * styleOptions is used internally by internal styled components like markers. + * This allows them to take priority over styling since OpenLayers swaps the styles + * completely in case of a setStyle happens. + */ + feature.set(Enums.InternalProperties.StyleOptions, styleOptions); + } } -const _client = Symbol('client'); -const _controls = Symbol('controls'); -const _drawingLayer = Symbol('drawingLayer'); -const _drawingSource = Symbol('drawingSource'); -const _features = Symbol('features'); -const _imageLayer = Symbol('imageLayer'); -const _interactions = Symbol('interactions'); -const _map = Symbol('map'); -const _metadata = Symbol('metadata'); -const _pyramidMetadata = Symbol('pyramidMetadata'); -const _pyramidFrameMappings = Symbol('pyramidFrameMappings'); -const _pyramidBaseMetadata = Symbol('pyramidMetadataBase'); -const _segmentations = Symbol('segmentations'); -const _usewebgl = Symbol('usewebgl'); - - -/** Interactive viewer for DICOM VL Whole Slide Microscopy Image instances +const _client = Symbol("client"); +const _controls = Symbol("controls"); +const _drawingLayer = Symbol("drawingLayer"); +const _drawingSource = Symbol("drawingSource"); +const _features = Symbol("features"); +const _imageLayer = Symbol("imageLayer"); +const _interactions = Symbol("interactions"); +const _map = Symbol("map"); +const _metadata = Symbol("metadata"); +const _pyramidMetadata = Symbol("pyramidMetadata"); +const _pyramidFrameMappings = Symbol("pyramidFrameMappings"); +const _pyramidBaseMetadata = Symbol("pyramidMetadataBase"); +const _segmentations = Symbol("segmentations"); +const _usewebgl = Symbol("usewebgl"); +const _annotationManager = Symbol("annotationManager"); +const _defaultStyleOptions = Symbol("defaultStyleOptions"); +const _overviewMap = Symbol("overviewMap"); + +/** + * Interactive viewer for DICOM VL Whole Slide Microscopy Image instances * with Image Type VOLUME. * * @class * @memberof viewer */ class VolumeImageViewer { - /** * Create a viewer instance for displaying VOLUME images. * * @param {object} options * @param {object} options.client - A DICOMwebClient instance for interacting with an origin server over HTTP. * @param {Object[]} options.metadata - An array of DICOM JSON metadata objects, one for each VL Whole Slide Microscopy Image instance. + * @param {object} options.styleOptions - Default style options for annotations * @param {string[]} [options.controls=[]] - Names of viewer control elements that should be included in the viewport. * @param {boolean} [options.retrieveRendered=true] - Whether image frames should be retrieved via DICOMweb prerendered by the server. * @param {boolean} [options.includeIccProfile=false] - Whether ICC Profile should be included for correction of image colors. * @param {boolean} [options.useWebGL=true] - Whether WebGL renderer should be used. */ constructor(options) { - if ('useWebGL' in options) { + if ("useWebGL" in options) { this[_usewebgl] = options.useWebGL; } else { this[_usewebgl] = true; } + this[_client] = options.client; - if (!('retrieveRendered' in options)) { + if (options.styleOptions) { + this[_defaultStyleOptions] = options.styleOptions; + } + + if (!("retrieveRendered" in options)) { options.retrieveRendered = true; } - if (!('controls' in options)) { + if (!("overview" in options)) { + options.overview = {}; + } + + if (!("controls" in options)) { options.controls = []; } options.controls = new Set(options.controls); @@ -482,65 +515,77 @@

      Source: viewer.js

      // Collection of Openlayers "Feature" instances this[_features] = new Collection([], { unique: true }); + // Add unique identifier to each created "Feature" instance - this[_features].on('add', (e) => { + this[_features].on("add", (e) => { // The ID may have already been set when drawn. However, features could // have also been added without a draw event. if (e.element.getId() === undefined) { e.element.setId(generateUID()); } + + this[_annotationManager].onAdd(e.element); + }); + + this[_features].on("remove", (e) => { + this[_annotationManager].onRemove(e.element); }); /* * To visualize images accross multiple scales, we first need to * determine the image pyramid structure, i.e. the size and resolution * images at the different pyramid levels. - */ + */ this[_metadata] = []; - options.metadata.forEach(m => { + options.metadata.forEach((m) => { const image = new VLWholeSlideMicroscopyImage({ metadata: m }); - if (image.ImageType[2] === 'VOLUME') { + if (image.ImageType[2] === "VOLUME") { this[_metadata].push(image); } }); + if (this[_metadata].length === 0) { - throw new Error('No VOLUME image provided.') + throw new Error("No VOLUME image provided."); } + // Sort instances and optionally concatenation parts if present. this[_metadata].sort((a, b) => { const sizeDiff = a.TotalPixelMatrixColumns - b.TotalPixelMatrixColumns; if (sizeDiff === 0) { if (a.ConcatenationFrameOffsetNumber !== undefined) { - return a.ConcatenationFrameOffsetNumber - b.ConcatenationFrameOffsetNumber; + return ( + a.ConcatenationFrameOffsetNumber - b.ConcatenationFrameOffsetNumber + ); } return sizeDiff; } return sizeDiff; }); + this[_pyramidMetadata] = []; this[_pyramidFrameMappings] = []; - let frameMappings = this[_metadata].map(m => getFrameMapping(m)); + let frameMappings = this[_metadata].map((m) => getFrameMapping(m)); for (let i = 0; i < this[_metadata].length; i++) { const cols = this[_metadata][i].TotalPixelMatrixColumns; const rows = this[_metadata][i].TotalPixelMatrixRows; const numberOfFrames = this[_metadata][i].NumberOfFrames; /* - * Instances may be broken down into multiple concatentation parts. + * Instances may be broken down into multiple concatenation parts. * Therefore, we have to re-assemble instance metadata. - */ + */ let alreadyExists = false; let index = null; for (let j = 0; j < this[_pyramidMetadata].length; j++) { if ( - (this[_pyramidMetadata][j].TotalPixelMatrixColumns === cols) && - (this[_pyramidMetadata][j].TotalPixelMatrixRows === rows) + this[_pyramidMetadata][j].TotalPixelMatrixColumns === cols && + this[_pyramidMetadata][j].TotalPixelMatrixRows === rows ) { alreadyExists = true; index = j; } } if (alreadyExists) { - // Update with information obtained from current concatentation part. + // Update with information obtained from current concatenation part. Object.assign(this[_pyramidFrameMappings][index], frameMappings[i]); this[_pyramidMetadata][index].NumberOfFrames += numberOfFrames; if ("PerFrameFunctionalGroupsSequence" in this[_metadata][index]) { @@ -551,12 +596,14 @@

      Source: viewer.js

      if (!"SOPInstanceUIDOfConcatenationSource" in this[_metadata][i]) { throw new Error( 'Attribute "SOPInstanceUIDOfConcatenationSource" is required ' + - 'for concatenation parts.' + "for concatenation parts." ); } - const sopInstanceUID = this[_metadata][i].SOPInstanceUIDOfConcatenationSource; + const sopInstanceUID = this[_metadata][i] + .SOPInstanceUIDOfConcatenationSource; this[_pyramidMetadata][index].SOPInstanceUID = sopInstanceUID; - delete this[_pyramidMetadata][index].SOPInstanceUIDOfConcatenationSource; + delete this[_pyramidMetadata][index] + .SOPInstanceUIDOfConcatenationSource; delete this[_pyramidMetadata][index].ConcatenationUID; delete this[_pyramidMetadata][index].InConcatenationNumber; delete this[_pyramidMetadata][index].ConcatenationFrameOffsetNumber; @@ -565,48 +612,51 @@

      Source: viewer.js

      this[_pyramidFrameMappings].push(frameMappings[i]); } } + const nLevels = this[_pyramidMetadata].length; + console.debug("Pyramid levels:", nLevels); if (nLevels === 0) { - console.error('empty pyramid - no levels found') + console.error("empty pyramid - no levels found"); } + this[_pyramidBaseMetadata] = this[_pyramidMetadata][nLevels - 1]; + /* * Collect relevant information from DICOM metadata for each pyramid * level to construct the Openlayers map. - */ + */ const tileSizes = []; const tileGridSizes = []; const resolutions = []; const origins = []; const offset = [0, -1]; - const basePixelSpacing = _getPixelSpacing(this[_pyramidBaseMetadata]); - const baseTotalPixelMatrixColumns = this[_pyramidBaseMetadata].TotalPixelMatrixColumns; - const baseTotalPixelMatrixRows = this[_pyramidBaseMetadata].TotalPixelMatrixRows; + const basePixelSpacing = getPixelSpacing(this[_pyramidBaseMetadata]); + const baseTotalPixelMatrixColumns = this[_pyramidBaseMetadata] + .TotalPixelMatrixColumns; + const baseTotalPixelMatrixRows = this[_pyramidBaseMetadata] + .TotalPixelMatrixRows; const baseColumns = this[_pyramidBaseMetadata].Columns; const baseRows = this[_pyramidBaseMetadata].Rows; const baseNColumns = Math.ceil(baseTotalPixelMatrixColumns / baseColumns); const baseNRows = Math.ceil(baseTotalPixelMatrixRows / baseRows); - for (let j = (nLevels - 1); j >= 0; j--) { + + for (let j = nLevels - 1; j >= 0; j--) { const columns = this[_pyramidMetadata][j].Columns; const rows = this[_pyramidMetadata][j].Rows; - const totalPixelMatrixColumns = this[_pyramidMetadata][j].TotalPixelMatrixColumns; - const totalPixelMatrixRows = this[_pyramidMetadata][j].TotalPixelMatrixRows; - const pixelSpacing = _getPixelSpacing(this[_pyramidMetadata][j]); + const totalPixelMatrixColumns = this[_pyramidMetadata][j] + .TotalPixelMatrixColumns; + const totalPixelMatrixRows = this[_pyramidMetadata][j] + .TotalPixelMatrixRows; + const pixelSpacing = getPixelSpacing(this[_pyramidMetadata][j]); const nColumns = Math.ceil(totalPixelMatrixColumns / columns); const nRows = Math.ceil(totalPixelMatrixRows / rows); - tileSizes.push([ - columns, - rows, - ]); - tileGridSizes.push([ - nColumns, - nRows, - ]); + tileSizes.push([columns, rows]); + tileGridSizes.push([nColumns, nRows]); /* * Compute the resolution at each pyramid level, since the zoom * factor may not be the same between adjacent pyramid levels. - */ + */ let zoomFactor = baseTotalPixelMatrixColumns / totalPixelMatrixColumns; resolutions.push(zoomFactor); @@ -614,7 +664,7 @@

      Source: viewer.js

      * TODO: One may have to adjust the offset slightly due to the * difference between extent of the image at a given resolution level * and the actual number of tiles (frames). - */ + */ origins.push(offset); } resolutions.reverse(); @@ -627,7 +677,7 @@

      Source: viewer.js

      const pyramidFrameMappings = this[_pyramidFrameMappings]; /* - * Define custom tile URL function to retrive frames via DICOMweb WADO-RS. + * Define custom tile URL function to retrieve frames via DICOMweb WADO-RS. */ const tileUrlFunction = (tileCoord, pixelRatio, projection) => { /* @@ -640,37 +690,48 @@

      Source: viewer.js

      * by Openlayers. */ const z = tileCoord[0]; + console.debug("Pyramid level:", z); const y = tileCoord[1] + 1; const x = tileCoord[2] + 1; const index = x + "-" + y; const path = pyramidFrameMappings[z][index]; if (path === undefined) { console.warn("tile " + index + " not found at level " + z); - return (null); + return null; } - let url = options.client.wadoURL + - "/studies/" + pyramid[z].StudyInstanceUID + - "/series/" + pyramid[z].SeriesInstanceUID + - '/instances/' + path; + let url = + options.client.wadoURL + + "/studies/" + + pyramid[z].StudyInstanceUID + + "/series/" + + pyramid[z].SeriesInstanceUID + + "/instances/" + + path; if (options.retrieveRendered) { - url = url + '/rendered'; + url = url + "/rendered"; } - return (url); - } + return url; + }; /* - * Define custonm tile loader function, which is required because the + * Define custom tile loader function, which is required because the * WADO-RS response message has content type "multipart/related". - */ + */ const tileLoadFunction = (tile, src) => { if (src !== null) { - const studyInstanceUID = DICOMwebClient.utils.getStudyInstanceUIDFromUri(src); - const seriesInstanceUID = DICOMwebClient.utils.getSeriesInstanceUIDFromUri(src); - const sopInstanceUID = DICOMwebClient.utils.getSOPInstanceUIDFromUri(src); + const studyInstanceUID = DICOMwebClient.utils.getStudyInstanceUIDFromUri( + src + ); + const seriesInstanceUID = DICOMwebClient.utils.getSeriesInstanceUIDFromUri( + src + ); + const sopInstanceUID = DICOMwebClient.utils.getSOPInstanceUIDFromUri( + src + ); const frameNumbers = DICOMwebClient.utils.getFrameNumbersFromUri(src); const img = tile.getImage(); if (options.retrieveRendered) { - const mediaType = 'image/png'; + const mediaType = "image/png"; const retrieveOptions = { studyInstanceUID, seriesInstanceUID, @@ -679,39 +740,39 @@

      Source: viewer.js

      mediaTypes: [{ mediaType }], }; if (options.includeIccProfile) { - retrieveOptions['queryParams'] = { - iccprofile: 'yes' - } + retrieveOptions["queryParams"] = { + iccprofile: "yes", + }; } - options.client.retrieveInstanceFramesRendered(retrieveOptions).then( - (renderedFrame) => { - const blob = new Blob([renderedFrame], {type: mediaType}); + options.client + .retrieveInstanceFramesRendered(retrieveOptions) + .then((renderedFrame) => { + const blob = new Blob([renderedFrame], { type: mediaType }); img.src = window.URL.createObjectURL(blob); - } - ); + }); } else { // TODO: support "image/jp2" and "image/jls" - const mediaType = 'image/jpeg'; + const mediaType = "image/jpeg"; const retrieveOptions = { studyInstanceUID, seriesInstanceUID, sopInstanceUID, frameNumbers, mediaTypes: [ - { mediaType, transferSyntaxUID: '1.2.840.10008.1.2.4.50' } - ] + { mediaType, transferSyntaxUID: "1.2.840.10008.1.2.4.50" }, + ], }; - options.client.retrieveInstanceFrames(retrieveOptions).then( - (rawFrames) => { - const blob = new Blob(rawFrames, {type: mediaType}); + options.client + .retrieveInstanceFrames(retrieveOptions) + .then((rawFrames) => { + const blob = new Blob(rawFrames, { type: mediaType }); img.src = window.URL.createObjectURL(blob); - } - ); + }); } } else { - console.warn('could not load tile'); + console.warn("could not load tile"); } - } + }; /** Frames may extend beyond the size of the total pixel matrix. * The excess pixels are empty, i.e. have only a padding value. @@ -720,12 +781,12 @@

      Source: viewer.js

      * Note that the vertical axis is flipped in the used tile source, * i.e. values on the axis lie in the range [-n, -1], where n is the * number of rows in the total pixel matrix. - */ + */ const extent = [ - 0, // min X - -(baseTotalPixelMatrixRows + 1), // min Y - baseTotalPixelMatrixColumns, // max X - -1 // max Y + 0, // min X + -(baseTotalPixelMatrixRows + 1), // min Y + baseTotalPixelMatrixColumns, // max X + -1, // max Y ]; const rotation = _getRotation(this[_pyramidBaseMetadata]); @@ -735,16 +796,16 @@

      Source: viewer.js

      * with the default Mercator projection. */ const projection = new Projection({ - code: 'DICOM', - units: 'metric', - extent: extent, + code: "DICOM", + units: "metric", + extent, getPointResolution: (pixelRes, point) => { /** DICOM Pixel Spacing has millimeter unit while the projection has * has meter unit. */ - const spacing = _getPixelSpacing(pyramid[nLevels - 1])[0] / 10 ** 3; + const spacing = getPixelSpacing(pyramid[nLevels - 1])[0] / 10 ** 3; return pixelRes * spacing; - } + }, }); /* * TODO: Register custom projection: @@ -760,11 +821,11 @@

      Source: viewer.js

      * factor between individual levels. */ const tileGrid = new TileGrid({ - extent: extent, - origins: origins, - resolutions: resolutions, + extent, + origins, + resolutions, sizes: tileGridSizes, - tileSizes: tileSizes + tileSizes, }); /* @@ -772,102 +833,137 @@

      Source: viewer.js

      * frames (load tiles) via DICOMweb WADO-RS. */ const rasterSource = new TileImage({ - crossOrigin: 'Anonymous', - tileGrid: tileGrid, - projection: projection, - wrapX: false + crossOrigin: "Anonymous", + tileGrid, + projection, + wrapX: false, }); rasterSource.setTileUrlFunction(tileUrlFunction); rasterSource.setTileLoadFunction(tileLoadFunction); this[_imageLayer] = new TileLayer({ - extent: extent, + extent, source: rasterSource, preload: 0, - projection: projection + projection, }); this[_drawingSource] = new VectorSource({ - tileGrid: tileGrid, - projection: projection, + tileGrid, + projection, features: this[_features], - wrapX: false + wrapX: false, }); this[_drawingLayer] = new VectorLayer({ - extent: extent, + extent, source: this[_drawingSource], - projection: projection, + projection, updateWhileAnimating: true, updateWhileInteracting: true, }); const view = new View({ center: getCenter(extent), - extent: extent, - projection: projection, - resolutions: resolutions, - rotation: rotation + extent, + projection, + resolutions, + rotation, }); - this[_interactions] = { - draw: undefined, - select: undefined, - modify: undefined - }; - this[_controls] = { scale: new ScaleLine({ - units: 'metric', - className: '' - }) - } - if (options.controls.has('fullscreen')) { + units: "metric", + className: "", + }), + }; + + if (options.controls.has("fullscreen")) { this[_controls].fullscreen = new FullScreen(); } - if (options.controls.has('overview')) { - const overviewImageLayer = new TileLayer({ - extent: extent, - source: rasterSource, - preload: 0, - projection: projection - }); - - const overviewView = new View({ - projection: projection, - resolutions: resolutions, - rotation: rotation - }); - - this[_controls].overview = new OverviewMap({ - view: overviewView, - layers: [overviewImageLayer], - collapsed: true, - }); - } + + const overviewImageLayer = new TileLayer({ + extent, + source: rasterSource, + preload: 0, + projection, + }); + + let overviewViewOptions = { + rotation, + projection, + /** resolutions, with this property the zoom doesn't work */ + zoom: 28 /** Default max zoom */, + }; + + console.debug("View resolutions:", resolutions); + + const overviewView = new View(overviewViewOptions); + + this[_overviewMap] = new OverviewMap({ + view: overviewView, + layers: [overviewImageLayer], + collapsed: options.overview.hasOwnProperty("collapsed") + ? options.overview.collapsed + : false, + collapsible: options.overview.hasOwnProperty("collapsible") + ? options.overview.collapsible + : false, + }); /** Creates the map with the defined layers and view and renders it. */ this[_map] = new Map({ layers: [this[_imageLayer], this[_drawingLayer]], - view: view, + view, controls: [], keyboardEventTarget: document, }); + /** + * OpenLayer's map has default active interactions + * https://openlayers.org/en/latest/apidoc/module-ol_interaction.html#.defaults + * + * We need to define them here to avoid duplications + * of interactions that could cause bugs in the application + * + * Enabling or disabling interactions could cause side effects on OverviewMap + * since it also uses the same interactions in the map + */ + const defaultInteractions = this[_map].getInteractions().getArray(); + this[_interactions] = { + draw: undefined, + select: undefined, + translate: undefined, + modify: undefined, + snap: undefined, + dragPan: defaultInteractions.find((i) => i instanceof DragPan), + }; + this[_map].addInteraction(new MouseWheelZoom()); for (let control in this[_controls]) { this[_map].addControl(this[_controls][control]); } + this[_map].getView().fit(extent); + + this[_annotationManager] = new _AnnotationManager({ + map: this[_map], + pyramid: this[_pyramidMetadata], + }); } - /** Resizes the viewer to fit the viewport. */ + /** + * Resizes the viewer to fit the viewport. + * + * @returns {void} + */ resize() { this[_map].updateSize(); } - /** Gets the size of the viewport. + /** + * Gets the size of the viewport. * * @type {number[]} */ @@ -875,45 +971,51 @@

      Source: viewer.js

      return this[_map].getSize(); } - /** Renders the images in the specified viewport container. + /** + * Renders the images in the specified viewport container. + * * @param {object} options - Rendering options. * @param {(string|HTMLElement)} options.container - HTML Element in which the viewer should be injected. */ render(options) { - if (!('container' in options)) { - console.error('container must be provided for rendering images') + if (!("container" in options)) { + console.error("container must be provided for rendering images"); } this[_map].setTarget(options.container); // Style scale element (overriding default Openlayers CSS "ol-scale-line") - let scaleElement = this[_controls]['scale'].element; - scaleElement.style.position = 'absolute'; - scaleElement.style.right = '.5em'; - scaleElement.style.bottom = '.5em'; - scaleElement.style.left = 'auto'; - scaleElement.style.padding = '2px'; - scaleElement.style.backgroundColor = 'rgba(255,255,255,.5)'; - scaleElement.style.borderRadius = '4px'; - scaleElement.style.margin = '1px'; - - let scaleInnerElement = this[_controls]['scale'].innerElement_; - scaleInnerElement.style.color = 'black'; - scaleInnerElement.style.fontWeight = '600'; - scaleInnerElement.style.fontSize = '10px'; - scaleInnerElement.style.textAlign = 'center'; - scaleInnerElement.style.borderWidth = '1.5px'; - scaleInnerElement.style.borderStyle = 'solid'; - scaleInnerElement.style.borderTop = 'none'; - scaleInnerElement.style.borderRightColor = 'black'; - scaleInnerElement.style.borderLeftColor = 'black'; - scaleInnerElement.style.borderBottomColor = 'black'; - scaleInnerElement.style.margin = '1px'; - scaleInnerElement.style.willChange = 'contents,width'; + let scaleElement = this[_controls]["scale"].element; + scaleElement.style.position = "absolute"; + scaleElement.style.right = ".5em"; + scaleElement.style.bottom = ".5em"; + scaleElement.style.left = "auto"; + scaleElement.style.padding = "2px"; + scaleElement.style.backgroundColor = "rgba(255,255,255,.5)"; + scaleElement.style.borderRadius = "4px"; + scaleElement.style.margin = "1px"; + + let scaleInnerElement = this[_controls]["scale"].innerElement_; + scaleInnerElement.style.color = "black"; + scaleInnerElement.style.fontWeight = "600"; + scaleInnerElement.style.fontSize = "10px"; + scaleInnerElement.style.textAlign = "center"; + scaleInnerElement.style.borderWidth = "1.5px"; + scaleInnerElement.style.borderStyle = "solid"; + scaleInnerElement.style.borderTop = "none"; + scaleInnerElement.style.borderRightColor = "black"; + scaleInnerElement.style.borderLeftColor = "black"; + scaleInnerElement.style.borderBottomColor = "black"; + scaleInnerElement.style.margin = "1px"; + scaleInnerElement.style.willChange = "contents,width"; const container = this[_map].getTargetElement(); this[_drawingSource].on(VectorEventType.ADDFEATURE, (e) => { - publish(container, EVENT.ROI_ADDED, _getROIFromFeature(e.feature, this[_pyramidMetadata])); + publish( + container, + EVENT.ROI_ADDED, + this._getROIFromFeature(e.feature, this[_pyramidMetadata]) + ); }); this[_drawingSource].on(VectorEventType.CHANGEFEATURE, (e) => { @@ -921,11 +1023,11 @@

      Source: viewer.js

      const geometry = e.feature.getGeometry(); const type = geometry.getType(); // The first and last point of a polygon must be identical. The last point - // is an implmentation detail and is hidden from the user in the graphical + // is an implementation detail and is hidden from the user in the graphical // interface. However, we must update the last point in case the first - // piont has been modified by the user. - if (type === 'Polygon') { - // NOTE: Polyon in GeoJSON format contains an array of geometries, + // point has been modified by the user. + if (type === "Polygon") { + // NOTE: Polygon in GeoJSON format contains an array of geometries, // where the first element represents the coordinates of the outer ring // and the second element represents the coordinates of the inner ring // (in our case the inner ring should not be present). @@ -933,101 +1035,182 @@

      Source: viewer.js

      const coordinates = geometry.getCoordinates(); const firstPoint = coordinates[0][0]; const lastPoint = coordinates[0][coordinates[0].length - 1]; - if (firstPoint[0] !== lastPoint[0] || firstPoint[1] !== lastPoint[1]) { + if ( + firstPoint[0] !== lastPoint[0] || + firstPoint[1] !== lastPoint[1] + ) { coordinates[0][coordinates[0].length - 1] = firstPoint; geometry.setCoordinates(coordinates, layout); e.feature.setGeometry(geometry); } } } - publish(container, EVENT.ROI_MODIFIED, _getROIFromFeature(e.feature, this[_pyramidMetadata])); + publish( + container, + EVENT.ROI_MODIFIED, + this._getROIFromFeature(e.feature, this[_pyramidMetadata]) + ); }); this[_drawingSource].on(VectorEventType.REMOVEFEATURE, (e) => { - publish(container, EVENT.ROI_REMOVED, _getROIFromFeature(e.feature, this[_pyramidMetadata])); + publish( + container, + EVENT.ROI_REMOVED, + this._getROIFromFeature(e.feature, this[_pyramidMetadata]) + ); }); } - /** Activates the draw interaction for graphic annotation of regions of interest. + /** + * Activates the draw interaction for graphic annotation of regions of interest. + * * @param {object} options - Drawing options. * @param {string} options.geometryType - Name of the geometry type (point, circle, box, polygon, freehandPolygon, line, freehandLine) + * @param {string} options.marker - Marker + * @param {string} options.markup - Markup + * @param {number} options.maxPoints - Geometry max points + * @param {number} options.minPoints - Geometry min points + * @param {boolean} options.vertexEnabled - Enable vertex + * @param {object} options.styleOptions - Style options + * @param {object} options.styleOptions.stroke - Style options for the outline of the geometry + * @param {number[]} options.styleOptions.stroke.color - RGBA color of the outline + * @param {number} options.styleOptions.stroke.width - Width of the outline + * @param {object} options.styleOptions.fill - Style options for body the geometry + * @param {number[]} options.styleOptions.fill.color - RGBA color of the body + * @param {object} options.styleOptions.image - Style options for image */ - activateDrawInteraction(options) { + activateDrawInteraction(options = {}) { this.deactivateDrawInteraction(); - console.info('activate "draw" interaction') + console.info('activate "draw" interaction'); - const customOptionsMapping = { + const geometryOptionsMapping = { point: { - type: 'Point', - geometryName: 'Point' + type: "Point", + geometryName: "Point", }, circle: { - type: 'Circle', - geometryName: 'Circle' + type: "Circle", + geometryName: "Circle", }, box: { - type: 'Circle', - geometryName: 'Box', + type: "Circle", + geometryName: "Box", geometryFunction: createRegularPolygon(4), }, polygon: { - type: 'Polygon', - geometryName: 'Polygon', + type: "Polygon", + geometryName: "Polygon", freehand: false, }, freehandpolygon: { - type: 'Polygon', - geometryName: 'FreeHandPolygon', + type: "Polygon", + geometryName: "FreeHandPolygon", freehand: true, }, line: { - type: 'LineString', - geometryName: 'Line', + type: "LineString", + geometryName: "Line", freehand: false, + maxPoints: options.maxPoints, + minPoints: options.minPoints, }, freehandline: { - type: 'LineString', - geometryName: 'FreeHandLine', + type: "LineString", + geometryName: "FreeHandLine", freehand: true, }, }; - if (!('geometryType' in options)) { - console.error('geometry type must be specified for drawing interaction') + + if (!("geometryType" in options)) { + console.error("geometry type must be specified for drawing interaction"); } - if (!(options.geometryType in customOptionsMapping)) { - console.error(`unsupported geometry type "${options.geometryType}"`) + + if (!(options.geometryType in geometryOptionsMapping)) { + console.error(`unsupported geometry type "${options.geometryType}"`); } - const defaultDrawOptions = { source: this[_drawingSource] }; - const customDrawOptions = customOptionsMapping[options.geometryType]; - if ('style' in options) { - customDrawOptions.style = options.style; + const internalDrawOptions = { source: this[_drawingSource] }; + const geometryDrawOptions = geometryOptionsMapping[options.geometryType]; + const builtInDrawOptions = { + [Enums.InternalProperties.Marker]: + options[Enums.InternalProperties.Marker], + [Enums.InternalProperties.Markup]: + options[Enums.InternalProperties.Markup], + vertexEnabled: options.vertexEnabled, + [Enums.InternalProperties.Label]: options[Enums.InternalProperties.Label], + }; + const drawOptions = Object.assign( + internalDrawOptions, + geometryDrawOptions, + builtInDrawOptions + ); + + /** + * This used to define which mouse buttons will fire the action. + * + * bindings: { + * mouseButtons can be 'left', 'right' and/or 'middle'. if absent, the action is bound to all mouse buttons. + * mouseButtons: ['left', 'right'], + * modifierKey can be 'shift', 'ctrl' or 'alt'. If not present, the action is bound to no modifier key. + * modifierKey: 'ctrl' // The modifier + * }, + */ + if (options.bindings) { + drawOptions.condition = _getInteractionBindingCondition(options.bindings); } - const allDrawOptions = Object.assign(defaultDrawOptions, customDrawOptions); - this[_interactions].draw = new Draw(allDrawOptions); + this[_interactions].draw = new Draw(drawOptions); const container = this[_map].getTargetElement(); - //attaching openlayers events handling - this[_interactions].draw.on('drawend', (e) => { - e.feature.setId(generateUID()); - publish(container, EVENT.ROI_DRAWN, _getROIFromFeature(e.feature, this[_pyramidMetadata])); + this[_interactions].draw.on(Enums.InteractionEvents.DRAW_START, (event) => { + event.feature.setProperties(builtInDrawOptions, true); + event.feature.setId(generateUID()); + + /** Set external styles before calling internal annotation hooks */ + _setFeatureStyle( + event.feature, + options[Enums.InternalProperties.StyleOptions] + ); + + this[_annotationManager].onDrawStart(event); + + _wireMeasurementsAndQualitativeEvaluationsEvents( + this[_map], + event.feature, + this[_pyramidMetadata] + ); }); - this[_map].addInteraction(this[_interactions].draw); + this[_interactions].draw.on(Enums.InteractionEvents.DRAW_ABORT, (event) => { + this[_annotationManager].onDrawAbort(event); + }); + this[_interactions].draw.on(Enums.InteractionEvents.DRAW_END, (event) => { + this[_annotationManager].onDrawEnd(event); + publish( + container, + EVENT.ROI_DRAWN, + this._getROIFromFeature(event.feature, this[_pyramidMetadata]) + ); + }); + + this[_map].addInteraction(this[_interactions].draw); } - /** Deactivates draw interaction. */ + /** + * Deactivates draw interaction. + * @returns {void} + */ deactivateDrawInteraction() { - console.info('deactivate "draw" interaction') + console.info('deactivate "draw" interaction'); if (this[_interactions].draw !== undefined) { this[_map].removeInteraction(this[_interactions].draw); this[_interactions].draw = undefined; } } - /** Whether draw interaction is active + /** + * Whether draw interaction is active * * @type {boolean} */ @@ -1035,36 +1218,281 @@

      Source: viewer.js

      return this[_interactions].draw !== undefined; } - /* Activates select interaction. + /** + * Whether dragPan interaction is active. + * + * @type {boolean} + */ + get isDragPanInteractionActive() { + return this[_interactions].dragPan !== undefined; + } + + /** + * Whether dragZoom interaction is active. + * + * @type {boolean} + */ + get isDragZoomInteractionActive() { + return this[_interactions].dragZoom !== undefined; + } + + /** + * Whether translate interaction is active. + * + * @type {boolean} + */ + get isTranslateInteractionActive() { + return this[_interactions].translate !== undefined; + } + + /** + * Activates translate interaction. + * + * @param {Object} options - Translation options. + */ + activateTranslateInteraction(options = {}) { + this.deactivateTranslateInteraction(); + + console.info('activate "translate" interaction'); + + const translateOptions = { layers: [this[_drawingLayer]] }; + + /** + * Get conditional mouse bindings + * See "options.binding" comment in activateDrawInteraction() definition. + */ + if (options.bindings) { + translateOptions.condition = _getInteractionBindingCondition( + options.bindings + ); + } + + this[_interactions].translate = new Translate(translateOptions); + + this[_map].addInteraction(this[_interactions].translate); + } + + /** + * Extracts and transforms the region of interest (ROI) from an Openlayers + * Feature. + * + * @param {object} feature - Openlayers Feature + * @param {Object[]} pyramid - Metadata for resolution levels of image pyramid + * @param {Object} context - Context + * @returns {ROI} Region of interest + * @private + */ + _getROIFromFeature(feature, pyramid) { + if (feature !== undefined && feature !== null) { + let scoord3d; + try { + scoord3d = geometry2Scoord3d(feature, pyramid); + } catch (error) { + const uid = feature.getId(); + this.removeROI(uid); + throw error; + } + + const properties = feature.getProperties(); + // Remove geometry from properties mapping + const geometryName = feature.getGeometryName(); + delete properties[geometryName]; + const uid = feature.getId(); + const roi = new ROI({ scoord3d, properties, uid }); + return roi; + } + return; + } + + /** + * Toggles overview map. + * + * @returns {void} + */ + toggleOverviewMap() { + const controls = this[_map].getControls(); + const overview = controls.getArray().find((c) => c === this[_overviewMap]); + if (overview) { + this[_map].removeControl(this[_overviewMap]); + return; + } + this[_map].addControl(this[_overviewMap]); + } + + /** + * Deactivates translate interaction. + * + * @returns {void} + */ + deactivateTranslateInteraction() { + console.info('deactivate "translate" interaction'); + if (this[_interactions].translate) { + this[_map].removeInteraction(this[_interactions].translate); + this[_interactions].translate = undefined; + } + } + + /** + * Activates dragZoom interaction. * - * @param {object} options - Selection options. + * @param {object} options - DragZoom options. + */ + activateDragZoomInteraction(options = {}) { + this.deactivateDragZoomInteraction(); + + console.info('activate "dragZoom" interaction'); + + const dragZoomOptions = { layers: [this[_drawingLayer]] }; + + /** + * Get conditional mouse bindings + * See "options.binding" comment in activateDrawInteraction() definition. + */ + if (options.bindings) { + dragZoomOptions.condition = _getInteractionBindingCondition( + options.bindings + ); + } + + this[_interactions].dragZoom = new DragZoom(dragZoomOptions); + + this[_map].addInteraction(this[_interactions].dragZoom); + } + + /** + * Deactivates dragZoom interaction. + * @returns {void} + */ + deactivateDragZoomInteraction() { + console.info('deactivate "dragZoom" interaction'); + if (this[_interactions].dragZoom) { + this[_map].removeInteraction(this[_interactions].dragZoom); + this[_interactions].dragZoom = undefined; + } + } + + /** + * Activates select interaction. + * + * @param {object} options selection options. */ activateSelectInteraction(options = {}) { this.deactivateSelectInteraction(); - console.info('activate "select" interaction') - this[_interactions].select = new Select({ - layers: [this[_drawingLayer]] - }); + + console.info('activate "select" interaction'); + + const selectOptions = { layers: [this[_drawingLayer]] }; + + /** + * Get conditional mouse bindings + * See "options.binding" comment in activateDrawInteraction() definition. + */ + if (options.bindings) { + selectOptions.condition = _getInteractionBindingCondition( + options.bindings + ); + } + + this[_interactions].select = new Select(selectOptions); const container = this[_map].getTargetElement(); - this[_interactions].select.on('select', (e) => { - publish(container, EVENT.ROI_SELECTED, _getROIFromFeature(e.selected[0], this[_pyramidMetadata])); + this[_interactions].select.on("select", (e) => { + publish( + container, + EVENT.ROI_SELECTED, + this._getROIFromFeature(e.selected[0], this[_pyramidMetadata]) + ); }); this[_map].addInteraction(this[_interactions].select); } - /** Deactivates select interaction. */ + /** + * Deactivates select interaction. + * + * @returns {void} + */ deactivateSelectInteraction() { - console.info('deactivate "select" interaction') + console.info('deactivate "select" interaction'); if (this[_interactions].select) { this[_map].removeInteraction(this[_interactions].select); this[_interactions].select = undefined; } } - /** Whether select interaction is active. + /** + * Activates dragpan interaction. + * + * @param {Object} options - DragPan options. + */ + activateDragPanInteraction(options = {}) { + this.deactivateDragPanInteraction(); + + console.info('activate "drag pan" interaction'); + + const dragPanOptions = { + features: this[_features], + }; + + /** + * Get conditional mouse bindings + * See "options.binding" comment in activateDrawInteraction() definition. + */ + if (options.bindings) { + dragPanOptions.condition = _getInteractionBindingCondition( + options.bindings + ); + } + + this[_interactions].dragPan = new DragPan(dragPanOptions); + + this[_map].addInteraction(this[_interactions].dragPan); + } + + /** + * Deactivate dragpan interaction. + * + * @returns {void} + */ + deactivateDragPanInteraction() { + console.info('deactivate "drag pan" interaction'); + if (this[_interactions].dragPan) { + this[_map].removeInteraction(this[_interactions].dragPan); + this[_interactions].dragPan = undefined; + } + } + + /** + * Activates snap interaction. + * + * @param {Object} options - Snap options. + */ + activateSnapInteraction(options = {}) { + this.deactivateSnapInteraction(); + console.info('activate "snap" interaction'); + this[_interactions].snap = new Snap({ + source: this[_drawingSource], + }); + + this[_map].addInteraction(this[_interactions].snap); + } + + /** + * Deactivates snap interaction. + * + * @returns {void} + */ + deactivateSnapInteraction() { + console.info('deactivate "snap" interaction'); + if (this[_interactions].snap) { + this[_map].removeInteraction(this[_interactions].snap); + this[_interactions].snap = undefined; + } + } + + /** + * Whether select interaction is active. * * @type {boolean} */ @@ -1078,23 +1506,41 @@

      Source: viewer.js

      */ activateModifyInteraction(options = {}) { this.deactivateModifyInteraction(); - console.info('activate "modify" interaction') - this[_interactions].modify = new Modify({ - features: this[_features], // TODO: or source, i.e. "drawings"??? - }); + + console.info('activate "modify" interaction'); + + const modifyOptions = { + features: this[_features], // TODO: or source, i.e. 'drawings'??? + insertVertexCondition: ({ feature }) => + feature && feature.get("vertexEnabled") === true, + }; + + /** + * Get conditional mouse bindings + * See "options.binding" comment in activateDrawInteraction() definition. + */ + if (options.bindings) { + modifyOptions.condition = _getInteractionBindingCondition( + options.bindings + ); + } + + this[_interactions].modify = new Modify(modifyOptions); + this[_map].addInteraction(this[_interactions].modify); } /** Deactivates modify interaction. */ deactivateModifyInteraction() { - console.info('deactivate "modify" interaction') + console.info('deactivate "modify" interaction'); if (this[_interactions].modify) { this[_map].removeInteraction(this[_interactions].modify); this[_interactions].modify = undefined; } } - /** Whether modify interaction is active. + /** + * Whether modify interaction is active. * * @type {boolean} */ @@ -1102,12 +1548,13 @@

      Source: viewer.js

      return this[_interactions].modify !== undefined; } - /** Gets all annotated regions of interest. + /** + * Gets all annotated regions of interest. * * @returns {ROI[]} Array of regions of interest. */ getAllROIs() { - console.info('get all ROIs') + console.info("get all ROIs"); let rois = []; this[_features].forEach((item) => { rois.push(this.getROI(item.getId())); @@ -1115,7 +1562,16 @@

      Source: viewer.js

      return rois; } - /** Number of annotated regions of interest. + collapseOverviewMap() { + this[_controls].overview.setCollapsed(true); + } + + expandOverviewMap() { + this[_controls].overview.setCollapsed(true); + } + + /** + * Number of annotated regions of interest. * * @type {number} */ @@ -1123,51 +1579,62 @@

      Source: viewer.js

      return this[_features].getLength(); } - /** Gets an individual annotated regions of interest. + /** + * Gets an individual annotated regions of interest. * * @param {string} uid - Unique identifier of the region of interest * @returns {ROI} Regions of interest. */ getROI(uid) { - console.info(`get ROI ${uid}`) + console.info(`get ROI ${uid}`); const feature = this[_drawingSource].getFeatureById(uid); - return _getROIFromFeature(feature, this[_pyramidMetadata]); + return this._getROIFromFeature(feature, this[_pyramidMetadata]); } - /** Adds a measurement to a region of interest. + /** + * Adds a measurement to a region of interest. * * @param {string} uid - Unique identifier of the region of interest * @param {Object} item - NUM content item representing a measurement */ addROIMeasurement(uid, item) { - const meaning = item.ConceptNameCodeSequence[0].CodeMeaning - console.info(`add measurement "${meaning}" to ROI ${uid}`) - this[_features].forEach(feature => { + const meaning = item.ConceptNameCodeSequence[0].CodeMeaning; + console.info(`add measurement "${meaning}" to ROI ${uid}`); + this[_features].forEach((feature) => { const id = feature.getId(); if (id === uid) { const properties = feature.getProperties(); - properties['measurements'].push(item); + if (!(Enums.InternalProperties.Measurements in properties)) { + properties[Enums.InternalProperties.Measurements] = [item]; + } else { + properties[Enums.InternalProperties.Measurements].push(item); + } feature.setProperties(properties, true); } - }) + }); } - /** Adds a qualitative evaluation to a region of interest. + /** + * Adds a qualitative evaluation to a region of interest. * * @param {string} uid - Unique identifier of the region of interest * @param {Object} item - CODE content item representing a qualitative evaluation */ addROIEvaluation(uid, item) { - const meaning = item.ConceptNameCodeSequence[0].CodeMeaning - console.info(`add qualitative evaluation "${meaning}" to ROI ${uid}`) - this[_features].forEach(feature => { + const meaning = item.ConceptNameCodeSequence[0].CodeMeaning; + console.info(`add qualitative evaluation "${meaning}" to ROI ${uid}`); + this[_features].forEach((feature) => { const id = feature.getId(); if (id === uid) { const properties = feature.getProperties(); - properties['evaluations'].push(item); + if (!(Enums.InternalProperties.Evaluations in properties)) { + properties[Enums.InternalProperties.Evaluations] = [item]; + } else { + properties[Enums.InternalProperties.Evaluations].push(item); + } feature.setProperties(properties, true); } - }) + }); } /** Pops the most recently annotated regions of interest. @@ -1175,25 +1642,95 @@

      Source: viewer.js

      * @returns {ROI} Regions of interest. */ popROI() { - console.info('pop ROI') + console.info("pop ROI"); const feature = this[_features].pop(); - return _getROIFromFeature(feature, this[_pyramidMetadata]); + return this._getROIFromFeature(feature, this[_pyramidMetadata]); } - /** Adds a regions of interest. + /** + * Adds a regions of interest. + * + * @param {ROI} roi - Regions of interest + * @param {object} roi.properties - ROI properties + * @param {object} roi.properties.measurements - ROI measurements + * @param {object} roi.properties.evaluations - ROI evaluations + * @param {object} roi.properties.label - ROI label + * @param {object} roi.properties.marker - ROI marker (this is used while we don't have presentation states) + * @param {object} styleOptions - Style options + * @param {object} styleOptions.stroke - Style options for the outline of the geometry + * @param {number[]} styleOptions.stroke.color - RGBA color of the outline + * @param {number} styleOptions.stroke.width - Width of the outline + * @param {object} styleOptions.fill - Style options for body the geometry + * @param {number[]} styleOptions.fill.color - RGBA color of the body + * @param {object} styleOptions.image - Style options for image * - * @param {ROI} Regions of interest. */ - addROI(item) { - console.info(`add ROI ${item.uid}`) - const geometry = _scoord3d2Geometry(item.scoord3d, this[_pyramidMetadata]); - const feature = new Feature(geometry); - feature.setProperties(item.properties, true); - feature.setId(item.uid); + addROI(roi, styleOptions) { + console.info(`add ROI ${roi.uid}`); + const geometry = scoord3d2Geometry(roi.scoord3d, this[_pyramidMetadata]); + const featureOptions = { geometry }; + + const feature = new Feature(featureOptions); + _addROIPropertiesToFeature(feature, roi.properties, true); + feature.setId(roi.uid); + + _wireMeasurementsAndQualitativeEvaluationsEvents( + this[_map], + feature, + this[_pyramidMetadata] + ); + this[_features].push(feature); + + _setFeatureStyle(feature, styleOptions); } - /** Adds a new viewport overlay. + /** + * Update properties of regions of interest. + * + * @param {object} roi - ROI to be updated + * @param {string} roi.uid - Unique identifier of the region of interest + * @param {object} roi.properties - ROI properties + * @param {object} roi.properties.measurements - ROI measurements + * @param {object} roi.properties.evaluations - ROI evaluations + * @param {object} roi.properties.label - ROI label + * @param {object} roi.properties.marker - ROI marker (this is used while we don't have presentation states) + */ + updateROI({ uid, properties = {} }) { + if (!uid) return; + console.info(`update ROI ${uid}`); + + const feature = this[_drawingSource].getFeatureById(uid); + + _addROIPropertiesToFeature(feature, properties); + + this[_annotationManager].onUpdate(feature); + } + + /** + * Sets the style of a region of interest. + * + * @param {string} uid - Unique identifier of the regions of interest. + * @param {object} styleOptions - Style options + * @param {object} styleOptions.stroke - Style options for the outline of the geometry + * @param {number[]} styleOptions.stroke.color - RGBA color of the outline + * @param {number} styleOptions.stroke.width - Width of the outline + * @param {object} styleOptions.fill - Style options for body the geometry + * @param {number[]} styleOptions.fill.color - RGBA color of the body + * @param {object} styleOptions.image - Style options for image + * + */ + setROIStyle(uid, styleOptions) { + this[_features].forEach((feature) => { + const id = feature.getId(); + if (id === uid) { + _setFeatureStyle(feature, styleOptions); + } + }); + } + + /** + * Adds a new viewport overlay. * * @param {object} options Overlay options * @param {object} options.element The custom overlay html element @@ -1203,39 +1740,59 @@

      Source: viewer.js

      this[_map].addOverlay(new Overlay({ element, className })); } - /** Removes an individual regions of interest. + /** + * Removes an individual regions of interest. * * @param {string} uid - Unique identifier of the region of interest */ removeROI(uid) { - console.info(`remove ROI ${uid}`) + console.info(`remove ROI ${uid}`); const feature = this[_drawingSource].getFeatureById(uid); - this[_features].remove(feature); + + if (feature) { + this[_features].remove(feature); + return; + } + + /** + * If failed to draw/cache feature in drawing source, call onFailure + * to avoid trash of broken annotations + */ + this[_annotationManager].onFailure(uid); } - /** Removes all annotated regions of interest. */ + /** + * Removes all annotated regions of interest. + * + * @returns {void} + */ removeAllROIs() { - console.info('remove all ROIs') + console.info("remove all ROIs"); this[_features].clear(); } - /** Hides annotated regions of interest such that they are no longer + /** + * Hides annotated regions of interest such that they are no longer * visible on the viewport. */ hideROIs() { - console.info('hide ROIs') + console.info("hide ROIs"); this[_drawingLayer].setVisible(false); + this[_annotationManager].setVisible(false); } - /** Shows annotated regions of interest such that they become visible + /** + * Shows annotated regions of interest such that they become visible * on the viewport ontop of the images. */ showROIs() { - console.info('show ROIs') + console.info("show ROIs"); this[_drawingLayer].setVisible(true); + this[_annotationManager].setVisible(true); } - /** Whether annotated regions of interest are currently visible. + /** + * Whether annotated regions of interest are currently visible. * * @type {boolean} */ @@ -1243,26 +1800,26 @@

      Source: viewer.js

      return this[_drawingLayer].getVisible(); } - /** DICOM metadata for each VL Whole Slide Microscopy Image instance. + /** + * DICOM metadata for each VL Whole Slide Microscopy Image instance. * * @type {VLWholeSlideMicroscopyImage[]} */ get imageMetadata() { return this[_pyramidMetadata]; } - } - -/** Static viewer for DICOM VL Whole Slide Microscopy Image instances +/** + * Static viewer for DICOM VL Whole Slide Microscopy Image instances * with Image Type other than VOLUME. * * @class * @private */ class _NonVolumeImageViewer { - - /** Creates a viewer instance for displaying non-VOLUME images. + /** + * Creates a viewer instance for displaying non-VOLUME images. * * @param {object} options * @param {object} options.client - A DICOMwebClient instance for interacting with an origin server over HTTP. @@ -1275,82 +1832,87 @@

      Source: viewer.js

      this[_client] = options.client; this[_metadata] = new VLWholeSlideMicroscopyImage({ - metadata: options.metadata + metadata: options.metadata, }); - if (this[_metadata].ImageType[2] === 'VOLUME') { - throw new Error('Viewer cannot render images of type VOLUME.') + + if (this[_metadata].ImageType[2] === "VOLUME") { + throw new Error("Viewer cannot render images of type VOLUME."); } const resizeFactor = options.resizeFactor ? options.resizeFactor : 1; const height = this[_metadata].TotalPixelMatrixRows * resizeFactor; const width = this[_metadata].TotalPixelMatrixColumns * resizeFactor; const extent = [ - 0, // min X - -(height + 1), // min Y - width, // max X - -1 // max Y + 0, // min X + -(height + 1), // min Y + width, // max X + -1, // max Y ]; const imageLoadFunction = (image, src) => { - console.info('load image') - const studyInstanceUID = DICOMwebClient.utils.getStudyInstanceUIDFromUri(src); - const seriesInstanceUID = DICOMwebClient.utils.getSeriesInstanceUIDFromUri(src); + console.info("load image"); + const studyInstanceUID = DICOMwebClient.utils.getStudyInstanceUIDFromUri( + src + ); + const seriesInstanceUID = DICOMwebClient.utils.getSeriesInstanceUIDFromUri( + src + ); const sopInstanceUID = DICOMwebClient.utils.getSOPInstanceUIDFromUri(src); - const mediaType = 'image/png'; + const mediaType = "image/png"; const queryParams = { viewport: [ this[_metadata].TotalPixelMatrixRows, - this[_metadata].TotalPixelMatrixColumns - ].join(',') + this[_metadata].TotalPixelMatrixColumns, + ].join(","), }; // We make this optional because a) not all archives currently support // this query parameter and b) because ICC Profiles can be large and // their inclusion can result in significant overhead. if (options.includeIccProfile) { - queryParams['iccprofile'] = 'yes'; + queryParams["iccprofile"] = "yes"; } const retrieveOptions = { studyInstanceUID: this[_metadata].StudyInstanceUID, seriesInstanceUID: this[_metadata].SeriesInstanceUID, sopInstanceUID: this[_metadata].SOPInstanceUID, mediaTypes: [{ mediaType }], - queryParams: queryParams + queryParams: queryParams, }; - options.client.retrieveInstanceRendered(retrieveOptions).then((thumbnail) => { - const blob = new Blob([thumbnail], { type: mediaType }); - image.getImage().src = window.URL.createObjectURL(blob); - }); - } + options.client + .retrieveInstanceRendered(retrieveOptions) + .then((thumbnail) => { + const blob = new Blob([thumbnail], { type: mediaType }); + image.getImage().src = window.URL.createObjectURL(blob); + }); + }; const projection = new Projection({ - code: 'DICOM', - units: 'metric', + code: "DICOM", + units: "metric", extent: extent, getPointResolution: (pixelRes, point) => { /** DICOM Pixel Spacing has millimeter unit while the projection has * has meter unit. */ - const mmSpacing = _getPixelSpacing(this[_metadata])[0]; - const spacing = (mmSpacing / resizeFactor) / 10 ** 3; + const mmSpacing = getPixelSpacing(this[_metadata])[0]; + const spacing = mmSpacing / resizeFactor / 10 ** 3; return pixelRes * spacing; - } + }, }); const rasterSource = new Static({ - crossOrigin: 'Anonymous', + crossOrigin: "Anonymous", imageExtent: extent, projection: projection, imageLoadFunction: imageLoadFunction, - url: '' // will be set by imageLoadFunction() + url: "", // will be set by imageLoadFunction() }); - this[_imageLayer] = new ImageLayer({ - source: rasterSource, - }); + this[_imageLayer] = new ImageLayer({ source: rasterSource }); // The default rotation is 'horizontal' with the slide label on the right var rotation = _getRotation(this[_metadata]); - if (options.orientation === 'vertical') { + if (options.orientation === "vertical") { // Rotate counterclockwise by 90 degrees to have slide label at the top rotation -= 90 * (Math.PI / 180); } @@ -1358,7 +1920,7 @@

      Source: viewer.js

      const view = new View({ center: getCenter(extent), rotation: rotation, - projection: projection + projection: projection, }); // Creates the map with the defined layers and view and renders it. @@ -1368,6 +1930,7 @@

      Source: viewer.js

      controls: [], keyboardEventTarget: document, }); + this[_map].getView().fit(extent); } @@ -1376,17 +1939,18 @@

      Source: viewer.js

      * @param {(string|HTMLElement)} options.container - HTML Element in which the viewer should be injected. */ render(options) { - if (!('container' in options)) { - console.error('container must be provided for rendering images') + if (!("container" in options)) { + console.error("container must be provided for rendering images"); } this[_map].setTarget(options.container); - this[_map].getInteractions().forEach(interaction => { + this[_map].getInteractions().forEach((interaction) => { this[_map].removeInteraction(interaction); }); } - /** DICOM metadata for the displayed VL Whole Slide Microscopy Image instance. + /** + * DICOM metadata for the displayed VL Whole Slide Microscopy Image instance. * * @type {VLWholeSlideMicroscopyImage} */ @@ -1394,29 +1958,33 @@

      Source: viewer.js

      return this[_metadata]; } - /** Resizes the viewer to fit the viewport. */ + /** + * Resizes the viewer to fit the viewport. + * + * @returns {void} + */ resize() { this[_map].updateSize(); } - /** Gets the size of the viewport. + /** + * Gets the size of the viewport. * * @type {number[]} */ get size() { return this[_map].getSize(); } - } -/** Static viewer for DICOM VL Whole Slide Microscopy Image instances +/** + * Static viewer for DICOM VL Whole Slide Microscopy Image instances * with Image Type OVERVIEW. * * @class * @memberof viewer */ class OverviewImageViewer extends _NonVolumeImageViewer { - /** Creates a viewer instance for displaying OVERVIEW images. * * @param {object} options @@ -1428,20 +1996,20 @@

      Source: viewer.js

      */ constructor(options) { if (options.orientation === undefined) { - options.orientation = 'horizontal'; + options.orientation = "horizontal"; } - super(options) + super(options); } } -/** Static viewer for DICOM VL Whole Slide Microscopy Image instances +/** + * Static viewer for DICOM VL Whole Slide Microscopy Image instances * with Image Type LABEL. * * @class * @memberof viewer */ class LabelImageViewer extends _NonVolumeImageViewer { - /** Creates a viewer instance for displaying LABEL images. * * @param {object} options @@ -1453,9 +2021,9 @@

      Source: viewer.js

      */ constructor(options) { if (options.orientation === undefined) { - options.orientation = 'vertical'; + options.orientation = "vertical"; } - super(options) + super(options); } } @@ -1470,13 +2038,13 @@

      Source: viewer.js


      - Documentation generated by JSDoc 3.6.5 on Sun Nov 29 2020 13:38:48 GMT-0500 (Eastern Standard Time) + Documentation generated by JSDoc 3.6.5 on Wed May 05 2021 11:47:39 GMT-0400 (Eastern Daylight Time)