1 /*******************************************************************************
\r
2 * Copyright (c) 2012 Association for Decentralized Information Management
\r
3 * in Industry THTH ry.
\r
4 * All rights reserved. This program and the accompanying materials
\r
5 * are made available under the terms of the Eclipse Public License v1.0
\r
6 * which accompanies this distribution, and is available at
\r
7 * http://www.eclipse.org/legal/epl-v10.html
\r
10 * VTT Technical Research Centre of Finland - initial API and implementation
\r
11 *******************************************************************************/
\r
12 package org.simantics.diagram.adapter;
\r
14 import gnu.trove.map.hash.THashMap;
\r
15 import gnu.trove.set.hash.THashSet;
\r
17 import java.awt.Shape;
\r
18 import java.awt.geom.AffineTransform;
\r
19 import java.awt.geom.Rectangle2D;
\r
20 import java.util.Collection;
\r
21 import java.util.Collections;
\r
22 import java.util.Map;
\r
23 import java.util.Set;
\r
25 import org.simantics.databoard.Bindings;
\r
26 import org.simantics.db.ReadGraph;
\r
27 import org.simantics.db.Resource;
\r
28 import org.simantics.db.Statement;
\r
29 import org.simantics.db.common.procedure.adapter.TransientCacheListener;
\r
30 import org.simantics.db.common.request.UnaryRead;
\r
31 import org.simantics.db.common.utils.NameUtils;
\r
32 import org.simantics.db.exception.DatabaseException;
\r
33 import org.simantics.diagram.adapter.RouteGraphConnectionClassFactory.BackendConnection;
\r
34 import org.simantics.diagram.connection.RouteGraph;
\r
35 import org.simantics.diagram.connection.RouteGraphConnectionClass;
\r
36 import org.simantics.diagram.connection.RouteLine;
\r
37 import org.simantics.diagram.connection.RouteNode;
\r
38 import org.simantics.diagram.connection.RouteTerminal;
\r
39 import org.simantics.diagram.connection.rendering.arrows.ArrowLineEndStyle;
\r
40 import org.simantics.diagram.connection.rendering.arrows.ILineEndStyle;
\r
41 import org.simantics.diagram.connection.rendering.arrows.PlainLineEndStyle;
\r
42 import org.simantics.diagram.content.EdgeResource;
\r
43 import org.simantics.diagram.content.TerminalMap;
\r
44 import org.simantics.diagram.query.DiagramRequests;
\r
45 import org.simantics.diagram.stubs.DiagramResource;
\r
46 import org.simantics.diagram.synchronization.graph.DiagramGraphUtil;
\r
47 import org.simantics.diagram.synchronization.graph.RouteGraphConnection;
\r
48 import org.simantics.g2d.canvas.ICanvasContext;
\r
49 import org.simantics.g2d.canvas.impl.CanvasContext;
\r
50 import org.simantics.g2d.diagram.IDiagram;
\r
51 import org.simantics.g2d.diagram.impl.ElementDiagram;
\r
52 import org.simantics.g2d.element.ElementUtils;
\r
53 import org.simantics.g2d.element.IElement;
\r
54 import org.simantics.g2d.element.handler.EdgeVisuals.EdgeEnd;
\r
55 import org.simantics.g2d.elementclass.FlagClass.Type;
\r
56 import org.simantics.layer0.Layer0;
\r
57 import org.simantics.scenegraph.utils.GeometryUtils;
\r
58 import org.simantics.structural.stubs.StructuralResource2;
\r
59 import org.simantics.structural2.modelingRules.CPTerminal;
\r
60 import org.simantics.structural2.modelingRules.IAttachmentRelationMap;
\r
61 import org.simantics.structural2.modelingRules.IModelingRules;
\r
62 import org.simantics.utils.threads.CurrentThread;
\r
64 public class RouteGraphUtils {
\r
66 public static boolean DEBUG = false;
\r
68 public static final ILineEndStyle HEAD = new ArrowLineEndStyle("fill 2 1 0");
\r
69 public static final ILineEndStyle TAIL = PlainLineEndStyle.INSTANCE;
\r
71 private static EdgeEnd toEdgeEnd(ReadGraph graph, Resource attachmentRelation, EdgeEnd defaultValue)
\r
72 throws DatabaseException {
\r
73 DiagramResource DIA = DiagramResource.getInstance(graph);
\r
74 if (graph.isSubrelationOf(attachmentRelation, DIA.HasTailConnector))
\r
75 return EdgeEnd.Begin;
\r
76 if (graph.isSubrelationOf(attachmentRelation, DIA.HasHeadConnector))
\r
78 return defaultValue;
\r
81 private static Resource resolveFlagAttachment(ReadGraph graph, Resource connection, Resource flag, IModelingRules modelingRules) throws DatabaseException {
\r
82 DiagramResource DIA = DiagramResource.getInstance(graph);
\r
83 Type type = resolveFlagType(graph, connection, flag, modelingRules);
\r
86 case In: return DIA.HasPlainConnector;
\r
87 case Out: return DIA.HasArrowConnector;
\r
93 private static Type resolveFlagType(ReadGraph graph, Resource connection, Resource flag, IModelingRules modelingRules) throws DatabaseException {
\r
94 return readFlagType(graph, flag);
\r
97 private static Type readFlagType(ReadGraph graph, Resource flag) throws DatabaseException {
\r
98 DiagramResource DIA = DiagramResource.getInstance(graph);
\r
99 Resource flagType = graph.getPossibleObject(flag, DIA.HasFlagType);
\r
100 Type type = DiagramGraphUtil.toFlagType(DIA, flagType);
\r
104 // public static ILineEndStyle loadLineEndStyle(ReadGraph graph, Resource attachmentRelation, ILineEndStyle defaultValue)
\r
105 // throws DatabaseException {
\r
106 // ILineEndStyle style = graph.syncRequest(new LineEndStyle(attachmentRelation),
\r
107 // TransientCacheListener.<ILineEndStyle>instance());
\r
108 // return style != null ? style : defaultValue;
\r
112 * A request for caching ILineEndStyle results.
\r
114 public static class LineEndStyle extends UnaryRead<Resource, ILineEndStyle> {
\r
115 public LineEndStyle(Resource attachmentRelation) {
\r
116 super(attachmentRelation);
\r
119 public ILineEndStyle perform(ReadGraph graph) throws DatabaseException {
\r
120 return loadLineEndStyle0(graph, parameter);
\r
124 public static ILineEndStyle loadLineEndStyle0(ReadGraph graph, Resource attachmentRelation)
\r
125 throws DatabaseException {
\r
126 ILineEndStyle style = graph.getPossibleAdapter(attachmentRelation, ILineEndStyle.class);
\r
129 DiagramResource DIA = DiagramResource.getInstance(graph);
\r
130 if (graph.isSubrelationOf(attachmentRelation, DIA.HasHeadConnector))
\r
132 if (graph.isSubrelationOf(attachmentRelation, DIA.HasTailConnector))
\r
137 private static Statement findTerminalStatement(ReadGraph graph, Resource connection, Resource connector)
\r
138 throws DatabaseException {
\r
140 StructuralResource2 STR = StructuralResource2.getInstance(graph);
\r
142 for (Statement stm : graph.getStatements(connector, STR.Connects)) {
\r
143 if (connection.equals(stm.getObject()))
\r
151 private static Resource getInverseAttachment(ReadGraph graph, Resource attachmentRelation)
\r
152 throws DatabaseException {
\r
153 DiagramResource DIA = DiagramResource.getInstance(graph);
\r
154 Resource inverse = graph.getPossibleObject(attachmentRelation, DIA.HasInverseAttachment);
\r
155 if (inverse != null)
\r
157 if (graph.isSubrelationOf(attachmentRelation, DIA.HasHeadConnector))
\r
158 return DIA.HasPlainConnector;
\r
159 if (graph.isSubrelationOf(attachmentRelation, DIA.HasTailConnector))
\r
160 return DIA.HasArrowConnector;
\r
164 public static RouteGraph load(ReadGraph graph, Resource diagramRuntime, Resource connection) throws DatabaseException {
\r
166 ICanvasContext canvas = new CanvasContext(CurrentThread.getThreadAccess());
\r
167 IDiagram diagram = new ElementDiagram(canvas);
\r
168 return load(graph, diagramRuntime, connection, canvas, diagram);
\r
172 public static RouteGraph load(ReadGraph graph, Resource diagramRuntime, Resource connection, ICanvasContext canvas, IDiagram diagram) throws DatabaseException {
\r
174 Layer0 L0 = Layer0.getInstance(graph);
\r
175 DiagramResource DIA = DiagramResource.getInstance(graph);
\r
176 StructuralResource2 STR = StructuralResource2.getInstance(graph);
\r
178 Resource diagramResource = graph.getPossibleObject(connection, L0.PartOf);
\r
180 IModelingRules modelingRules = graph.syncRequest(DiagramRequests.getModelingRules(diagramResource, null), TransientCacheListener.<IModelingRules>instance());
\r
182 // IModelingRules modelingRules = diagram.getHint(DiagramModelHints.KEY_MODELING_RULES);
\r
183 // Resource diagramRuntime = diagram.getHint(DiagramModelHints.KEY_DIAGRAM_RUNTIME_RESOURCE);
\r
185 RouteGraph rg = new RouteGraph();
\r
187 // Default capacity should be enough for common cases.
\r
188 Set<EdgeResource> links = new THashSet<EdgeResource>();
\r
189 Map<Object, RouteNode> nodeByData = new THashMap<Object, RouteNode>();
\r
191 // Load all route graph interior RouteNodes: route lines and points
\r
192 for (Resource interiorNode : graph.getObjects(connection, DIA.HasInteriorRouteNode)) {
\r
193 if (graph.isInstanceOf(interiorNode, DIA.RouteLine)) {
\r
194 Boolean isHorizontal = graph.getRelatedValue(interiorNode, DIA.IsHorizontal, Bindings.BOOLEAN);
\r
195 Double position = graph.getRelatedValue(interiorNode, DIA.HasPosition, Bindings.DOUBLE);
\r
196 RouteLine line = rg.addLine(isHorizontal, position);
\r
197 line.setData( RouteGraphConnection.serialize(graph, interiorNode) );
\r
199 nodeByData.put( interiorNode, line );
\r
201 for (Resource connectedTo : graph.getObjects(interiorNode, DIA.AreConnected)) {
\r
202 links.add( new EdgeResource(interiorNode, connectedTo) );
\r
204 } else if (graph.isInstanceOf(interiorNode, DIA.RoutePoint)) {
\r
205 // Not supported yet. Ignore.
\r
209 Rectangle2D bounds = new Rectangle2D.Double();
\r
210 Map<Resource, Resource> connectorToModeledAttachment = null;
\r
212 // Primarily the loader will believe what modeling rules say about
\r
213 // connector attachment relations.
\r
215 // If modeling rules decide nothing, then we simply believe what the
\r
216 // the attachment relations in the graph say.
\r
218 // Special case 1: connection with two (2) terminals
\r
219 // If the attachment of one of two terminals is decided by modeling
\r
220 // rules, the other attachment shall be the opposite of the decided
\r
221 // attachment (see forcedAttachmentRelation below).
\r
223 // Special case 2: connected to a flag
\r
224 // If the attached element is a flag and modeling rules say nothing
\r
225 // about it, believe the direction stated by the flag type.
\r
227 Collection<Statement> toConnectorStatements = graph.getStatements(connection, DIA.HasConnector);
\r
228 int terminalCount = 0;
\r
230 // See if modeling rules judge any of the connection terminal attachments.
\r
231 if (modelingRules != null) {
\r
232 for (Statement toConnector : toConnectorStatements) {
\r
233 Resource connector = toConnector.getObject();
\r
235 Statement terminalStm = findTerminalStatement(graph, connection, connector);
\r
236 if (terminalStm == null)
\r
237 // Ignore broken connector: attached to the connection but not to any terminal.
\r
240 Resource terminalElement = terminalStm.getObject();
\r
241 Resource connectionRelation = graph.getPossibleInverse(terminalStm.getPredicate());
\r
242 if (connectionRelation == null)
\r
247 IAttachmentRelationMap map = modelingRules.getAttachmentRelations(graph, connection);
\r
248 Resource attachment = map.get(graph, new CPTerminal(terminalElement, connectionRelation));
\r
249 if (attachment != null) {
\r
250 // Primary: believe modeling rules
\r
251 if (connectorToModeledAttachment == null)
\r
252 connectorToModeledAttachment = new THashMap<Resource, Resource>(toConnectorStatements.size());
\r
253 connectorToModeledAttachment.put(connector, attachment);
\r
255 System.out.println("modeling rules decided attachment: " + NameUtils.getSafeName(graph, attachment, true) + " for (" + NameUtils.toString(graph, toConnector, true) + ") & (" + NameUtils.toString(graph, terminalStm, true) + ")");
\r
256 } else if (graph.isInstanceOf(terminalElement, DIA.Flag)) {
\r
257 // Secondary: believe flag type
\r
258 attachment = resolveFlagAttachment(graph, connection, terminalElement, modelingRules);
\r
259 if (attachment != null) {
\r
260 if (connectorToModeledAttachment == null)
\r
261 connectorToModeledAttachment = new THashMap<Resource, Resource>(toConnectorStatements.size());
\r
262 connectorToModeledAttachment.put(connector, attachment);
\r
264 System.out.println("flag type decided attachment: " + NameUtils.getSafeName(graph, attachment, true) + " for (" + NameUtils.toString(graph, toConnector, true) + ") & (" + NameUtils.toString(graph, terminalStm, true) + ")");
\r
270 if (connectorToModeledAttachment == null)
\r
271 connectorToModeledAttachment = Collections.emptyMap();
\r
273 Resource forcedAttachmentRelation = null;
\r
274 if (terminalCount == 2 && connectorToModeledAttachment.size() == 1) {
\r
275 forcedAttachmentRelation = getInverseAttachment(graph, connectorToModeledAttachment.values().iterator().next());
\r
277 System.out.println("set forced attachment: " + NameUtils.getSafeLabel(graph, forcedAttachmentRelation));
\r
280 Resource connectionType = graph.getPossibleObject(connection, STR.HasConnectionType);
\r
282 // Needed to support ConnectionEntity#getTerminalConnections
\r
283 Set<BackendConnection> backendConnections = new THashSet<BackendConnection>(toConnectorStatements.size(), 0.75f);
\r
285 // Load all node terminal connections as RouteTerminals
\r
286 for (Statement toConnector : toConnectorStatements) {
\r
287 Resource connector = toConnector.getObject();
\r
288 Resource attachmentRelation = toConnector.getPredicate();
\r
290 System.out.println("original attachment relation: " + NameUtils.getSafeLabel(graph, attachmentRelation));
\r
292 Statement terminalStm = findTerminalStatement(graph, connection, connector);
\r
293 if (terminalStm == null)
\r
294 // Ignore broken connector: attached to the connection but not to any terminal.
\r
297 Resource terminalElement = terminalStm.getObject();
\r
298 Resource terminalElementType = graph.getPossibleType(terminalElement, DIA.Element);
\r
299 if (terminalElementType == null)
\r
300 // Ignore non-element terminal elements
\r
303 Resource connectionRelation = graph.getPossibleInverse(terminalStm.getPredicate());
\r
304 if (connectionRelation == null)
\r
307 // Discover node and terminal this connector is connected to.
\r
308 TerminalMap terminals = graph.syncRequest(DiagramRequests.elementTypeTerminals(terminalElementType),
\r
309 TransientCacheListener.<TerminalMap>instance());
\r
310 Resource terminal = terminals.getTerminal(connectionRelation);
\r
311 if (terminal == null) {
\r
312 System.err.println(
\r
313 "RouteGraphUtils: Could not find terminal for connection point "
\r
314 + NameUtils.getSafeName(graph, connectionRelation, true)
\r
316 + NameUtils.getSafeName(graph, terminalElement, true));
\r
320 double[] position = graph.getRelatedValue(connector, DIA.HasRelativeLocation, Bindings.DOUBLE_ARRAY);
\r
321 if (position.length != 2)
\r
322 position = new double[] { 0, 0 };
\r
325 System.out.println("terminalStm: " + NameUtils.toString(graph, terminalStm));
\r
326 System.out.println("terminal: " + graph.getURI(terminalStm.getPredicate()));
\r
328 AffineTransform terminalElementTr = diagramRuntime != null ?
\r
329 DiagramGraphUtil.getDynamicWorldTransform(graph, diagramRuntime, terminalElement) :
\r
330 DiagramGraphUtil.getWorldTransform(graph, terminalElement);
\r
333 System.out.println("terminalElementTr: " + terminalElementTr);
\r
335 double x = terminalElementTr.getTranslateX();
\r
336 double y = terminalElementTr.getTranslateY();
\r
337 double minx = x-1, miny = y-1, maxx = x+1, maxy = y+1;
\r
338 int direction = 0x0;
\r
340 // Use modelingRules to ascertain the proper attachmentRelation
\r
341 // for this terminal connection, if available.
\r
342 Resource att = connectorToModeledAttachment.get(connector);
\r
344 attachmentRelation = att;
\r
346 System.out.println("modeling rules attachment: " + NameUtils.getSafeLabel(graph, attachmentRelation));
\r
347 } else if (forcedAttachmentRelation != null) {
\r
348 attachmentRelation = forcedAttachmentRelation;
\r
350 System.out.println("forced rules attachment: " + NameUtils.getSafeLabel(graph, attachmentRelation));
\r
353 System.out.println("decided attachment: " + NameUtils.getSafeLabel(graph, attachmentRelation));
\r
355 // // Get element bounds to decide allowed terminal direction(s)
\r
356 IElement te = graph.syncRequest(DiagramRequests.getElement(canvas, diagram, terminalElement, null));
\r
357 ElementUtils.getElementBounds(te, bounds);
\r
359 Shape shp = org.simantics.g2d.utils.GeometryUtils.transformShape(bounds, terminalElementTr);
\r
360 bounds.setFrame(shp.getBounds2D());
\r
363 // Expand bounds by 2mm to make the connections enter the terminals
\r
364 // at a straight angle and from a distance instead of coming in
\r
366 GeometryUtils.expandRectangle(bounds, 2);
\r
367 minx = bounds.getMinX();
\r
368 miny = bounds.getMinY();
\r
369 maxx = bounds.getMaxX();
\r
370 maxy = bounds.getMaxY();
\r
372 AffineTransform terminalPos = DiagramGraphUtil.getDynamicAffineTransform(graph, terminalElement, terminal);
\r
373 //AffineTransform terminalPos2 = DiagramGraphUtil.getAffineTransform(graph, terminal);
\r
374 if (terminalPos != null) {
\r
376 System.out.println("terminalPos: " + terminalPos);
\r
377 //System.out.println("terminalPos2: " + terminalPos2);
\r
379 terminalElementTr.concatenate(terminalPos);
\r
381 System.out.println("terminalElementTr: " + terminalElementTr);
\r
382 x = terminalElementTr.getTranslateX();
\r
383 y = terminalElementTr.getTranslateY();
\r
386 Integer allowedDirections = graph.getPossibleRelatedValue(terminal, DIA.Terminal_AllowedDirections, Bindings.INTEGER);
\r
387 if (allowedDirections != null) {
\r
388 direction |= allowedDirections;
\r
389 direction = rotateDirection(direction, terminalElementTr);
\r
391 direction |= RouteGraphConnectionClass.shortestDirectionOutOfBounds(x, y, bounds);
\r
393 //System.out.println("DIR(" + x + ", " + y + ", " + bounds + "): " + Integer.toHexString(direction));
\r
395 backendConnections.add(
\r
396 new BackendConnection(
\r
397 toEdgeEnd(graph, attachmentRelation, EdgeEnd.Begin),
\r
402 if (direction == 0)
\r
403 // Accept any horizontal/vertical direction if nothing is defined
\r
406 if (graph.<Boolean>getRelatedValue(connector, DIA.Connector_straight, Bindings.BOOLEAN))
\r
407 direction |= RouteTerminal.DIR_DIRECT;
\r
408 // FIXME: routegraph crashes if this is done for all terminals regardless of the amount of terminals.
\r
411 System.out.println("load line style: " + NameUtils.getSafeLabel(graph, attachmentRelation));
\r
412 ILineEndStyle endStyle = RouteGraphConnectionClassFactory.loadLineEndStyle(graph, attachmentRelation, connectionType, RouteGraphConnectionClassFactory.TAIL);
\r
414 RouteTerminal routeTerminal = rg.addTerminal(x, y, minx, miny, maxx, maxy, direction, endStyle);
\r
415 routeTerminal.setData( RouteGraphConnection.serialize(graph, connector) );
\r
417 nodeByData.put( connector, routeTerminal );
\r
419 for (Resource connectedTo : graph.getObjects(connector, DIA.AreConnected)) {
\r
420 links.add( new EdgeResource(connectedTo, connector) );
\r
424 // Finish route graph loading by Linking route nodes together
\r
425 for (EdgeResource link : links) {
\r
426 RouteNode n1 = nodeByData.get(link.first());
\r
427 RouteNode n2 = nodeByData.get(link.second());
\r
428 if (n1 == null || n2 == null) {
\r
429 System.err.println("Stray connection link found: " + link.toString(graph));
\r
435 // // Load connection line style.
\r
436 // ConnectionStyle style = readConnectionStyle(graph, canvas, modelingRules, connection);
\r
437 // StyledRouteGraphRenderer renderer = getRenderer(graph, canvas, style);
\r
439 // // Finish element load
\r
440 // element.setHint(RouteGraphConnectionClass.KEY_ROUTEGRAPH, rg);
\r
441 // element.setHint(RouteGraphConnectionClass.KEY_RENDERER, renderer);
\r
442 // element.setHint(RouteGraphConnectionClass.KEY_PICK_TOLERANCE, 0.5);
\r
444 // // Initialize ConnectionEntity in element
\r
445 // element.setHint(ElementHints.KEY_CONNECTION_ENTITY, new CE(diagram, connection, element, backendConnections));
\r
447 // // Setup graph writeback support for route graph modifications
\r
448 // final Session session = graph.getSession();
\r
449 // element.setHint(RouteGraphConnectionClass.KEY_RG_LISTENER, new IRouteGraphListener() {
\r
451 // public void routeGraphChanged(RouteGraphChangeEvent event) {
\r
452 // scheduleSynchronize(session, connection, event);
\r
460 // ------------------------------------------------------------------------
\r
461 // RouteGraph RouteTerminal allowed direction rotation support
\r
462 // ------------------------------------------------------------------------
\r
464 public static int rotateDirection(int direction, AffineTransform at) {
\r
465 // When direct routing is enabled, no point in wasting time rotating.
\r
466 if ((direction & RouteTerminal.DIR_DIRECT) != 0)
\r
469 final int mask = (AffineTransform.TYPE_MASK_ROTATION | AffineTransform.TYPE_FLIP);
\r
470 boolean rotatedOrFlipped = (at.getType() & mask) != 0;
\r
471 if (rotatedOrFlipped) {
\r
472 double xAxisAngle = Math.atan2( at.getShearY(), at.getScaleX() );
\r
473 double yAxisAngle = Math.atan2( at.getScaleY(), at.getShearX() );
\r
475 int xQuadrant = mainQuadrant(xAxisAngle);
\r
476 int yQuadrant = mainQuadrant(yAxisAngle);
\r
478 int xDirMask = direction & (RouteTerminal.DIR_LEFT | RouteTerminal.DIR_RIGHT);
\r
479 int yDirMask = direction & (RouteTerminal.DIR_DOWN | RouteTerminal.DIR_UP);
\r
481 int xDirMaskRotated = rotl4(xDirMask, xQuadrant);
\r
482 int yDirMaskRotated = rotl4(yDirMask, yQuadrant-1);
\r
484 direction = xDirMaskRotated | yDirMaskRotated;
\r
491 * 4-bit rotate left without carry operation.
\r
492 * Operates on the 4 least sensitive bits of the integer
\r
493 * and leaves the higher bits untouched.
\r
494 * @param x the bits to rotate
\r
495 * @param n the amount of rotation [0..3]
\r
498 private static int rotl4(int x, int n) {
\r
502 int hx = x & 0xfffffff0;
\r
504 int xh = (lx << n) & 0xf;
\r
505 int xl = (lx >>> (4-n));
\r
506 return xh | xl | hx;
\r
517 * @param theta angle in radians
\r
518 * @return the quadrant based on the ASCII art above
\r
520 private static int mainQuadrant(double theta) {
\r
521 if (theta > -DEG_45 && theta <= DEG_45) {
\r
523 } else if ((theta > DEG_45 && theta <= DEG_135)) {
\r
525 } else if (theta >= -DEG_135 && theta < -DEG_45) {
\r
531 private static final double DEG_45 = Math.PI/4.;
\r
532 private static final double DEG_135 = Math.PI*3./4.;
\r