view java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/DiagramModel.java @ 8:ea7885bd9bff tip

fixed bug : render solid line as dotted/dashed when moving the stylus from dotted/dashed to solid
author ccmi-guest
date Thu, 03 Jul 2014 16:12:20 +0100
parents d66dd5880081
children
line wrap: on
line source
/*  
 CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool

 Copyright (C) 2011  Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/)

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package uk.ac.qmul.eecs.ccmi.diagrammodel;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;

import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeNode;

import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers;
import uk.ac.qmul.eecs.ccmi.utils.InteractionLog;

/**
 * This class represent a model as per in the model-view control architecture. 
 * The model is "double sided" in the sense that it can be accessed through either 
 * a CollectionModel or a TreeModel returned by the respective getter methods.
 * The TreeModel is suitable for JTree classes of the swing library, while 
 * the CollectionModel can be used by view classes by registering a CollectionListener 
 * to the CollectionModel itself.    
 * It is important to notice that changes made on one side will reflect on the other, 
 * eventually triggering the registered listeners.
 * The tree model is structured according to a special layout which is suitable for 
 * browsing the tree view via audio interface ( text to speech synthesis and sound).
 *
 * @param <N> a subclass of DiagramNode 
 * @param <E> a subclass of DiagramEdge
 */
public class DiagramModel<N extends DiagramNode, E extends DiagramEdge>{
	/**
	 * Create a model instance starting from some nodes and edges prototypes. 
	 * All subsequently added element must be clones of such prototypes.
	 * @param nodePrototypes an array of {@code DiagramNode} prototypes, from which 
	 * nodes that will be inserted in this model will be cloned
	 * @param edgePrototypes an array of {@code DiagramEdge} prototypes, from which 
	 * edges that will be inserted in this model will be cloned
	 */
	@SuppressWarnings("serial")
	public DiagramModel(N [] nodePrototypes, E [] edgePrototypes) {
		root = new DiagramTreeNode(ROOT_LABEL){
			@Override
			public boolean isRoot(){
				return true;
			}
		};
		modified = false;

		nodeCounter = 0;
		edgeCounter = 0;

		notifier = new ReentrantLockNotifier();

		treeModel = new InnerTreeModel(root);
		treeModel.setEventSource(treeModel);/* default event source is the tree itself */
		diagramCollection = new InnerDiagramCollection();

		nodes = new ArrayList<N>(INITIAL_NODES_SIZE);
		edges = new ArrayList<E>(INITIAL_EDGES_SIZE);
		elements = new ArrayList<DiagramElement>(INITIAL_NODES_SIZE+INITIAL_EDGES_SIZE);

		changeListeners = new LinkedList<ChangeListener>();

		for(N n : nodePrototypes)
			addType(n);
		for(E e : edgePrototypes){
			addType(e);
		}
	}

	/**
	 * Returns a CollectionModel for this diagram
	 * 
	 * @return a CollectionModel for this diagram 
	 */
	public CollectionModel<N,E> getDiagramCollection(){
		return diagramCollection;
	}

	/**
	 * Returns a TreeModel for this diagram
	 * 
	 * @return a TreeModel for this diagram 
	 */
	public TreeModel<N,E> getTreeModel(){
		return treeModel;
	}	

	private void handleChangeListeners(Object source){
		if(modified) // fire the listener only the first time a change happens
			return;
		modified = true;
		fireChangeListeners(source);
	}

	private void addChangeListener(ChangeListener l){
		changeListeners.add(l);
	}

	private void removeChangeListener(ChangeListener l){
		changeListeners.remove(l);
	}

	protected void fireChangeListeners(Object source){
		ChangeEvent changeEvent = new ChangeEvent(source);
		for(ChangeListener l : changeListeners)
			l.stateChanged(changeEvent);
	}

	private void addType(DiagramElement element){
		DiagramTreeNode typeNode = _lookForChild(root, element.getType());
		if(typeNode == null){
			typeNode = new TypeMutableTreeNode(element);
			treeModel.insertNodeInto(typeNode, root, root.getChildCount());
		}
	}

	private class InnerDiagramCollection implements CollectionModel<N,E> {

		public InnerDiagramCollection(){
			listeners = new ArrayList<CollectionListener>();
		}

		@Override
		public boolean insert(N n, Object source){
			if(source == null)
				source = this;
			return _insert(n,source);
		}

