UNPKG

mirador

Version:

An open-source, web-based 'multi-up' viewer that supports zoom-pan-rotate functionality, ability to display/compare simple images, and images with annotations.

506 lines (424 loc) 22.5 kB
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } function _possibleConstructorReturn(self, call) { if (call && (typeof call === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } } function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import isEqual from 'lodash/isEqual'; import debounce from 'lodash/debounce'; import flatten from 'lodash/flatten'; import sortBy from 'lodash/sortBy'; import xor from 'lodash/xor'; import OpenSeadragonCanvasOverlay from '../lib/OpenSeadragonCanvasOverlay'; import CanvasWorld from '../lib/CanvasWorld'; import CanvasAnnotationDisplay from '../lib/CanvasAnnotationDisplay'; /** * Represents a OpenSeadragonViewer in the mirador workspace. Responsible for mounting * and rendering OSD. */ export var AnnotationsOverlay = /*#__PURE__*/function (_Component) { _inherits(AnnotationsOverlay, _Component); var _super = _createSuper(AnnotationsOverlay); /** * @param {Object} props */ function AnnotationsOverlay(props) { var _this; _classCallCheck(this, AnnotationsOverlay); _this = _super.call(this, props); _this.ref = /*#__PURE__*/React.createRef(); _this.osdCanvasOverlay = null; // An initial value for the updateCanvas method _this.updateCanvas = function () {}; _this.onUpdateViewport = _this.onUpdateViewport.bind(_assertThisInitialized(_this)); _this.onCanvasClick = _this.onCanvasClick.bind(_assertThisInitialized(_this)); _this.onCanvasMouseMove = debounce(_this.onCanvasMouseMove.bind(_assertThisInitialized(_this)), 10); _this.onCanvasExit = _this.onCanvasExit.bind(_assertThisInitialized(_this)); return _this; } /** * React lifecycle event */ _createClass(AnnotationsOverlay, [{ key: "componentDidMount", value: function componentDidMount() { this.initializeViewer(); } /** * When the tileSources change, make sure to close the OSD viewer. * When the annotations change, reset the updateCanvas method to make sure * they are added. * When the viewport state changes, pan or zoom the OSD viewer as appropriate */ }, { key: "componentDidUpdate", value: function componentDidUpdate(prevProps) { var _this$props = this.props, drawAnnotations = _this$props.drawAnnotations, drawSearchAnnotations = _this$props.drawSearchAnnotations, annotations = _this$props.annotations, searchAnnotations = _this$props.searchAnnotations, hoveredAnnotationIds = _this$props.hoveredAnnotationIds, selectedAnnotationId = _this$props.selectedAnnotationId, highlightAllAnnotations = _this$props.highlightAllAnnotations, viewer = _this$props.viewer; this.initializeViewer(); var annotationsUpdated = !AnnotationsOverlay.annotationsMatch(annotations, prevProps.annotations); var searchAnnotationsUpdated = !AnnotationsOverlay.annotationsMatch(searchAnnotations, prevProps.searchAnnotations); var hoveredAnnotationsUpdated = xor(hoveredAnnotationIds, prevProps.hoveredAnnotationIds).length > 0; if (this.osdCanvasOverlay && hoveredAnnotationsUpdated) { if (hoveredAnnotationIds.length > 0) { this.osdCanvasOverlay.canvasDiv.style.cursor = 'pointer'; } else { this.osdCanvasOverlay.canvasDiv.style.cursor = ''; } } var selectedAnnotationsUpdated = selectedAnnotationId !== prevProps.selectedAnnotationId; var redrawAnnotations = drawAnnotations !== prevProps.drawAnnotations || drawSearchAnnotations !== prevProps.drawSearchAnnotations || highlightAllAnnotations !== prevProps.highlightAllAnnotations; if (searchAnnotationsUpdated || annotationsUpdated || selectedAnnotationsUpdated || hoveredAnnotationsUpdated || redrawAnnotations) { this.updateCanvas = this.canvasUpdateCallback(); viewer.forceRedraw(); } } /** */ }, { key: "componentWillUnmount", value: function componentWillUnmount() { var viewer = this.props.viewer; viewer.removeHandler('canvas-click', this.onCanvasClick); viewer.removeHandler('canvas-exit', this.onCanvasExit); viewer.removeHandler('update-viewport', this.onUpdateViewport); viewer.removeHandler('mouse-move', this.onCanvasMouseMove); } /** */ }, { key: "onCanvasClick", value: function onCanvasClick(event) { var _this2 = this; var canvasWorld = this.props.canvasWorld; var webPosition = event.position, viewport = event.eventSource.viewport; var point = viewport.pointFromPixel(webPosition); var canvas = canvasWorld.canvasAtPoint(point); if (!canvas) return; var _canvasWorld$canvasTo = canvasWorld.canvasToWorldCoordinates(canvas.id), _canvasWorld$canvasTo2 = _slicedToArray(_canvasWorld$canvasTo, 4), _canvasX = _canvasWorld$canvasTo2[0], _canvasY = _canvasWorld$canvasTo2[1], canvasWidth = _canvasWorld$canvasTo2[2], canvasHeight // eslint-disable-line no-unused-vars = _canvasWorld$canvasTo2[3]; // get all the annotations that contain the click var annos = this.annotationsAtPoint(canvas, point); if (annos.length > 0) { event.preventDefaultAction = true; // eslint-disable-line no-param-reassign } if (annos.length === 1) { this.toggleAnnotation(annos[0].id); } else if (annos.length > 0) { /** * Try to find the "right" annotation to select after a click. * * This is perhaps a naive method, but seems to deal with rectangles and SVG shapes: * * - figure out how many points around a circle are inside the annotation shape * - if there's a shape with the fewest interior points, it's probably the one * with the closest boundary? * - if there's a tie, make the circle bigger and try again. */ var annosWithClickScore = function annosWithClickScore(radius) { var degreesToRadians = Math.PI / 180; return function (anno) { var score = 0; for (var degrees = 0; degrees < 360; degrees += 1) { var x = Math.cos(degrees * degreesToRadians) * radius + point.x; var y = Math.sin(degrees * degreesToRadians) * radius + point.y; if (_this2.isAnnotationAtPoint(anno, canvas, { x: x, y: y })) score += 1; } return { anno: anno, score: score }; }; }; var annosWithScore = []; var radius = 1; annosWithScore = sortBy(annos.map(annosWithClickScore(radius)), 'score'); while (radius < Math.max(canvasWidth, canvasHeight) && annosWithScore[0].score === annosWithScore[1].score) { radius *= 2; annosWithScore = sortBy(annos.map(annosWithClickScore(radius)), 'score'); } this.toggleAnnotation(annosWithScore[0].anno.id); } } /** */ }, { key: "onCanvasMouseMove", value: function onCanvasMouseMove(event) { var _this$props2 = this.props, annotations = _this$props2.annotations, canvasWorld = _this$props2.canvasWorld, hoverAnnotation = _this$props2.hoverAnnotation, hoveredAnnotationIds = _this$props2.hoveredAnnotationIds, searchAnnotations = _this$props2.searchAnnotations, viewer = _this$props2.viewer, windowId = _this$props2.windowId; if (annotations.length === 0 && searchAnnotations.length === 0) return; var webPosition = event.position; var point = viewer.viewport.pointFromPixel(webPosition); var canvas = canvasWorld.canvasAtPoint(point); if (!canvas) { hoverAnnotation(windowId, []); return; } var annos = this.annotationsAtPoint(canvas, point); if (xor(hoveredAnnotationIds, annos.map(function (a) { return a.id; })).length > 0) { hoverAnnotation(windowId, annos.map(function (a) { return a.id; })); } } /** If the cursor leaves the canvas, wipe out highlights */ }, { key: "onCanvasExit", value: function onCanvasExit(event) { var _this$props3 = this.props, hoverAnnotation = _this$props3.hoverAnnotation, windowId = _this$props3.windowId; // a move event may be queued up by the debouncer this.onCanvasMouseMove.cancel(); hoverAnnotation(windowId, []); } /** * onUpdateViewport - fires during OpenSeadragon render method. */ }, { key: "onUpdateViewport", value: function onUpdateViewport(event) { this.updateCanvas(); } /** @private */ }, { key: "initializeViewer", value: function initializeViewer() { var viewer = this.props.viewer; if (!viewer) return; if (this.osdCanvasOverlay) return; this.osdCanvasOverlay = new OpenSeadragonCanvasOverlay(viewer, this.ref); viewer.addHandler('canvas-click', this.onCanvasClick); viewer.addHandler('canvas-exit', this.onCanvasExit); viewer.addHandler('update-viewport', this.onUpdateViewport); viewer.addHandler('mouse-move', this.onCanvasMouseMove); this.updateCanvas = this.canvasUpdateCallback(); } /** */ }, { key: "canvasUpdateCallback", value: function canvasUpdateCallback() { var _this3 = this; return function () { _this3.osdCanvasOverlay.clear(); _this3.osdCanvasOverlay.resize(); _this3.osdCanvasOverlay.canvasUpdate(_this3.renderAnnotations.bind(_this3)); }; } /** @private */ }, { key: "isAnnotationAtPoint", value: function isAnnotationAtPoint(resource, canvas, point) { var canvasWorld = this.props.canvasWorld; var _canvasWorld$canvasTo3 = canvasWorld.canvasToWorldCoordinates(canvas.id), _canvasWorld$canvasTo4 = _slicedToArray(_canvasWorld$canvasTo3, 2), canvasX = _canvasWorld$canvasTo4[0], canvasY = _canvasWorld$canvasTo4[1]; var relativeX = point.x - canvasX; var relativeY = point.y - canvasY; if (resource.svgSelector) { var context = this.osdCanvasOverlay.context2d; var _CanvasAnnotationDisp = new CanvasAnnotationDisplay({ resource: resource }), svgPaths = _CanvasAnnotationDisp.svgPaths; return _toConsumableArray(svgPaths).some(function (path) { return context.isPointInPath(new Path2D(path.attributes.d.nodeValue), relativeX, relativeY); }); } if (resource.fragmentSelector) { var _resource$fragmentSel = _slicedToArray(resource.fragmentSelector, 4), x = _resource$fragmentSel[0], y = _resource$fragmentSel[1], w = _resource$fragmentSel[2], h = _resource$fragmentSel[3]; return x <= relativeX && relativeX <= x + w && y <= relativeY && relativeY <= y + h; } return false; } /** @private */ }, { key: "annotationsAtPoint", value: function annotationsAtPoint(canvas, point) { var _this4 = this; var _this$props4 = this.props, annotations = _this$props4.annotations, searchAnnotations = _this$props4.searchAnnotations; var lists = [].concat(_toConsumableArray(annotations), _toConsumableArray(searchAnnotations)); var annos = flatten(lists.map(function (l) { return l.resources; })).filter(function (resource) { if (canvas.id !== resource.targetId) return false; return _this4.isAnnotationAtPoint(resource, canvas, point); }); return annos; } /** */ }, { key: "toggleAnnotation", value: function toggleAnnotation(id) { var _this$props5 = this.props, selectedAnnotationId = _this$props5.selectedAnnotationId, selectAnnotation = _this$props5.selectAnnotation, deselectAnnotation = _this$props5.deselectAnnotation, windowId = _this$props5.windowId; if (selectedAnnotationId === id) { deselectAnnotation(windowId, id); } else { selectAnnotation(windowId, id); } } /** * annotationsToContext - converts anontations to a canvas context */ }, { key: "annotationsToContext", value: function annotationsToContext(annotations, palette) { var _this$props6 = this.props, highlightAllAnnotations = _this$props6.highlightAllAnnotations, hoveredAnnotationIds = _this$props6.hoveredAnnotationIds, selectedAnnotationId = _this$props6.selectedAnnotationId, canvasWorld = _this$props6.canvasWorld, viewer = _this$props6.viewer; var context = this.osdCanvasOverlay.context2d; var zoomRatio = viewer.viewport.getZoom(true) / viewer.viewport.getMaxZoom(); annotations.forEach(function (annotation) { annotation.resources.forEach(function (resource) { if (!canvasWorld.canvasIds.includes(resource.targetId)) return; var offset = canvasWorld.offsetByCanvas(resource.targetId); var canvasAnnotationDisplay = new CanvasAnnotationDisplay({ hovered: hoveredAnnotationIds.includes(resource.id), offset: offset, palette: _objectSpread(_objectSpread({}, palette), {}, { "default": _objectSpread(_objectSpread({}, palette["default"]), !highlightAllAnnotations && palette.hidden) }), resource: resource, selected: selectedAnnotationId === resource.id, zoomRatio: zoomRatio }); canvasAnnotationDisplay.toContext(context); }); }); } /** */ }, { key: "renderAnnotations", value: function renderAnnotations() { var _this$props7 = this.props, annotations = _this$props7.annotations, drawAnnotations = _this$props7.drawAnnotations, drawSearchAnnotations = _this$props7.drawSearchAnnotations, searchAnnotations = _this$props7.searchAnnotations, palette = _this$props7.palette; if (drawSearchAnnotations) { this.annotationsToContext(searchAnnotations, palette.search); } if (drawAnnotations) { this.annotationsToContext(annotations, palette.annotations); } } /** * Renders things */ }, { key: "render", value: function render() { var viewer = this.props.viewer; if (!viewer) return /*#__PURE__*/React.createElement(React.Fragment, null); return /*#__PURE__*/ReactDOM.createPortal( /*#__PURE__*/React.createElement("div", { ref: this.ref, style: { height: '100%', left: 0, position: 'absolute', top: 0, width: '100%' } }, /*#__PURE__*/React.createElement("canvas", null)), viewer.canvas); } }], [{ key: "annotationsMatch", value: /** * annotationsMatch - compares previous annotations to current to determine * whether to add a new updateCanvas method to draw annotations * @param {Array} currentAnnotations * @param {Array} prevAnnotations * @return {Boolean} */ function annotationsMatch(currentAnnotations, prevAnnotations) { if (!currentAnnotations && !prevAnnotations) return true; if (currentAnnotations && !prevAnnotations || !currentAnnotations && prevAnnotations) return false; if (currentAnnotations.length === 0 && prevAnnotations.length === 0) return true; if (currentAnnotations.length !== prevAnnotations.length) return false; return currentAnnotations.every(function (annotation, index) { var newIds = annotation.resources.map(function (r) { return r.id; }); var prevIds = prevAnnotations[index].resources.map(function (r) { return r.id; }); if (newIds.length === 0 && prevIds.length === 0) return true; if (newIds.length !== prevIds.length) return false; if (annotation.id === prevAnnotations[index].id && isEqual(newIds, prevIds)) { return true; } return false; }); } }]); return AnnotationsOverlay; }(Component); AnnotationsOverlay.defaultProps = { annotations: [], deselectAnnotation: function deselectAnnotation() {}, drawAnnotations: true, drawSearchAnnotations: true, highlightAllAnnotations: false, hoverAnnotation: function hoverAnnotation() {}, hoveredAnnotationIds: [], palette: {}, searchAnnotations: [], selectAnnotation: function selectAnnotation() {}, selectedAnnotationId: undefined, viewer: null };