Configurable connection crossing styles 06/4106/2
authorJussi Koskela <jussi.koskela@semantum.fi>
Tue, 7 Apr 2020 09:26:23 +0000 (12:26 +0300)
committerJussi Koskela <jussi.koskela@semantum.fi>
Wed, 8 Apr 2020 12:08:55 +0000 (15:08 +0300)
gitlab #515

Change-Id: I859ce915743c13c37be9ca13cdb0c71a2f077d87

bundles/org.simantics.diagram.connection/src/org/simantics/diagram/connection/rendering/ConnectionCrossings.java [new file with mode: 0644]
bundles/org.simantics.diagram.connection/src/org/simantics/diagram/connection/rendering/ConnectionRenderingHints.java [new file with mode: 0644]
bundles/org.simantics.diagram.connection/src/org/simantics/diagram/connection/rendering/PathModifier.java [new file with mode: 0644]
bundles/org.simantics.diagram.connection/src/org/simantics/diagram/connection/rendering/StyledRouteGraphRenderer.java
bundles/org.simantics.diagram.ontology/graph/Diagram.pgraph
bundles/org.simantics.diagram/src/org/simantics/diagram/participant/ConnectionCrossingsParticipant.java [new file with mode: 0644]
bundles/org.simantics.modeling.ui/src/org/simantics/modeling/ui/diagramEditor/DiagramViewer.java
bundles/org.simantics.modeling.ui/src/org/simantics/modeling/ui/sg/DiagramSceneGraphProvider.java
bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/ConnectionCrossingsNode.java [new file with mode: 0644]