		@Override
		public boolean insert(E e, Object source){
			if(source == null)
				source = this;
			return _insert(e,source);
		}

		@Override
		public boolean takeOut(DiagramElement element, Object source){
			if(source == null)
				source = this;
			if(element instanceof DiagramNode)
				return _takeOut((DiagramNode)element,source);
			if(element instanceof DiagramEdge)
				return _takeOut((DiagramEdge)element,source);
			return false;
		}

		@Override
		public void addCollectionListener(CollectionListener listener) {
			listeners.add(listener);
		}

		@Override
		public void removeCollectionListener(CollectionListener listener) {
			listeners.remove(listener);
		}

		protected void fireElementInserted(Object source, DiagramElement element) {
			for(CollectionListener l : listeners){
				l.elementInserted(new CollectionEvent(source,element));
			}
		}

		protected void fireElementTakenOut(Object source, DiagramElement element) {
			for(CollectionListener l : listeners){
				l.elementTakenOut(new CollectionEvent(source,element));
			}
		}

		protected void fireElementChanged(ElementChangedEvent evt){
			for(CollectionListener l : listeners){
				l.elementChanged(evt);
			}
		}

		@Override
		public Collection<N> getNodes() {
			return Collections.unmodifiableCollection(nodes);
		}

		@Override
		public Collection<E> getEdges() {
			return Collections.unmodifiableCollection(edges);
		}

		@Override
		public Collection<DiagramElement> getElements(){
			return Collections.unmodifiableCollection(elements);
		}

		@Override
		public ReentrantLock getMonitor(){
			return notifier;
		}

		@Override
		public void addChangeListener(ChangeListener l){
			DiagramModel.this.addChangeListener(l);
		}

		@Override
		public void removeChangeListener(ChangeListener l){
			DiagramModel.this.removeChangeListener(l);
		}

		/* sort the collections according to the id of nodes */
		public void sort(){
			Collections.sort(nodes, DiagramElementComparator.getInstance());
			Collections.sort(edges, DiagramElementComparator.getInstance());
		}

		public boolean isModified(){
			return modified;
		}

		public void setUnmodified(){
			modified = false;
		}

		protected ArrayList<CollectionListener> listeners;

	}

	@SuppressWarnings("serial")
	private class InnerTreeModel extends DefaultTreeModel implements TreeModel<N,E>{

		public InnerTreeModel(TreeNode root){
			super(root);
			bookmarks = new LinkedHashMap<String, DiagramTreeNode>();
			diagramTreeNodeListeners = new ArrayList<DiagramTreeNodeListener>();
		}

		@Override
		public boolean insertTreeNode(N treeNode, Object source){
			if(source == null)
				source = this;
			return _insert(treeNode,source);
		}

		@Override
		public boolean insertTreeNode(E treeNode, Object source){
			if(source == null)
				source = this;
			return _insert(treeNode,source);
		}

		@Override
		public boolean takeTreeNodeOut(DiagramElement treeNode, Object source){
			if(source == null)
				source = this;
			boolean result;
			if(treeNode instanceof DiagramEdge){
				result = _takeOut((DiagramEdge)treeNode,source);
			}
			else{
				result = _takeOut((DiagramNode)treeNode,source);
			}
			/* remove the bookmarks associated with the just deleted diagram element, if any */
			for(String key : treeNode.getBookmarkKeys())
				bookmarks.remove(key);
			return result;
		}

		@Override
		public DiagramTreeNode putBookmark(String bookmark, DiagramTreeNode treeNode, Object source){
			if(bookmark == null)
				throw new IllegalArgumentException("bookmark cannot be null");
			if(source == null)
				source = this;
			setEventSource(source);
			treeNode.addBookmarkKey(bookmark);
			DiagramTreeNode result =  bookmarks.put(bookmark, treeNode);
			nodeChanged(treeNode);
			iLog("bookmark added",bookmark);
			DiagramTreeNodeEvent evt = new DiagramTreeNodeEvent(treeNode,bookmark,source);
			for(DiagramTreeNodeListener l : diagramTreeNodeListeners){
				l.bookmarkAdded(evt);
			}
			handleChangeListeners(this);
			return result;
		}

		@Override
		public DiagramTreeNode getBookmarkedTreeNode(String bookmark) {
			return bookmarks.get(bookmark);
		}

