]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.diagram/src/org/simantics/diagram/adapter/RouteGraphUtils.java
Store initial links between route notes for JS connection rendering
[simantics/platform.git] / bundles / org.simantics.diagram / src / org / simantics / diagram / adapter / RouteGraphUtils.java
1 /*******************************************************************************
2  * Copyright (c) 2012, 2016 Association for Decentralized Information Management
3  * in Industry THTH ry.
4  * All rights reserved. This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License v1.0
6  * which accompanies this distribution, and is available at
7  * http://www.eclipse.org/legal/epl-v10.html
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *     Semantum Oy - refactoring
12  *******************************************************************************/
13 package org.simantics.diagram.adapter;
14
15 import java.awt.BasicStroke;
16 import java.awt.Color;
17 import java.awt.Shape;
18 import java.awt.Stroke;
19 import java.awt.geom.AffineTransform;
20 import java.awt.geom.Rectangle2D;
21 import java.util.ArrayList;
22 import java.util.Collection;
23 import java.util.Collections;
24 import java.util.Map;
25 import java.util.Set;
26
27 import org.simantics.databoard.Bindings;
28 import org.simantics.db.ReadGraph;
29 import org.simantics.db.Resource;
30 import org.simantics.db.Session;
31 import org.simantics.db.Statement;
32 import org.simantics.db.common.procedure.adapter.TransientCacheListener;
33 import org.simantics.db.common.request.ResourceRead2;
34 import org.simantics.db.common.request.UnaryRead;
35 import org.simantics.db.common.utils.NameUtils;
36 import org.simantics.db.exception.DatabaseException;
37 import org.simantics.diagram.connection.ConnectionVisuals;
38 import org.simantics.diagram.connection.Link;
39 import org.simantics.diagram.connection.RouteGraph;
40 import org.simantics.diagram.connection.RouteGraphConnectionClass;
41 import org.simantics.diagram.connection.RouteLine;
42 import org.simantics.diagram.connection.RouteNode;
43 import org.simantics.diagram.connection.RouteTerminal;
44 import org.simantics.diagram.connection.RouteTerminalPosition;
45 import org.simantics.diagram.connection.rendering.BasicConnectionStyle;
46 import org.simantics.diagram.connection.rendering.ConnectionStyle;
47 import org.simantics.diagram.connection.rendering.ExampleConnectionStyle;
48 import org.simantics.diagram.connection.rendering.StyledRouteGraphRenderer;
49 import org.simantics.diagram.connection.rendering.arrows.ArrowLineEndStyle;
50 import org.simantics.diagram.connection.rendering.arrows.ILineEndStyle;
51 import org.simantics.diagram.connection.rendering.arrows.PlainLineEndStyle;
52 import org.simantics.diagram.content.EdgeResource;
53 import org.simantics.diagram.content.ResourceTerminal;
54 import org.simantics.diagram.content.TerminalMap;
55 import org.simantics.diagram.query.DiagramRequests;
56 import org.simantics.diagram.stubs.DiagramResource;
57 import org.simantics.diagram.stubs.G2DResource;
58 import org.simantics.diagram.synchronization.graph.DiagramGraphUtil;
59 import org.simantics.diagram.synchronization.graph.RouteGraphConnection;
60 import org.simantics.g2d.canvas.ICanvasContext;
61 import org.simantics.g2d.canvas.impl.CanvasContext;
62 import org.simantics.g2d.diagram.IDiagram;
63 import org.simantics.g2d.diagram.handler.DataElementMap;
64 import org.simantics.g2d.diagram.handler.Topology.Connection;
65 import org.simantics.g2d.diagram.handler.Topology.Terminal;
66 import org.simantics.g2d.diagram.impl.ElementDiagram;
67 import org.simantics.g2d.element.ElementUtils;
68 import org.simantics.g2d.element.IElement;
69 import org.simantics.g2d.element.handler.EdgeVisuals.EdgeEnd;
70 import org.simantics.g2d.element.handler.TerminalLayout;
71 import org.simantics.g2d.elementclass.FlagClass.Type;
72 import org.simantics.layer0.Layer0;
73 import org.simantics.scenegraph.g2d.nodes.connection.RouteGraphChangeEvent;
74 import org.simantics.scenegraph.utils.GeometryUtils;
75 import org.simantics.structural.stubs.StructuralResource2;
76 import org.simantics.structural2.modelingRules.CPTerminal;
77 import org.simantics.structural2.modelingRules.IAttachmentRelationMap;
78 import org.simantics.structural2.modelingRules.IModelingRules;
79 import org.simantics.utils.threads.CurrentThread;
80 import org.slf4j.Logger;
81 import org.slf4j.LoggerFactory;
82
83 import gnu.trove.map.hash.THashMap;
84 import gnu.trove.set.hash.THashSet;
85
86 public class RouteGraphUtils {
87
88     private static final Logger LOGGER = LoggerFactory.getLogger(RouteGraph.class);
89     public static boolean DEBUG = false;
90
91     public static final ILineEndStyle HEAD  = new ArrowLineEndStyle("fill 2 1 0");
92     public static final ILineEndStyle TAIL  = PlainLineEndStyle.INSTANCE;
93
94     public static RouteGraph load(ReadGraph graph, Resource diagramRuntime, Resource connection) throws DatabaseException {
95         ICanvasContext canvas = new CanvasContext(CurrentThread.getThreadAccess());
96         IDiagram diagram = new ElementDiagram(canvas);
97         return load(graph, diagramRuntime, connection, canvas, diagram);
98     }
99
100     public static RouteGraph load(ReadGraph graph, Resource diagramRuntime, Resource connection, ICanvasContext canvas, IDiagram diagram) throws DatabaseException {
101         Layer0 L0 = Layer0.getInstance(graph);
102         Resource diagramResource = graph.getPossibleObject(connection, L0.PartOf);
103         IModelingRules modelingRules = graph.syncRequest(DiagramRequests.getModelingRules(diagramResource, null), TransientCacheListener.<IModelingRules>instance());
104         return load(graph, diagramRuntime, connection, canvas, diagram, null, modelingRules, null);
105     }
106
107     public static RouteGraph load(ReadGraph graph, Resource diagramRuntime, Resource connection, ICanvasContext canvas, IDiagram diagram, IElement element, IModelingRules modelingRules, Set<BackendConnection> backendConnections) throws DatabaseException {
108
109         DiagramResource DIA = DiagramResource.getInstance(graph);
110         StructuralResource2 STR = StructuralResource2.getInstance(graph);
111
112         RouteGraph rg = new RouteGraph();
113
114         // Default capacity should be enough for common cases.
115         Set<EdgeResource> links = new THashSet<>();
116         Map<Object, RouteNode> nodeByData = new THashMap<>();
117
118         // Load all route graph interior RouteNodes: route lines and points
119         for (Resource interiorNode : graph.getObjects(connection, DIA.HasInteriorRouteNode)) {
120             if (graph.isInstanceOf(interiorNode, DIA.RouteLine)) {
121                 Collection<Resource> areConnected = graph.getObjects(interiorNode, DIA.AreConnected);
122                 if (areConnected.size() < 2) {
123                     // Degenerated route line encountered, most likely due to a bug somewhere else.
124                     // Ignoring them because adding them to the RouteGraph structure would cause
125                     // problems during rendering.
126                     LOGGER.warn("Stray RouteLine found: " + NameUtils.getSafeName(graph, interiorNode));
127                     continue;
128                 }
129
130                 Boolean isHorizontal = graph.getRelatedValue(interiorNode, DIA.IsHorizontal, Bindings.BOOLEAN);
131                 Double position = graph.getRelatedValue(interiorNode, DIA.HasPosition, Bindings.DOUBLE);
132                 RouteLine line = rg.addLine(isHorizontal, position);
133                 line.setData( RouteGraphConnection.serialize(graph, interiorNode) );
134
135                 nodeByData.put( interiorNode, line );
136
137                 for (Resource connectedTo : areConnected) {
138                     links.add( new EdgeResource(interiorNode, connectedTo) );
139                 }
140             } else if (graph.isInstanceOf(interiorNode, DIA.RoutePoint)) {
141                 // Not supported yet. Ignore.
142             }
143         }
144
145         Rectangle2D bounds = new Rectangle2D.Double();
146         Map<Resource, Resource> connectorToModeledAttachment = null;
147
148         // Primarily the loader will believe what modeling rules say about
149         // connector attachment relations.
150         // 
151         // If modeling rules decide nothing, then we simply believe what the
152         // the attachment relations in the graph say.
153         // 
154         // Special case 1: connection with two (2) terminals
155         // If the attachment of one of two terminals is decided by modeling
156         // rules, the other attachment shall be the opposite of the decided
157         // attachment (see forcedAttachmentRelation below).
158         // 
159         // Special case 2: connected to a flag
160         // If the attached element is a flag and modeling rules say nothing
161         // about it, believe the direction stated by the flag type.
162
163         Collection<Statement> toConnectorStatements = graph.getStatements(connection, DIA.HasConnector);
164         int terminalCount = 0;
165
166         // See if modeling rules judge any of the connection terminal attachments.
167         if (modelingRules != null) {
168             for (Statement toConnector : toConnectorStatements) {
169                 Resource connector = toConnector.getObject();
170
171                 Statement terminalStm = findTerminalStatement(graph, connection, connector, STR);
172                 if (terminalStm == null)
173                     // Ignore broken connector: attached to the connection but not to any terminal.
174                     continue;
175
176                 Resource terminalElement = terminalStm.getObject();
177                 Resource connectionRelation = graph.getPossibleInverse(terminalStm.getPredicate());
178                 if (connectionRelation == null)
179                     continue;
180
181                 ++terminalCount;
182
183                 IAttachmentRelationMap map = modelingRules.getAttachmentRelations(graph, connection);
184                 Resource attachment = map.get(graph, new CPTerminal(terminalElement, connectionRelation));
185                 if (attachment != null) {
186                     // Primary: believe modeling rules
187                     if (connectorToModeledAttachment == null)
188                         connectorToModeledAttachment = new THashMap<Resource, Resource>(toConnectorStatements.size());
189                     connectorToModeledAttachment.put(connector, attachment);
190                     if (DEBUG)
191                         LOGGER.debug("modeling rules decided attachment: " + NameUtils.getSafeName(graph, attachment, true) + " for (" + NameUtils.toString(graph, toConnector, true) + ") & (" + NameUtils.toString(graph, terminalStm, true) + ")");
192                 } else if (graph.isInstanceOf(terminalElement, DIA.Flag)) {
193                     // Secondary: believe flag type
194                     attachment = resolveFlagAttachment(graph, connection, terminalElement, modelingRules, DIA);
195                     if (attachment != null) {
196                         if (connectorToModeledAttachment == null)
197                             connectorToModeledAttachment = new THashMap<Resource, Resource>(toConnectorStatements.size());
198                         connectorToModeledAttachment.put(connector, attachment);
199                         if (DEBUG)
200                             LOGGER.debug("flag type decided attachment: " + NameUtils.getSafeName(graph, attachment, true) + " for (" + NameUtils.toString(graph, toConnector, true) + ") & (" + NameUtils.toString(graph, terminalStm, true) + ")");
201                     }
202                 }
203             }
204         }
205
206         if (connectorToModeledAttachment == null)
207             connectorToModeledAttachment = Collections.emptyMap();
208
209         Resource forcedAttachmentRelation = null;
210         if (terminalCount == 2 && connectorToModeledAttachment.size() == 1) {
211             forcedAttachmentRelation = getInverseAttachment(graph, connectorToModeledAttachment.values().iterator().next(), DIA);
212             if (DEBUG)
213                 LOGGER.debug("set forced attachment: " + NameUtils.getSafeLabel(graph, forcedAttachmentRelation));
214         }
215
216         Resource connectionType = graph.getPossibleObject(connection, STR.HasConnectionType); 
217         DataElementMap diagramDataElementMap = diagram.getDiagramClass().getSingleItem(DataElementMap.class);
218
219         // Load all node terminal connections as RouteTerminals
220         for (Statement toConnector : toConnectorStatements) {
221             Resource connector = toConnector.getObject();
222             Resource attachmentRelation = toConnector.getPredicate();
223             if (DEBUG)
224                 LOGGER.debug("original attachment relation: " + NameUtils.getSafeLabel(graph, attachmentRelation));
225
226             Statement terminalStm = findTerminalStatement(graph, connection, connector, STR);
227             if (terminalStm == null)
228                 // Ignore broken connector: attached to the connection but not to any terminal.
229                 continue;
230
231             Resource terminalElement = terminalStm.getObject();
232             Resource terminalElementType = graph.getPossibleType(terminalElement, DIA.Element);
233             if (terminalElementType == null)
234                 // Ignore non-element terminal elements
235                 continue;
236
237             Resource connectionRelation = graph.getPossibleInverse(terminalStm.getPredicate());
238             if (connectionRelation == null)
239                 continue;
240
241             // Discover node and terminal this connector is connected to.
242             TerminalMap terminals = graph.syncRequest(DiagramRequests.elementTypeTerminals(terminalElementType),
243                     TransientCacheListener.<TerminalMap>instance());
244             Resource terminal = terminals.getTerminal(connectionRelation);
245             if (terminal == null) {
246                 System.err.println(
247                         "RouteGraphUtils: Could not find terminal for connection point "
248                         + NameUtils.getSafeName(graph, connectionRelation, true)
249                         + " in element "
250                         + NameUtils.getSafeName(graph, terminalElement, true)); 
251                 continue;
252             }
253
254             double[] position = graph.getRelatedValue(connector, DIA.HasRelativeLocation, Bindings.DOUBLE_ARRAY);
255             if (position.length != 2)
256                 position = new double[] { 0, 0 };
257
258             AffineTransform terminalTr = DiagramGraphUtil.getDynamicWorldTransform(graph, diagramRuntime, terminalElement); 
259             final AffineTransform terminalElementTransform = new AffineTransform(terminalTr);
260
261             if (DEBUG) {
262                 LOGGER.debug("terminalStm: " + NameUtils.toString(graph, terminalStm));
263                 LOGGER.debug("terminal: " + NameUtils.getURIOrSafeNameInternal(graph, terminalStm.getPredicate()));
264                 LOGGER.debug("terminalElement: " + NameUtils.getURIOrSafeNameInternal(graph, terminalElement) + " : " + NameUtils.getURIOrSafeNameInternal(graph, terminalElementType));
265                 LOGGER.debug("terminalElementTr: " + terminalTr);
266             }
267
268             double x = terminalTr.getTranslateX();
269             double y = terminalTr.getTranslateY();
270             double minx = x-1, miny = y-1, maxx = x+1, maxy = y+1;
271             int direction = 0x0;
272
273             // Use modelingRules to ascertain the proper attachmentRelation
274             // for this terminal connection, if available.
275             Resource att = connectorToModeledAttachment.get(connector);
276             if (att != null) {
277                 attachmentRelation = att;
278                 if (DEBUG)
279                     LOGGER.debug("modeling rules attachment: " + NameUtils.getSafeLabel(graph, attachmentRelation));
280             } else if (forcedAttachmentRelation != null) {
281                 attachmentRelation = forcedAttachmentRelation;
282                 if (DEBUG)
283                     LOGGER.debug("forced rules attachment: " + NameUtils.getSafeLabel(graph, attachmentRelation));
284             }
285             if (DEBUG)
286                 LOGGER.debug("decided attachment: " + NameUtils.getSafeLabel(graph, attachmentRelation));
287
288             // Get element bounds to decide allowed terminal direction(s)
289             IElement te = graph.syncRequest(DiagramRequests.getElement(canvas, diagram, terminalElement, null));
290             ElementUtils.getElementBounds(te, bounds);
291             {
292                 Shape shp = org.simantics.g2d.utils.GeometryUtils.transformShape(bounds, terminalTr);
293                 bounds.setFrame(shp.getBounds2D());
294             }
295
296             // Expand bounds by 2mm to make the connections enter the terminals
297             // at a straight angle and from a distance instead of coming in
298             // "horizontally".
299             GeometryUtils.expandRectangle(bounds, 2);
300             minx = bounds.getMinX();
301             miny = bounds.getMinY();
302             maxx = bounds.getMaxX();
303             maxy = bounds.getMaxY();
304
305             final ResourceTerminal rt = new ResourceTerminal(terminal);
306             final TerminalLayout tl = te.getElementClass().getSingleItem(TerminalLayout.class);
307             AffineTransform terminalPos = tl.getTerminalPosition(te, rt);
308
309             if (terminalPos != null) {
310                 terminalTr.concatenate(terminalPos);
311                 x = terminalTr.getTranslateX();
312                 y = terminalTr.getTranslateY();
313                 if (DEBUG)
314                     LOGGER.debug("terminalPos/Tr: " + terminalPos + ", " + terminalTr);
315             }
316
317             Integer allowedDirections = graph.getPossibleRelatedValue(terminal, DIA.Terminal_AllowedDirections, Bindings.INTEGER);
318             if (allowedDirections != null) {
319                 direction |= allowedDirections;
320                 direction = rotateDirection(direction, terminalTr);
321             } else {
322                 direction |= RouteGraphConnectionClass.shortestDirectionOutOfBounds(x, y, bounds);
323             }
324             //LOGGER.debug("DIR(" + x + ", " + y + ", " + bounds + "): " + Integer.toHexString(direction));
325
326             if (backendConnections != null) {
327                 backendConnections.add(
328                         new BackendConnection(
329                                 toEdgeEnd(graph, attachmentRelation, EdgeEnd.Begin, DIA),
330                                 terminalElement,
331                                 terminal)
332                         );
333             }
334
335             if (direction == 0)
336                 // Accept any horizontal/vertical direction if nothing is defined
337                 direction = 0xf;
338
339             if (graph.<Boolean>getRelatedValue(connector, DIA.Connector_straight, Bindings.BOOLEAN))
340                 direction |= RouteTerminal.DIR_DIRECT;
341             // FIXME: routegraph crashes if this is done for all terminals regardless of the amount of terminals.
342
343             if (DEBUG)
344                 LOGGER.debug("load line style: " + NameUtils.getSafeLabel(graph, attachmentRelation));
345             ILineEndStyle endStyle = loadLineEndStyle(graph, attachmentRelation, connectionType, TAIL);
346
347             RouteTerminal routeTerminal = rg.addTerminal(x, y, minx, miny, maxx, maxy, direction, endStyle,
348                     new RouteTerminalPositionImpl(diagram, diagramDataElementMap, terminalElement, terminalElementTransform, tl, rt));
349             routeTerminal.setData( RouteGraphConnection.serialize(graph, connector) );
350
351             nodeByData.put( connector, routeTerminal );
352
353             for (Resource connectedTo : graph.getObjects(connector, DIA.AreConnected)) {
354                 links.add( new EdgeResource(connectedTo, connector) );
355             }
356         }
357
358         Collection<Link> initialLinks = new ArrayList<>();
359         
360         // Finish route graph loading by Linking route nodes together
361         for (EdgeResource link : links) {
362             RouteNode n1 = nodeByData.get(link.first());
363             RouteNode n2 = nodeByData.get(link.second());
364             if (n1 == null || n2 == null) {
365                 LOGGER.warn("Stray connection link found: " + link.toString(graph));
366                 continue;
367             }
368             rg.link(n1, n2);
369             initialLinks.add(new Link(n1, n2));
370         }
371         rg.setInitialLinks(initialLinks);
372
373         return rg;
374
375     }
376
377     public static EdgeEnd toEdgeEnd(ReadGraph graph, Resource attachmentRelation, EdgeEnd defaultValue, DiagramResource DIA)
378             throws DatabaseException {
379         if (graph.isSubrelationOf(attachmentRelation, DIA.HasTailConnector))
380             return EdgeEnd.Begin;
381         if (graph.isSubrelationOf(attachmentRelation, DIA.HasHeadConnector))
382             return EdgeEnd.End;
383         return defaultValue;
384     }
385
386     public static Resource resolveFlagAttachment(ReadGraph graph, Resource connection, Resource flag, IModelingRules modelingRules, DiagramResource DIA) throws DatabaseException {
387         Type type = resolveFlagType(graph, connection, flag, modelingRules, DIA);
388         if (type != null) {
389             switch (type) {
390                 case In: return DIA.HasPlainConnector;
391                 case Out: return DIA.HasArrowConnector;
392             }
393         }
394         return null;
395     }
396
397     private static Type resolveFlagType(ReadGraph graph, Resource connection, Resource flag, IModelingRules modelingRules, DiagramResource DIA) throws DatabaseException {
398         return readFlagType(graph, flag, DIA);
399     }
400
401     private static Type readFlagType(ReadGraph graph, Resource flag, DiagramResource DIA) throws DatabaseException {
402         Resource flagType = graph.getPossibleObject(flag, DIA.HasFlagType);
403         Type type = DiagramGraphUtil.toFlagType(DIA, flagType);
404         return type;
405     }
406
407     public static Statement findTerminalStatement(ReadGraph graph, Resource connection, Resource connector, StructuralResource2 STR)
408             throws DatabaseException {
409         for (Statement stm : graph.getStatements(connector, STR.Connects)) {
410             if (connection.equals(stm.getObject()))
411                 continue;
412             return stm;
413         }
414         return null;
415     }
416
417     public static Resource getInverseAttachment(ReadGraph graph, Resource attachmentRelation, DiagramResource DIA)
418             throws DatabaseException {
419         Resource inverse = graph.getPossibleObject(attachmentRelation, DIA.HasInverseAttachment);
420         if (inverse != null)
421             return inverse;
422         if (graph.isSubrelationOf(attachmentRelation, DIA.HasHeadConnector))
423             return DIA.HasPlainConnector;
424         if (graph.isSubrelationOf(attachmentRelation, DIA.HasTailConnector))
425             return DIA.HasArrowConnector;
426         return null;
427     }
428
429     public static ILineEndStyle loadLineEndStyle(ReadGraph graph, Resource attachmentRelation, ILineEndStyle defaultValue)
430             throws DatabaseException {
431         ILineEndStyle style = graph.syncRequest(new LineEndStyle(attachmentRelation),
432                 TransientCacheListener.<ILineEndStyle>instance());
433         return style != null ? style : defaultValue;
434     }
435
436     public static ILineEndStyle loadLineEndStyle(ReadGraph graph, Resource attachmentRelation, Resource connectionType, ILineEndStyle defaultValue)
437             throws DatabaseException {
438         if(connectionType != null) {
439             ILineEndStyle style = graph.syncRequest(new LineEndStyleWithType(attachmentRelation, connectionType),
440                     TransientCacheListener.<ILineEndStyle>instance());
441             return style != null ? style : defaultValue;
442         } else {
443             ILineEndStyle style = graph.syncRequest(new LineEndStyle(attachmentRelation),
444                     TransientCacheListener.<ILineEndStyle>instance());
445             return style != null ? style : defaultValue;
446         }
447     }
448
449     /**
450      * A request for caching ILineEndStyle results.
451      */
452     public static class LineEndStyle extends UnaryRead<Resource, ILineEndStyle> {
453         public LineEndStyle(Resource attachmentRelation) {
454             super(attachmentRelation);
455         }
456         @Override
457         public ILineEndStyle perform(ReadGraph graph) throws DatabaseException {
458             return loadLineEndStyle0(graph, parameter);
459         }
460     }
461
462     public static class LineEndStyleWithType extends ResourceRead2<ILineEndStyle> {
463         public LineEndStyleWithType(Resource attachmentRelation, Resource connectionType) {
464             super(attachmentRelation, connectionType);
465         }
466         @Override
467         public ILineEndStyle perform(ReadGraph graph) throws DatabaseException {
468             return loadLineEndStyle0(graph, resource, resource2);
469         }
470     }
471     
472     private static ILineEndStyle loadLineEndStyle0(ReadGraph graph, Resource attachmentRelation)
473             throws DatabaseException {
474         ILineEndStyle style = graph.getPossibleAdapter(attachmentRelation, ILineEndStyle.class);
475         if (style != null)
476             return style;
477         DiagramResource DIA = DiagramResource.getInstance(graph);
478         if (graph.isSubrelationOf(attachmentRelation, DIA.HasHeadConnector))
479             return HEAD;
480         if (graph.isSubrelationOf(attachmentRelation, DIA.HasTailConnector))
481             return TAIL;
482         return null;
483     }
484
485     private static ILineEndStyle loadLineEndStyle0(ReadGraph graph, Resource attachmentRelation, Resource connectionType)
486             throws DatabaseException {
487         DiagramResource DIA = DiagramResource.getInstance(graph);
488         if (graph.isSubrelationOf(attachmentRelation, DIA.HasHeadConnector)) {
489             if(connectionType != null) {
490                 G2DResource G2D = G2DResource.getInstance(graph);
491                 Resource end = graph.getPossibleObject(connectionType, G2D.HasEndArrow);
492                 if(end != null) {
493                     Double size = graph.getPossibleRelatedValue(end, G2D.HasSize, Bindings.DOUBLE);
494                     if(size == null) size = 0.0;
495                     Double widthRatio = graph.getPossibleRelatedValue(end, G2D.HasWidthRatio, Bindings.DOUBLE);
496                     if(widthRatio == null) widthRatio = 1.0;
497                     Double space = graph.getPossibleRelatedValue(end, G2D.HasSpace, Bindings.DOUBLE);
498                     if(space == null) space = 0.0;
499
500                     Resource c = graph.getPossibleObject(end, G2D.HasColor);
501                     Color color = null;
502                     if (c != null) {
503                         float[] col = graph.getPossibleValue(c, Bindings.FLOAT_ARRAY);
504                         if (col != null && col.length >= 3) {
505                             color = new Color(col[0], col[1], col[2]);
506                         }
507                     }
508
509                     return new ArrowLineEndStyle(size, widthRatio*size, space, color);
510                 }
511             }
512         }
513         return loadLineEndStyle0(graph, attachmentRelation);
514     }
515
516     /**
517      * @param graph
518      * @param canvas
519      * @param style
520      * @return
521      * @throws DatabaseException
522      */
523     public static StyledRouteGraphRenderer getRenderer(ReadGraph graph, ConnectionStyle style)
524             throws DatabaseException {
525         return graph.syncRequest(new Renderer(style),
526                 TransientCacheListener.<StyledRouteGraphRenderer>instance());
527     }
528
529     /**
530      * A request for caching StyledRouteGraphRenderer results.
531      */
532     public static class Renderer extends UnaryRead<ConnectionStyle, StyledRouteGraphRenderer> {
533         public Renderer(ConnectionStyle style) {
534             super(style);
535         }
536         @Override
537         public StyledRouteGraphRenderer perform(ReadGraph graph) throws DatabaseException {
538             return new StyledRouteGraphRenderer(parameter);
539         }
540     }
541
542     private static final ConnectionStyle DEFAULT_CONNECTION_STYLE = new BasicConnectionStyle(Color.BLACK, Color.BLACK, 3, ExampleConnectionStyle.SOLID, ExampleConnectionStyle.SOLID, 8);
543
544     /**
545      * @param graph
546      * @param canvas
547      * @param modelingRules
548      * @param connection
549      * @return
550      * @throws DatabaseException
551      */
552     protected static ConnectionStyle readConnectionStyle(ReadGraph graph, IModelingRules modelingRules, Resource connection, StructuralResource2 STR) throws DatabaseException {
553         Resource connectionType = null;
554         if (modelingRules != null)
555             connectionType = modelingRules.getConnectionType(graph, connection);
556         if (connectionType == null)
557             connectionType = graph.getPossibleObject(connection, STR.HasConnectionType);
558         return connectionType != null ? readConnectionStyleFromConnectionType(graph, connectionType) : DEFAULT_CONNECTION_STYLE;
559     }
560
561     protected static ConnectionStyle readConnectionStyleFromConnectionType(ReadGraph graph, Resource connectionType) throws DatabaseException {
562         return graph.syncRequest(new ReadConnectionStyleFromConnectionType(connectionType),
563                 TransientCacheListener.<ConnectionStyle>instance());
564     }
565
566     /**
567      * A request for caching ConnectionStyle results.
568      */
569     public static class ReadConnectionStyleFromConnectionType extends UnaryRead<Resource, ConnectionStyle> {
570         public ReadConnectionStyleFromConnectionType(Resource connectionType) {
571             super(connectionType);
572         }
573         @Override
574         public ConnectionStyle perform(ReadGraph graph) throws DatabaseException {
575             return readConnectionStyleFromConnectionType0(graph, parameter);
576         }
577     }
578
579     protected static ConnectionStyle readConnectionStyleFromConnectionType0(ReadGraph graph, Resource connectionType) throws DatabaseException {
580         ConnectionVisuals cv = null;
581         if (connectionType != null)
582             cv = graph.syncRequest(DiagramRequests.getConnectionVisuals(connectionType),
583                     TransientCacheListener.<ConnectionVisuals>instance());
584
585         // Fixed style settings
586         Color branchPointColor = Color.BLACK;
587         double branchPointRadius = cv != null && cv.branchPointRadius != null ? cv.branchPointRadius : 0.5;
588         double degenerateLineLength = 0.8;
589         
590         Color lineColor = cv != null ? cv.toColor() : null;
591         if (lineColor == null)
592             lineColor = Color.DARK_GRAY;
593         Stroke lineStroke = cv != null ? cv.stroke : null;
594         if (lineStroke == null)
595             lineStroke = new BasicStroke(0.1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 10, null, 0);
596         Stroke routeLineStroke = GeometryUtils.scaleStrokeWidth(lineStroke, 2);
597         double rounding = cv.rounding == null ? 0.0 : cv.rounding;
598
599         return new BasicConnectionStyle(
600                 lineColor,
601                 branchPointColor,
602                 branchPointRadius,
603                 lineStroke,
604                 routeLineStroke,
605                 degenerateLineLength,
606                 rounding);
607     }
608
609     public static void scheduleSynchronize(Session session, Resource connection, RouteGraphChangeEvent event) {
610         session.asyncRequest(RouteGraphConnection.synchronizer(connection, event));
611     }
612
613     // ------------------------------------------------------------------------
614     // RouteGraph RouteTerminal allowed direction rotation support
615     // ------------------------------------------------------------------------
616
617     public static int rotateDirection(int direction, AffineTransform at) {
618         // When direct routing is enabled, no point in wasting time rotating.
619         if ((direction & RouteTerminal.DIR_DIRECT) != 0)
620             return direction;
621
622         final int mask = (AffineTransform.TYPE_MASK_ROTATION | AffineTransform.TYPE_FLIP);
623         boolean rotatedOrFlipped = (at.getType() & mask) != 0;
624         if (rotatedOrFlipped) {
625             double xAxisAngle = Math.atan2( at.getShearY(), at.getScaleX() );
626             double yAxisAngle = Math.atan2( at.getScaleY(), at.getShearX() );
627
628             int xQuadrant = mainQuadrant(xAxisAngle);
629             int yQuadrant = mainQuadrant(yAxisAngle);
630
631             int xDirMask = direction & (RouteTerminal.DIR_LEFT | RouteTerminal.DIR_RIGHT);
632             int yDirMask = direction & (RouteTerminal.DIR_DOWN | RouteTerminal.DIR_UP);
633
634             int xDirMaskRotated = rotl4(xDirMask, xQuadrant);
635             int yDirMaskRotated = rotl4(yDirMask, yQuadrant-1);
636
637             direction = xDirMaskRotated | yDirMaskRotated;
638         }
639
640         return direction;
641     }
642
643     /**
644      * 4-bit rotate left without carry operation.
645      * Operates on the 4 least sensitive bits of the integer
646      * and leaves the higher bits untouched.
647      * @param x the bits to rotate
648      * @param n the amount of rotation [0..3]
649      * @return
650      */
651     private static int rotl4(int x, int n) {
652         n &= 3;
653         if (n == 0)
654             return x;
655         int hx = x & 0xfffffff0;
656         int lx = x & 0xf;
657         int xh = (lx << n) & 0xf;
658         int xl = (lx >>> (4-n));
659         return xh | xl | hx;
660     }
661
662         /**
663      * <pre>
664      *  33
665      * 2\/0
666      * 2/\0
667      *  11
668      * </pre>
669      * 
670      * @param theta angle in radians
671      * @return the quadrant based on the ASCII art above
672      */
673     private static int mainQuadrant(double theta) {
674         if (theta > -DEG_45 && theta <= DEG_45) {
675             return 0;
676         } else if ((theta > DEG_45 && theta <= DEG_135)) {
677             return 1;
678         } else if (theta >= -DEG_135 && theta < -DEG_45) {
679             return 3;
680         }
681         return 2;
682     }
683
684     private static final double DEG_45 = Math.PI/4.;
685     private static final double DEG_135 = Math.PI*3./4.;
686
687     public static class BackendConnection {
688         public final Resource node;
689         public final Resource terminal;
690         public final EdgeEnd  end;
691         public final int hash;
692         public BackendConnection(EdgeEnd end, Resource node, Resource terminal) {
693             assert end != null;
694             assert node != null;
695             assert terminal != null;
696             this.end = end;
697             this.node = node;
698             this.terminal = terminal;
699             this.hash = makeHash();
700         }
701         @Override
702         public boolean equals(Object obj) {
703             if (this == obj)
704                 return true;
705             if (!(obj instanceof Connection))
706                 return false;
707             Connection other = (Connection) obj;
708             return other.terminal == terminal
709                     && other.node == node
710                     && other.end == end;
711         }
712         private int makeHash() {
713             final int prime = 31;
714             int result = 1;
715             result = prime * result + end.hashCode();
716             result = prime * result + ((node == null) ? 0 : node.hashCode());
717             result = prime * result + ((terminal == null) ? 0 : terminal.hashCode());
718             return result;
719         }
720         @Override
721         public int hashCode() {
722             return hash;
723         }
724         @Override
725         public String toString() {
726             return "BackendConnection[node=" + node + ", terminal=" + terminal + ", end=" + end + "]";
727         }
728     }
729
730     private static class RouteTerminalPositionImpl implements RouteTerminalPosition {
731
732         private IDiagram diagram;
733         private DataElementMap dataElementMap;
734         private Resource element;
735         private AffineTransform elementTransform;
736         private TerminalLayout terminalLayout;
737         private Terminal elementTerminal;
738
739         private transient AffineTransform lastTerminalTr;
740         private transient AffineTransform transform;
741
742         public RouteTerminalPositionImpl(IDiagram diagram, DataElementMap dem, Resource element, AffineTransform elementTransform, TerminalLayout terminalLayout, Terminal terminal) {
743             this.diagram = diagram;
744             this.dataElementMap = dem;
745             this.element = element;
746             this.elementTransform = elementTransform;
747             this.terminalLayout = terminalLayout;
748             this.elementTerminal = terminal;
749         }
750
751         @Override
752         public AffineTransform getTransform() {
753             IElement actualElement = dataElementMap.getElement(diagram, element);
754             AffineTransform terminalTr = actualElement != null ? terminalLayout.getTerminalPosition(actualElement, elementTerminal) : null;
755             if (terminalTr == null)
756                 return elementTransform;
757
758             // Return cached transform if terminal transform has not changed.
759             AffineTransform result = this.transform;
760             AffineTransform lastTerminalTr = this.lastTerminalTr;
761             if (lastTerminalTr != null) {
762                 if (terminalTr.equals(lastTerminalTr))
763                     return result;
764                 lastTerminalTr.setTransform(terminalTr);
765             } else {
766                 lastTerminalTr = this.lastTerminalTr = new AffineTransform(terminalTr);
767                 result = this.transform = new AffineTransform();
768             }
769
770             result.setTransform(elementTransform);
771             result.concatenate(terminalTr);
772             return result;
773         }
774
775     }
776 }