diff --git a/bundles/org.simantics.diagram.connection/src/org/simantics/diagram/connection/rendering/ConnectionCrossings.java b/bundles/org.simantics.diagram.connection/src/org/simantics/diagram/connection/rendering/ConnectionCrossings.java
new file mode 100644 (file)
index 0000000..3ccb492
--- /dev/null
@@ -0,0 +1,214 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Association for Decentralized Information Management in
+ * Industry THTH ry.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Semantum Oy - initial API and implementation
+ *******************************************************************************/
+package org.simantics.diagram.connection.rendering;
+
+import java.awt.geom.Arc2D;
+import java.awt.geom.Path2D;
+import java.awt.geom.PathIterator;
+import java.awt.geom.Point2D;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class ConnectionCrossings implements PathModifier {
+    public enum Type {
+        NONE,
+        GAP,
+        ARC,
+        SQUARE
+    }
+
+    private List<Segment> segments = new ArrayList<>();
+    private double width;
+    private Type type;
+
+    public void setWidth(double gapWidth) {
+        this.width = gapWidth;
+    }
+
+    public double getWidth() {
+        return width;
+    }
+
+    public void setType(Type type) {
+        this.type = type;
+    }
+
+    public Type getType() {
+        return type;
+    }
+
+    static class Segment {
+        public double x1, y1, x2, y2;
+
+        public Segment(double x1, double y1, double x2, double y2) {
+            this.x1 = x1;
+            this.y1 = y1;
+            this.x2 = x2;
+            this.y2 = y2;
+        }
+    };
+
+    public void reset() {
+        segments.clear();
+    }
+
+    static Double lineLineIntersection(Segment l1, Segment l2) { 
+        double epsilon = 0.001;
+
+        double d = (l1.x1 - l1.x2) * (l2.y1 - l2.y2) - (l1.y1 - l1.y2) * (l2.x1 - l2.x2);
+        if (d == 0.0) return null;
+        double s = ((l1.x1 - l2.x1) * (l2.y1 - l2.y2) - (l1.y1 - l2.y1) * (l2.x1 - l2.x2)) / d;
+        if ((s > epsilon) && (s < 1 - epsilon)) {
+            double t = -((l1.x1 - l1.x2) * (l1.y1 - l2.y1) - (l1.y1 - l1.y2) * (l1.x1 - l2.x1)) / d;
+            if ((t > epsilon) && (t < 1 - epsilon)) {
+                return t;
+            }
+        }
+        return null; 
+    }
+
+    public Path2D modify(Path2D path) {
+        Path2D.Double path2 = new Path2D.Double();
+        PathIterator iter = path.getPathIterator(null);
+
+        while (!iter.isDone()) {
+
+            double c[] = new double[6];
+            int i = iter.currentSegment(c);
+            switch (i) {
+            case PathIterator.SEG_MOVETO:
+                path2.moveTo(c[0], c[1]);
+                break;
+            case PathIterator.SEG_LINETO:
+                Segment l = new Segment(path2.getCurrentPoint().getX(), path2.getCurrentPoint().getY(), c[0], c[1]);
+                
+                List<Double> gaps = new ArrayList<>();
+                for (Segment old : segments) {
+                    Double t = lineLineIntersection(old, l);
+                    if (t != null) {
+                        gaps.add(t);
+                    }
+                }
+
+                if (gaps.isEmpty()) {
+                    path2.lineTo(c[0], c[1]);
+                } else {
+                    Collections.sort(gaps);
+                    double dx = l.x2 - l.x1;
+                    double dy = l.y2 - l.y1;
+
+                    double pos = 0.0;
+                    double len = Math.sqrt(dx*dx + dy*dy);
+
+                    boolean finish = true;
+                    Point2D prevGapEnd = null;
+                    for (Double gapCenter : gaps) {
+                        double pos2 = gapCenter - width / 2 / len;
+                        double pos3 = gapCenter + width / 2 / len;
+                        if (pos2 > pos) {
+                            handleGap(path2, prevGapEnd);
+                            prevGapEnd = null;
+                            path2.lineTo(l.x1 + pos2 * dx, l.y1 + pos2 * dy);
+                        }
+                        if (pos3 < 1.0) {
+                            double x = l.x1 + pos3 * dx;
+                            double y = l.y1 + pos3 * dy;
+                            prevGapEnd = new Point2D.Double(x, y);
+                        } else {
+                            finish = false;
+                        }
+                        pos = pos3;
+                    }
+                    
+                    if (finish) {
+                        handleGap(path2, prevGapEnd);
+                        path2.lineTo(l.x2, l.y2);
+                    } else {
+                        prevGapEnd = new Point2D.Double(l.x2, l.y2);
+                        handleGap(path2, prevGapEnd);
+                    }
+                }
+                segments.add(l);
+
+                break;
+            case PathIterator.SEG_QUADTO:
+                // TODO: implement gaps
+                path2.quadTo(c[0], c[1], c[2], c[3]);
+                break;
+            case PathIterator.SEG_CUBICTO:
+                // TODO: implement gaps
+                path2.curveTo(c[0], c[1], c[2], c[3], c[4], c[5]);
+                break;
+            case PathIterator.SEG_CLOSE:
+                // TODO: implement gaps
+                path2.closePath();
+                break;
+            default:
+                throw new RuntimeException("Unexpected segment type " + i);
+            }
+            iter.next();
+        }
+        return path2;
+    }
+
+    private void handleGap(Path2D path, Point2D prevGapEnd) {
+        if (prevGapEnd != null) {
+            switch (type) {
+            case ARC:
+                arcTo(path, prevGapEnd.getX(), prevGapEnd.getY());
+                break;
+            case SQUARE:
+                squareTo(path, prevGapEnd.getX(), prevGapEnd.getY(), width);
+                break;
+            case GAP:
+                path.moveTo(prevGapEnd.getX(), prevGapEnd.getY());
+                break;
+            case NONE:
+                break;
+            }
+        }
+    }
+
+    private static void arcTo(Path2D path, double x2, double y2) {
+        Arc2D arc = new Arc2D.Double();
+        double x1 = path.getCurrentPoint().getX();
+        double y1 = path.getCurrentPoint().getY();
+        double dx = x2 - x1;
+        double dy = y2 - y1;
+        double r = Math.sqrt(dx * dx + dy * dy) / 2;
+        double angle = Math.atan2(dx,  dy) * 180 / Math.PI + 90;
+        double span = (angle > 225 || angle < 45) ? 180 : -180;
+        arc.setArcByCenter((x1 + x2) / 2, (y1 + y2) / 2, r, angle, span, Arc2D.OPEN);
+        path.append(arc, true);
+    }
+    
+    private static void squareTo(Path2D path, double x2, double y2, double width) {
+        double x1 = path.getCurrentPoint().getX();
+        double y1 = path.getCurrentPoint().getY();
+        double dx = x2 - x1;
+        double dy = y2 - y1;
+        double l = Math.sqrt(dx * dx + dy* dy);
+        if (l > 0) {
+            double nx = -dy / l;
+            double ny = dx / l;
+            if (nx - ny < 0) {
+                nx = -nx;
+                ny = -ny;
+            }
+            path.lineTo(x1 + nx * width / 2, y1 + ny * width / 2);
+            path.lineTo(x2 + nx * width / 2, y2 + ny * width / 2);
+            path.lineTo(x2, y2);
+        }
+    }
+
+}
diff --git a/bundles/org.simantics.diagram.connection/src/org/simantics/diagram/connection/rendering/ConnectionRenderingHints.java b/bundles/org.simantics.diagram.connection/src/org/simantics/diagram/connection/rendering/ConnectionRenderingHints.java
new file mode 100644 (file)
index 0000000..a78bec0
--- /dev/null
@@ -0,0 +1,24 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Association for Decentralized Information Management in
+ * Industry THTH ry.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Semantum Oy - initial API and implementation
+ *******************************************************************************/
+package org.simantics.diagram.connection.rendering;
+
+import java.awt.RenderingHints.Key;
+
+public final class ConnectionRenderingHints {
+
+    public static final Key KEY_PATH_MODIFIER = new Key(0) {
+        @Override
+        public boolean isCompatibleValue(Object val) {
+            return val == null || val instanceof PathModifier;
+        }
+    };
+}
\ No newline at end of file
diff --git a/bundles/org.simantics.diagram.connection/src/org/simantics/diagram/connection/rendering/PathModifier.java b/bundles/org.simantics.diagram.connection/src/org/simantics/diagram/connection/rendering/PathModifier.java
new file mode 100644 (file)
index 0000000..50d771a
--- /dev/null
@@ -0,0 +1,18 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Association for Decentralized Information Management in
+ * Industry THTH ry.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Semantum Oy - initial API and implementation
+ *******************************************************************************/
+package org.simantics.diagram.connection.rendering;
+
+import java.awt.geom.Path2D;
+
+public interface PathModifier {
+       public Path2D modify(Path2D source);
+}
index dd9473042152d0c3af43244d9549d63e4c649e4b..fa14474691f281ad312df693dec50e242bdb9503 100644 (file)
@@ -62,6 +62,10 @@ public class StyledRouteGraphRenderer implements IRouteGraphRenderer, Serializab
 
                path.reset();
                rg.getPath2D(path);
