@doc hierarchy="GMLDOM">
Aspect for drawing lines in a zoomable drawing. A line is a graphic used for drawing connections between shapes.
(c) SAP AG 2003-2006. All rights reserved.
///////////////////////////////////////////////////////////////////////
// ASPECT HEADER
Aspect ZLine for ZDrawing;
inherit ZGraphic;
constructor(diagram, parent)
this.supercall();
var src=base[base.@srcKey];
var srcpin = diagram.getPin(src.id);
if (!srcpin) throw new Error('Source pin '+src.id+' not found');
this.srcpinID = srcpin.id;
var trg=base[base.@trgKey];
var trgpin = diagram.getPin(trg.id);
if (!trgpin) throw new Error('Target pin '+trg.id+' not found');
this.trgpinID = trgpin.id;
srcpin.lines[this.id] = true;
trgpin.lines[this.id] = true;
this.lineRouting = GETVAR('SVG_ROUTING_MODE') || base.@routingMode || diagram.defaultRouting;
this.lineColor = base.@strokeColor;
this.hilightColor = diagram.base.@hilightColor;
this.endMarker = (base.@endArrow ? 'endArrow'+base.@endArrow : '');
this.startMarker = (base.@startArrow ? 'startArrow'+base.@startArrow : '');
this.paint();
this.repaint();
end
destructor
this.hideSelectionEffect();
var canvas = this.canvas;
var srcpin = this.getSrcpin();
var trgpin = this.getTrgpin();
if (srcpin) delete srcpin.lines[this.id];
if (trgpin) delete trgpin.lines[this.id];
SVG.removeElement(this.graphicNode);
this.graphicNode = null;
this.lineNode = null;
this.labelNode = null;
this.labelNode2 = null;
this.isSelected = false;
this.canvas.removeWidget(this.id);
//TODO: delete this['base'];
end
virtual method getSrcpin()
return this.canvas.getWidget(this.srcpinID);
end
virtual method getTrgpin()
return this.canvas.getWidget(this.trgpinID);
end
//////////////////////////////////////////////////////////////////////////////////////
// BASE PROPERTIES
// Line style
<@doc group="Instance Properties">
Defines the line corners' radius
This property controls the rounding of line corners for orthogonal line routing mode.
By default, this property is set to 0 which results in square corners.
property cornerRadius = 0;
<@doc type="@LINE_ARROW" default="SVG_BLOCK_ARROW|SVG_FILLED">
Defines the line's end arrow
property endArrow = 0;
<@doc>
Defines the line's dash pattern
The ~dash property controls the pattern of dashes and gaps used to stroke the line.
The ~dash property value can be either 'none' (indicating that the line is to be drawn solid),
or a list of whitespace-separated numbers that specify the lengths of alternating dashes and gaps.
property lineDash = 'none';
<@doc type="@LINE_ROUTING" default="SVG_INHERIT_LINE">Defines the line's routing constraints
property routingMode = #[SVG_INHERIT_LINE];
<@doc>
Defines line connection mode
The connectionMode can be one of the following values:
Value | Description
SVG_CONNECT_PINS | The line will be drawn from source to target pins (default)
SVG_CONNECT_SHAPES | The line will be drawn from source to target shapes
property connectionMode = #[SVG_CONNECT_PINS];
<@doc type="@LINE_ARROW" default="SVG_NONE">
Defines the line's start arrow
property startArrow = 0;
// Line path
<@doc scope="private">
Gets or sets the line's control points
The value of this property depends on the specific type of line that uses it.
property controls = '';
<@doc scope="private">
Gets or sets the line's label position and angle
The value of this property depends on the specific type of line that uses it.
property lpos = '';
<@doc scope="private">
Gets or sets the line's path
The ~path property contains a whitespace-separated of path drawing commands relative to
the source pin.
property path = '';
<@struct name="LINE_ARROW" group="Structures">
A bitwise structure for defining line arrows
A line arrow can be any bitwise combination of the following groups of flags:
Name | Description
SVG_NONE | No arrow
|
SVG_BLOCK_ARROW | Block arrow
SVG_CLASSIC_ARROW | Classic arrow
SVG_DIAMOND_ARROW | Diamond arrow
SVG_CIRCLE_ARROW | Circle arrow
SVG_DOUBLE_ARROW | Double arrow
SVG_NOTCH_ARROW | Block arrow with a notch
SVG_SINGLE_NOTCH | Single notch with no arrow
SVG_DOUBLE_NOTCH | Double notch with no arrow
|
SVG_HOLLOW | The arrow head is hollow, i.e. filled with white color.
SVG_FILLED | The arrow head is filled with the primary color.
<@struct name="LINE_ROUTING">
A bitwise structure for defining line routing constraints
A line routing can be any one of the following bitwise flags:
Name | Description
SVG_INHERIT_LINE | Inherit line routing mode from containing diagram
|
SVG_STRAIGHT_LINE | Straight line routing
SVG_ANGULAR_LINE | Angular line routing
SVG_CURVED_LINE | Curved line routing using quadratic bezier function
SVG_ORTHOGONAL_LINE | Orthogonal line routing using only horizontal and vertical line segments
//////////////////////////////////////////////////////////////////////////////////////
// STATIC PROPERTIES
<@doc group="Aspect Properties">
Defines the line's source key
The ~srcKey is the name of the property on the base object which points to the line's source element
static readonly property srcKey = null;
<@doc>
Defines the line's target key
The ~srcKey is the name of the property on the base object which points to the line's target element
static readonly property trgKey = null;
//////////////////////////////////////////////////////////////////////////////////////
// ASPECT PROPERTIES
<@doc type="RGB" scope="private">Gets the line's color (inerited from the stroke color)
virtual property lineColor = '';
<@doc type="RGB" scope="private">Gets the line's routing mode (inerited from the corresponding base property)
virtual property lineRouting = #[SVG_STRAIGHT_LINE];
<@doc type="RGB" scope="private">Gets the end arrow marker (uncolored)
virtual property endMarker = '';
<@doc type="RGB" scope="private">Gets the start arrow marker (uncolored)
virtual property startMarker = '';
// Associated objects
<@doc scope="private">Gets the source pin
virtual property srcpinID = '';
<@doc scope="private">Gets the target pin
virtual property trgpinID = '';
// Status flags
<@doc type="b" scope="private">Indicates that this graphic is a line (for quick tests)
virtual property isLine = true;
// SVG painting objects
<@doc type="SVGNode" scope="private">Gets the line node
virtual property lineNode = null;
<@doc type="SVGNode" scope="private">Gets the label node
virtual property labelNode = null;
<@doc type="SVGNode" scope="private">Gets the aditional label node
virtual property labelNode2 = null;
<@doc type="SVGNode []" scope="private">Gets the aditional body part nodes
readonly property bodyParts = null;
//////////////////////////////////////////////////////////////////////////////////////
// MODEL EVENTS
override virtual method onModelUpdate(evt)
this.repaint(evt.name);
//TODO: repaint also: strokeColor, endArrow, startArrow, routingMode, cornerRadius, lineDash
end
override virtual method onModelInsert(evt)
// nothing to do (line has no child graphics)
end
override virtual method onModelRemove(evt)
// nothing to do (line has no child graphics)
end
//////////////////////////////////////////////////////////////////////////////////////
// PAINTING METHODS
<@doc scope="private">
Paints the shape
virtual method paint()
var lineAddParts = this.parseParts(this.Class, base.Class);
this.formulas = lineAddParts.formulas;
this.graphicNode = SVG.createElement(this.getParent().linesLayer, 'a', {
stroke: base.@strokeColor,
fill: base.@strokeColor
});
this.graphicNode.peerID = this.id;
// create line node
var diag = this.getDiagram(), cp = this.getConnectionPoints();
var fragment = '');
var sParts = lineAddParts.shapeParts;
for (var m in sParts) {
var sPart = sParts[m];
var svgElem = SVG.createElement(this.graphicNode, sPart.type, sPart.props);
if (this.bodyParts == null) this.bodyParts = [];
this.bodyParts.push(svgElem);
}
this.rerouteBodyParts();
end
<@doc scope="private">
Repaints the line after a property has changed
virtual method repaint(prop)
if (!prop) {
this.reroute();
}
if (!prop || prop == 'name') {
var name=base.name;
if (ISNULL(name)) {
if (this.labelNode) {
SVG.removeElement(this.labelNode);
this.labelNode = null;
}
} else {
if (!this.labelNode) {
var lpos=SPLITN(base.@lpos,3);
this.labelNode = SVG.createElement(this.graphicNode, 'text', {
'class':'lineText', x:lpos[0], y:lpos[1]-3,
transform:'rotate('+lpos[2]+' '+lpos[0]+' '+lpos[1]+')'});
}
SVG.setText(this.labelNode, name);
}
}
if (!prop || prop == 'guard') {
var guard=base.guard || '';
if (guard == 'true') {
if (this.labelNode2) {
SVG.removeElement(this.labelNode2);
this.labelNode2 = null;
}
return;
}
// String.fromCharCode(60) == '<' String.fromCharCode(62) == '>'
if (guard != 'false' && guard != '') guard = String.fromCharCode(60) + String.fromCharCode(60) + 'cond' + String.fromCharCode(62) + String.fromCharCode(62);
if (ISNULL(guard)) {
if (this.labelNode2) {
SVG.removeElement(this.labelNode2);
this.labelNode2 = null;
}
} else {
if (!this.labelNode2) {
var lpos=SPLITN(base.@lpos,3);
this.labelNode2 = SVG.createElement(this.graphicNode, 'text', {
'class':'lineText', x:lpos[0], y:lpos[1]+9,
transform:'rotate('+lpos[2]+' '+lpos[0]+' '+lpos[1]+')'});
}
SVG.setText(this.labelNode2, guard);
}
}
end
<@doc scope="private">
Reroutes the line after either of its endpoints was repositioned
virtual method reroute()
// get connection points
var cp = this.getConnectionPoints();
// apply routing method
this['reroute'+this.lineRouting](cp.p1,cp.p2);
// repaint line and label
this.lineNode.setAttribute ('d', 'M'+cp.p1.x+','+cp.p1.y+base.@path);
if (this.labelNode) {
var lpos=SPLITN(base.@lpos,3);
this.labelNode.setAttribute('x', lpos[0]);
this.labelNode.setAttribute('y', lpos[1]-3);
this.labelNode.setAttribute('transform', 'rotate('+lpos[2]+' '+lpos[0]+' '+lpos[1]+')');
}
if (this.labelNode2) {
var lpos=SPLITN(base.@lpos,3);
this.labelNode2.setAttribute('x', lpos[0]);
this.labelNode2.setAttribute('y', lpos[1] + 9);
this.labelNode2.setAttribute('transform', 'rotate('+lpos[2]+' '+lpos[0]+' '+lpos[1]+')');
}
this.rerouteBodyParts();
end
<@doc scope="private">
Reroutes the body parts after either of its endpoints was repositioned
virtual method rerouteBodyParts()
var lpos=SPLITN(base.@lpos,3);
var aspectData = {w: lpos[0] * -2, h: lpos[1] * -2}; // the (*-2) is due to passing defineShape x1/y1 agrs
for (var k in this.formulas) {
var formula = this.formulas[k];
if (formula) formula(this.bodyParts, aspectData, base);
}
end
//////////////////////////////////////////////////////////////////////////////////////
// INTERACTIVE EFFECTS
override virtual method showHoverEffect();
if (this.isSelected || this.marked) return;
SVG.setProperty(this.graphicNode, 'stroke', this.hilightColor);
SVG.setProperty(this.graphicNode, 'fill', this.hilightColor);
end
override virtual method hideHoverEffect();
if (this.isSelected || this.marked) return;
SVG.setProperty(this.graphicNode, 'stroke', base.@strokeColor);
SVG.setProperty(this.graphicNode, 'fill', base.@strokeColor);
end
override virtual method showSelectionEffect()
if (this.isSelected) return;
SVG.raiseElement(this.graphicNode);
this.showHoverEffect();
var diag = this.getDiagram();
if (this.endMarker) {
var marker = diag.getColoredMarker(this.endMarker, this.hilightColor);
this.lineNode.setAttribute('marker-end', marker);
}
if (this.startMarker) {
var marker = diag.getColoredMarker(this.startMarker, this.hilightColor);
this.lineNode.setAttribute('marker-start', marker);
}
if (base.@connectionMode == #[SVG_CONNECT_PINS]) {
this.getSrcpin().showSelectionEffect(true);
this.getTrgpin().showSelectionEffect(true);
}
this.isSelected = true;
end
override virtual method hideSelectionEffect()
if (!this.isSelected) return;
this.isSelected = false;
this.hideHoverEffect();
var diag = this.getDiagram();
if (this.endMarker) {
var marker = diag.getColoredMarker(this.endMarker, this.lineColor);
this.lineNode.setAttribute('marker-end', marker);
}
if (this.startMarker) {
var marker = diag.getColoredMarker(this.startMarker, this.lineColor);
this.lineNode.setAttribute('marker-start', marker);
}
if (base.@connectionMode == #[SVG_CONNECT_PINS]) {
this.getSrcpin().hideSelectionEffect(true);
this.getTrgpin().hideSelectionEffect(true);
}
end
override virtual method showFocusEffect()
this.inFocus = true;
end
override virtual method hideFocusEffect()
this.inFocus = false;
end
override virtual method flyIntoView()
var b = this.getBBox(), p = this.getParent();
return this.canvas.flyToArea({x:p.qx+b.x, y:p.qy+b.y, w:b.width, h:b.height});
end
//////////////////////////////////////////////////////////////////////////////////////
// EDITING METHODS
<@doc scope="private">
Starts a line rename operation
virtual method startRename()
var diag = this.getDiagram();
var lbox = this.getLabelPos();
var cbox = this.canvas.canvasToClient(lbox.x, lbox.y, lbox.width, lbox.height);
var cpos = this.board.clientToBrowser({x:cbox.x, y:cbox.y});
var data = {
object: base,
graphic: this,
attr: 'name',
callback: diag.endRename,
delegate: diag,
select: true,
x: cpos.x+cbox.w/2, centerX:true,
y: cpos.y+cbox.h/2, centerY:true,
w: cbox.w,
h: cbox.h
};
this.board.textedit.show(data);
end
//////////////////////////////////////////////////////////////////////////////////////
// MOUSE EVENTS
virtual method setupPointer(evt)
var diag=this.getDiagram();
diag.selectById(this.id, evt.ctrlKey);
if (evt.detail>1) {
RULE('drillIntoShape', diag.base, base);
} else if (evt.button == 2) {
var pos = this.canvas.clientToCanvas(evt.clientX, evt.clientY);
var menu = RULE('defineContextMenu', diag.base, diag.boardAspect, this.base, pos);
if (menu) diag.openMenu(menu, pos, menu.callback);
}
return null;
end
virtual method mouseover(evt, ptr)
if (ptr) return;
var diag=this.getDiagram();
diag.showTooltip(this);
if (diag.hilightMode) this.showHoverEffect();
end
virtual method mouseout(evt, ptr)
if (ptr) return;
this.getDiagram().hideTooltip();
this.hideHoverEffect();
end
virtual method onHandleMove(ptr)
var func='onHandleMove'+this.lineRouting;
switch (ptr.phase) {
case #[PTR_START]:
ptr.scrollMode = #[PTR_AUTOSCROLL];
ptr.handle = ptr.source.getAttribute('handle');
var parent=this.getParent(), qx=parent.qx, qy=parent.qy, s=this.canvas.scale;
ptr.wire = SVG.createElement(this.getDiagram().wiresLayer, 'path', {
fill:'none', 'stroke':this.hilightColor, 'stroke-width':2/s, 'stroke-dasharray':2/s,
transform:'translate('+qx+' '+qy+')', 'pointer-events':'none'
});
ptr.hx = FLOAT(ptr.source.getAttribute('cx')) - ptr.pos.x;
ptr.hy = FLOAT(ptr.source.getAttribute('cy')) - ptr.pos.y;
this[func](ptr);
break;
case #[PTR_FIRSTMOVE]:
SVG.setProperty(this.lineNode, 'stroke-opacity', 0.4);
break;
case #[PTR_MOVE]:
this[func](ptr);
break;
case #[PTR_LASTMOVE]:
SVG.setProperty(this.lineNode, 'stroke-opacity', 1);
break;
case #[PTR_FINISH]:
BEGIN('#TEXT[XMIT_TRANS_MOVE_LINK]'.replace('{0}', base.Class.metadata.title));
this[func](ptr);
this.updateLineData(ptr.path, ptr.controls);
this.reroute();
COMMIT();
// fall-through
case #[PTR_CANCEL]:
SVG.removeElement(ptr.wire);
if (!ptr.isOverSource) SVG.setProperty(ptr.source, 'class', 'handleNormal');
this.getDiagram().adjustHandles();
break;
}
end
//////////////////////////////////////////////////////////////////////////////////////
// GEOMETRIC CALCULATIONS
<@doc scope="private">
Gets the control handles data
virtual method getHandlesData()
var data=this['getHandlesData'+this.lineRouting](), p=this.getParent();
if (!data) return null;
for (var k in data) {
var d=data[k];
d.cx = p.qx + d.cx*p.qs;
d.cy = p.qy + d.cy*p.qs;
}
return data;
end
<@doc scope="private">
Gets the label positioning data
virtual method getLabelPos()
var diag = this.getDiagram();
var label = this.labelNode;
var bbox = label && label.getBBox() || null;
if (bbox && bbox.width>0) {
var x=bbox.x, y=bbox.y, w=bbox.width, h=bbox.height;
} else {
if (!base.@lpos) this.reroute();
var lpos=SPLITN(base.@lpos,3);
var w=75, h=16, x=lpos[0]-w/2, y=lpos[1]-h/2;
}
var p=this.getParent();
x = p.qx + x*p.qs;
y = p.qy + y*p.qs;
return {x:x, y:y, width:w, height:h};
end
<@doc scope="private">
Gets the tooltip positioning data
virtual method getTooltipPos()
var b=this.getLabelPos();
return {x:b.x+b.width/2, y:b.y+b.height, align:'center below', offset:4};
end
<@doc scope="private">
Gets the line's bounding box
virtual method getBBox()
var b=this.lineNode.getBBox(), p=this.getParent();
return {x:b.x*p.qs, y:b.y*p.qs, width:b.width*p.qs, height:b.height*p.qs};
end
<@doc scope="private">
Tests whether the line is enclosed by a given rectangle
virtual method enclosedIn(rect)
var b = this.getBBox(), p = this.getParent();
var x1=p.qx+b.x, x2=x1+b.width, y1=p.qy+b.y, y2=y1+b.height;
return (rect.x <= x1 && rect.x+rect.w >= x2 && rect.y <= y1 && rect.y+rect.h >= y2);
end
<@doc scope="private">
Tests whether the line intersects a given rectangle
virtual method intersects(rect)
if (this.enclosedIn(rect)) return true;
return false;
end
<@doc scope="private">
Converts an array of path drawing commands to string
virtual method path2str()
var A=arguments, len=A.length;
var B=new Array(len);
for (var i=0; i
Silently updates the line path data
virtual method updateLineData(path, controls, lpos)
BEGIN('#TEXT[XMIT_TRANS_MOVE_LINK]'.replace('{0}', "'" + base.Class.metadata.title + "'"));
// As an optimization, the rerouting methods call this method to update the line path silently.
// This is allowed since line routing is always invoked inside the current transaction scope.
base.setProperty('@path', path||'', true);
base.setProperty('@controls', controls||'', true);
if (lpos != undefined) base.setProperty('@lpos', lpos, true);
COMMIT();
end
<@doc scope="private">
Gets calculated line connection points, taking connection mode into account
virtual method getConnectionPoints()
var diag = this.getDiagram();
var srcpin = this.getSrcpin();
var trgpin = this.getTrgpin();
var srcshape = srcpin.getShape();
var trgshape = trgpin.getShape();
var useSrcShape = false, useTrgShape = false;
if (base.@connectionMode == #[SVG_CONNECT_SHAPES]) { useSrcShape = true; useTrgShape = true; }
if (diag.isZoomable) { // TODO: BlockZoom work
for (var b = srcshape.getParent(); b && b.isBlock && !b.isExpanded; b = b.getParent()) { srcshape = b; useSrcShape = true; }
for (var b = trgshape.getParent(); b && b.isBlock && !b.isExpanded; b = b.getParent()) { trgshape = b; useTrgShape = true; }
}
var p1 = (useSrcShape) ? srcshape.getLineCP(this, trgshape) : srcpin.getLineCP(this, trgpin);
var p2 = (useTrgShape) ? trgshape.getLineCP(this, srcshape) : trgpin.getLineCP(this, srcpin);
var count = 1;
if (useSrcShape || useTrgShape) {
// prevent line overlapping - count all lines that have same srcshape and trgshape as this line.
// the offset is calculated basing on the order of this line among all similar other lines.
var srcbase = srcshape.base, trgbase = trgshape.base;
var parent = diag.base.getCommonParent(srcbase, trgbase);
var block = diag.getGraphic(parent.id);
// diag.getGraphic returns null before diag.paint is finished. In such case - recursively find the block among parents
if (!block) for (block = srcshape; block && (block.id != parent.id); block = block.getParent());
var lines = block && block.base[block.base.@linesKey];
var canvas = this.canvas;
for (var k in lines) {
var ln = canvas.getWidget(k);
if (!ln) continue;
if (ln.id == this.id) break;
var src = ln.getSrcpin().getShape(), trg = ln.getTrgpin().getShape();
var srcsrc = (srcbase.id == src.base.id), trgtrg = (trgbase.id == trg.base.id);
var srctrg = (srcbase.id == trg.base.id), trgsrc = (trgbase.id == src.base.id);
if (diag.isZoomable) { // TODO: BlockZoom - for collapsed block - count inner lines as well
if (srcshape.isBlock && !srcshape.isExpanded) { srcsrc = srcbase.contains(src.base); srctrg = srcbase.contains(trg.base); }
if (trgshape.isBlock && !trgshape.isExpanded) { trgsrc = trgbase.contains(src.base); trgtrg = trgbase.contains(trg.base); }
}
if (srcsrc && trgtrg || srctrg && trgsrc) count++;
}
var dy = count * 12;
var p1Shape = canvas.getWidget(p1.shapeID), p2Shape = canvas.getWidget(p2.shapeID);
p1.y = (dy < p1Shape.h) ? p1.y + dy : p1.y + p1Shape.h;
p2.y = (dy < p2Shape.h) ? p2.y + dy : p2.y + p2Shape.h;
}
var pts = (useSrcShape) ? perimeterCP(srcshape, this.getParent(), p1, p2) : null;
var ptt = (useTrgShape) ? perimeterCP(trgshape, this.getParent(), p2, p1) : null;
// TODO: the following attempts to implement heuristics for too-close srcshape/trgshape. Finish it.
/*
if (pts && ptt && base.name && (SVG.getDistance(pts, ptt) < 60)) {
var pts = topCP(srcshape, this.getParent()), ptt = topCP(trgshape, this.getParent());
var sign = SIGN(ptt.x - pts.x), dxs = MIN(count * 12, srcshape.w/2), dxt = MIN(count * 12, trgshape.w/2);
pts.x += dxs * sign;
ptt.x -= dxt * sign;
}
*/
if (pts) { p1.x = pts.x; p1.y = pts.y; p1.rot = pts.rot; }
if (ptt) { p2.x = ptt.x; p2.y = ptt.y; p2.rot = ptt.rot; }
return {p1: p1, p2: p2};
function perimeterCP(shape, block, p1, p2) {
// get shape's rect in block's coordinate system
var rect = {x: shape.ox + shape.x1*shape.os - block.qx, y: shape.oy + shape.y1*shape.os - block.qy, w: shape.w, h: shape.h};
var pt = SVG.intersection(rect, p1, p2);
// convert direction character to rotation property value
// shape.rot is ignored because the line is between given points, not from a pin on shape's specific side
var dir2rot = {T: 1, R: 0, B: 3, L: 2};
var rot = Math.round(dir2rot[pt.dir]);
return {x: pt.x, y: pt.y, rot: rot};
}
function topCP(shape, block) {
// get shape's rect in block's coordinate system
var rect = {x: shape.ox + shape.x1*shape.os - block.qx, y: shape.oy + shape.y1*shape.os - block.qy, w: shape.w, h: shape.h};
pt = {x: rect.x + rect.w/2, y: rect.y, dir: 'T'};
// convert direction character to rotation property value
var dir2rot = {T: 1, R: 0, B: 3, L: 2};
var rot = Math.round(dir2rot[pt.dir]);
return {x: pt.x, y: pt.y, rot: rot};
}
end
//////////////////////////////////////////////////////////////////////////////////////
// STRAIGHT LINE ROUTING
virtual method reroute#[SVG_STRAIGHT_LINE](p1,p2)
// calculate line path
var path = 'l '+(p2.x-p1.x)+' '+(p2.y-p1.y);
// calculate label position
var lx = (p1.x+p2.x)/2;
var ly = (p1.y+p2.y)/2;
var la = Math.atan2(p2.y-p1.y, p2.x-p1.x)*SVG.R2D;
if (Math.abs(la) > 90) la += 180;
// update line data
this.updateLineData(path, '', this.path2str(lx,ly,la));
return path;
end
<@doc scope="private">
Gets the control handles data
virtual method getHandlesData#[SVG_STRAIGHT_LINE]()
return null;
end
virtual method onHandleMove#[SVG_STRAIGHT_LINE](ptr)
end
//////////////////////////////////////////////////////////////////////////////////////
// ANGULAR LINE ROUTING
virtual method reroute#[SVG_ANGULAR_LINE](p1,p2)
// calculate line path
var controls=base.@controls, C=SPLITN(controls);
var dx=p2.x-p1.x, dy=p2.y-p1.y;
if (C.length==5 && C[0]==(p2.rot*4+p1.rot)) {
var cx1=C[1], cy1=C[2], cx2=C[3], cy2=C[4];
} else {
var leg1=20*p1.scale, leg2=20*p2.scale;
var cx1=(p1.rot&1 ? 0 : leg1*(1-p1.rot)), cy1=(p1.rot&1 ? leg1*(p1.rot-2) : 0);
var cx2=(p2.rot&1 ? 0 : leg2*(1-p2.rot)), cy2=(p2.rot&1 ? leg2*(p2.rot-2) : 0);
controls = '';
}
var path = this.path2str('l', cx1, cy1, cx2-cx1+dx, cy2-cy1+dy, -cx2, -cy2);
// calculate label position
var x1 = p1.x+cx1;
var x2 = p2.x+cx2;
var y1 = p1.y+cy1
var y2 = p2.y+cy2;
var lx = (x1+x2)/2;
var ly = (y1+y2)/2;
var la = Math.atan2(y2-y1, x2-x1)*SVG.R2D;
if (Math.abs(la) > 90) la += 180;
// update line data
this.updateLineData(path, controls, this.path2str(lx,ly,la));
return path;
end
virtual method getHandlesData#[SVG_ANGULAR_LINE]()
var path=SPLITN(base.@path);
if (path.length != 7) return null;
var cp = this.getConnectionPoints();
return {
C1: {cx:path[1]+cp.p1.x,cy:path[2]+cp.p1.y},
C2: {cx:cp.p2.x-path[5],cy:cp.p2.y-path[6]}
}
end
virtual method onHandleMove#[SVG_ANGULAR_LINE](ptr)
switch (ptr.phase) {
case #[PTR_START]:
var path=SPLITN(base.@path);
if (path.length != 7) { ptr.cancel=true; break; }
var cp = this.getConnectionPoints();
ptr.p1 = cp.p1;
ptr.p2 = cp.p2;
ptr.cx1 = path[1];
ptr.cy1 = path[2];
ptr.cx2 = -path[5];
ptr.cy2 = -path[6];
break;
case #[PTR_MOVE]:
var p1=ptr.p1, p2=ptr.p2, dx=p2.x-p1.x, dy=p2.y-p1.y, x=ptr.pos.x, y=ptr.pos.y;
var cx1=ptr.cx1, cy1=ptr.cy1, cx2=ptr.cx2, cy2=ptr.cy2, hx=0, hy=0;
if (ptr.handle == 'C1') {
if (p1.rot & 1) { cy1+=ptr.offset.y; x=p1.x+cx1; hy=1; } else { cx1+=ptr.offset.x; y=p1.y+cy1; hx=1; }
} else if (ptr.handle == 'C2') {
if (p2.rot & 1) { cy2+=ptr.offset.y; x=p2.x+cx2; hy=1; } else { cx2+=ptr.offset.x; y=p2.y+cy2; hx=1; }
}
ptr.path = this.path2str('l', cx1, cy1, cx2-cx1+dx, cy2-cy1+dy, -cx2, -cy2);
ptr.controls = this.path2str(p2.rot*4+p1.rot, cx1, cy1, cx2, cy2);
ptr.wire.setAttribute('d', 'M'+p1.x+','+p1.y+ptr.path);
hx = (hx ? ptr.pos.x : ptr.origin.x);
hy = (hy ? ptr.pos.y : ptr.origin.y);
this.getDiagram().moveHandle(ptr.handle, hx+ptr.hx, hy+ptr.hy);
break;
}
end
//////////////////////////////////////////////////////////////////////////////////////
// CURVED LINE ROUTING
virtual method reroute#[SVG_CURVED_LINE](p1,p2)
// calculate line path
var controls=base.@controls, C=SPLITN(controls);
if (C.length==5 && C[0]==(p2.rot*4+p1.rot)) {
var cx1=C[1], cy1=C[2], cx2=C[3], cy2=C[4];
} else {
var leg1=20*p1.scale, leg2=20*p2.scale;
var cx1=(p1.rot==0 ? leg1 : p1.rot==2 ? -leg1 : 0), cy1=(p1.rot==3 ? leg1 : p1.rot==1 ? -leg1 : 0);
var cx2=(p2.rot==0 ? leg2 : p2.rot==2 ? -leg2 : 0), cy2=(p2.rot==3 ? leg2 : p2.rot==1 ? -leg2 : 0);
controls = '';
}
// calculate radius around p1 and p2 so that the curve starts after the marker
// TODO: same radius calculation is better be applied to other line types as well
var rad1=10*p1.scale, rad2=10*p2.scale;
var rw1=(p1.rot==0 ? rad1 : p1.rot==2 ? -rad1 : 0), rh1=(p1.rot==3 ? rad1 : p1.rot==1 ? -rad1 : 0);
var rw2=(p2.rot==0 ? -rad2 : p2.rot==2 ? rad2 : 0), rh2=(p2.rot==3 ? -rad2 : p2.rot==1 ? rad2 : 0);
var dx=p2.x-p1.x-rw1-rw2, dy=p2.y-p1.y-rh1-rh2;
var mx=(cx1+cx2+dx)/2, my=(cy1+cy2+dy)/2;
var path = this.path2str('l', rw1, rh1, 'q', cx1, cy1, mx, my, 'q', cx2+dx-mx, cy2+dy-my, dx-mx, dy-my, 'l', rw2, rh2);
// calculate label position
var x1 = p1.x+rw1+cx1;
var x2 = p2.x-rw2+cx2;
var y1 = p1.y+rh1+cy1
var y2 = p2.y-rh2+cy2;
var lx = (x1+x2)/2;
var ly = (y1+y2)/2;
var la = Math.atan2(y2-y1, x2-x1)*SVG.R2D;
if (Math.abs(la) > 90) la += 180;
// update line data
this.updateLineData(path, controls, this.path2str(lx,ly,la));
return path;
end
virtual method getHandlesData#[SVG_CURVED_LINE]()
var path=SPLITN(base.@path);
if (path.length != 16) return null;
var cp = this.getConnectionPoints(), p1 = cp.p1;
return {
C1: {cx:path[1]+path[4]+p1.x,cy:path[2]+path[5]+p1.y},
C2: {cx:path[1]+path[6]+path[9]+p1.x,cy:path[2]+path[7]+path[10]+p1.y}
}
end
virtual method onHandleMove#[SVG_CURVED_LINE](ptr)
switch (ptr.phase) {
case #[PTR_START]:
var path=SPLITN(base.@path);
if (path.length != 16) { ptr.cancel=true; break; }
var cp = this.getConnectionPoints();
ptr.p1 = cp.p1;
ptr.p2 = cp.p2;
ptr.rw1 = path[1]; ptr.rh1 = path[2]; ptr.rw2 = path[14]; ptr.rh2=path[15];
ptr.dx = ptr.p2.x-ptr.p1.x-ptr.rw1-ptr.rw2;
ptr.dy = ptr.p2.y-ptr.p1.y-ptr.rh1-ptr.rh2;
ptr.cx1 = path[4];
ptr.cy1 = path[5];
ptr.cx2 = path[6]+path[9]-ptr.dx;
ptr.cy2 = path[7]+path[10]-ptr.dy;
break;
case #[PTR_MOVE]:
var p1=ptr.p1, p2=ptr.p2, dx=ptr.dx, dy=ptr.dy;
var cx1=ptr.cx1, cy1=ptr.cy1, cx2=ptr.cx2, cy2=ptr.cy2;
switch (ptr.handle) {
case 'C1': cx1 += ptr.offset.x; cy1 += ptr.offset.y; break;
case 'C2': cx2 += ptr.offset.x; cy2 += ptr.offset.y; break;
}
var mx=(cx1+cx2+dx)/2, my=(cy1+cy2+dy)/2;
ptr.path = this.path2str('l', ptr.rw1, ptr.rh1, 'q', cx1, cy1, mx, my, 'q', cx2+dx-mx, cy2+dy-my, dx-mx, dy-my, 'l', ptr.rw2, ptr.rh2);
ptr.controls = this.path2str(p2.rot*4+p1.rot, cx1, cy1, cx2, cy2);
ptr.wire.setAttribute('d', 'M'+p1.x+','+p1.y+ptr.path);
this.getDiagram().moveHandle(ptr.handle, ptr.pos.x+ptr.hx, ptr.pos.y+ptr.hy);
break;
}
end
//////////////////////////////////////////////////////////////////////////////////////
// ORTHOGONAL LINE ROUTING
virtual method reroute#[SVG_ORTHOGONAL_LINE](p1,p2)
// get line's coordinate system (p1, p2 are relative to it)
var parent = this.getParent();
var pqx = parent.qx, pqy = parent.qy;
// normalize points (so that source point is at origin and oriented at 0 degrees)
var S1 = this.canvas.getWidget(p1.shapeID);
var ox1 = S1.ox, oy1=S1.oy, os1=S1.os;
var spx = p1.x - (ox1+S1.cx*os1 - pqx); // source point, relative to source shape center
var spy = p1.y - (oy1+S1.cy*os1 - pqy);
var Sx = ox1+(S1.cx+spx)*os1;
var Sy = oy1+(S1.cy+spy)*os1;
var Sz = p1.rot;
var Sx1 = ox1+S1.x1*os1-Sx; // source shape bounds
var Sy1 = oy1+S1.y1*os1-Sy;
var Sx2 = ox1+S1.x2*os1-Sx;
var Sy2 = oy1+S1.y2*os1-Sy;
var S2 = this.canvas.getWidget(p2.shapeID);
var ox2 = S2.ox, oy2=S2.oy, os2=S2.os;
var tpx = p2.x - (ox2+S2.cx*os2 - pqx); // target point, relative to target shape center
var tpy = p2.y - (oy2+S2.cy*os2 - pqy);
var Tx = ox2+(S2.cx+tpx)*os2-Sx;
var Ty = oy2+(S2.cy+tpy)*os2-Sy;
var Tz = p2.rot;
var Tx1 = ox2+S2.x1*os2-Sx; // target shape bounds
var Ty1 = oy2+S2.y1*os2-Sy;
var Tx2 = ox2+S2.x2*os2-Sx;
var Ty2 = oy2+S2.y2*os2-Sy;
var f1=((Sz%2) == 1), f2=(Sz==1 || Sz==2);
var t, mx, my;
if (f1) {
t=Tx; Tx=Ty; Ty=t;
t=Sx1; Sx1=Sy1; Sy1=t; t=Sx2; Sx2=Sy2; Sy2=t;
t=Tx1; Tx1=Ty1; Ty1=t; t=Tx2; Tx2=Ty2; Ty2=t;
}
if (f2) {
Tx=-Tx; Ty=-Ty;
Sx1=-Sx1; Sy1=-Sy1; Sx2=-Sx2; Sy2=-Sy2;
Tx1=-Tx1; Ty1=-Ty1; Tx2=-Tx2; Ty2=-Ty2;
}
t=Math.max(Sx1,Sx2); Sx1=Math.min(Sx1,Sx2); Sx2=t;
t=Math.max(Sy1,Sy2); Sy1=Math.min(Sy1,Sy2); Sy2=t;
t=Math.max(Tx1,Tx2); Tx1=Math.min(Tx1,Tx2); Tx2=t;
t=Math.max(Ty1,Ty2); Ty1=Math.min(Ty1,Ty2); Ty2=t;
var mr=10, MR=40; // minimum and maximum shape margins (absolute)
var Sa=(-Sy1)/(Sy2-Sy1), Sm=Math.max(mr,MR*Sa)*os1; // source margins (relative and absolute)
var Ta=(Ty-Ty1)/(Ty2-Ty1), Tm=Math.max(mr,MR*Ta)*os2; // target margins (relative and absolute)
Tz = (Tz-Sz)%4;
if (Tz<0) Tz+=4;
// compute path layout
var path=[], contour=0;
// pins are in opposite directions
if (Tz == 2) {
var flag = (Ty>0);
if (Tx > 0) {
// pins are facing each other
if (Ty==0) {
// trivial path (1 segment)
contour = 11;
path.push('h',Tx);
} else {
// path without obstacles (3 segments)
contour = (flag ? 12 : 13);
mx = Tx*(flag ? 1-Sa : Sa);
path.push('h',mx,'v',Ty,'h',Tx-mx);
}
} else {
// pins are facing opposite directions
var my1=(flag ? Ty1 : Sy1), my2=(flag ? Sy2 : Ty2), mx1, mx2;
if (my1 > my2) {
// path snakes through vertical space between shapes (5 segments)
contour = (flag ? 14 : 15);
mx1 = (flag ? MR-Sm : Sm)*1.5;
mx2 = (MR*1.5-mx1);
my = my1+(my2-my1)*Sa;
} else {
// path encircles shapes (5 segments)
contour = 16;
mx2 = (flag ? MR-Sm : Sm)*1.5;
mx1 = Math.max(0,Tx2)+mx2;
my = (flag ? Ty2+(MR-Sm)*1.5 : Ty1-Sm*1.5);
}
path.push('h',mx1,'v',my,'h',Tx-(mx1+mx2),'v',Ty-my,'h',mx2);
}
// pins are in same direction
} else if (Tz == 0) {
if (Tx>0 && Ty1*Ty2<=0) {
// path with bypass for target shape (5 segments)
var flag = (Math.abs(Ty1) < Math.abs(Ty2)), mx1, mx2;
contour = (flag ? 21 : 22);
mx1 = Tx1*(flag ? Sa : 1-Sa);
mx2 = (flag ? MR-Sm : Sm)*1.5;
my = (flag ? Ty1-(MR-Sm)*1.5 : Ty2+Sm*1.5);
path.push('h',mx1,'v',my,'h',Tx-mx1+mx2,'v',Ty-my,'h',-mx2);
} else if (Tx=Sy1 && Ty<=Sy2) {
// path with bypass for source shape (5 segments)
var flag = (Math.abs(Sy1) < Math.abs(Sy2)), mx1, mx2;
contour = (flag ? 23 : 24);
mx1 = Tx+(Sx1-Tx)*(flag ? 1-Sa : Sa);
mx2 = Sm*1.5;
my = (flag ? Sy1-mx2 : Sy2+mx2);
path.push('h',mx2,'v',my,'h',mx1-mx2,'v',Ty-my,'h',Tx-mx1);
} else {
// path without obstacles (3 segments)
var flag = (Ty>0);
contour = (flag ? 25 : 26);
mx = (flag ? (MR-Sm) : Sm)*1.5+Math.max(Tx,0);
path.push('h',mx,'v',Ty,'h',Tx-mx);
}
// pins are perpendicular
} else {
var flag = ((Sz&1) ? (Tz==3) : (Tz==1));
var sign = (flag ? 1 : -1), my1=(flag ? Ty1 : Sy1), my2=(flag ? Sy2 : Ty2);
if (Tx>0 && sign*Ty>0) {
// path without obstacles (2 segments)
contour = 31;
path.push('h',Tx,'v',Ty);
} else if (Tx<=0 && my1>my2) {
// path snakes through vertical space between shapes (4 segments)
contour = (flag ? 32 : 33);
mx = (flag ? MR-Sm : Sm)*1.5;
my = my1+(my2-my1)*Sa;
path.push('h',mx,'v',my,'h',Tx-mx,'v',Ty-my);
} else if (Tx1>0) {
// path snakes through horizontal space between shapes (4 segments)
contour = (flag ? 34 : 35);
mx=Tx1*(flag ? Sa : 1-Sa);
my=(flag ? MR-sign*Sm : sign*Sm)*1.5;
path.push('h',mx,'v',Ty-my,'h',Tx-mx,'v',my);
} else {
// path encircles shapes (4 segments)
contour = (flag ? 36 : 37);
my=(flag ? Math.min(Sy1,Ty1)-Sm*1.5 : Math.max(Sy2,Ty2)+(MR-Sm)*1.5);
mx=Math.max(0,Tx2)+(flag ? Sm : MR-Sm)*1.5;
path.push('h',mx,'v',my,'h',Tx-mx,'v',Ty-my);
}
}
// rotate path to original orientation
for (var i=0, len=path.length, sign=(f2 ? -1 : 1); i= 3) { if (path[0]=='h') lx+=path[1]; else ly+=path[1]; }
if (ln >= 5) { if (path[2]=='h') lx+=path[3]; else ly+=path[3]; }
if (path[ln-1]=='h') {
lx += path[ln]/2;
} else {
if (ABS(path[ln]) >= lmin) {
ly += path[ln]/2;
la = SIGN(path[ln])*90;
} else if (ln >= 3 && ABS(path[ln-2]) >= lmin) {
lx -= path[ln-2]/2;
} else if (ln <= path.length-2 && ABS(path[ln+2]) >= lmin) {
ly += path[ln];
lx += path[ln+2]/2;
} else {
ly += path[ln]/2;
}
}
lx += p1.x;
ly += p1.y;
// apply round corners
var r=base.@cornerRadius||0;
if (r > 0) path = this.applyRoundCorners(path, r);
// update line data
path = path.join(' ');
this.updateLineData(path, controls.join(' '), this.path2str(lx,ly,la));
return path;
end
virtual method getHandlesData#[SVG_ORTHOGONAL_LINE]()
var path=SPLIT(base.@path), len=path.length;
if (len <= 4) return null;
var cp = this.getConnectionPoints(), p1 = cp.p1, cx=p1.x, cy=p1.y, data={};
for (var i=0; i0) data['C'+(i/2)] = {cx:cx+dx/2, cy:cy+dy/2}
cx+=dx; cy+=dy;
}
return data;
end
virtual method onHandleMove#[SVG_ORTHOGONAL_LINE](ptr)
switch (ptr.phase) {
case #[PTR_START]:
var path=SPLIT(base.@path), len=path.length;
var n=POS((ptr.source.getAttribute('handle')||'').charAt(1)), axis=path[2*(n-1)];
if (axis != 'h' && axis != 'v') { ptr.cancel=true; break; }
for (var i=0; i 0) path = this.applyRoundCorners(path, r);
ptr.wire.setAttribute('d', 'M'+p1.x+','+p1.y+path.join(' '));
this.getDiagram().moveHandle('C'+n, org.x+dx+ptr.hx, org.y+dy+ptr.hy);
break;
case #[PTR_FINISH]:
var controls=SPLITN(base.@controls);
controls[ptr.n] += ptr.d0;
ptr.path = ptr.path.join(' ');
ptr.controls = controls.join(' ');
break;
}
end
virtual method applyRoundCorners(path, radius)
var R=[], p1=[], p2=[], len=path.length-2;
for (var i=0; i0 ? 0 : 1)+' '+d2+' '+d1);
}
p1[i+3] -= d2;
}
for (var len=p1.length; i
Shows the line's movable effect
virtual method showMovableEffect()
if (this.getDiagram().alphaMode) SVG.setProperty(this.graphicNode, 'opacity', 1);
end
<@doc scope="private">
Hides the line's movable effect
virtual method hideMovableEffect()
if (this.getDiagram().alphaMode) SVG.setProperty(this.graphicNode, 'opacity', 0.2);
end