		@Override
		public DiagramTreeNode removeBookmark(String bookmark,Object source) {
			if(source == null)
				source = this;
			setEventSource(source);
			DiagramTreeNode treeNode = bookmarks.remove(bookmark);
			treeNode.removeBookmarkKey(bookmark);
			nodeChanged(treeNode);
			iLog("bookmark removed",bookmark);
			DiagramTreeNodeEvent evt = new DiagramTreeNodeEvent(treeNode,bookmark,source);
			for(DiagramTreeNodeListener l : diagramTreeNodeListeners){
				l.bookmarkRemoved(evt);
			}
			handleChangeListeners(this);
			return treeNode;
		}

		@Override
		public Set<String> getBookmarks(){
			return new LinkedHashSet<String>(bookmarks.keySet());
		}

		@Override
		public void setNotes(DiagramTreeNode treeNode, String notes,Object source){
			if(source == null)
				source = this;
			setEventSource(source);
			String oldValue = treeNode.getNotes();
			treeNode.setNotes(notes,source);
			nodeChanged(treeNode);
			iLog("notes set for "+treeNode.getName(),"".equals(notes) ? "empty notes" : notes.replaceAll("\n", "\\\\n"));
			DiagramTreeNodeEvent evt = new DiagramTreeNodeEvent(treeNode,oldValue,source);
			for(DiagramTreeNodeListener l : diagramTreeNodeListeners){
				l.notesChanged(evt);
			}
			handleChangeListeners(source);
		}

		private void setEventSource(Object source){
			this.src = source;
		}

		@Override
		public ReentrantLock getMonitor(){
			return notifier;
		}

		@Override
		public void addDiagramTreeNodeListener(DiagramTreeNodeListener l){
			diagramTreeNodeListeners.add(l);
		}

		@Override
		public void removeDiagramTreeNodeListener(DiagramTreeNodeListener l){
			diagramTreeNodeListeners.remove(l);
		}

		/* redefine the fire methods so that they set the source object according */
		/* to whether the element was inserted from the graph or from the tree    */
		@Override
		protected void fireTreeNodesChanged(Object source, Object[] path,
				int[] childIndices, Object[] children) {
			super.fireTreeNodesChanged(src, path, childIndices, children);
		}

		@Override
		protected void fireTreeNodesInserted(Object source, Object[] path,
				int[] childIndices, Object[] children) {
			super.fireTreeNodesInserted(src, path, childIndices, children);
		}

		@Override
		protected void fireTreeNodesRemoved(Object source, Object[] path,
				int[] childIndices, Object[] children) {
			super.fireTreeNodesRemoved(src, path, childIndices, children);
		}

		@Override
		protected void fireTreeStructureChanged(Object source, Object[] path,
				int[] childIndices, Object[] children) {
			super.fireTreeStructureChanged(src, path, childIndices, children);
		}

		public boolean isModified(){
			return modified;
		}

		public void setUnmodified(){
			modified = false;
		}

		private Object src;
		private Map<String, DiagramTreeNode> bookmarks;
		private ArrayList<DiagramTreeNodeListener> diagramTreeNodeListeners; 
	}
	
	@SuppressWarnings("serial")
	class ReentrantLockNotifier extends ReentrantLock implements ElementNotifier {
		@Override
		public void notifyChange(ElementChangedEvent evt) {
			_change(evt);
			handleChangeListeners(evt.getDiagramElement());
		}
	}

	private boolean _insert(N n, Object source) {
		assert(n != null);

		/* if id has already been given then sync the counter so that a surely new value is given to the next nodes */
		if(n.getId() == DiagramElement.NO_ID)
			n.setId(++nodeCounter);
		else if(n.getId() > nodeCounter)
			nodeCounter = n.getId();

		treeModel.setEventSource(source);
		nodes.add(n);	
		elements.add(n);
		/* add the node to outer node's (if any) inner nodes */
		if(n.getExternalNode() != null)
			n.getExternalNode().addInternalNode(n);

		/* decide where to insert the node based on whether this is an inner node or not */
		MutableTreeNode parent;
		if(n.getExternalNode() == null){
			DiagramTreeNode typeNode = _lookForChild(root, n.getType());
			if(typeNode == null)
				throw new IllegalArgumentException("Node type "+n.getType()+" not present in the model");
			parent = typeNode;
		}else{
			parent = n.getExternalNode();
		}

		/* add to the node one child per property type */
		for(String propertyType : n.getProperties().getTypes())
			n.insert(new PropertyTypeMutableTreeNode(propertyType,n), n.getChildCount());

		/* inject the notifier for managing changes internal to the edge */
		n.setNotifier(notifier);

		/* insert node into tree which fires tree listeners */
		treeModel.insertNodeInto(n, parent, parent.getChildCount());
		/* this is necessary to increment the child counter displayed between brackets */
		treeModel.nodeChanged(parent);
		diagramCollection.fireElementInserted(source,n);
		handleChangeListeners(n);

		iLog("node inserted",DiagramElement.toLogString(n));
		return true;
	}