+               PathModifier pm = (PathModifier) g.getRenderingHint(ConnectionRenderingHints.KEY_PATH_MODIFIER);
+               if (pm != null) {
+                       path = pm.modify(path);
+               }
                style.drawPath(g, path, false);
 
                branchPoints.clear();
index b26bfc698ee214ac9040b7228d2fe0a13c59b334..6ea207f88d754cd59825b9c2e533fadc6190cb4a 100644 (file)
@@ -68,6 +68,9 @@ DIA.IOTableRename <T L0.Entity
 DIA.Diagram <T DIA.Composite
     >-- DIA.Diagram.IOTableRenaming --> DIA.IOTableRename <T L0.DependsOn 
     @L0.optionalProperty DIA.HasModCount
+    @L0.assert DIA.ConnectionCrossingStyle ""
+    @L0.assert DIA.ConnectionCrossingStyle.Width 0.0
+    @L0.assert DIA.ConnectionCrossingStyle.HasType DIA.ConnectionCrossingStyle.Type.None
 
 DIA.HasModCount <R L0.HasProperty : L0.FunctionalRelation
     L0.HasLabel "Modification Counter"
@@ -143,4 +146,31 @@ DIA.diagramActivityCondition : L0.Template
 
 DIA.ProfileEntryContribution <T L0.Entity
   --> DIA.ProfileEntryContribution.HasEntry --> DIA.ProfileEntry <R L0.IsRelatedTo  
