UNPKG

devexpress-diagram

Version:

DevExpress Diagram Control

413 lines (393 loc) 19.4 kB
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; } }