	private boolean _takeOut(DiagramNode n, Object source) {
		treeModel.setEventSource(source);
		/* recursively remove internal nodes of this node */
		_removeInternalNodes(n,source);
		/* clear external node and clear edges attached to this node and updates other ends of such edges */
		_clearNodeReferences(n,source);
		/* remove the node from the tree (fires listeners) */
		treeModel.removeNodeFromParent(n);
		/* this is necessary to increment the child counter displayed between brackets */
		treeModel.nodeChanged(n.getParent());
		/* remove the nodes from the collection */
		nodes.remove(n);
		elements.remove(n);
		/* notify all the listeners a new node has been removed */
		diagramCollection.fireElementTakenOut(source,n);
		handleChangeListeners(n);

		if(nodes.isEmpty()){
			nodeCounter = 0;
		}else{
			long lastNodeId = nodes.get(nodes.size()-1).getId(); 
			if(n.getId() > lastNodeId)
				nodeCounter = lastNodeId;
		}
		iLog("node removed",DiagramElement.toLogString(n));
		return true;
	}

	private boolean _insert(E e, Object source)  {
		assert(e != null);
		/* executes formal controls over the edge's node, which must be specified from the outer class*/
		if(e.getNodesNum() < 2)
			throw new MalformedEdgeException("too few (" +e.getNodesNum()+ ") nodes");

		/* if id has already been given then sync the counter so that a surely new value is given to the next edges */
		if(e.getId() > edgeCounter)
			edgeCounter = e.getId();
		else
			e.setId(++edgeCounter);

		treeModel.setEventSource(source);
		edges.add(e);
		elements.add(e);

		/* updates the nodes' edge reference and the edge tree references */ 
		for(int i = e.getNodesNum()-1; i >= 0; i--){
			DiagramNode n = e.getNodeAt(i);
			assert(n != null);
			/* insert first the type of the edge, if not already present */
			DiagramTreeNode edgeType = _lookForChild(n, e.getType());
			if(edgeType == null){
				edgeType = new EdgeReferenceHolderMutableTreeNode(e.getType());
				treeModel.insertNodeInto(edgeType, n, 0);
			}

			/* insert the edge reference under its type tree node, in the  node*/
			treeModel.insertNodeInto(new EdgeReferenceMutableTreeNode(e,n), edgeType, 0);
			/* this is necessary to increment the child counter displayed between brackets */
			treeModel.nodeChanged(edgeType);

			n.addEdge(e);
			/* insert the node reference into the edge tree node */
			e.insert(new NodeReferenceMutableTreeNode(n,e), 0);
		}

		DiagramTreeNode parent = _lookForChild(root, e.getType());
		if(parent == null)
			throw new IllegalArgumentException("Edge type "+e.getType()+" not present in the model");

		/* inject the controller and notifier to manage changes internal to the edge */
		e.setNotifier(notifier);

		/* c'mon baby light my fire */
		treeModel.insertNodeInto(e, parent, parent.getChildCount());
		/* this is necessary to increment the child counter displayed between brackets */
		treeModel.nodeChanged(parent);
		diagramCollection.fireElementInserted(source,e);
		handleChangeListeners(e);

		StringBuilder builder = new StringBuilder(DiagramElement.toLogString(e));
		builder.append(" connecting:");
		for(int i=0; i<e.getNodesNum();i++)
			builder.append(DiagramElement.toLogString(e.getNodeAt(i))).append(' ');

		iLog("edge inserted",builder.toString());
		return true;
	}