-                
\ No newline at end of file
+
+DIA.ConnectionCrossingStyle <R L0.HasProperty : L0.FunctionalRelation : SEL.GenericParameterType
+    L0.HasLabel "Connection Crossing Style"
+    SEL.HasDisplayValue ""
+    L0.readOnly true
+    SEL.canBeLifted false
+
+DIA.ConnectionCrossingStyle.Width <R L0.HasProperty : L0.FunctionalRelation : SEL.GenericParameterType
+    L0.HasLabel "Width"
+    L0.HasDescription "Width of connection crossings."
+    L0.HasDomain DIA.Diagram
+    L0.HasRange L0.Double
+    SEL.IsShownUnder DIA.ConnectionCrossingStyle
+
+DIA.ConnectionCrossingStyle.HasType <R L0.HasProperty : L0.FunctionalRelation : SEL.GenericParameterType
+    L0.HasLabel "Type"
+    L0.HasDescription "Type of connection crossings."
+    L0.HasDomain DIA.Diagram
+    L0.HasRange DIA.ConnectionCrossingStyle.Type
+    SEL.IsShownUnder DIA.ConnectionCrossingStyle
+
+DIA.ConnectionCrossingStyle.Type <T L0.Value
+    @L0.tag L0.Enumeration
+
+DIA.ConnectionCrossingStyle.Type.Arc : DIA.ConnectionCrossingStyle.Type
+DIA.ConnectionCrossingStyle.Type.Square : DIA.ConnectionCrossingStyle.Type
+DIA.ConnectionCrossingStyle.Type.Gap : DIA.ConnectionCrossingStyle.Type
+DIA.ConnectionCrossingStyle.Type.None : DIA.ConnectionCrossingStyle.Type
\ No newline at end of file
diff --git a/bundles/org.simantics.diagram/src/org/simantics/diagram/participant/ConnectionCrossingsParticipant.java b/bundles/org.simantics.diagram/src/org/simantics/diagram/participant/ConnectionCrossingsParticipant.java
new file mode 100644 (file)
index 0000000..10949ca
--- /dev/null
@@ -0,0 +1,136 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Association for Decentralized Information Management in
+ * Industry THTH ry.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Semantum Oy - initial API and implementation
+ *******************************************************************************/
+package org.simantics.diagram.participant;
+
+import org.simantics.Simantics;
+import org.simantics.db.ReadGraph;
+import org.simantics.db.Resource;
+import org.simantics.db.common.request.UnaryRead;
+import org.simantics.db.exception.DatabaseException;
+import org.simantics.db.procedure.Listener;
+import org.simantics.diagram.connection.rendering.ConnectionCrossings;
+import org.simantics.diagram.stubs.DiagramResource;
+import org.simantics.g2d.canvas.ICanvasContext;
+import org.simantics.g2d.canvas.SGDesignation;
+import org.simantics.g2d.canvas.impl.SGNodeReflection.SGCleanup;
+import org.simantics.g2d.canvas.impl.SGNodeReflection.SGInit;
+import org.simantics.g2d.diagram.participant.AbstractDiagramParticipant;
+import org.simantics.scenegraph.g2d.G2DParentNode;
+import org.simantics.scenegraph.g2d.nodes.ConnectionCrossingsNode;
+import org.simantics.utils.datastructures.Pair;
+import org.simantics.utils.ui.ErrorLogger;
+
+public class ConnectionCrossingsParticipant extends AbstractDiagramParticipant {
+    private static final String CONNECTION_CROSSINGS_NODE_KEY = "connection-crossings";
+
+    private ConnectionCrossingsNode ccNode;
+    private final ConnectionCrossings crossings = new ConnectionCrossings();
+    private ConnectionCrossingStyleListener listener;
+    private Resource diagram;
+    
+    public ConnectionCrossingsParticipant(Resource diagram) {
+        this.diagram = diagram;
+    }
+    public ConnectionCrossingsParticipant(double width, ConnectionCrossings.Type type) {
+        crossings.setWidth(width);
+        crossings.setType(type);
+    }
+
+    @SGInit(designation = SGDesignation.CONTROL)
+    public void initSG(G2DParentNode parent) {
+        ccNode = parent.addNode(CONNECTION_CROSSINGS_NODE_KEY, ConnectionCrossingsNode.class);
+        ccNode.setCrossings(crossings);
+        ccNode.setZIndex(Integer.MIN_VALUE / 4);
+    }
+
+    @SGCleanup
+    public void cleanupSG() {
+        if (ccNode != null) {
+            ccNode.remove();
+            ccNode = null;
+        }
+    }
+
+    @Override
+    public void addedToContext(ICanvasContext ctx) {
+        super.addedToContext(ctx);
+
+        if (diagram != null) {
+            listener = new ConnectionCrossingStyleListener(ctx);
+            Simantics.getSession().async(new UnaryRead<Resource, Pair<Double, ConnectionCrossings.Type>>(diagram){
+    
+                @Override
+                public Pair<Double, ConnectionCrossings.Type> perform(ReadGraph graph) throws DatabaseException {
+                    DiagramResource DIA = DiagramResource.getInstance(graph);
+                    Double gap = graph.getPossibleRelatedValue(diagram, DIA.ConnectionCrossingStyle_Width);
+                    Resource typeRes = graph.getPossibleObject(diagram, DIA.ConnectionCrossingStyle_HasType);
+                    ConnectionCrossings.Type type;
+                    if (DIA.ConnectionCrossingStyle_Type_Gap.equals(typeRes)) {
+                        type = ConnectionCrossings.Type.GAP;
+                    } else if (DIA.ConnectionCrossingStyle_Type_Arc.equals(typeRes)) {
+                        type = ConnectionCrossings.Type.ARC;
+                    } else if (DIA.ConnectionCrossingStyle_Type_Square.equals(typeRes)) {
+                        type = ConnectionCrossings.Type.SQUARE;
+                    } else { 
+                        type = ConnectionCrossings.Type.NONE;
+                    }
+                    return new Pair<>(gap, type);
+                }
+                
+            }, listener);
+        }
+    }
+
+    @Override
+    public void removedFromContext(ICanvasContext ctx) {
+        if (listener != null) {
+            listener.dispose();
+            listener = null;
+        }
+        super.removedFromContext(ctx);
+    }
+
+    class ConnectionCrossingStyleListener implements Listener<Pair<Double, ConnectionCrossings.Type>> {
+        ICanvasContext context;
+        public ConnectionCrossingStyleListener(ICanvasContext context) {
+            this.context = context;
+        }
+        @Override
+        public void execute(final Pair<Double, ConnectionCrossings.Type> result) {
+            context.getThreadAccess().asyncExec(new Runnable() {
+                @Override
+                public void run() {
+                    ICanvasContext ctx = context;
+                    if (ctx == null)
+                        return;
+                    if (ctx.isDisposed())
+                        return;
+                    crossings.setWidth(result.first != null ? result.first : 0.0);
+                    crossings.setType(result.second);
+                    ccNode.repaint();
+                }
+            });
+        }
+        public void dispose() {
+            context = null;
+        }
+        @Override
+        public boolean isDisposed() {
+            return context == null || context.isDisposed();
+        }
+        @Override
+        public void exception(Throwable t) {
+            ErrorLogger.defaultLogError(t);
+        }
+    }
+
+}
index 409aecbea26dc04ef73d9851b5fa31f9eb0eb234..489ebd6c700a65a6dd7e70dcf8a42d6c9e93a2eb 100644 (file)
@@ -67,6 +67,7 @@ import org.simantics.diagram.handler.DeleteHandler;
 import org.simantics.diagram.handler.ExpandSelectionHandler;
 import org.simantics.diagram.handler.SimpleElementTransformHandler;
 import org.simantics.diagram.layer.ILayersViewPage;
