dicom-microscopy-viewer
Version:
Interactive web-based viewer for DICOM Microscopy Images
577 lines (494 loc) • 17.6 kB
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: annotations/markups/_MarkupManager.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: annotations/markups/_MarkupManager.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>import Circle from 'ol/geom/Circle'
import DragPan from 'ol/interaction/DragPan'
import LineString from 'ol/geom/LineString'
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 { fromCircle } from 'ol/geom/Polygon'
import Enums from '../../enums'
import { _getUnitSuffix } from '../../utils'
import { coordinateWithOffset } from '../../scoord3dUtils'
import defaultStyles from '../styles'
/**
* Build 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
*
* @private
*/
const _getShortestLineBetweenOverlayAndFeature = (feature, overlay) => {
let result
let distanceSq = Infinity
let featureGeometry = feature.getGeometry()
if (featureGeometry instanceof Circle) {
featureGeometry = fromCircle(featureGeometry).clone()
}
const 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)
}
class _MarkupManager {
constructor ({
map,
pyramid,
affine,
drawingSource,
formatters,
onClick,
onStyle
} = {}) {
this._map = map
this._pyramid = pyramid
this._affine = affine
this._formatters = formatters
this._drawingSource = drawingSource
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
}
/**
* Set markup visibility.
*
* @param {string} id The markup id
* @param {boolean} isVisible The markup visibility
*/
setVisibility (id, isVisible) {
const markup = this.get(id)
if (!markup) {
return
}
const links = this._links.getArray()
const link = links.find((feature) => feature.getId() === id)
const feature = this._drawingSource.getFeatureById(id)
if (!isVisible) {
this._map.removeOverlay(markup.overlay)
this._links.remove(link)
} else {
this._map.addOverlay(markup.overlay)
this._drawLink(feature)
}
return isVisible
}
/**
* 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 listener = feature.on(
Enums.FeatureEvents.CHANGE,
(event) => {
if (this.has(id)) {
const view = this._map.getView()
const unitSuffix = _getUnitSuffix(view)
const format = this._getFormatter(event.target)
const output = format(
event.target,
unitSuffix,
this._pyramid,
this._affine
)
this.update({
feature,
value: output,
coordinate: event.target.getGeometry().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
const 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(coordinate)
}
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)
}
}
}
/**
* This event is responsible assign markup classes on update event
*
* @param {object} event The event
*/
onUpdate (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) => {// eslint-disable-line
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
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Namespaces</h3><ul><li><a href="annotation.html">annotation</a></li><li><a href="api.html">api</a></li><li><a href="color.html">color</a></li><li><a href="events.html">events</a></li><li><a href="mapping.html">mapping</a></li><li><a href="metadata.html">metadata</a></li><li><a href="opticalPath.html">opticalPath</a></li><li><a href="roi.html">roi</a></li><li><a href="scoord3d.html">scoord3d</a></li><li><a href="segment.html">segment</a></li><li><a href="utils.html">utils</a></li><li><a href="viewer.html">viewer</a></li></ul><h3>Classes</h3><ul><li><a href="annotation.AnnotationGroup.html">AnnotationGroup</a></li><li><a href="color.PaletteColorLookupTable.html">PaletteColorLookupTable</a></li><li><a href="mapping.ParameterMapping.html">ParameterMapping</a></li><li><a href="mapping.Transformation.html">Transformation</a></li><li><a href="metadata.Comprehensive3DSR.html">Comprehensive3DSR</a></li><li><a href="metadata.MicroscopyBulkSimpleAnnotations.html">MicroscopyBulkSimpleAnnotations</a></li><li><a href="metadata.ParametricMap.html">ParametricMap</a></li><li><a href="metadata.Segmentation.html">Segmentation</a></li><li><a href="metadata.SOPClass.html">SOPClass</a></li><li><a href="metadata.VLWholeSlideMicroscopyImage.html">VLWholeSlideMicroscopyImage</a></li><li><a href="module.exports_module.exports.html">exports</a></li><li><a href="opticalPath.OpticalPath.html">OpticalPath</a></li><li><a href="roi.ROI.html">ROI</a></li><li><a href="scoord3d.Ellipse.html">Ellipse</a></li><li><a href="scoord3d.Ellipsoid.html">Ellipsoid</a></li><li><a href="scoord3d.Multipoint.html">Multipoint</a></li><li><a href="scoord3d.Point.html">Point</a></li><li><a href="scoord3d.Polygon.html">Polygon</a></li><li><a href="scoord3d.Polyline.html">Polyline</a></li><li><a href="scoord3d.Scoord3D.html">Scoord3D</a></li><li><a href="segment.Segment.html">Segment</a></li><li><a href="viewer.LabelImageViewer.html">LabelImageViewer</a></li><li><a href="viewer.OverviewImageViewer.html">OverviewImageViewer</a></li><li><a href="viewer.VolumeImageViewer.html">VolumeImageViewer</a></li></ul><h3>Global</h3><ul><li><a href="global.html#addTask">addTask</a></li><li><a href="global.html#cancelTask">cancelTask</a></li><li><a href="global.html#decode">decode</a></li><li><a href="global.html#getStatistics">getStatistics</a></li><li><a href="global.html#handleMessageFromWorker">handleMessageFromWorker</a></li><li><a href="global.html#initialize">initialize</a></li><li><a href="global.html#loadWebWorkerTask">loadWebWorkerTask</a></li><li><a href="global.html#setTaskPriority">setTaskPriority</a></li><li><a href="global.html#spawnWebWorker">spawnWebWorker</a></li><li><a href="global.html#startTaskOnWebWorker">startTaskOnWebWorker</a></li><li><a href="global.html#terminateAllWebWorkers">terminateAllWebWorkers</a></li><li><a href="global.html#transform">transform</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Thu Sep 29 2022 16:54:54 GMT-0400 (Eastern Daylight Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>