recogito-client-core/src/relations/Connection.js

248 lines
7.5 KiB
JavaScript

import EventEmitter from 'tiny-emitter';
import Bounds from './Bounds';
import Handle from './Handle';
import CONST from './SVGConst';
import { getNodeById } from './RelationUtils'
/**
* The connecting line between two annotation highlights.
*/
export default class Connection extends EventEmitter {
constructor(contentEl, svgEl, nodeOrAnnotation) {
super();
this.svgEl = svgEl;
// SVG elements
this.path = document.createElementNS(CONST.NAMESPACE, 'path'),
this.startDot = document.createElementNS(CONST.NAMESPACE, 'circle'),
this.endDot = document.createElementNS(CONST.NAMESPACE, 'circle'),
svgEl.appendChild(this.path);
svgEl.appendChild(this.startDot);
svgEl.appendChild(this.endDot);
// Connections are initialized either from a relation annotation
// (when loading), or as a 'floating' relation, attached to a start
// node (when drawing a new one).
const props = nodeOrAnnotation.type === 'Annotation' ?
this.initFromAnnotation(contentEl, svgEl, nodeOrAnnotation) :
this.initFromStartNode(svgEl, nodeOrAnnotation);
this.annotation = props.annotation;
// 'Descriptive' instance properties
this.fromNode = props.fromNode;
this.fromBounds = props.fromBounds;
this.toNode = props.toNode;
this.toBounds = props.toBounds;
this.currentEnd = props.currentEnd;
this.handle = props.handle;
// A floating connection is not yet attached to an end node.
this.floating = props.floating;
this.redraw();
}
/** Initializes a fixed connection from an annotation **/
initFromAnnotation = function(contentEl, svgEl, annotation) {
const [ fromId, toId ] = annotation.target.map(t => t.id);
const relation = annotation.bodies[0].value;
const fromNode = getNodeById(contentEl, fromId);
const fromBounds = new Bounds(fromNode.elements, svgEl);
const toNode = getNodeById(contentEl, toId);
const toBounds = new Bounds(toNode.elements, svgEl);
const currentEnd = toNode;
const handle = new Handle(relation, svgEl);
// RelationsLayer uses click as a selection event
handle.on('click', () => this.emit('click', {
annotation,
from: fromNode.annotation,
to: toNode.annotation,
midX: this.currentMidXY[0],
midY: this.currentMidXY[1]
}));
return { annotation, fromNode, fromBounds, toNode, toBounds, currentEnd, handle, floating: false };
}
/** Initializes a floating connection from a start node **/
initFromStartNode = function(svgEl, fromNode) {
const fromBounds = new Bounds(fromNode.elements, svgEl);
return { fromNode, fromBounds, floating: true };
}
/**
* Fixes the end of the connection to the current end node,
* turning a floating connection into a non-floating one.
*/
unfloat = function() {
if (this.currentEnd.elements)
this.floating = false;
}
/** Moves the end of a (floating!) connection to the given [x,y] or node **/
dragTo = function(xyOrNode) {
if (this.floating) {
this.currentEnd = xyOrNode;
if (xyOrNode.elements) {
this.toNode = xyOrNode;
this.toBounds = new Bounds(xyOrNode.elements, this.svgEl);
}
}
}
destroy = () => {
this.svgEl.removeChild(this.path);
this.svgEl.removeChild(this.startDot);
this.svgEl.removeChild(this.endDot);
if (this.handle)
this.handle.destroy();
}
/** Redraws this connection **/
redraw = function() {
if (this.currentEnd) {
const end = this.endXY;
const startsAtTop = end[1] <= (this.fromBounds.top + this.fromBounds.height / 2);
const start = (startsAtTop) ?
this.fromBounds.topHandleXY : this.fromBounds.bottomHandleXY;
const deltaX = end[0] - start[0];
const deltaY = end[1] - start[1];
const half = (Math.abs(deltaX) + Math.abs(deltaY)) / 2; // Half of length, for middot pos computation
const midX = (half > Math.abs(deltaX)) ? start[0] + deltaX : start[0] + half * Math.sign(deltaX);
let midY; // computed later
const orientation = (half > Math.abs(deltaX)) ?
(deltaY > 0) ? 'down' : 'up' :
(deltaX > 0) ? 'right' : 'left';
const d = CONST.LINE_DISTANCE - CONST.BORDER_RADIUS; // Shorthand: vertical straight line length
// Path that starts at the top edge of the annotation highlight
const compileBottomPath = function() {
const arc1 = (deltaX > 0) ? CONST.ARC_9CC : CONST.ARC_3CW;
const arc2 = (deltaX > 0) ? CONST.ARC_0CW : CONST.ARC_0CC;
midY = (half > Math.abs(deltaX)) ?
start[1] + half - Math.abs(deltaX) + CONST.LINE_DISTANCE :
start[1] + CONST.LINE_DISTANCE;
return 'M' + start[0] +
' ' + start[1] +
'v' + d +
arc1 +
'h' + (deltaX - 2 * Math.sign(deltaX) * CONST.BORDER_RADIUS) +
arc2 +
'V' + end[1];
};
// Path that starts at the bottom edge of the annotation highlight
const compileTopPath = function() {
const arc1 = (deltaX > 0) ? CONST.ARC_9CW : CONST.ARC_3CC;
const arc2 = (deltaX > 0) ?
(deltaY >= 0) ? CONST.ARC_0CW : CONST.ARC_6CC :
(deltaY >= 0) ? CONST.ARC_0CC : CONST.ARC_6CW;
midY = (half > Math.abs(deltaX)) ?
start[1] - (half - Math.abs(deltaX)) - CONST.LINE_DISTANCE :
start[1] - CONST.LINE_DISTANCE;
return 'M' + start[0] +
' ' + start[1] +
'v-' + (CONST.LINE_DISTANCE - CONST.BORDER_RADIUS) +
arc1 +
'h' + (deltaX - 2 * Math.sign(deltaX) * CONST.BORDER_RADIUS) +
arc2 +
'V' + end[1];
};
this.startDot.setAttribute('cx', start[0]);
this.startDot.setAttribute('cy', start[1]);
this.startDot.setAttribute('r', 2);
this.startDot.setAttribute('class', 'start');
this.endDot.setAttribute('cx', end[0]);
this.endDot.setAttribute('cy', end[1]);
this.endDot.setAttribute('r', 2);
this.endDot.setAttribute('class', 'end');
if (startsAtTop)
this.path.setAttribute('d', compileTopPath());
else
this.path.setAttribute('d', compileBottomPath());
this.path.setAttribute('class', 'connection');
this.currentMidXY = [ midX, midY ];
if (this.handle)
this.handle.setPosition(this.currentMidXY, orientation);
}
}
/**
* Redraws this connection, and additionally forces a recompute of
* the start and end coordinates. This is only needed if the position
* of the annotation highlights changes, e.g. after a window resize.
*/
recompute = () => {
this.fromBounds.recompute();
if (this.currentEnd.elements)
this.toBounds.recompute();
this.redraw();
}
/**
* Returns true if the given relation matches this connection,
* meaning that this connection has the same start and end point
* as recorded in the relation.
*/
matchesRelation = relation =>
relation.from.isEqual(this.fromNode.annotation) &&
relation.to.isEqual(this.toNode.annotation);
/** Getter/setter shorthands **/
get isFloating() {
return this.floating;
}
get startAnnotation() {
return this.fromNode.annotation;
}
get endAnnotation() {
return this.toNode.annotation;
}
get endXY() {
return (this.currentEnd instanceof Array) ?
this.currentEnd :
(this.fromBounds.top > this.toBounds.top) ?
this.toBounds.bottomHandleXY : this.toBounds.topHandleXY;
}
get midXY() {
return this.currentMidXY;
}
}