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