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