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