	private boolean _takeOut(DiagramEdge e, Object source) {
		treeModel.setEventSource(source);
		/* update the nodes attached to this edge */
		_clearEdgeReferences(e);    		
		/* remove the edge from the collection */	
		edges.remove(e);
		elements.remove(e);
		/* remove the edge from the tree (fires tree listeners) */
		treeModel.removeNodeFromParent(e);
		/* this is necessary to increment the child counter displayed between brackets */
		treeModel.nodeChanged(e.getParent());
		/* notify listeners for collection */
		diagramCollection.fireElementTakenOut(source,e);
		handleChangeListeners(e);

		if(edges.isEmpty()){
			edgeCounter = 0;
		}else{
			long lastEdgeId = edges.get(edges.size()-1).getId(); 
			if(e.getId() > lastEdgeId)
				edgeCounter = lastEdgeId;
		}
		iLog("edge removed",DiagramElement.toLogString(e));
		return true;
	}

	private void _removeInternalNodes(DiagramNode n, Object source){
		for(int i=0; i<n.getInternalNodesNum(); i++){
			DiagramNode innerNode = n.getInternalNodeAt(i);
			_clearNodeReferences(innerNode, source);
			_removeInternalNodes(innerNode, source);
			n.removeInternalNode(innerNode);
			nodes.remove(n);
		}   	
	}

	/* removes both inner and tree node references to an edge from nodes it's attached to */
	private void _clearEdgeReferences(DiagramEdge e){
		for(int i=0; i<e.getNodesNum();i++){
			DiagramNode n = e.getNodeAt(i);
			EdgeReferenceMutableTreeNode reference;
			/* find the category tree node under which our reference is */ 

			reference = _lookForEdgeReference(n, e);
			assert(reference != null);

			treeModel.removeNodeFromParent(reference);
			DiagramTreeNode type = _lookForChild(n, e.getType());
			if(type.isLeaf())
				treeModel.removeNodeFromParent(type);   		
			n.removeEdge(e);
		}
	}

	/* removes references from node */
	private void _clearNodeReferences(DiagramNode n, Object source){
		/* remove the node itself from its external node, if any */
		if(n.getExternalNode() != null)
			n.getExternalNode().removeInternalNode(n);
		/* remove edges attached to this node from the collection */
		ArrayList<DiagramEdge> edgesToRemove = new ArrayList<DiagramEdge>(edges.size());
		for(int i=0; i<n.getEdgesNum(); i++){
			DiagramEdge e = n.getEdgeAt(i);
			if(e.getNodesNum() == 2){ // deleting a node on a two ends edge means deleting the edge itself
				edgesToRemove.add(e);
			}else{
				e.removeNode(n,source);
				DiagramTreeNode nodeTreeReference = _lookForNodeReference(e, n);
				treeModel.removeNodeFromParent(nodeTreeReference);
			}

		}
		/* remove the edges that must no longer exist as were two ended edges attached to this node */
		for(DiagramEdge e : edgesToRemove)
			_takeOut(e, source);
	}

