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