devexpress-diagram
Version:
DevExpress Diagram Control
413 lines (393 loc) • 19.4 kB
text/typescript
import { UnitConverter } from "@devexpress/utils/lib/class/unit-converter";
import { Point } from "@devexpress/utils/lib/geometry/point";
import { Rectangle } from "@devexpress/utils/lib/geometry/rectangle";
import { Segment } from "@devexpress/utils/lib/geometry/segment";
import { Size } from "@devexpress/utils/lib/geometry/size";
import { PAGE_BG_TEXTFLOOR_FILTER_ID } from "../../../src/Render/CanvasManagerBase";
import { INativeConnector } from "../../Api/INativeItem";
import { NativeConnector } from "../../Api/NativeItem";
import { DiagramUnit } from "../../Enums";
import { MouseEventElementType } from "../../Events/Event";
import { TextOwner } from "../../Render/Measurer/ITextMeasurer";
import {
PathPrimitive, PathPrimitiveLineToCommand, PathPrimitiveMoveToCommand
} from "../../Render/Primitives/PathPrimitive";
import { SvgPrimitive } from "../../Render/Primitives/Primitive";
import { TextPrimitive } from "../../Render/Primitives/TextPrimitive";
import { RenderUtils } from "../../Render/Utils";
import { ConnectorRoutingMode } from "../../Settings";
import { GeometryUtils } from "../../Utils";
import { ConnectionPoint } from "../ConnectionPoint";
import { ConnectionPointSide, DiagramItem } from "../DiagramItem";
import { ModelUtils } from "../ModelUtils";
import { ConnectorPointsCalculator } from "./Calculators/ConnectorPointsCalculator";
import { ConnectorPointsCalculatorBase } from "./Calculators/ConnectorPointsCalculatorBase";
import {
ConnectorPointsOrthogonalCalculator
} from "./Calculators/ConnectorPointsOrthogonalCalculator";
import {
ConnectorLineEndingArrowStrategy, ConnectorLineEndingFilledTriangleStrategy,
ConnectorLineEndingNoneStrategy, ConnectorLineEndingOutlinedTriangleStrategy,
ConnectorLineEndingStrategy
} from "./ConnectorLineEndingStrategies";
import {
ConnectorLineEnding, ConnectorLineOption, ConnectorProperties
} from "./ConnectorProperties";
import { ConnectorRenderPoint } from "./ConnectorRenderPoint";
import { ConnectorText, ConnectorTexts } from "./ConnectorTexts";
import { ConnectorRenderPointsContext } from "./Routing/ConnectorRenderPointsContext";
import { IConnectorRoutingStrategy } from "./Routing/ConnectorRoutingModel";
export enum ConnectorPosition { Begin, End }
export const CONNECTOR_DEFAULT_TEXT_POSITION = 0.5;
export class Connector extends DiagramItem {
points: Point[];
beginItem: DiagramItem;
beginConnectionPointIndex: number = -1;
endItem: DiagramItem;
endConnectionPointIndex: number = -1;
texts: ConnectorTexts;
properties: ConnectorProperties;
private routingStrategy : IConnectorRoutingStrategy;
static minOffset: number = UnitConverter.pixelsToTwips(24);
static minTextHeight: number = UnitConverter.pixelsToTwips(12);
private renderPoints: ConnectorRenderPoint[];
private renderPointsWithoutSkipped: ConnectorRenderPoint[];
private lockCreateRenderPoints: boolean;
private shouldInvalidateRenderPoints: boolean;
private actualRoutingMode : ConnectorRoutingMode;
constructor(points: Point[]) {
super();
this.properties = new ConnectorProperties();
this.points = points.map(pt => pt.clone());
if(points.length < 2)
throw Error("Points count should be greater than 1");
this.texts = new ConnectorTexts();
}
get rectangle(): Rectangle {
return GeometryUtils.createRectagle(this.getRenderPoints(true));
}
get skippedRenderPoints(): ConnectorRenderPoint[] {
return this.renderPoints ? this.renderPoints.filter(p => p.skipped) : undefined;
}
private get shouldChangeRenderPoints(): boolean {
return this.renderPoints !== undefined && this.routingStrategy !== undefined;
}
assign(item: Connector): void {
super.assign(item);
item.beginItem = this.beginItem;
item.beginConnectionPointIndex = this.beginConnectionPointIndex;
item.endItem = this.endItem;
item.endConnectionPointIndex = this.endConnectionPointIndex;
item.properties = this.properties.clone();
item.texts = this.texts.clone();
if(this.routingStrategy !== undefined)
item.routingStrategy = this.routingStrategy.clone();
if(this.renderPoints !== undefined)
item.renderPoints = this.renderPoints.map(p => p.clone());
if(this.renderPointsWithoutSkipped !== undefined)
item.renderPointsWithoutSkipped = this.renderPointsWithoutSkipped.map(p => p.clone());
if(this.actualRoutingMode !== undefined)
item.actualRoutingMode = this.actualRoutingMode;
if(this.lockCreateRenderPoints !== undefined)
item.lockCreateRenderPoints = this.lockCreateRenderPoints;
if(this.shouldInvalidateRenderPoints !== undefined)
item.shouldInvalidateRenderPoints = this.shouldInvalidateRenderPoints;
}
clone(): Connector {
const clone = new Connector(this.points);
this.assign(clone);
return clone;
}
getTextCount(): number {
return this.texts.count();
}
getText(position: number = CONNECTOR_DEFAULT_TEXT_POSITION): string {
const textObj = this.texts.get(position);
return textObj ? textObj.value : "";
}
setText(text: string, position: number = CONNECTOR_DEFAULT_TEXT_POSITION): void {
if(!text || text === "")
this.texts.remove(position);
else
this.texts.set(position, new ConnectorText(position, text));
}
getTextPoint(position: number): Point {
const points = this.getRenderPoints();
return GeometryUtils.getPathPointByPosition(points, position)[0];
}
getTextPositionByPoint(point: Point): number {
const points = this.getRenderPoints();
const length = GeometryUtils.getPathLength(points);
const pos = GeometryUtils.getPathPositionByPoint(points, point);
const minTextHeight = UnitConverter.pointsToTwips(parseInt(this.styleText["font-size"]));
if(minTextHeight > pos * length)
return minTextHeight / length;
if(minTextHeight > length - pos * length)
return (length - minTextHeight) / length;
return pos;
}
getTextRectangle(position: number): Rectangle {
return Rectangle.fromGeometry(this.getTextPoint(position), new Size(0, 0));
}
changeRoutingStrategy(strategy: IConnectorRoutingStrategy) {
this.routingStrategy = strategy;
this.invalidateRenderPoints();
}
clearRoutingStrategy() : void {
delete this.routingStrategy;
delete this.renderPoints;
delete this.renderPointsWithoutSkipped;
delete this.lockCreateRenderPoints;
delete this.actualRoutingMode;
delete this.shouldInvalidateRenderPoints;
this.invalidateRenderPoints();
}
getCustomRenderPoints(keepSkipped: boolean = false) : ConnectorRenderPoint[] {
const renderPoints = this.getRenderPoints(keepSkipped);
const result : ConnectorRenderPoint[] = [];
renderPoints.forEach((p, index) => {
if(index > 0 && index < renderPoints.length - 1)
result.push(p);
});
return result;
}
getRenderPoints(keepSkipped: boolean = false): ConnectorRenderPoint[] {
if(this.shouldInvalidateRenderPoints === undefined || this.shouldInvalidateRenderPoints) {
this.shouldInvalidateRenderPoints = false;
if(!this.routingStrategy)
this.changeRenderPoints(this.getCalculator().getPoints());
else if(!this.lockCreateRenderPoints) {
this.changeRenderPoints(new ConnectorPointsOrthogonalCalculator(this).getPoints());
if(this.actualRoutingMode !== ConnectorRoutingMode.None && this.points && this.renderPoints) {
const beginPoint = this.points[0];
const endPoint = this.points[this.points.length - 1];
if(!beginPoint.equals(endPoint)) {
const newRenderPoints = this.routingStrategy.createRenderPoints(
this.points, this.renderPoints, this.beginItem, this.endItem,
this.beginConnectionPointIndex, this.endConnectionPointIndex,
ModelUtils.getConnectorContainer(this));
if(newRenderPoints) {
this.changeRenderPoints(newRenderPoints);
this.actualRoutingMode = ConnectorRoutingMode.AllShapesOnly;
}
else
this.actualRoutingMode = ConnectorRoutingMode.None;
}
}
}
}
return keepSkipped ? this.renderPoints : this.renderPointsWithoutSkipped;
}
createRenderPointsContext(): ConnectorRenderPointsContext {
return this.shouldChangeRenderPoints ? new ConnectorRenderPointsContext(this.renderPoints.map(p=>p.clone()), this.lockCreateRenderPoints, this.actualRoutingMode) : undefined;
}
updatePointsOnPageResize(offsetX: number, offsetY: number): void {
this.points = this.points.map(p => p.clone().offset(offsetX, offsetY));
if(this.renderPoints)
this.changeRenderPoints(this.renderPoints.map(p => {
const result = p.clone().offset(offsetX, offsetY);
result.pointIndex = p.pointIndex;
result.skipped = p.skipped;
return result;
}));
}
addPoint(pointIndex: number, point: Point): void {
this.points.splice(pointIndex, 0, point);
}
deletePoint(pointIndex: number): void {
this.points.splice(pointIndex, 1);
}
movePoint(pointIndex: number, point: Point): void {
this.points[pointIndex] = point;
}
onAddPoint(pointIndex: number, point: Point): void {
if(this.shouldChangeRenderPoints)
this.replaceRenderPointsCore(this.routingStrategy.onAddPoint(this.points, pointIndex, point, this.renderPoints), true, ConnectorRoutingMode.AllShapesOnly);
else
this.invalidateRenderPoints();
}
onDeletePoint(pointIndex: number): void {
if(this.shouldChangeRenderPoints)
this.replaceRenderPointsCore(this.routingStrategy.onDeletePoint(this.points, pointIndex, this.renderPoints), this.points.length > 2, ConnectorRoutingMode.AllShapesOnly);
else
this.invalidateRenderPoints();
}
onMovePoint(pointIndex: number, point: Point) : void {
if(this.shouldChangeRenderPoints) {
if(pointIndex === 0 || pointIndex === this.points.length - 1)
this.lockCreateRenderPoints = false;
this.replaceRenderPointsCore(this.routingStrategy.onMovePoint(this.points, pointIndex, point, this.renderPoints), this.lockCreateRenderPoints, ConnectorRoutingMode.AllShapesOnly);
}
else
this.invalidateRenderPoints();
}
onMovePoints(beginPointIndex: number, beginPoint: Point, lastPointIndex: number, lastPoint: Point) : void {
if(this.shouldChangeRenderPoints) {
if(beginPointIndex === 0 || lastPointIndex === this.points.length - 1)
this.lockCreateRenderPoints = false;
this.replaceRenderPointsCore(this.routingStrategy.onMovePoints(this.points, beginPointIndex, beginPoint, lastPointIndex, lastPoint, this.renderPoints), this.lockCreateRenderPoints, ConnectorRoutingMode.AllShapesOnly);
}
else
this.invalidateRenderPoints();
}
replaceRenderPoints(context: ConnectorRenderPointsContext): void {
if(context !== undefined)
this.replaceRenderPointsCore(context.renderPoints, context.lockCreateRenderPoints, context.actualRoutingMode);
else
this.invalidateRenderPoints();
}
clearRenderPoints() {
this.changeRenderPoints(undefined);
this.lockCreateRenderPoints = false;
this.actualRoutingMode = undefined;
this.invalidateRenderPoints();
}
private replaceRenderPointsCore(renderPoints: ConnectorRenderPoint[], lockCreateRenderPoints: boolean, mode: ConnectorRoutingMode): void{
this.changeRenderPoints(renderPoints);
this.lockCreateRenderPoints = lockCreateRenderPoints;
this.actualRoutingMode = mode;
this.invalidateRenderPoints();
}
private changeRenderPoints(renderPoints: ConnectorRenderPoint[]) {
this.renderPoints = renderPoints;
this.renderPointsWithoutSkipped = renderPoints ? this.renderPoints.filter(pt => !pt.skipped) : undefined;
}
getCalculator(): ConnectorPointsCalculatorBase {
return (this.properties.lineOption === ConnectorLineOption.Straight) ?
new ConnectorPointsCalculator(this) :
new ConnectorPointsOrthogonalCalculator(this);
}
invalidateRenderPoints() : void {
this.shouldInvalidateRenderPoints = true;
}
createPrimitives(): SvgPrimitive<SVGGraphicsElement>[] {
let result = [];
const points = this.getRenderPoints();
const path = new PathPrimitive(
points.map((pt, index) => {
return index === 0 ? new PathPrimitiveMoveToCommand(pt.x, pt.y) : new PathPrimitiveLineToCommand(pt.x, pt.y);
}), this.style);
result.push(path);
result = result.concat(this.createLineEndingPrimitives(points, path));
result = result.concat(this.createTextPrimitives());
return result;
}
createLineEndingPrimitives(points: Point[], connectorPath: PathPrimitive): SvgPrimitive<SVGGraphicsElement>[] {
const result = [];
if(points.length > 1) {
const lineEndingInfo = [
{ strategy: this.createLineEndingStrategy(this.properties.startLineEnding), point1: points[0], point2: points[1] },
{ strategy: this.createLineEndingStrategy(this.properties.endLineEnding), point1: points[points.length - 1], point2: points[points.length - 2] }
];
lineEndingInfo.forEach(info => {
const strategy = info.strategy;
if(strategy.hasCommands()) {
let lineEndingPath = connectorPath;
if(strategy.needCreateSeparatePrimitive())
result.push(lineEndingPath = strategy.createPrimitive());
lineEndingPath.commands = lineEndingPath.commands.concat(strategy.createCommands(info.point1, info.point2));
}
});
}
return result;
}
createLineEndingStrategy(lineEnding: ConnectorLineEnding): ConnectorLineEndingStrategy {
switch(lineEnding) {
case ConnectorLineEnding.None:
return new ConnectorLineEndingNoneStrategy(this.style);
case ConnectorLineEnding.Arrow:
return new ConnectorLineEndingArrowStrategy(this.style);
case ConnectorLineEnding.OutlinedTriangle:
return new ConnectorLineEndingOutlinedTriangleStrategy(this.style);
case ConnectorLineEnding.FilledTriangle:
return new ConnectorLineEndingFilledTriangleStrategy(this.style);
default:
return new ConnectorLineEndingStrategy(this.style);
}
}
createSelectorPrimitives(): SvgPrimitive<SVGGraphicsElement>[] {
const result = [];
const points = this.getRenderPoints();
result.push(new PathPrimitive(
points.map((pt, index) => {
if(index === 0)
return new PathPrimitiveMoveToCommand(pt.x, pt.y);
else
return new PathPrimitiveLineToCommand(pt.x, pt.y);
}), null, "selector"
));
return result;
}
createTextPrimitives(): SvgPrimitive<SVGGraphicsElement>[] {
if(!this.enableText) return [];
let result = [];
this.texts.forEach(textObj => {
const text = this.getText(textObj.position);
if(text && text !== "") {
const pt = this.getTextPoint(textObj.position);
result = result.concat([
new TextPrimitive(pt.x, pt.y, text, TextOwner.Connector, undefined, undefined, undefined, this.styleText, true, null, PAGE_BG_TEXTFLOOR_FILTER_ID, undefined, el => {
RenderUtils.setElementEventData(el, MouseEventElementType.ConnectorText, this.key,
textObj.position);
})
]);
}
});
return result;
}
getExtremeItem(position: ConnectorPosition): DiagramItem {
if(position === ConnectorPosition.Begin)
return this.beginItem;
if(position === ConnectorPosition.End)
return this.endItem;
return null;
}
getExtremeConnectionPointIndex(position: ConnectorPosition): number {
if(position === ConnectorPosition.Begin)
return this.beginConnectionPointIndex;
if(position === ConnectorPosition.End)
return this.endConnectionPointIndex;
return -1;
}
getMinX(): number {
const points = this.getRenderPoints();
const xarr = points.map(p => p.x);
return xarr.reduce((prev, cur) => Math.min(prev, cur), Number.MAX_VALUE);
}
getMinY(): number {
const points = this.getRenderPoints();
const yarr = points.map(p => p.y);
return yarr.reduce((prev, cur) => Math.min(prev, cur), Number.MAX_VALUE);
}
getConnectionPoints(): ConnectionPoint[] {
return [];
}
getConnectionPointSide(point: ConnectionPoint, targetPoint?: Point): ConnectionPointSide {
return ConnectionPointSide.Undefined;
}
getSegments(): Segment<ConnectorRenderPoint>[] {
const result = [];
const renderPoints = this.getRenderPoints();
renderPoints.forEach((pt, index) => {
if(index > 0)
result.push(new Segment(renderPoints[index - 1], pt));
});
return result;
}
intersectedByRect(rect: Rectangle): boolean {
return this.getSegments().some(s => s.isIntersectedByRect(rect));
}
toNative(units?: DiagramUnit): INativeConnector {
const item = new NativeConnector(this.key, this.dataKey);
item.fromKey = this.beginItem && this.beginItem.dataKey;
item.toKey = this.endItem && this.endItem.dataKey;
item.texts = this.texts.map(t => t).sort((a, b) => a.position - b.position).map(a => a.value);
item.fromId = this.beginItem && this.beginItem.key;
item.fromPointIndex = this.beginConnectionPointIndex;
item.toId = this.endItem && this.endItem.key;
item.toPointIndex = this.endConnectionPointIndex;
item.points = this.points.map(pt => pt.clone());
item.applyUnits(units);
return item;
}
}