+import org.simantics.diagram.participant.ConnectionCrossingsParticipant;
 import org.simantics.diagram.participant.ContextUtil;
 import org.simantics.diagram.participant.PointerInteractor2;
 import org.simantics.diagram.participant.SGFocusParticipant;
@@ -330,6 +331,7 @@ public class DiagramViewer
         //ctx.add(new ZoomTransitionParticipant(TransitionFunction.SIGMOID));
         //ctx.add(new TooltipParticipant());
         ctx.add(new TerminalTooltipParticipant());
+        ctx.add(new ConnectionCrossingsParticipant(getInputResource()));
     }
 
     protected void addPainterParticipants(ICanvasContext ctx) {
index 1db2ba151c972dccd2c52bd5f92e95d40c2a73bf..8f223a62285bbf6892240019deea73d6ed68a31b 100644 (file)
@@ -35,6 +35,7 @@ import org.simantics.diagram.handler.CopyPasteStrategy;
 import org.simantics.diagram.handler.DefaultCopyPasteStrategy;
 import org.simantics.diagram.handler.DeleteHandler;
 import org.simantics.diagram.handler.SimpleElementTransformHandler;
+import org.simantics.diagram.participant.ConnectionCrossingsParticipant;
 import org.simantics.diagram.query.DiagramRequests;
 import org.simantics.diagram.runtime.RuntimeDiagramManager;
 import org.simantics.diagram.stubs.DiagramResource;
@@ -249,6 +250,7 @@ public class DiagramSceneGraphProvider implements ICanvasSceneGraphProvider, IDi
         ctx.add( new Selection() );
         ctx.add( new DiagramParticipant() );
         ctx.add( new ElementPainter(true) );
+        ctx.add( new ConnectionCrossingsParticipant(resource));
 
         //ctx.add( new ElementHeartbeater() );
         ctx.add( new ZOrderHandler() );
diff --git a/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/ConnectionCrossingsNode.java b/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/ConnectionCrossingsNode.java
new file mode 100644 (file)
index 0000000..a46c4e7
--- /dev/null
@@ -0,0 +1,49 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Association for Decentralized Information Management in
+ * Industry THTH ry.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Semantum Oy - initial API and implementation
+ *******************************************************************************/
+package org.simantics.scenegraph.g2d.nodes;
+
+import java.awt.Graphics2D;
+import java.awt.geom.Rectangle2D;
+
+import org.simantics.diagram.connection.rendering.ConnectionCrossings;
+import org.simantics.diagram.connection.rendering.ConnectionRenderingHints;
+import org.simantics.scenegraph.g2d.G2DNode;
+
+public class ConnectionCrossingsNode extends G2DNode {
+
+    private static final long serialVersionUID = -696142275610396889L;
+
+    private ConnectionCrossings crossings;
+    
+    @Override
+    public void render(Graphics2D g) {
+        crossings.reset();
+        if (crossings.getWidth() > 0 && crossings.getType() != ConnectionCrossings.Type.NONE)  {
+            g.setRenderingHint(ConnectionRenderingHints.KEY_PATH_MODIFIER, crossings);
+        } else {
+            g.setRenderingHint(ConnectionRenderingHints.KEY_PATH_MODIFIER, null);
+        }
+    }
+    
+    public void setCrossings(ConnectionCrossings crossings) {
+        this.crossings = crossings;
+    }
+
+    @Override
+    public Rectangle2D getBoundsInLocal() {
+        return null;
+    }
+    
+    public ConnectionCrossings getConnectionCrossings() {
+        return crossings;
+    }
+}