*******************************************************************************/
package org.simantics.g3d.vtk.common;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
+import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.progress.IProgressService;
import org.simantics.db.ReadGraph;
-import org.simantics.db.Resource;
import org.simantics.db.Session;
+import org.simantics.db.UndoContext;
import org.simantics.db.WriteGraph;
-import org.simantics.db.common.request.ReadRequest;
+import org.simantics.db.common.request.UniqueRead;
import org.simantics.db.common.request.WriteRequest;
import org.simantics.db.exception.DatabaseException;
+import org.simantics.db.layer0.util.Layer0Utils;
+import org.simantics.db.procedure.SyncProcedure;
+import org.simantics.db.service.UndoRedoSupport;
import org.simantics.g3d.ontology.G3D;
-import org.simantics.g3d.scenegraph.G3DNode;
-import org.simantics.g3d.scenegraph.IG3DNode;
import org.simantics.g3d.scenegraph.RenderListener;
import org.simantics.g3d.scenegraph.base.INode;
import org.simantics.g3d.scenegraph.base.NodeListener;
import org.simantics.g3d.scenegraph.base.ParentNode;
+import org.simantics.objmap.exceptions.MappingException;
import org.simantics.objmap.graph.IMapping;
import org.simantics.objmap.graph.IMappingListener;
-import org.simantics.utils.datastructures.Callback;
import org.simantics.utils.datastructures.MapList;
import org.simantics.utils.datastructures.MapSet;
import org.simantics.utils.datastructures.Pair;
import org.simantics.utils.ui.ExceptionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import vtk.vtkProp;
-public abstract class AbstractVTKNodeMap<E extends IG3DNode> implements VTKNodeMap<E>, IMappingListener, RenderListener, NodeListener{
+public abstract class AbstractVTKNodeMap<DBObject,E extends INode> implements VTKNodeMap<DBObject,E>, IMappingListener, RenderListener, NodeListener, UndoRedoSupport.ChangeListener{
- private static final boolean DEBUG = false;
+ private static final boolean DEBUG = true;
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AbstractVTKNodeMap.class);
protected Session session;
- protected IMapping<Object,E> mapping;
- protected InteractiveVtkPanel panel;
+ protected IMapping<DBObject, INode> mapping;
+ protected VtkView view;
- protected MapList<E, vtkProp> nodeToActor = new MapList<E, vtkProp>();
- protected Map<vtkProp,E> actorToNode = new HashMap<vtkProp, E>();
+ private MapList<E, vtkProp> nodeToActor = new MapList<E, vtkProp>();
+ private Map<vtkProp,E> actorToNode = new HashMap<vtkProp, E>();
protected ParentNode<E> rootNode;
- public AbstractVTKNodeMap(Session session, IMapping<Object,E> mapping, InteractiveVtkPanel panel, ParentNode<E> rootNode) {
+ protected UndoRedoSupport undoRedoSupport;
+ protected int undoOpCount = 0;
+ protected int redoOpCount = 0;
+ protected boolean runUndo = false;
+ protected boolean runRedo = false;
+ public AbstractVTKNodeMap(Session session, IMapping<DBObject,INode> mapping, VtkView view, ParentNode<E> rootNode) {
this.session = session;
this.mapping = mapping;
- this.panel = panel;
+ this.view = view;
this.rootNode = rootNode;
- panel.addListener(this);
+ view.addListener(this);
mapping.addMappingListener(this);
rootNode.addListener(this);
+
+ undoRedoSupport = session.getService(UndoRedoSupport.class);
+ undoRedoSupport.subscribe(this);
+ try {
+ UndoContext undoContext = undoRedoSupport.getUndoContext(session);
+ undoOpCount = undoContext.getAll().size();
+ redoOpCount = undoContext.getRedoList().size();
+ } catch(DatabaseException e) {
+ e.printStackTrace();
+ }
}
-
-
+
protected abstract void addActor(E node);
protected abstract void removeActor(E node);
protected abstract void updateActor(E node,Set<String> ids);
public void repaint() {
- panel.repaint();
+ view.refresh();
}
public void populate() {
@SuppressWarnings("unchecked")
@Override
- public Collection<vtkProp> getRenderObjects(IG3DNode node) {
- return nodeToActor.getValues((E)node);
+ public Collection<vtkProp> getRenderObjects(INode node) {
+ return nodeToActor.getValues((E) node);
+ }
+
+ protected <T extends vtkProp> void map(E node, Collection<T> props) {
+ for (vtkProp p : props) {
+ nodeToActor.add(node, p);
+ actorToNode.put(p, node);
+ }
+ }
+
+ protected void removeMap(E node) {
+ Collection<vtkProp> coll = nodeToActor.getValuesUnsafe(node);
+ if (coll.size() > 0) {
+ view.lock();
+ for (vtkProp p : coll) {
+ actorToNode.remove(p);
+ if (p.GetVTKId() != 0) {
+ view.getRenderer().RemoveActor(p);
+ p.Delete();
+ }
+ }
+ view.unlock();
+ }
+ nodeToActor.remove(node);
}
- @SuppressWarnings("unchecked")
@Override
public ParentNode<E> getRootNode() {
return (ParentNode<E>)rootNode;
private List<Pair<E,String>> added = new ArrayList<Pair<E,String>>();
private List<Pair<E,String>> removed = new ArrayList<Pair<E,String>>();
- //private List<Pair<E,String>> updated = new ArrayList<Pair<E,String>>();
private MapSet<E, String> updated = new MapSet.Hash<E, String>();
private boolean rangeModified = false;
- @SuppressWarnings("unchecked")
+ public boolean isRangeModified() {
+ return rangeModified;
+ }
+
@Override
- public void updateRenderObjectsFor(IG3DNode node) {
+ public void onChanged() {
+ try {
+ UndoContext undoContext = undoRedoSupport.getUndoContext(session);
+ int ucount = undoContext.getAll().size();
+ int rcount = undoContext.getRedoList().size();
+ if (DEBUG) System.out.println("Previous U:" + undoOpCount +" R:" + redoOpCount +" Current U:"+ucount+" R:"+rcount);
+ if (ucount < undoOpCount) {
+ runUndo = true;
+ } else {
+ runUndo = false;
+ }
+ if (!runUndo && rcount > 0)
+ runRedo = true;
+ else
+ runRedo = false;
+ undoOpCount = ucount;
+ redoOpCount = rcount;
+
+ if (DEBUG) System.out.println("Undo " + runUndo + " Redo " + runRedo);
+ } catch (DatabaseException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+
+ }
+
+ @Override
+ public void updateRenderObjectsFor(E node) {
List<vtkProp> toDelete = new ArrayList<vtkProp>();
- for (vtkProp prop : nodeToActor.getValues((E)node)) {
+ view.lock();
+ for (vtkProp prop : nodeToActor.getValues(node)) {
if (prop.GetVTKId() != 0) {
- panel.GetRenderer().RemoveActor(prop);
+ view.getRenderer().RemoveActor(prop);
//prop.Delete();
toDelete.add(prop);
}
actorToNode.remove(prop);
}
- nodeToActor.remove((E)node);
- Collection<vtkProp> coll = getActors((E)node);
- if (coll == null)
- return;
- for (vtkProp prop : coll) {
- nodeToActor.add((E)node,prop);
- actorToNode.put(prop, (E)node);
- toDelete.remove(prop);
+ view.unlock();
+ nodeToActor.remove(node);
+ Collection<vtkProp> coll = getActors(node);
+ if (coll != null) {
+ for (vtkProp prop : coll) {
+ nodeToActor.add(node,prop);
+ actorToNode.put(prop, node);
+ toDelete.remove(prop);
+ }
}
for (vtkProp p : toDelete)
p.Delete();
@SuppressWarnings("unchecked")
private void receiveAdd(E node, String id, boolean db) {
- if (DEBUG) System.out.println("receiveAdd " + node + " " + id + " " + db);
+ if (DEBUG) System.out.println("receiveAdd " + debugString(node) + " " + id + " " + db);
synchronized (syncMutex) {
for (Pair<E, String> n : added) {
if (n.first.equals(node))
}
if (changeTracking) {
mapping.rangeModified((E)node.getParent());
+ mapping.rangeModified((E)node);
}
added.add(new Pair<E, String>(node, id));
rangeModified = true;
}
- panel.repaint();
+ repaint();
}
@SuppressWarnings("unchecked")
private void receiveRemove(E node, String id, boolean db) {
- if (DEBUG) System.out.println("receiveRemove " + node + " " + id + " " + db);
+ if (DEBUG) System.out.println("receiveRemove " + debugString(node) + " " + id + " " + db);
synchronized (syncMutex) {
for (Pair<E, String> n : removed) {
if (n.first.equals(node))
return;
}
- if (changeTracking && !db)
+ if (changeTracking && !db) {
+ mapping.rangeModified((E)node);
mapping.rangeModified((E)node.getParent());
+ }
removed.add(new Pair<E, String>(node, id));
rangeModified = true;
}
- panel.repaint();
+ repaint();
}
- @SuppressWarnings("unchecked")
private void receiveUpdate(E node, String id, boolean db) {
- if (DEBUG) System.out.println("receiveUpdate " + node + " " + id + " " + db);
+ if (DEBUG) System.out.println("receiveUpdate " + debugString(node) + " " + id + " " + db);
synchronized (syncMutex) {
-// for (Pair<E, String> n : updated) {
-// if (n.first.equals(node))
-// return;
-// }
+// for (Pair<E, String> n : updated) {
+// if (n.first.equals(node))
+// return;
+// }
if (changeTracking && !db)
mapping.rangeModified(node);
//updated.add(new Pair<E, String>(node, id));
updated.add(node, id);
rangeModified = true;
}
- panel.repaint();
+ repaint();
}
private boolean graphUpdates = false;
private Set<E> graphModified = new HashSet<E>();
private boolean requestCommit = false;
+ private String commitMessage = null;
@Override
- public void commit() {
+ public void commit(String message) {
requestCommit = true;
+ commitMessage = message;
}
protected void doCommit() {
- session.asyncRequest(new WriteRequest() {
-
- @Override
- public void perform(WriteGraph graph) throws DatabaseException {
- commit(graph);
- }
-
- }, new Callback<DatabaseException>() {
-
- @Override
- public void run(DatabaseException parameter) {
- if (parameter != null)
- ExceptionUtils.logAndShowError("Cannot commit editor changes", parameter);
- }
- });
+ IProgressService service = PlatformUI.getWorkbench().getProgressService();
+ try {
+ service.busyCursorWhile(monitor -> {
+ try {
+ session.syncRequest(new WriteRequest() {
+ @Override
+ public void perform(WriteGraph graph) throws DatabaseException {
+ if (DEBUG) System.out.println("Commit " + commitMessage);
+ if (commitMessage != null) {
+ Layer0Utils.addCommentMetadata(graph, commitMessage);
+ graph.markUndoPoint();
+ commitMessage = null;
+ }
+ commit(graph);
+ }
+ });
+ } catch (DatabaseException e) {
+ ExceptionUtils.logAndShowError("Cannot commit editor changes", e);
+ }
+ });
+ } catch (InvocationTargetException | InterruptedException e) {
+ LOGGER.error("Unexpected exception", e);
+ }
}
protected void commit(WriteGraph graph) throws DatabaseException {
graphUpdates = true;
mapping.updateDomain(graph);
graphUpdates = false;
+ clearDeletes();
+ if (DEBUG) System.out.println("Commit done");
}
}
+
+
@Override
public void domainModified() {
if (graphUpdates)
return;
if (DEBUG)System.out.println("domainModified");
- session.asyncRequest(new ReadRequest() {
-
- @SuppressWarnings("unchecked")
+ session.asyncRequest(new UniqueRead<Object>() {
@Override
- public void run(ReadGraph graph) throws DatabaseException {
+ public Object perform(ReadGraph graph) throws DatabaseException {
+ return new Object();
+ }
+ }, new SyncProcedure<Object>() {
+ @Override
+ public void execute(ReadGraph graph, Object result) throws DatabaseException {
+ // Perform all updates to the model in a single query thread
update(graph);
+ };
+
+ @Override
+ public void exception(ReadGraph graph, Throwable throwable) throws DatabaseException {
+ LOGGER.error("Failed to update pipeline changes" + throwable);
}
});
+ }
+
+ protected void reset(ReadGraph graph) throws MappingException {
+ if (DEBUG) System.out.println("Reset");
+ graphUpdates = true;
+ mapping.getRangeModified().clear();
+ for (DBObject o : mapping.getDomain())
+ mapping.domainModified(o);
+ mapping.updateRange(graph);
+ graphModified.clear();
+ graphUpdates = false;
}
+ private boolean useFullSyncWithUndo = false;
+
protected void update(ReadGraph graph) throws DatabaseException {
synchronized (syncMutex) {
- graphUpdates = true;
- for (Object domainObject : mapping.getDomainModified()) {
- E rangeObject = mapping.get(domainObject);
- if (rangeObject != null)
- graphModified.add(rangeObject);
- }
- mapping.updateRange(graph);
- graphModified.clear();
- graphUpdates = false;
+ if (DEBUG) System.out.println("Graph update start");
+
+ if (runUndo && useFullSyncWithUndo) {
+ reset(graph);
+ } else {
+ graphUpdates = true;
+ for (DBObject domainObject : mapping.getDomainModified()) {
+ @SuppressWarnings("unchecked")
+ E rangeObject = (E) mapping.get(domainObject);
+ if (rangeObject != null)
+ graphModified.add(rangeObject);
+ }
+ mapping.updateRange(graph);
+ graphModified.clear();
+ syncDeletes();
+ clearDeletes();
+ graphUpdates = false;
+ }
+
+ if (mapping.isRangeModified() && !runUndo && !runRedo)
+ commit((String)null);
+
+ if (DEBUG) System.out.println("Graph update done");
}
-
- if (mapping.isRangeModified())
- commit();
}
@Override
}
}
- List<Pair<E, String>> rem = new ArrayList<Pair<E,String>>();
- List<Pair<E, String>> add = new ArrayList<Pair<E,String>>();
- MapSet<E, String> mod = new MapSet.Hash<E, String>();
- Set<E> propagation = new HashSet<E>();
- Stack<E> stack = new Stack<E>();
-
+ // Reusable containers for data synchronisation
+ List<Pair<E, String>> rem = new ArrayList<Pair<E,String>>(); // Removed objects
+ List<Pair<E, String>> add = new ArrayList<Pair<E,String>>(); // Added objects
+ MapSet<E, String> mod = new MapSet.Hash<E, String>(); // Modified objects
+ Set<E> propagation = new HashSet<E>(); // Objects with propagated changes
+ Stack<E> stack = new Stack<E>(); // Stack for handling propagation
+ Set<E> delete = Collections.synchronizedSet(new HashSet<E>()); // Objects to be completely deleted
+ Set<E> deleteUC = new HashSet<E>();
@Override
public synchronized void preRender() {
updateCycle();
}
+
+ /**
+ * When objects are removed (either from Java or Graph), after remove processing the Java objects remain in mapping cache.
+ * This causes problems with Undo and Redo, which cause re-using the removed objects from mapping cache.
+ *
+ * This code here synchronizes removed and added objects to collect deletable objects. (a deletable object is one which is removed but not added).
+ *
+ */
+ @SuppressWarnings("unused")
+ protected void syncDeletes() {
+ deleteUC.clear();
+ for (Pair<E, String> n : removed) {
+ deleteUC.add(n.first);
+ }
+ for (Pair<E, String> n : added) {
+ deleteUC.remove(n.first);
+ }
+ if (DEBUG && deleteUC.size() > 0) {
+ System.out.println("Delete sync");
+ for (E n : delete) {
+ System.out.println(debugString(n));
+ }
+ }
+ delete.addAll(deleteUC);
+ deleteUC.clear();
+ }
+
+ /**
+ * Clears deletable objects from mapping cache.
+ */
+ @SuppressWarnings("unused")
+ protected void clearDeletes() {
+ if (DEBUG && delete.size() > 0) System.out.println("Delete");
+ for (E n : delete) {
+ if (DEBUG) System.out.println(debugString(n));
+ mapping.getRange().remove(n);
+ stopListening(n);
+ }
+ delete.clear();
+ }
+
+ protected String debugString(E n) {
+ return n + "@" + Integer.toHexString(n.hashCode());
+ }
+
+ protected boolean filterChange(List<Pair<E,String>> list,E n) {
+ for (int i = list.size()-1; i >= 0; i--) {
+ if (list.get(i).first == n) {
+ list.remove(i);
+ return true;
+ }
+ }
+ return false;
+ }
+
@SuppressWarnings("unchecked")
protected void updateCycle() {
rem.clear();
mod.clear();
propagation.clear();
+
synchronized (syncMutex) {
+ // Check for overlapping additions and deletions, prevent deleting objects that are also added and vice versa.
+ Deque<E> stack = new ArrayDeque<E>();
+ for (Pair<E, String> n : added) {
+ stack.add(n.first);
+ }
+ while (!stack.isEmpty()) {
+ E n = stack.pop();
+ boolean conflict = filterChange(removed, n);
+ if (conflict) {
+ if (DEBUG) System.out.println("Prevent removing " + n);
+ //filterChange(added, n)
+ if (filterChange(added, n))
+ if (DEBUG) System.out.println("Prevent adding " + n);
+ }
+ if (n instanceof ParentNode) {
+ ParentNode<INode> pn = (ParentNode<INode>)n;
+ for (INode cn : pn.getNodes()) {
+ stack.push((E)cn);
+ }
+ }
+ }
+ // Do not process updates for removed nodes.
+ for (Pair<E, String> r : removed) {
+ updated.removeValues(r.first);
+ }
rem.addAll(removed);
add.addAll(added);
for (E e : updated.getKeys()) {
mod.add(e, s);
}
}
-
+ syncDeletes();
removed.clear();
added.clear();
updated.clear();
}
+
for (Pair<E, String> n : rem) {
stopListening(n.first);
removeActor(n.first);
-
+ n.first.remove();
}
- for (Pair<E, String> n : add) {
- addActor(n.first);
- listen(n.first);
- }
+ for (Pair<E, String> n : add) {
+ addActor(n.first);
+ listen(n.first);
+ }
for (E e : mod.getKeys()) {
Set<String> ids = mod.getValues(e);
}
}
-// synchronized (syncMutex) {
-// rem.addAll(removed);
-// add.addAll(added);
-// //mod.addAll(updated);
-// for (E e : updated.getKeys()) {
-// for (String s : updated.getValues(e))
-// mod.add(e, s);
-// }
-//
-// removed.clear();
-// added.clear();
-// updated.clear();
-// }
-
for (E e : mod.getKeys()) {
Set<String> ids = mod.getValues(e);
updateActor(e,ids);
}
-
-
+
for (Pair<E, String> n : rem) {
for (NodeListener l : nodeListeners)
l.nodeRemoved(null, n.first, n.second);
for (NodeListener l : nodeListeners)
l.nodeAdded(n.first.getParent(), n.first, n.second);
}
-// for (Pair<E, String> n : mod) {
-// for (NodeListener l : nodeListeners)
-// l.propertyChanged(n.first, n.second);
-// }
+// for (Pair<E, String> n : mod) {
+// for (NodeListener l : nodeListeners)
+// l.propertyChanged(n.first, n.second);
+// }
for (E e : mod.getKeys()) {
for (NodeListener l : nodeListeners)
for (String s : mod.getValues(e))
l.propertyChanged(e, s);
}
+
synchronized (syncMutex) {
if (added.isEmpty() && removed.isEmpty() && updated.getKeys().size() == 0)
rangeModified = false;
if (DEBUG) System.out.println("Node added " + child + " parent " + node);
//receiveAdd((E)child, rel ,graphUpdates);
receiveAdd((E)child, rel ,graphModified.contains(node));
-
}
@SuppressWarnings("unchecked")
//receiveRemove((E)child, rel, graphUpdates);
receiveRemove((E)child, rel, graphModified.contains(node));
- //FIXME : sometimes removed structural models cause ObjMap to add their children again.
- // removing the listener here prevents corruption of visual model, but better fix is needed.
- stopListening(child);
+ //FIXME : 1. sometimes removed structural models cause ObjMap to add their children again.
+ // removing the listener here prevents corruption of visual model, but better fix is needed.
+ // 2. detach causes nodeRemoved event, which then causes other critical events to be missed. Took out call:
+ //stopListening(child);
}
@Override
public void delete() {
+ if (undoRedoSupport != null)
+ undoRedoSupport.cancel(this);
+
changeTracking = false;
- panel.removeListener(this);
+ view.removeListener(this);
mapping.removeMappingListener(this);
List<E> nodes = new ArrayList<E>(nodeToActor.getKeySize());
}
- public IMapping<Object,E> getMapping() {
+ public IMapping<DBObject,INode> getMapping() {
return mapping;
}