	/* the tree structure is changed as a consequence of this method, therefore it's synchronized *
	 * so that external classes accessing the tree can get exclusive access through getMonitor()  */
	private void _change(ElementChangedEvent evt){
		synchronized(this){
			String changeType = evt.getChangeType();
			/* don't use the event source as it might collide with other threads as 
			 * changes on the collections and inner changes on the node are synch'ed thought different monitors */
			/* treeModel.setEventSource(evt.getSource());*/
			if("name".equals(changeType)){
				if(evt.getDiagramElement() instanceof DiagramNode){
					DiagramNode n = (DiagramNode)evt.getDiagramElement();
					for(int i=0; i<n.getEdgesNum(); i++){
						DiagramEdge e = n.getEdgeAt(i);
						treeModel.nodeChanged(_lookForNodeReference(e,n));
						treeModel.nodeChanged(_lookForEdgeReference(n,e));
						for(int j=0; j<e.getNodesNum(); j++){
							DiagramNode n2 = e.getNodeAt(j);
							if(n2 != n)
								treeModel.nodeChanged(_lookForEdgeReference(n2,e));
						}
					}
					iLog("node name changed",DiagramElement.toLogString(n));
				}else{
					DiagramEdge e = (DiagramEdge)evt.getDiagramElement();
					for(int i=0; i<e.getNodesNum();i++){
						DiagramNode n = e.getNodeAt(i);
						treeModel.nodeChanged(_lookForEdgeReference(n,e));
					}
					iLog("edge name changed",DiagramElement.toLogString(e));
				}
				treeModel.nodeChanged(evt.getDiagramElement());
			}else if("properties".equals(changeType)){
				DiagramNode n = (DiagramNode)evt.getDiagramElement();
				for(String type : n.getProperties().getTypes()){
					PropertyTypeMutableTreeNode typeNode = null;
					for(int i=0; i<n.getChildCount();i++){
						/* find the child treeNode corresponding to the current type */
						if(n.getChildAt(i) instanceof PropertyTypeMutableTreeNode)
							if(type.equals(((PropertyTypeMutableTreeNode)n.getChildAt(i)).getType())){
								typeNode = (PropertyTypeMutableTreeNode)n.getChildAt(i);
								break;
							}
					}

					if(typeNode == null) 
						throw new IllegalArgumentException("Inserted Node property type "+ type + " not present in the tree" );

					/* set the name and modifier string of all the children PropertyNodes */
					typeNode.setValues(n.getProperties().getValues(type), n.getProperties().getModifiers(type));
				}
				treeModel.nodeStructureChanged(evt.getDiagramElement());
				iLog("node properties changed",n.getProperties().toString());
			}else if("properties.clear".equals(changeType)){
				DiagramNode n = (DiagramNode)evt.getDiagramElement();
				List<String> empty = Collections.emptyList();
				for(int i=0; i<n.getChildCount();i++){
					/* find the child treeNode corresponding to the current type */
					if(n.getChildAt(i) instanceof PropertyTypeMutableTreeNode){
						((PropertyTypeMutableTreeNode)n.getChildAt(i)).setValues(empty, null);
					}
				}
				treeModel.nodeStructureChanged(evt.getDiagramElement());
			}else if("property.add".equals(changeType)){
				DiagramNode n = (DiagramNode)evt.getDiagramElement();
				ElementChangedEvent.PropertyChangeArgs args = (ElementChangedEvent.PropertyChangeArgs)evt.getArguments();
				PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)_lookForChild(n,args.getPropertyType());
				PropertyMutableTreeNode propertyNode = new PropertyMutableTreeNode(n.getProperties().getValues(args.getPropertyType()).get(args.getPropertyIndex()));
				typeNode.add(propertyNode);
				treeModel.insertNodeInto(propertyNode, typeNode, args.getPropertyIndex());
				/* this is necessary to increment the child counter displayed between brackets */
				treeModel.nodeChanged(typeNode);
				iLog("property inserted",propertyNode.getName());
			}else if("property.set".equals(changeType)){
				DiagramNode n = (DiagramNode)evt.getDiagramElement();
				ElementChangedEvent.PropertyChangeArgs args = (ElementChangedEvent.PropertyChangeArgs)evt.getArguments();
				PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)_lookForChild(n,args.getPropertyType());
				((DiagramTreeNode)typeNode.getChildAt(args.getPropertyIndex()))
				.setUserObject(n.getProperties().getValues(args.getPropertyType()).get(args.getPropertyIndex()));
				treeModel.nodeChanged((typeNode.getChildAt(args.getPropertyIndex())));
				iLog("property changed",n.getProperties().getValues(args.getPropertyType()).get(args.getPropertyIndex()));
			}else if("property.remove".equals(changeType)){
				DiagramNode n = (DiagramNode)evt.getDiagramElement();
				ElementChangedEvent.PropertyChangeArgs args = (ElementChangedEvent.PropertyChangeArgs)evt.getArguments();
				PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)_lookForChild(n,args.getPropertyType());
				iLog("property removed",((DiagramTreeNode)typeNode.getChildAt(args.getPropertyIndex())).getName()); //must do it before actual removing
				treeModel.removeNodeFromParent((DiagramTreeNode)typeNode.getChildAt(args.getPropertyIndex()));
				/* remove the bookmark keys associated with this property tree node, if any */
				for(String key : treeModel.getBookmarks()){
					treeModel.bookmarks.remove(key);
				}
			}else if("property.modifiers".equals(changeType)){
				DiagramNode n = (DiagramNode)evt.getDiagramElement();
				ElementChangedEvent.PropertyChangeArgs args = (ElementChangedEvent.PropertyChangeArgs)evt.getArguments();
				PropertyTypeMutableTreeNode typeNode = (PropertyTypeMutableTreeNode)_lookForChild(n,args.getPropertyType());
				PropertyMutableTreeNode propertyNode = ((PropertyMutableTreeNode)typeNode.getChildAt(args.getPropertyIndex()));
				StringBuilder builder = new StringBuilder();
				Modifiers modifiers = n.getProperties().getModifiers(args.getPropertyType());
				for(int index : modifiers.getIndexes(args.getPropertyIndex()))
					builder.append(modifiers.getTypes().get(index)).append(' ');
				propertyNode.setModifierString(builder.toString());
				treeModel.nodeChanged(propertyNode);
			}else if("arrowHead".equals(changeType)||"endLabel".equals(changeType)){
				/* source is considered to be the node whose end of the edge was changed */
				DiagramNode source = (DiagramNode)evt.getArguments();
				DiagramEdge e = (DiagramEdge)evt.getDiagramElement();
				treeModel.nodeChanged(e);
				for(int i=0; i<e.getChildCount();i++){
					NodeReferenceMutableTreeNode ref = (NodeReferenceMutableTreeNode)e.getChildAt(i);
					if(ref.getNode() == source){
						treeModel.nodeChanged(ref);
						iLog(("arrowHead".equals(changeType) ? "arrow head changed" :"end label changed"),
								"edge:"+DiagramElement.toLogString(e)+" node:"+DiagramElement.toLogString(ref.getNode())+
								" value:"+ ("arrowHead".equals(changeType) ? e.getEndDescription(ref.getNode()): e.getEndLabel(ref.getNode())));
						break;
					}
				}
			}else if("notes".equals(changeType)){
				/* do nothing as the tree update is taken care in the tree itself
				 * and cannot do it here because it must work for all the diagram tree nodes */

			}
		}
		/* do nothing for other ElementChangedEvents as they only concern the diagram listeners */
		/* just forward the event to other listeners which might have been registered           */ 
		diagramCollection.fireElementChanged(evt);
	}

	private static DiagramTreeNode _lookForChild(DiagramTreeNode parentNode, String name){
		DiagramTreeNode child = null, temp;
		for(@SuppressWarnings("unchecked")
				Enumeration<DiagramTreeNode> children = parentNode.children(); children.hasMoreElements();){
			temp = children.nextElement();
			if(temp.getName().equals(name)){
				child = temp;
				break;
			}
		}
		return child;
	}

	private static NodeReferenceMutableTreeNode _lookForNodeReference(DiagramEdge parent, DiagramNode n){
		NodeReferenceMutableTreeNode child = null, temp;
		for(@SuppressWarnings("unchecked")
				Enumeration<DiagramTreeNode> children = parent.children(); children.hasMoreElements();){
			temp = (NodeReferenceMutableTreeNode)children.nextElement();
			if( ((NodeReferenceMutableTreeNode)temp).getNode().equals(n)){
				child = temp;
				break;
			}
		}
		return child;
	}

	private static EdgeReferenceMutableTreeNode _lookForEdgeReference( DiagramNode parentNode, DiagramEdge e){
		DiagramTreeNode edgeType = _lookForChild(parentNode, e.getType());
		assert(edgeType != null);
		EdgeReferenceMutableTreeNode child = null, temp;
		for(@SuppressWarnings("unchecked")
				Enumeration<DiagramTreeNode> children = edgeType.children(); children.hasMoreElements();){
			temp = (EdgeReferenceMutableTreeNode)children.nextElement();
			if( ((EdgeReferenceMutableTreeNode)temp).getEdge().equals(e)){
				child = temp;
				break;
			}
		}
		return child;
	}

	private void iLog(String action,String args){
		InteractionLog.log("MODEL",action,args);
	}

	private DiagramTreeNode root;
	private InnerDiagramCollection diagramCollection;
	private ArrayList<N> nodes;
	private ArrayList<E> edges;
	private ArrayList<DiagramElement> elements;
	private InnerTreeModel treeModel;

	private long edgeCounter;
	private long nodeCounter;

	private ReentrantLockNotifier notifier;
	private List<ChangeListener> changeListeners;

	private boolean modified;

	private final static String ROOT_LABEL = "Diagram";
	private final static int INITIAL_EDGES_SIZE = 20;
	private final static int INITIAL_NODES_SIZE = 30;}