/******************************************************************************* * Copyright (c) 2011 Association for Decentralized Information Management in * Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.diagram.synchronization.graph; import gnu.trove.map.hash.TObjectIntHashMap; import gnu.trove.procedure.TObjectIntProcedure; import gnu.trove.set.hash.THashSet; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.simantics.db.ReadGraph; import org.simantics.db.Resource; import org.simantics.db.ServiceLocator; import org.simantics.db.Statement; import org.simantics.db.WriteGraph; import org.simantics.db.common.CommandMetadata; import org.simantics.db.common.CommentMetadata; import org.simantics.db.common.request.IndexRoot; import org.simantics.db.common.request.WriteRequest; import org.simantics.db.common.utils.NameUtils; import org.simantics.db.exception.DatabaseException; import org.simantics.db.request.Write; import org.simantics.db.service.SerialisationSupport; import org.simantics.diagram.connection.RouteGraph; import org.simantics.diagram.connection.RouteLine; import org.simantics.diagram.connection.RouteLink; import org.simantics.diagram.connection.RouteNode; import org.simantics.diagram.connection.RoutePoint; import org.simantics.diagram.connection.RouteTerminal; import org.simantics.diagram.connection.delta.RouteGraphDelta; import org.simantics.diagram.connection.delta.RouteGraphDelta.RouteLinePair; import org.simantics.diagram.connection.delta.RouteGraphDelta.RouteTerminalPair; import org.simantics.diagram.content.ConnectionUtil; import org.simantics.diagram.stubs.DiagramResource; import org.simantics.layer0.Layer0; import org.simantics.modeling.ModelingResources; import org.simantics.scenegraph.g2d.nodes.connection.RouteGraphChangeEvent; import org.simantics.scl.commands.Commands; import org.simantics.scl.compiler.top.ValueNotFound; import org.simantics.scl.osgi.SCLOsgi; import org.simantics.scl.runtime.SCLContext; import org.simantics.scl.runtime.function.Function; import org.simantics.structural.stubs.StructuralResource2; import org.simantics.utils.datastructures.Pair; /** * FIXME: Adding new route lines does not reconnect the new route lines to existing terminals/other route lines * * @author Tuukka Lehtonen */ @SuppressWarnings("rawtypes") public class RouteGraphConnection { private static final boolean DEBUG_SYNC = false; private static final boolean USE_COMMAND_BASED_SYNCHRONIZATION = true; public static Write synchronizer(final Resource connection, final RouteGraphChangeEvent event) { return new WriteRequest() { @Override public void perform(WriteGraph graph) throws DatabaseException { RouteGraphConnection rgc = new RouteGraphConnection(graph, connection); rgc.synchronize(graph, event.before, event.after, event.delta); CommentMetadata cm = graph.getMetadata(CommentMetadata.class); graph.addMetadata(cm.add("Modified connection route")); graph.markUndoPoint(); } }; } private Layer0 L0; private DiagramResource DIA; private ConnectionUtil cu; private Resource connection; public RouteGraphConnection(WriteGraph graph, Resource connection) { this.L0 = Layer0.getInstance(graph); this.DIA = DiagramResource.getInstance(graph); this.cu = new ConnectionUtil(graph); this.connection = connection; } private static Function ConnectionPoint; private static Function Element; private static Function RouteGraphStructure; private static Function UpdateLine; private static Function RemoveNode; private static Function RemoveLink; private static Function CreateLine; private static Function CreateLink; private static boolean initialized = false; private static void initialize(ReadGraph graph) { if(!initialized) { Object oldGraph = SCLContext.getCurrent().put("graph", graph); try { ConnectionPoint = (Function)SCLOsgi.MODULE_REPOSITORY.getValue("Simantics/RouteGraph/ConnectionPoint"); Element = (Function)SCLOsgi.MODULE_REPOSITORY.getValue("Simantics/RouteGraph/Element"); RouteGraphStructure = (Function)SCLOsgi.MODULE_REPOSITORY.getValue("Simantics/RouteGraph/RouteGraphStructure"); UpdateLine = (Function)SCLOsgi.MODULE_REPOSITORY.getValue("Simantics/RouteGraph/UpdateLine"); RemoveNode = (Function)SCLOsgi.MODULE_REPOSITORY.getValue("Simantics/RouteGraph/RemoveNode"); RemoveLink = (Function)SCLOsgi.MODULE_REPOSITORY.getValue("Simantics/RouteGraph/RemoveLink"); CreateLine = (Function)SCLOsgi.MODULE_REPOSITORY.getValue("Simantics/RouteGraph/CreateLine"); CreateLink = (Function)SCLOsgi.MODULE_REPOSITORY.getValue("Simantics/RouteGraph/CreateLink"); initialized = true; } catch(ValueNotFound e) { e.printStackTrace(); } finally { SCLContext.getCurrent().put("graph", oldGraph); } } } @SuppressWarnings("unchecked") private static Object getConnectorIdentifier(ReadGraph graph, Resource connector) throws DatabaseException { Layer0 L0 = Layer0.getInstance(graph); DiagramResource DIA = DiagramResource.getInstance(graph); StructuralResource2 STR = StructuralResource2.getInstance(graph); ModelingResources MOD = ModelingResources.getInstance(graph); for(Statement stat : graph.getStatements(connector, STR.Connects)) { Resource pred = stat.getPredicate(); Resource element = stat.getObject(); if(!graph.isSubrelationOf(pred, DIA.IsConnectorOf)) { Resource component = graph.getPossibleObject(element, MOD.ElementToComponent); if(component != null) { String componentName = graph.getRelatedValue(component, L0.HasName); String relationName = graph.getRelatedValue(graph.getInverse(pred), L0.HasName); return ConnectionPoint.apply(componentName, relationName); } else /* element is flag or reference element */ { String elementName = graph.getRelatedValue(element, L0.HasName); return Element.apply(elementName); } } } throw new DatabaseException("Didn't find indentification for " + connector + "."); } @SuppressWarnings("unchecked") public void synchronize( WriteGraph graph, RouteGraph before, RouteGraph after, RouteGraphDelta delta) throws DatabaseException { if (DEBUG_SYNC) delta.print(); if(USE_COMMAND_BASED_SYNCHRONIZATION) { initialize(graph); Layer0 L0 = Layer0.getInstance(graph); ModelingResources MOD = ModelingResources.getInstance(graph); Resource diagram = graph.getSingleObject(connection, L0.PartOf); Resource composite = graph.getSingleObject(diagram, MOD.DiagramToComposite); // Structure Object structure; TObjectIntHashMap routeNodeMap = new TObjectIntHashMap() { @Override protected int hash(Object notnull) { RouteNode node = (RouteNode)notnull; Object data = node.getData(); if(data != null) return data.hashCode(); else return node.hashCode(); } @Override protected boolean equals(Object notnull, Object two) { RouteNode node1 = (RouteNode)notnull; RouteNode node2 = (RouteNode)two; Object data1 = node1.getData(); if(data1 == null) return node1 == node2; Object data2 = node2.getData(); return data1.equals(data2); } }; { ArrayList connectorIdentifiers = new ArrayList(); for(RouteTerminal terminal : before.getTerminals()) { Resource connector = deserialize(graph, terminal.getData()); connectorIdentifiers.add(getConnectorIdentifier(graph, connector)); routeNodeMap.put(terminal, routeNodeMap.size()); } ArrayList routeLinks; if(before.isSimpleConnection()) { routeLinks = new ArrayList(2); routeLinks.add(0); routeLinks.add(1); } else { THashSet linkSet = new THashSet(); for(RouteLine line : before.getLines()) { int id = routeNodeMap.size(); routeNodeMap.put(line, id); for(RoutePoint rp : line.getPoints()) if(rp instanceof RouteLink) { RouteLink link = (RouteLink)rp; if(!link.getA().isTransient() && !link.getB().isTransient()) linkSet.add(link); } } routeLinks = new ArrayList(linkSet.size()*2+before.getTerminals().size()); for(RouteTerminal terminal : before.getTerminals()) { routeLinks.add(routeNodeMap.get(terminal)); routeLinks.add(routeNodeMap.get(terminal.getLine())); } for(RouteLink link : linkSet) { routeLinks.add(routeNodeMap.get(link.getA())); routeLinks.add(routeNodeMap.get(link.getB())); } } structure = RouteGraphStructure.apply(composite, connectorIdentifiers, routeNodeMap.size()-connectorIdentifiers.size(), routeLinks ); } // Process modifications ArrayList modifications = new ArrayList(); ArrayList> newRouteLines = new ArrayList>(); { // Make direct changes to connection components for (RouteLinePair pair : delta.getLinesThatDiffer()) modifications.add(UpdateLine.apply(routeNodeMap.get(pair.left), pair.right.getPosition(), pair.right.isHorizontal())); // Write new components into connection for (RouteLine added : delta.getLinesOnlyInRight()) { modifications.add(CreateLine.apply(added.getPosition(), added.isHorizontal())); newRouteLines.add(Pair.make(added, routeNodeMap.size())); routeNodeMap.put(added, routeNodeMap.size()); } // Remove all removed links from connection for (RouteLink removed : delta.getLinksOnlyInLeft()) modifications.add(RemoveLink.apply( routeNodeMap.get(removed.getA()), routeNodeMap.get(removed.getB()))); // Write modifications to connectivity between terminals and interior // route nodes. for (RouteLink added : delta.getLinksOnlyInRight()) modifications.add(CreateLink.apply( routeNodeMap.get(added.getA()), routeNodeMap.get(added.getB()))); // Update modifications to terminals if(before.isSimpleConnection()) { if(!after.isSimpleConnection()) { modifications.add(RemoveLink.apply(0, 1)); for(RouteTerminal terminal : after.getTerminals()) modifications.add(CreateLink.apply( routeNodeMap.get(terminal), routeNodeMap.get(terminal.getLine()) )); } } else if(after.isSimpleConnection()) { for(RouteTerminal terminal : before.getTerminals()) modifications.add(RemoveLink.apply( routeNodeMap.get(terminal), routeNodeMap.get(terminal.getLine()) )); modifications.add(CreateLink.apply(0, 1)); } else { for (RouteTerminalPair pair : delta.getTerminalsThatDiffer()) { // If terminals are connected to different route lines, // disconnect left terminal connector from its connected counterpart // and connect the same terminal connector to the connected counterpart // listed in the right terminal. int terminalId = routeNodeMap.get(pair.left); int leftLine = routeNodeMap.get(pair.left.getLine()); int rightLine = routeNodeMap.get(pair.right.getLine()); if(leftLine != rightLine) { modifications.add(RemoveLink.apply(terminalId, leftLine)); modifications.add(CreateLink.apply(terminalId, rightLine)); } } } // Disconnect and remove removed terminal connectors for(RouteTerminal removedTerminal : delta.getTerminalsOnlyInLeft()) modifications.add(RemoveNode.apply(routeNodeMap.get(removedTerminal))); // Finally delete removed route lines for(RouteLine removed : delta.getLinesOnlyInLeft()) modifications.add(RemoveNode.apply(routeNodeMap.get(removed))); } // Execute command if(!modifications.isEmpty()) { /*System.out.println("--"); System.out.println(structure); System.out.println(modifications);*/ final List allRouteNodes = (List) Commands.get(graph, "Simantics/RouteGraph/modifyRouteGraph") .execute(graph, graph.syncRequest(new IndexRoot(connection)), structure, modifications); for(Pair pair : newRouteLines) pair.first.setData(allRouteNodes.get(pair.second).getResourceId()); } } else { Map newComponents = new HashMap(); CommentMetadata comments = graph.getMetadata(CommentMetadata.class); comments.add("Modified connection " + NameUtils.getSafeLabel(graph, connection)); writeCommandMetadata(graph, before, delta, after); // Make direct changes to connection components for (RouteLinePair pair : delta.getLinesThatDiffer()) { Resource line = deserialize(graph, pair.right.getData()); if (DEBUG_SYNC) { System.out.print("synchronizing existing route line: " + NameUtils.getSafeLabel(graph, line) + " - "); pair.right.print(System.out); } writeLine(graph, line, pair.right); comments.add("Synchronized existing route line: " + NameUtils.getSafeLabel(graph, line)); } // Write new components into connection for (RouteLine added : delta.getLinesOnlyInRight()) { Resource line = tryDeserialize(graph, added.getData()); if (DEBUG_SYNC) { System.out.print("adding new route line (" + line + "): "); added.print(System.out); } newComponents.put( added, line = writeLine(graph, line, added) ); comments.add("Added new route line " + NameUtils.getSafeLabel(graph, line)); } // Remove all removed links from connection for (RouteLink removed : delta.getLinksOnlyInLeft()) { Resource a = deserialize(graph, removed.getA().getData()); Resource b = deserialize(graph, removed.getB().getData()); if (DEBUG_SYNC) System.out.println("removing link: " + NameUtils.getSafeLabel(graph, a) + " <> " + NameUtils.getSafeLabel(graph, b)); cu.disconnect(a, b); comments.add("Removed link: " + NameUtils.getSafeLabel(graph, a) + " <> " + NameUtils.getSafeLabel(graph, b)); } // Write modifications to connectivity between terminals and interior // route nodes. for (RouteLink added : delta.getLinksOnlyInRight()) { if (DEBUG_SYNC) { System.out.println("processing added link:"); added.getA().print(System.out); added.getB().print(System.out); } Resource a = deserialize(graph, added.getA().getData()); Resource b = deserialize(graph, added.getB().getData()); if (DEBUG_SYNC) System.out.println("adding link between " + NameUtils.getSafeLabel(graph, a) + " <> " + NameUtils.getSafeLabel(graph, b)); cu.connect(a, b); comments.add("Added link between " + NameUtils.getSafeLabel(graph, a) + " <> " + NameUtils.getSafeLabel(graph, b)); } Set looseConnectors = new HashSet(); // Handle terminals that differ for (RouteTerminalPair pair : delta.getTerminalsThatDiffer()) { if (DEBUG_SYNC) { System.out.println("processing differing terminal:"); pair.left.print(System.out); pair.right.print(System.out); } // If terminals are connected to different route lines, // disconnect left terminal connector from its connected counterpart // and connect the same terminal connector to the connected counterpart // listed in the right terminal. if (pair.left.getLine() != pair.right.getLine()) { Resource connector = deserialize( graph, pair.left.getData() ); // Case 1: // - two terminals connected directly to each other. // - terminals moved so that a transient route line is added but // nothing should be written into the graph. // Case recognition logic: // left.line == null // && right.line != null // && right.line.data == null if (pair.left.getLine() == null && pair.right.getLine() != null && pair.right.getLine().getData() == null) continue; // Case 2a: // - terminal previously connected to either another terminal or a persistent route line // Case 2b: // - terminal previously connected to another terminal through a transient route line // Case 2c: // - terminal previously connected to another terminal if (pair.left.getLine() != null && pair.left.getLine().getData() != null) { Resource line = deserialize(graph, pair.left.getLine().getData()); // Case 2a if (DEBUG_SYNC) System.out.println("removing link between terminal " + NameUtils.getSafeLabel(graph, connector) + " and route line " + NameUtils.getSafeLabel(graph, line)); cu.disconnect(connector, line); comments.add("Removed link between terminal " + NameUtils.getSafeLabel(graph, connector) + " and route line " + NameUtils.getSafeLabel(graph, line)); } else { // Case 2b & 2c if (DEBUG_SYNC) System.out.println("removing link between terminal " + NameUtils.getSafeLabel(graph, connector) + " and other terminals "); cu.disconnectFromAllRouteNodes(connector); comments.add("Removed link between terminal " + NameUtils.getSafeLabel(graph, connector) + " and other terminals "); } // Case 3a: // - terminal is now connected to a persistent route line // Case 3b: // - terminal is now connected to another terminal if (pair.right.getLine() != null && pair.right.getLine().getData() != null) { // Case 3a Resource line = deserialize(graph, pair.right.getLine().getData()); if (DEBUG_SYNC) System.out.println("adding link between terminal " + NameUtils.getSafeLabel(graph, connector) + " and route line " + NameUtils.getSafeLabel(graph, line)); cu.connect(connector, line); comments.add("Added link between terminal " + NameUtils.getSafeLabel(graph, connector) + " and route line " + NameUtils.getSafeLabel(graph, line)); } else { // Case 3b // Connector was disconnected from route line. // This can currently only mean one thing: // there are no more route lines in the connection. // If there are still connectors in the connection, the // only possible assumption is that there are two // connectors and they need to be connected together looseConnectors.add(connector); } } } if (looseConnectors.size() == 2) { Resource[] cns = looseConnectors.toArray(Resource.NONE); if (DEBUG_SYNC) System.out.println("linking two loose terminal connectors " + NameUtils.getSafeLabel(graph, cns[0]) + " and " + NameUtils.getSafeLabel(graph, cns[1])); cu.connect(cns[0], cns[1]); comments.add("Linking two loose terminal connectors " + NameUtils.getSafeLabel(graph, cns[0]) + " and " + NameUtils.getSafeLabel(graph, cns[1])); } else if (!looseConnectors.isEmpty()) { System.err.println("BUG: there can only be 0 or 2 loose terminal connectors in any connection. Found " + looseConnectors.size() + ":"); for (Resource cn : looseConnectors) System.err.println(NameUtils.getSafeLabel(graph, cn)); System.err.flush(); } // Disconnect and remove removed terminal connectors for (RouteTerminal removedTerminal : delta.getTerminalsOnlyInLeft()) { Resource connector = deserialize(graph, removedTerminal.getData()); if (DEBUG_SYNC) System.out.println("removing route terminal: " + NameUtils.getSafeLabel(graph, connector)); cu.removeConnectionPart(connector); comments.add("Removed route terminal " + NameUtils.getSafeLabel(graph, connector)); } // Finally delete removed route lines for (RouteLine removed : delta.getLinesOnlyInLeft()) { Resource line = deserialize(graph, removed.getData()); if (DEBUG_SYNC) System.out.println("removing route line: " + NameUtils.getSafeLabel(graph, line)); cu.removeConnectionPart(line); comments.add("Removed route line " + NameUtils.getSafeLabel(graph, line)); } graph.addMetadata(comments); } } private void writeCommandMetadata(WriteGraph graph, RouteGraph left, RouteGraphDelta delta, RouteGraph right) { try { if(!delta.getTerminalsOnlyInLeft().isEmpty() || !delta.getTerminalsOnlyInRight().isEmpty()) return; // These are not changes layout organizer may do. RouteGraphModification proxy = new RouteGraphModification(graph.getService(SerialisationSupport.class), left); TObjectIntHashMap idMap = proxy.getIdMap(); TObjectIntHashMap rightIdMap = new TObjectIntHashMap(); { final TObjectIntHashMap keyToId = new TObjectIntHashMap(); idMap.forEachEntry(new TObjectIntProcedure() { @Override public boolean execute(RouteNode a, int b) { Object data = a.getData(); if(data != null) keyToId.put(data, b); return true; } }); for(RouteLine line : right.getLines()) { Object data = line.getData(); if(keyToId.containsKey(data)) rightIdMap.put(line, keyToId.get(data)); } for(RouteTerminal terminal : right.getTerminals()) { Object data = terminal.getData(); if(keyToId.containsKey(data)) rightIdMap.put(terminal, keyToId.get(data)); } } int id = idMap.size(); for(RouteLink link : delta.getLinksOnlyInLeft()) { proxy.addModi(new RouteGraphModification.RemoveLink(idMap.get(link.getA()), idMap.get(link.getB()))); } for(RouteLine line : delta.getLinesOnlyInLeft()) { proxy.addModi(new RouteGraphModification.RemoveLine(idMap.get(line))); } for(RouteLinePair pair : delta.getLinesThatDiffer()) { proxy.addModi(new RouteGraphModification.UpdateLine(idMap.get(pair.left), pair.right.getPosition(), pair.right.isHorizontal())); } for(RouteLine line : delta.getLinesOnlyInRight()) { rightIdMap.put(line, id++); proxy.addModi(new RouteGraphModification.CreateLine(line.getPosition(), line.isHorizontal())); } if(left.isSimpleConnection() && !right.isSimpleConnection()) proxy.addModi(new RouteGraphModification.RemoveLink(0, 1)); for(RouteTerminalPair pair : delta.getTerminalsThatDiffer()) if(pair.left.getLine() != pair.right.getLine()) { if(pair.left.getLine() != null) proxy.addModi(new RouteGraphModification.RemoveLink(idMap.get(pair.left), idMap.get(pair.left.getLine()))); if(pair.right.getLine() != null) proxy.addModi(new RouteGraphModification.CreateLink(idMap.get(pair.left), rightIdMap.get(pair.right.getLine()))); } if(!left.isSimpleConnection() && right.isSimpleConnection() && right.getTerminals().size() == 2) { Iterator it = right.getTerminals().iterator(); proxy.addModi(new RouteGraphModification.CreateLink( rightIdMap.get(it.next()), rightIdMap.get(it.next()))); } for(RouteLink link : delta.getLinksOnlyInRight()) { proxy.addModi(new RouteGraphModification.CreateLink(rightIdMap.get(link.getA()), rightIdMap.get(link.getB()))); } Resource model = proxy.findTerminalIdentifiers(graph); StringBuilder b = new StringBuilder(); b.append("modifyConnection \""); proxy.toString(b); b.append("\""); if (model != null) CommandMetadata.add(graph, model.getResourceId(), b.toString()); } catch(DatabaseException e) { // Failure in command writing must not cancel the write transaction e.printStackTrace(); } catch(NullPointerException e) { // Failure in command writing must not cancel the write transaction e.printStackTrace(); } } public Resource writeLine(WriteGraph graph, Resource line, RouteLine routeLine) throws DatabaseException { if (line == null) { line = graph.newResource(); graph.claim(line, L0.InstanceOf, null, DIA.RouteLine); graph.claim(connection, DIA.HasInteriorRouteNode, line); } graph.claimLiteral(line, DIA.IsHorizontal, routeLine.isHorizontal()); graph.claimLiteral(line, DIA.HasPosition, routeLine.getPosition()); routeLine.setData( serialize(graph, line) ); if (DEBUG_SYNC) { System.out.print("wrote route line: "); routeLine.print(System.out); } return line; } public static Object serialize(ServiceLocator locator, Resource r) throws DatabaseException { SerialisationSupport ss = locator.getService(SerialisationSupport.class); return ss.getRandomAccessId(r); } public static Resource deserialize(ServiceLocator locator, Object o) throws DatabaseException { Resource r = tryDeserialize(locator, o); if (r != null) return r; throw new IllegalArgumentException("unrecognized object: " + o); } public static Resource tryDeserialize(ServiceLocator locator, Object o) throws DatabaseException { if (o instanceof Long) return locator.getService(SerialisationSupport.class).getResource((Long) o); return null; } }