using System; using System.Collections.Generic; using System.Windows.Controls; using AIStudio.Wpf.DiagramDesigner.Geometry; namespace AIStudio.Wpf.DiagramDesigner { // Note: I couldn't find a useful open source library that does // orthogonal routing so started to write something on my own. // Categorize this as a quick and dirty short term solution. // I will keep on searching. // Helper class to provide an orthogonal connection path public class OrthogonalPathFinder : IPathFinder { private const int const_margin = 20; public List UpdateConnectionPoints(IDiagramViewModel diagramViewModel, PointBase sourceA, PointBase sourceB, FullyCreatedConnectorInfo sourceConnectorInfo, ConnectorInfoBase sinkConnectorInfo) { List connectionPoints; var isFullConnection = sinkConnectorInfo is FullyCreatedConnectorInfo; var area = new RectangleBase(sourceA, sourceB); var points = new List() { new PointBase(sourceA.X < sourceB.X ? 0d : area.Width, sourceA.Y < sourceB.Y ? 0d : area.Height ), new PointBase(sourceA.X > sourceB.X ? 0d : area.Width, sourceA.Y > sourceB.Y ? 0d : area.Height) }; ConnectorInfo sourceInfo = ConnectorInfo(sourceConnectorInfo.Orientation, points[0].X, points[0].Y, sourceConnectorInfo.DataItem.ItemWidth, sourceConnectorInfo.DataItem.ItemHeight, points[0]); //StartPoint = points[0]; if (isFullConnection) { ConnectorInfo sinkInfo = ConnectorInfo(sinkConnectorInfo.Orientation, points[1].X, points[1].Y, ((FullyCreatedConnectorInfo)sinkConnectorInfo).DataItem.ItemWidth, ((FullyCreatedConnectorInfo)sinkConnectorInfo).DataItem.ItemHeight, points[1]); connectionPoints = ConnectorPoint.ToList(GetConnectionLine(diagramViewModel, sourceInfo, sinkInfo, false, sourceConnectorInfo.IsInnerPoint)); //EndPoint = ConnectionPoints.Last(); } else { connectionPoints = ConnectorPoint.ToList(GetConnectionLine(diagramViewModel, sourceInfo, points[1], sourceConnectorInfo.Orientation, false, sourceConnectorInfo.IsInnerPoint)); //EndPoint = new PointBase(); } return connectionPoints; } public ConnectorInfo ConnectorInfo(ConnectorOrientation orientation, double left, double top, double width, double height, PointBase position) { return new ConnectorInfo() { Orientation = orientation, DesignerItemSize = new SizeBase(width, height), DesignerItemLeft = left, DesignerItemTop = top, Position = position }; } public List GetConnectionLine(IDiagramViewModel diagramViewModel, ConnectorInfo source, ConnectorInfo sink, bool showLastLine, bool sourceInnerPoint = false) { List linePoints = new List(); int margin1 = sourceInnerPoint ? 0 : const_margin; int margin2 = const_margin; RectangleBase rectSource = GetRectWithMargin(source, margin1); RectangleBase rectSink = GetRectWithMargin(sink, margin2); PointBase startPoint = GetOffsetPoint(source, rectSource, sourceInnerPoint); PointBase endPoint = GetOffsetPoint(sink, rectSink); linePoints.Add(startPoint); PointBase currentPoint = startPoint; if (!rectSink.Contains(currentPoint) && !rectSource.Contains(endPoint)) { while (true) { #region source node if (IsPointVisible(currentPoint, endPoint, new RectangleBase[] { rectSource, rectSink })) { linePoints.Add(endPoint); currentPoint = endPoint; break; } PointBase neighbour = GetNearestVisibleNeighborSink(currentPoint, endPoint, sink, rectSource, rectSink); if (!double.IsNaN(neighbour.X)) { linePoints.Add(neighbour); linePoints.Add(endPoint); currentPoint = endPoint; break; } if (currentPoint == startPoint) { bool flag; PointBase n = GetNearestNeighborSource(source, endPoint, rectSource, rectSink, out flag, sourceInnerPoint); if (linePoints.Contains(n)) { break; } linePoints.Add(n); currentPoint = n; if (!IsRectVisible(currentPoint, rectSink, new RectangleBase[] { rectSource })) { PointBase n1, n2; GetOppositeCorners(source.Orientation, rectSource, out n1, out n2, sourceInnerPoint); if (flag) { linePoints.Add(n1); currentPoint = n1; } else { linePoints.Add(n2); currentPoint = n2; } if (!IsRectVisible(currentPoint, rectSink, new RectangleBase[] { rectSource })) { if (flag) { linePoints.Add(n2); currentPoint = n2; } else { linePoints.Add(n1); currentPoint = n1; } } } } #endregion #region sink node else // from here on we jump to the sink node { PointBase n1, n2; // neighbour corner PointBase s1, s2; // opposite corner GetNeighborCorners(sink.Orientation, rectSink, out s1, out s2); GetOppositeCorners(sink.Orientation, rectSink, out n1, out n2); bool n1Visible = IsPointVisible(currentPoint, n1, new RectangleBase[] { rectSource, rectSink }); bool n2Visible = IsPointVisible(currentPoint, n2, new RectangleBase[] { rectSource, rectSink }); if (n1Visible && n2Visible) { if (rectSource.Contains(n1)) { linePoints.Add(n2); if (rectSource.Contains(s2)) { linePoints.Add(n1); linePoints.Add(s1); } else linePoints.Add(s2); linePoints.Add(endPoint); currentPoint = endPoint; break; } if (rectSource.Contains(n2)) { linePoints.Add(n1); if (rectSource.Contains(s1)) { linePoints.Add(n2); linePoints.Add(s2); } else linePoints.Add(s1); linePoints.Add(endPoint); currentPoint = endPoint; break; } if ((Distance(n1, endPoint) <= Distance(n2, endPoint))) { linePoints.Add(n1); if (rectSource.Contains(s1)) { linePoints.Add(n2); linePoints.Add(s2); } else linePoints.Add(s1); linePoints.Add(endPoint); currentPoint = endPoint; break; } else { linePoints.Add(n2); if (rectSource.Contains(s2)) { linePoints.Add(n1); linePoints.Add(s1); } else linePoints.Add(s2); linePoints.Add(endPoint); currentPoint = endPoint; break; } } else if (n1Visible) { linePoints.Add(n1); if (rectSource.Contains(s1)) { linePoints.Add(n2); linePoints.Add(s2); } else linePoints.Add(s1); linePoints.Add(endPoint); currentPoint = endPoint; break; } else { linePoints.Add(n2); if (rectSource.Contains(s2)) { linePoints.Add(n1); linePoints.Add(s1); } else linePoints.Add(s2); linePoints.Add(endPoint); currentPoint = endPoint; break; } } #endregion } } else { linePoints.Add(endPoint); } linePoints = OptimizeLinePoints(linePoints, new RectangleBase[] { rectSource, rectSink }, source.Orientation, sink.Orientation); CheckPathEnd(source, sink, showLastLine, linePoints, sourceInnerPoint); return linePoints; } public List GetConnectionLine(IDiagramViewModel diagramViewModel, ConnectorInfo source, PointBase sinkPoint, ConnectorOrientation preferredOrientation, bool showLastLine, bool isInnerPoint = false) { List linePoints = new List(); int margin = isInnerPoint ? 0 : const_margin; RectangleBase rectSource = GetRectWithMargin(source, margin); PointBase startPoint = GetOffsetPoint(source, rectSource, isInnerPoint); PointBase endPoint = sinkPoint; linePoints.Add(startPoint); PointBase currentPoint = startPoint; if (!rectSource.Contains(endPoint)) { while (true) { if (IsPointVisible(currentPoint, endPoint, new RectangleBase[] { rectSource })) { linePoints.Add(endPoint); break; } bool sideFlag; PointBase n = GetNearestNeighborSource(source, endPoint, rectSource, out sideFlag, isInnerPoint); linePoints.Add(n); currentPoint = n; if (IsPointVisible(currentPoint, endPoint, new RectangleBase[] { rectSource })) { linePoints.Add(endPoint); break; } else { PointBase n1, n2; GetOppositeCorners(source.Orientation, rectSource, out n1, out n2, isInnerPoint); if (sideFlag) linePoints.Add(n1); else linePoints.Add(n2); linePoints.Add(endPoint); break; } } } else { linePoints.Add(endPoint); } if (preferredOrientation != ConnectorOrientation.None) linePoints = OptimizeLinePoints(linePoints, new RectangleBase[] { rectSource }, source.Orientation, preferredOrientation); else linePoints = OptimizeLinePoints(linePoints, new RectangleBase[] { rectSource }, source.Orientation, GetOpositeOrientation(source.Orientation)); if (!showLastLine) { linePoints.Insert(0, source.Position); } return linePoints; } private static List OptimizeLinePoints(List linePoints, RectangleBase[] rectangles, ConnectorOrientation sourceOrientation, ConnectorOrientation sinkOrientation) { List points = new List(); int cut = 0; for (int i = 0; i < linePoints.Count; i++) { if (i >= cut) { for (int k = linePoints.Count - 1; k > i; k--) { if (IsPointVisible(linePoints[i], linePoints[k], rectangles)) { cut = k; break; } } points.Add(linePoints[i]); } } #region Line for (int j = 0; j < points.Count - 1; j++) { if (points[j].X != points[j + 1].X && points[j].Y != points[j + 1].Y) { ConnectorOrientation orientationFrom; ConnectorOrientation orientationTo; // orientation from point if (j == 0) orientationFrom = sourceOrientation; else orientationFrom = GetOrientation(points[j], points[j - 1]); // orientation to pint if (j == points.Count - 2) orientationTo = sinkOrientation; else orientationTo = GetOrientation(points[j + 1], points[j + 2]); if ((orientationFrom == ConnectorOrientation.Left || orientationFrom == ConnectorOrientation.Right) && (orientationTo == ConnectorOrientation.Left || orientationTo == ConnectorOrientation.Right)) { double centerX = Math.Min(points[j].X, points[j + 1].X) + Math.Abs(points[j].X - points[j + 1].X) / 2; points.Insert(j + 1, new PointBase(centerX, points[j].Y)); points.Insert(j + 2, new PointBase(centerX, points[j + 2].Y)); if (points.Count - 1 > j + 3) points.RemoveAt(j + 3); return points; } if ((orientationFrom == ConnectorOrientation.Top || orientationFrom == ConnectorOrientation.Bottom) && (orientationTo == ConnectorOrientation.Top || orientationTo == ConnectorOrientation.Bottom)) { double centerY = Math.Min(points[j].Y, points[j + 1].Y) + Math.Abs(points[j].Y - points[j + 1].Y) / 2; points.Insert(j + 1, new PointBase(points[j].X, centerY)); points.Insert(j + 2, new PointBase(points[j + 2].X, centerY)); if (points.Count - 1 > j + 3) points.RemoveAt(j + 3); return points; } if ((orientationFrom == ConnectorOrientation.Left || orientationFrom == ConnectorOrientation.Right) && (orientationTo == ConnectorOrientation.Top || orientationTo == ConnectorOrientation.Bottom)) { points.Insert(j + 1, new PointBase(points[j + 1].X, points[j].Y)); return points; } if ((orientationFrom == ConnectorOrientation.Top || orientationFrom == ConnectorOrientation.Bottom) && (orientationTo == ConnectorOrientation.Left || orientationTo == ConnectorOrientation.Right)) { points.Insert(j + 1, new PointBase(points[j].X, points[j + 1].Y)); return points; } } } #endregion return points; } private static ConnectorOrientation GetOrientation(PointBase p1, PointBase p2) { if (p1.X == p2.X) { if (p1.Y >= p2.Y) return ConnectorOrientation.Bottom; else return ConnectorOrientation.Top; } else if (p1.Y == p2.Y) { if (p1.X >= p2.X) return ConnectorOrientation.Right; else return ConnectorOrientation.Left; } throw new Exception("Failed to retrieve orientation"); } private static Orientation GetOrientation(ConnectorOrientation sourceOrientation, bool isInnerPoint) { if (isInnerPoint) { return Orientation.Vertical; } switch (sourceOrientation) { case ConnectorOrientation.Left: return Orientation.Horizontal; case ConnectorOrientation.Top: return Orientation.Vertical; case ConnectorOrientation.Right: return Orientation.Horizontal; case ConnectorOrientation.Bottom: return Orientation.Vertical; default: throw new Exception("Unknown ConnectorOrientation"); } } private static PointBase GetNearestNeighborSource(ConnectorInfo source, PointBase endPoint, RectangleBase rectSource, RectangleBase rectSink, out bool flag, bool isInnerPoint) { PointBase n1, n2; // neighbors GetNeighborCorners(source.Orientation, rectSource, out n1, out n2, isInnerPoint); if (rectSink.Contains(n1)) { flag = false; return n2; } if (rectSink.Contains(n2)) { flag = true; return n1; } if ((Distance(n1, endPoint) <= Distance(n2, endPoint))) { flag = true; return n1; } else { flag = false; return n2; } } private static PointBase GetNearestNeighborSource(ConnectorInfo source, PointBase endPoint, RectangleBase rectSource, out bool flag, bool isInnerPoint) { PointBase n1, n2; // neighbors GetNeighborCorners(source.Orientation, rectSource, out n1, out n2, isInnerPoint); if ((Distance(n1, endPoint) <= Distance(n2, endPoint))) { flag = true; return n1; } else { flag = false; return n2; } } private static PointBase GetNearestVisibleNeighborSink(PointBase currentPoint, PointBase endPoint, ConnectorInfo sink, RectangleBase rectSource, RectangleBase rectSink) { PointBase s1, s2; // neighbors on sink side GetNeighborCorners(sink.Orientation, rectSink, out s1, out s2); bool flag1 = IsPointVisible(currentPoint, s1, new RectangleBase[] { rectSource, rectSink }); bool flag2 = IsPointVisible(currentPoint, s2, new RectangleBase[] { rectSource, rectSink }); if (flag1) // s1 visible { if (flag2) // s1 and s2 visible { if (rectSink.Contains(s1)) return s2; if (rectSink.Contains(s2)) return s1; if ((Distance(s1, endPoint) <= Distance(s2, endPoint))) return s1; else return s2; } else { return s1; } } else // s1 not visible { if (flag2) // only s2 visible { return s2; } else // s1 and s2 not visible { return new PointBase(double.NaN, double.NaN); } } } private static bool IsPointVisible(PointBase fromPoint, PointBase targetPoint, RectangleBase[] rectangles) { foreach (RectangleBase rect in rectangles) { if (RectangleIntersectsLine(rect, fromPoint, targetPoint)) return false; } return true; } private static bool IsRectVisible(PointBase fromPoint, RectangleBase targetRect, RectangleBase[] rectangles) { if (IsPointVisible(fromPoint, targetRect.TopLeft, rectangles)) return true; if (IsPointVisible(fromPoint, targetRect.TopRight, rectangles)) return true; if (IsPointVisible(fromPoint, targetRect.BottomLeft, rectangles)) return true; if (IsPointVisible(fromPoint, targetRect.BottomRight, rectangles)) return true; return false; } private static bool RectangleIntersectsLine(RectangleBase rect, PointBase startPoint, PointBase endPoint) { rect.Inflate(-1, -1); return rect.IntersectsWith(new RectangleBase(startPoint, endPoint)); } private static void GetOppositeCorners(ConnectorOrientation orientation, RectangleBase rect, out PointBase n1, out PointBase n2, bool isInnerPoint = false) { if (isInnerPoint) { n1 = rect.Location; n2 = rect.Location; return; } switch (orientation) { case ConnectorOrientation.Left: n1 = rect.TopRight; n2 = rect.BottomRight; break; case ConnectorOrientation.Top: n1 = rect.BottomLeft; n2 = rect.BottomRight; break; case ConnectorOrientation.Right: n1 = rect.TopLeft; n2 = rect.BottomLeft; break; case ConnectorOrientation.Bottom: n1 = rect.TopLeft; n2 = rect.TopRight; break; default: throw new Exception("No opposite corners found!"); } } private static void GetNeighborCorners(ConnectorOrientation orientation, RectangleBase rect, out PointBase n1, out PointBase n2, bool isInnerPoint = false) { if (isInnerPoint) { n1 = rect.Location; n2 = rect.Location; return; } switch (orientation) { case ConnectorOrientation.Left: n1 = rect.TopLeft; n2 = rect.BottomLeft; break; case ConnectorOrientation.Top: n1 = rect.TopLeft; n2 = rect.TopRight; break; case ConnectorOrientation.Right: n1 = rect.TopRight; n2 = rect.BottomRight; break; case ConnectorOrientation.Bottom: n1 = rect.BottomLeft; n2 = rect.BottomRight; break; default: throw new Exception("No neighour corners found!"); } } private static double Distance(PointBase p1, PointBase p2) { return PointBase.Subtract(p1, p2).Length; } private static RectangleBase GetRectWithMargin(ConnectorInfo connectorThumb, double margin) { RectangleBase rect = new RectangleBase(connectorThumb.DesignerItemLeft, connectorThumb.DesignerItemTop, 0, 0); rect.Inflate(margin, margin); return rect; } private static PointBase GetOffsetPoint(ConnectorInfo connector, RectangleBase rect, bool isInnerPoint = false) { PointBase offsetPoint = new PointBase(); if (isInnerPoint) { offsetPoint = new PointBase(connector.Position.X, connector.Position.Y); return offsetPoint; } switch (connector.Orientation) { case ConnectorOrientation.Left: offsetPoint = new PointBase(rect.Left, connector.Position.Y); break; case ConnectorOrientation.Top: offsetPoint = new PointBase(connector.Position.X, rect.Top); break; case ConnectorOrientation.Right: offsetPoint = new PointBase(rect.Right, connector.Position.Y); break; case ConnectorOrientation.Bottom: offsetPoint = new PointBase(connector.Position.X, rect.Bottom); break; default: break; } return offsetPoint; } private static void CheckPathEnd(ConnectorInfo source, ConnectorInfo sink, bool showLastLine, List linePoints, bool sourceInnerPoint) { if (showLastLine) { PointBase startPoint = new PointBase(0, 0); PointBase endPoint = new PointBase(0, 0); double marginPath = 15; switch (source.Orientation) { case ConnectorOrientation.Left: startPoint = new PointBase(source.Position.X - marginPath, source.Position.Y); break; case ConnectorOrientation.Top: startPoint = new PointBase(source.Position.X, source.Position.Y - marginPath); break; case ConnectorOrientation.Right: startPoint = new PointBase(source.Position.X + marginPath, source.Position.Y); break; case ConnectorOrientation.Bottom: startPoint = new PointBase(source.Position.X, source.Position.Y + marginPath); break; default: break; } if (sourceInnerPoint) { startPoint = new PointBase(source.Position.X, source.Position.Y); } switch (sink.Orientation) { case ConnectorOrientation.Left: endPoint = new PointBase(sink.Position.X - marginPath, sink.Position.Y); break; case ConnectorOrientation.Top: endPoint = new PointBase(sink.Position.X, sink.Position.Y - marginPath); break; case ConnectorOrientation.Right: endPoint = new PointBase(sink.Position.X + marginPath, sink.Position.Y); break; case ConnectorOrientation.Bottom: endPoint = new PointBase(sink.Position.X, sink.Position.Y + marginPath); break; default: break; } linePoints.Insert(0, startPoint); linePoints.Add(endPoint); } else { linePoints.Insert(0, source.Position); linePoints.Add(sink.Position); } } private static ConnectorOrientation GetOpositeOrientation(ConnectorOrientation connectorOrientation) { switch (connectorOrientation) { case ConnectorOrientation.Left: return ConnectorOrientation.Right; case ConnectorOrientation.Top: return ConnectorOrientation.Bottom; case ConnectorOrientation.Right: return ConnectorOrientation.Left; case ConnectorOrientation.Bottom: return ConnectorOrientation.Top; default: return ConnectorOrientation.Top; } } } }