add view management on server
authorymh <ymh.work@gmail.com>
Wed, 04 Jun 2014 12:36:17 +0200
changeset 299 c5086f714631
parent 298 2f35c2ae7de8
child 300 9b74370bcc7f
add view management on server
.classpath
client/js/models.js
client/js/renderer/scene.js
server/pom.xml
server/src/main/java/org/iri_research/renkan/coweb/event/AbstractBaseSyncEventManager.java
server/src/main/java/org/iri_research/renkan/coweb/event/AbstractSyncEventManager.java
server/src/main/java/org/iri_research/renkan/coweb/event/ISyncEventManager.java
server/src/main/java/org/iri_research/renkan/coweb/event/RosterSyncEventManager.java
server/src/main/java/org/iri_research/renkan/coweb/event/ViewSyncEventManager.java
server/src/main/java/org/iri_research/renkan/models/Project.java
server/src/main/java/org/iri_research/renkan/models/ProjectRevision.java
server/src/main/java/org/iri_research/renkan/models/View.java
server/src/main/webapp/static/js/corenkan.js
--- a/.classpath	Sun May 25 13:45:24 2014 +0900
+++ b/.classpath	Wed Jun 04 12:36:17 2014 +0200
@@ -204,5 +204,6 @@
 	<classpathentry kind="var" path="M2_REPO/com/fasterxml/jackson/jaxrs/jackson-jaxrs-json-provider/2.2.3/jackson-jaxrs-json-provider-2.2.3.jar"/>
 	<classpathentry kind="var" path="M2_REPO/com/fasterxml/jackson/jaxrs/jackson-jaxrs-base/2.2.3/jackson-jaxrs-base-2.2.3.jar"/>
 	<classpathentry kind="var" path="M2_REPO/org/apache/commons/commons-collections4/4.0/commons-collections4-4.0.jar"/>
+	<classpathentry kind="var" path="M2_REPO/com/google/guava/guava/17.0/guava-17.0.jar"/>
 	<classpathentry kind="output" path="server/target/classes"/>
 </classpath>
--- a/client/js/models.js	Sun May 25 13:45:24 2014 +0900
+++ b/client/js/models.js	Wed Jun 04 12:36:17 2014 +0200
@@ -152,12 +152,29 @@
                 relatedModel: User
             }
         ],
+        prepare: function(options) {
+            var project = options.project;
+            this.addReference(options, "created_by", project.get("users"), options.created_by, project.current_user);
+            options.description = options.description || "";
+            if(typeof options.offset !== "undefined") {
+                var offset = {};
+                if (Array.isArray(options.offset)) {
+                  offset.x = options.offset[0];
+                  offset.y = options.offset.length > 1 ? options.offset[1] : options.offset[0];
+                }
+                else if (options.offset.x != null) {
+                  offset.x = options.offset.x;
+                  offset.y = options.offset.y;
+                }
+                options.offset = offset;
+            }
+            return options;
+        },
         toJSON: function() {
             return {
                 _id: this.get("_id"),
                 zoom_level: this.get("zoom_level"),
-                offset_x: this.get("offset_x"),
-                offset_y: this.get("offset_y"),
+                offset: this.get("offset"),
                 title: this.get("title"),
                 description: this.get("description"),
                 created_by: this.get("created_by") ? this.get("created_by").get("_id") : null
@@ -241,17 +258,7 @@
         },
         validate: function(options) {
             var _project = this;
-            _(options.users).each(function(_item) {
-                if(_item) {
-                    _item.project = _project;
-                }
-            });
-            _(options.nodes).each(function(_item) {
-                if(_item) {
-                    _item.project = _project;
-                }
-            });
-            _(options.edges).each(function(_item) {
+            _([].concat(options.users, options.nodes, options.edges, options.views)).each(function(_item) {
                 if(_item) {
                     _item.project = _project;
                 }
--- a/client/js/renderer/scene.js	Sun May 25 13:45:24 2014 +0900
+++ b/client/js/renderer/scene.js	Wed Jun 04 12:36:17 2014 +0200
@@ -233,12 +233,12 @@
         bindClick(".Rk-ZoomFit", "autoScale");
         this.$.find(".Rk-ZoomSave").click( function() {
             // Save scale and offset point
-            _this.renkan.project.addView( { zoom_level:_this.scale, offset_x:_this.offset.x, offset_y:_this.offset.y } );
+            _this.renkan.project.addView( { zoom_level:_this.scale, offset:_this.offset } );
         });
         this.$.find(".Rk-ZoomSetSaved").click( function() {
             var view = _this.renkan.project.get("views").last();
             if(view){
-                _this.setScale(view.get("zoom_level"), new paper.Point(view.get("offset_x"), view.get("offset_y")));
+                _this.setScale(view.get("zoom_level"), new paper.Point(view.get("offset")));
             }
         });
         if(this.renkan.read_only && !isNaN(parseInt(this.renkan.options.default_view))){
@@ -660,8 +660,8 @@
                 var _scale = Math.min( (paper.view.size.width - 2 * this.renkan.options.autoscale_padding) / (_maxx - _minx), (paper.view.size.height - 2 * this.renkan.options.autoscale_padding) / (_maxy - _miny));
                 this.initialScale = _scale;
                 // Override calculated scale if asked
-                if((typeof force_view !== "undefined") && parseFloat(force_view.zoom_level)>0 && parseFloat(force_view.offset_x)>0 && parseFloat(force_view.offset_y)>0){
-                    this.setScale(parseFloat(force_view.zoom_level), new paper.Point(parseFloat(force_view.offset_x), parseFloat(force_view.offset_y)));
+                if((typeof force_view !== "undefined") && parseFloat(force_view.zoom_level)>0 && parseFloat(force_view.offset.x)>0 && parseFloat(force_view.offset.y)>0){
+                    this.setScale(parseFloat(force_view.zoom_level), new paper.Point(parseFloat(force_view.offset.x), parseFloat(force_view.offset.y)));
                 }
                 else{
                     this.setScale(_scale, paper.view.center.subtract(new paper.Point([(_maxx + _minx) / 2, (_maxy + _miny) / 2]).multiply(_scale)));
--- a/server/pom.xml	Sun May 25 13:45:24 2014 +0900
+++ b/server/pom.xml	Wed Jun 04 12:36:17 2014 +0200
@@ -40,6 +40,7 @@
         <commons-codec-version>1.8</commons-codec-version>
         <bson4jackson-version>2.2.3</bson4jackson-version>
         <fasterxml-java-uuid-generator-version>3.1.3</fasterxml-java-uuid-generator-version>
+        <guava-version>17.0</guava-version>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     </properties>
 
@@ -441,6 +442,11 @@
             <artifactId>commons-collections4</artifactId>
             <version>4.0</version>
         </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>${guava-version}</version>
+        </dependency>
     </dependencies>
     <organization>
         <name>IRI</name>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/src/main/java/org/iri_research/renkan/coweb/event/AbstractBaseSyncEventManager.java	Wed Jun 04 12:36:17 2014 +0200
@@ -0,0 +1,100 @@
+package org.iri_research.renkan.coweb.event;
+
+import java.io.Serializable;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import org.iri_research.renkan.RenkanException;
+import org.iri_research.renkan.models.IRenkanModel;
+import org.iri_research.renkan.models.Project;
+import org.iri_research.renkan.models.ProjectSync;
+import org.iri_research.renkan.repositories.ProjectSyncsRepository;
+import org.iri_research.renkan.repositories.ProjectsRepository;
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class AbstractBaseSyncEventManager<T extends IRenkanModel<ID>, ID extends Serializable>
+        implements ISyncEventManager<T, ID> {
+
+    private final Logger logger = LoggerFactory
+            .getLogger(AbstractBaseSyncEventManager.class);
+
+    @Inject
+    private ProjectsRepository projectsRepository;
+
+    @Inject
+    private ProjectSyncsRepository projectSyncsRepository;
+
+    @Override
+    public ProjectsRepository getProjectsRepository() {
+        return this.projectsRepository;
+    }
+
+    @Override
+    public void dispatchEvent(String clientId, Map<String, Object> data) {
+
+        this.saveSyncEvent(data);
+
+        String eventType = (String) data.get("type");
+
+        if ("null".equalsIgnoreCase(eventType)) {
+            this.nullOperation(null, data);
+        } else if ("update".equalsIgnoreCase(eventType)) {
+            this.update(clientId, data);
+        } else if ("insert".equalsIgnoreCase(eventType)) {
+            this.insert(clientId, data);
+        } else if ("delete".equalsIgnoreCase(eventType)) {
+            this.delete(clientId, data);
+        } else {
+            logger.warn(String.format("dispatchEvent : eventType unknown %s",
+                    eventType));
+        }
+    }
+
+    private void saveSyncEvent(Map<String, Object> data) {
+
+        String project_id = null;
+        String user_id = null;
+
+        @SuppressWarnings("unchecked")
+        Map<String, Object> values = (Map<String, Object>) data.get("value");
+
+        if (values != null) {
+            project_id = (String) values.get("_project_id");
+        }
+
+        if (project_id == null || "".equals(project_id)) {
+            logger.warn("saveSyncEvent : project id is null. Can not save sync event");
+            return;
+        }
+
+        Project p = this.projectsRepository.findOne(project_id);
+
+        if (p == null) {
+            logger.warn("saveSyncEvent : project not found. Can not save sync event");
+            return;
+        }
+
+        p.setUpdated(new DateTime());
+        this.projectsRepository.save(p);
+
+        user_id = (String) values.get("_user_id");
+
+        if (user_id == null) {
+            logger.warn("saveSyncEvent : No user id");
+        }
+
+        try {
+            ProjectSync ps = this.projectSyncsRepository.getProjectSync(
+                    data.toString(), p, user_id);
+            this.projectSyncsRepository.save(ps);
+        } catch (RenkanException e) {
+            logger.warn(
+                    "saveSyncEvent : Error when getting Projectr syn object", e);
+        }
+
+    }
+
+}
--- a/server/src/main/java/org/iri_research/renkan/coweb/event/AbstractSyncEventManager.java	Sun May 25 13:45:24 2014 +0900
+++ b/server/src/main/java/org/iri_research/renkan/coweb/event/AbstractSyncEventManager.java	Wed Jun 04 12:36:17 2014 +0200
@@ -4,17 +4,10 @@
 import java.util.List;
 import java.util.Map;
 
-import javax.inject.Inject;
-
 import org.coweb.CowebException;
-import org.iri_research.renkan.RenkanException;
 import org.iri_research.renkan.models.IRenkanModel;
 import org.iri_research.renkan.models.Project;
-import org.iri_research.renkan.models.ProjectSync;
 import org.iri_research.renkan.repositories.IRenkanRepository;
-import org.iri_research.renkan.repositories.ProjectSyncsRepository;
-import org.iri_research.renkan.repositories.ProjectsRepository;
-import org.joda.time.DateTime;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -24,90 +17,15 @@
 import com.mongodb.WriteResult;
 
 public abstract class AbstractSyncEventManager<T extends IRenkanModel<ID>, ID extends Serializable>
+        extends AbstractBaseSyncEventManager<T, ID>
         implements IPersistedSyncEventManager<T, ID> {
 
     private final Logger logger = LoggerFactory
             .getLogger(AbstractSyncEventManager.class);
 
-    @Inject
-    private ProjectsRepository projectsRepository;
-
-    @Inject
-    private ProjectSyncsRepository projectSyncsRepository;
-
-    @Override
-    public ProjectsRepository getProjectsRepository() {
-        return this.projectsRepository;
-    }
-
     @Override
     public abstract IRenkanRepository<T, ID> getObjectRepository();
 
-    @Override
-    public void dispatchEvent(String clientId, Map<String, Object> data) {
-
-        this.saveSyncEvent(data);
-
-        String eventType = (String) data.get("type");
-
-        if ("null".equalsIgnoreCase(eventType)) {
-            this.nullOperation(null, data);
-        } else if ("update".equalsIgnoreCase(eventType)) {
-            this.update(clientId, data);
-        } else if ("insert".equalsIgnoreCase(eventType)) {
-            this.insert(clientId, data);
-        } else if ("delete".equalsIgnoreCase(eventType)) {
-            this.delete(clientId, data);
-        } else {
-            logger.warn(String.format("dispatchEvent : eventType unknown %s",
-                    eventType));
-        }
-    }
-
-    private void saveSyncEvent(Map<String, Object> data) {
-
-        String project_id = null;
-        String user_id = null;
-
-        @SuppressWarnings("unchecked")
-        Map<String, Object> values = (Map<String, Object>) data.get("value");
-
-        if (values != null) {
-            project_id = (String) values.get("_project_id");
-        }
-
-        if (project_id == null || "".equals(project_id)) {
-            logger.warn("saveSyncEvent : project id is null. Can not save sync event");
-            return;
-        }
-
-        Project p = this.projectsRepository.findOne(project_id);
-
-        if (p == null) {
-            logger.warn("saveSyncEvent : project not found. Can not save sync event");
-            return;
-        }
-
-        p.setUpdated(new DateTime());
-        this.projectsRepository.save(p);
-
-        user_id = (String) values.get("_user_id");
-
-        if (user_id == null) {
-            logger.warn("saveSyncEvent : No user id");
-        }
-
-        try {
-            ProjectSync ps = this.projectSyncsRepository.getProjectSync(
-                    data.toString(), p, user_id);
-            this.projectSyncsRepository.save(ps);
-        } catch (RenkanException e) {
-            logger.warn(
-                    "saveSyncEvent : Error when getting Projectr syn object", e);
-        }
-
-    }
-
     protected abstract List<T> getObjectList(Project project);
     
     protected abstract void checkUpdate(String clientId, Map<String, Object> data);
--- a/server/src/main/java/org/iri_research/renkan/coweb/event/ISyncEventManager.java	Sun May 25 13:45:24 2014 +0900
+++ b/server/src/main/java/org/iri_research/renkan/coweb/event/ISyncEventManager.java	Wed Jun 04 12:36:17 2014 +0200
@@ -3,6 +3,8 @@
 import java.io.Serializable;
 import java.util.Map;
 
+import org.iri_research.renkan.repositories.ProjectsRepository;
+
 public interface ISyncEventManager<T, ID extends Serializable> {
 
     public void dispatchEvent(String clientId, Map<String, Object> data);
@@ -15,4 +17,6 @@
 
     public void nullOperation(String clientId, Map<String, Object> data);
 
+    public ProjectsRepository getProjectsRepository();
+
 }
--- a/server/src/main/java/org/iri_research/renkan/coweb/event/RosterSyncEventManager.java	Sun May 25 13:45:24 2014 +0900
+++ b/server/src/main/java/org/iri_research/renkan/coweb/event/RosterSyncEventManager.java	Wed Jun 04 12:36:17 2014 +0200
@@ -13,8 +13,8 @@
 import org.slf4j.LoggerFactory;
 
 @Named
-public class RosterSyncEventManager implements
-        ISyncEventManager<RosterUser, String> {
+public class RosterSyncEventManager extends
+        AbstractBaseSyncEventManager<RosterUser, String> {
 
     private final Logger logger = LoggerFactory
             .getLogger(RosterSyncEventManager.class);
@@ -185,23 +185,4 @@
 
     }
 
-    @Override
-    public void dispatchEvent(String clientId, Map<String, Object> data) {
-
-        String eventType = (String) data.get("type");
-
-        if ("null".equalsIgnoreCase(eventType)) {
-            this.nullOperation(clientId, data);
-        } else if ("update".equalsIgnoreCase(eventType)) {
-            this.update(clientId, data);
-        } else if ("insert".equalsIgnoreCase(eventType)) {
-            this.insert(clientId, data);
-        } else if ("delete".equalsIgnoreCase(eventType)) {
-            this.delete(clientId, data);
-        } else {
-            logger.warn(String.format("dispatchEvent : eventType unknown %s",
-                    eventType));
-        }
-    }
-
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/src/main/java/org/iri_research/renkan/coweb/event/ViewSyncEventManager.java	Wed Jun 04 12:36:17 2014 +0200
@@ -0,0 +1,209 @@
+package org.iri_research.renkan.coweb.event;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.coweb.CowebException;
+import org.iri_research.renkan.models.Project;
+import org.iri_research.renkan.models.View;
+import org.iri_research.renkan.repositories.UsersRepository;
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.data.mongodb.core.geo.Point;
+
+import com.google.common.base.CaseFormat;
+
+@Named
+public class ViewSyncEventManager extends AbstractBaseSyncEventManager<View, String> {
+
+    private final Logger logger = LoggerFactory
+            .getLogger(ViewSyncEventManager.class);
+
+    @Inject
+    private UsersRepository usersRepository;
+
+    public UsersRepository getUsersRepository() {
+        return usersRepository;
+    }
+    
+
+    @Override
+    public void insert(String clientId, Map<String, Object> data) {
+
+        // get project
+        this.logger.debug("EdgeSyncEventManager: insert view");
+
+        @SuppressWarnings("unchecked")
+        Map<String, Object> values = (Map<String, Object>) data.get("value");
+        Project project = getProject(values);
+
+        String view_id = (String) values.get("id");
+        //check that view id is unique
+        for (View pview : project.getViews()) {
+            if(pview.getId() != null && pview.getId().equals(view_id)) {
+                throw new CowebException("view insert: view exists",
+                        String.format("view %s already exists", view_id));
+            }
+        }
+        View view = new View(view_id, null, null , null, null, null, 1.0, null); 
+
+        updateViewInstance(values, view_id, view);
+
+        int index = this.getPosition(data);
+        
+        List<View> views = project.getViews();
+        if (index > views.size()) {
+            index = views.size();
+        }
+        views.add(index, view);
+
+        this.getProjectsRepository().save(project);
+
+    }
+
+    private Project getProject(Map<String, Object> values)
+            throws CowebException {
+        String project_id = (String) values.get("_project_id");
+
+        Project project = this.getProjectsRepository().findOne(project_id);
+
+        if (null == project) {
+            throw new CowebException("View insert: project not found",
+                    String.format("Project %s not found", project_id));
+        }
+        return project;
+    }
+
+    @Override
+    public void nullOperation(String clientId, Map<String, Object> data) {
+        this.logger.debug("nullOperation: NOP");
+    }
+
+    @Override
+    public void update(String clientId, Map<String, Object> data) {
+        this.logger.debug("ViewSyncEventManager: update "
+                + this.getClass().getName());
+        
+        @SuppressWarnings("unchecked")
+        Map<String, Object> values = (Map<String, Object>) data.get("value");
+        String obj_id = (String) values.get("id");
+
+        this.logger.debug(String.format("update %s %s", this.getClass()
+                .getName(), obj_id));
+
+        Project project = getProject(values);
+
+        int position = getPosition(data);
+
+        if(position<0 || position >= project.getViews().size()) {
+            throw new CowebException("View update: bad position",
+                    String.format("View %s bad position %d", obj_id, position));
+        }
+        View targetView = project.getViews().get(position);
+        
+        boolean obj_changed = updateViewInstance(values, obj_id, targetView);
+        
+        if(obj_changed) {
+            targetView.setUpdated(new DateTime());
+            this.getProjectsRepository().save(project);
+        }
+    }
+
+    private boolean updateViewInstance(Map<String, Object> values,
+            String obj_id, View targetView) throws CowebException {
+        boolean obj_changed = false;
+        // update object
+        for (String fieldname : values.keySet()) {
+            if (!"id".equalsIgnoreCase(fieldname) && !fieldname.startsWith("_")) {
+                String upperCaseFieldname = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, fieldname);
+                switch(fieldname) {
+                case "offset":
+                    @SuppressWarnings("unchecked")
+                    HashMap<String, Double> offset = (HashMap<String, Double>) values.get("offset");
+                    Point offsetPoint = null;
+                    if(offset != null) {
+                        offsetPoint = new Point(offset.get("x")!= null?offset.get("x").doubleValue():0, offset.get("y")!=null?offset.get("y").doubleValue():0);
+                    }
+                    Point oldOffsetPoint = targetView.getOffset();
+                    if ((offsetPoint == null && oldOffsetPoint != null)
+                            || (offsetPoint != null && !offsetPoint.equals(oldOffsetPoint))) {
+                        targetView.setOffset(offsetPoint);
+                        obj_changed = true;
+                    }
+                    break;
+                case "zoom_level":
+                    Double newZoomLevelDouble = (Double)values.get("zoom_level");
+                    double newZoomLevel = newZoomLevelDouble==null?1.0:newZoomLevelDouble.doubleValue();
+                    double oldNewZoomLevel = targetView.getZoomLevel();
+                    if(newZoomLevel != oldNewZoomLevel) {
+                        targetView.setZoomLevel(newZoomLevel);
+                        obj_changed = true;
+                    }
+                default:
+                    try {
+                        Object new_value = values.get(fieldname);
+                        logger.debug(String.format("field %s : new value class : %s ", fieldname, new_value == null?"NULL":new_value.getClass().toString()));
+                        Object old_value = View.class.getMethod("get"+upperCaseFieldname).invoke(targetView);
+                        if ((new_value == null && old_value != null)
+                                || (new_value != null && !new_value.equals(old_value))) {
+                            View.class.getMethod("set"+upperCaseFieldname, View.class.getMethod("get"+upperCaseFieldname).getReturnType()).invoke(targetView, new_value);
+                            obj_changed = true;
+                        }
+                    }
+                    catch (IllegalAccessException | IllegalArgumentException
+                            | InvocationTargetException | NoSuchMethodException
+                            | SecurityException e) {
+                        throw new CowebException("View update: problem on field update",
+                                String.format("View %s bad field update %s : %s", obj_id, fieldname, e.toString()));
+                    }
+                    break;
+                }
+            }
+        }
+        return obj_changed;
+    }
+
+    private int getPosition(Map<String, Object> data) throws CowebException {
+        Integer position = (Integer) data.get("position");
+
+        if (position == null || position < 0) {
+            throw new CowebException("get position: bad insert position",
+                    String.format("Bad position %s not found",
+                            position == null ? "null" : position.toString()));
+        }
+        
+        return position.intValue();
+    }
+
+    @Override
+    public void delete(String clientId, Map<String, Object> data) {
+        
+        this.logger.debug("ViewSyncEventManager: delete "
+                + this.getClass().getName());
+        
+        @SuppressWarnings("unchecked")
+        Map<String, Object> values = (Map<String, Object>) data.get("value");
+        
+        Project project = getProject(values);
+        int position = getPosition(values);
+
+        this.logger.debug(String.format("delete %s %d", this.getClass()
+                .getName(), position));
+
+        if(position<0 || position > project.getViews().size()) {
+            throw new CowebException("node delete: bad delete position",
+                    String.format("Bad position %d not found", position));
+        }
+
+        project.getViews().remove(position);
+        
+        this.getProjectsRepository().save(project);
+    }
+
+}
--- a/server/src/main/java/org/iri_research/renkan/models/Project.java	Sun May 25 13:45:24 2014 +0900
+++ b/server/src/main/java/org/iri_research/renkan/models/Project.java	Wed Jun 04 12:36:17 2014 +0200
@@ -30,7 +30,6 @@
 @Document(collection = "projects")
 public class Project extends AbstractRenkanModel<String> {
 
-    @SuppressWarnings("unused")
     private static Logger logger = LoggerFactory.getLogger(Project.class);
 
     @Field("rev_counter")
@@ -53,6 +52,9 @@
     // edges
     @DBRef
     private List<Edge> edges = new ArrayList<Edge>();
+    
+    //views
+    private List<View> views = new ArrayList<View>();
 
     // Users
     private List<RenkanUser> users = new ArrayList<RenkanUser>();
@@ -117,6 +119,10 @@
         return this.edges;
     }
 
+    public List<View> getViews() {
+        return this.views;
+    }
+ 
     public List<RenkanUser> getUsers() {
         return this.users;
     }
--- a/server/src/main/java/org/iri_research/renkan/models/ProjectRevision.java	Sun May 25 13:45:24 2014 +0900
+++ b/server/src/main/java/org/iri_research/renkan/models/ProjectRevision.java	Wed Jun 04 12:36:17 2014 +0200
@@ -3,7 +3,6 @@
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
-import java.util.UUID;
 
 import org.bson.types.ObjectId;
 import org.joda.time.DateTime;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/src/main/java/org/iri_research/renkan/models/View.java	Wed Jun 04 12:36:17 2014 +0200
@@ -0,0 +1,101 @@
+package org.iri_research.renkan.models;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.mongodb.core.geo.Point;
+import org.springframework.data.mongodb.core.mapping.Field;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class View extends AbstractRenkanModel<String> {
+
+    private DateTime created;
+    private DateTime updated;
+    
+    @Field("zoom_level")
+    @JsonProperty("zoom_level")
+    private double zoomLevel;
+    
+    private Point offset;
+
+    @Field("created_by")
+    @JsonProperty("created_by")
+    private String createdBy;
+    
+    @SuppressWarnings("unused")
+    private View() {
+    }
+
+    public View(String view_id, String title, String description, String uri,
+            String color, String creatorId, double zoomLevel, Point offset) {
+        super(view_id, title, description, uri, color);
+        this.createdBy = creatorId;
+        this.zoomLevel = zoomLevel;
+        this.offset = offset;
+        this.created = new DateTime();
+        this.updated = new DateTime();
+    }
+    
+    @Autowired(required = true)
+    public View(String view_id, String title, String description, String uri,
+            String color, String creatorId, double zoomLevel, Point offset, DateTime created) {
+        this(view_id, title, description, uri, color, creatorId, zoomLevel, offset);
+        if(created != null) {
+            this.created= created;
+        }
+    }
+
+    @Override
+    protected String getRawKeyPart() {
+        return this.id+Long.toString(this.getCreated().getMillis());
+    }
+
+    @Override
+    protected String getDefaultId() {
+        return UUID.randomUUID().toString();
+    }
+
+    public DateTime getCreated() {
+        return created;
+    }
+
+    public void setCreated(DateTime created) {
+        this.created = created;
+    }
+
+    public DateTime getUpdated() {
+        return updated;
+    }
+
+    public void setUpdated(DateTime updated) {
+        this.updated = updated;
+    }
+
+    public String getCreatedBy() {
+        return createdBy;
+    }
+
+    public void setCreatedBy(String createdBy) {
+        this.createdBy = createdBy;
+    }
+
+    public double getZoomLevel() {
+        return zoomLevel;
+    }
+
+    public void setZoomLevel(double zoomLevel) {
+        this.zoomLevel = zoomLevel;
+    }
+
+    public Point getOffset() {
+        return offset;
+    }
+
+    public void setOffset(Point offset) {
+        this.offset = offset;
+    }
+    
+
+}
--- a/server/src/main/webapp/static/js/corenkan.js	Sun May 25 13:45:24 2014 +0900
+++ b/server/src/main/webapp/static/js/corenkan.js	Wed Jun 04 12:36:17 2014 +0200
@@ -3,372 +3,394 @@
  */
 
 define([
-    "dojo",
-    "dojo/cookie",
-    "dojo/json",
-    "dojo/ready",
-    "coweb/main",
-    "rcolor",
-], function(dojo, cookie, json, ready, coweb, RColor) {
-	
+        "dojo",
+        "dojo/cookie",
+        "dojo/json",
+        "dojo/ready",
+        "coweb/main",
+        "rcolor",
+        ], function(dojo, cookie, json, ready, coweb, RColor) {
+
+    'use strict';
+
     var CoRenkan = function() {
     };
 
     var proto = CoRenkan.prototype;
-       
+
     proto.init = function() {
-    	console.log("ready callback", dojo.config.corenkanConfig);
-    	
-    	this.renkan = dojo.config.corenkanConfig.renkan;
-    	this.project = dojo.config.corenkanConfig.renkan.project;
+        console.log("ready callback", dojo.config.corenkanConfig);
+
+        this.renkan = dojo.config.corenkanConfig.renkan;
+        this.project = dojo.config.corenkanConfig.renkan.project;
+
+        this.initCollab(dojo.config.corenkanConfig.projectId);
+
+
+        var that = this;
+
+        ready(function() {
+
+            var sess = coweb.initSession();
+            that.session = sess;
 
-    	this.initCollab(dojo.config.corenkanConfig.projectId);
-    	
-    	
-    	var that = this;
-    	
-    	ready(function() {
-    		
-	    	var sess = coweb.initSession();
-	    	that.session = sess;
-	
-	    	sess.prepare({userDefined: {project_id:dojo.config.corenkanConfig.projectId}, collab: true}).then(function(data) {
-	    		console.log("Prepare ok : ", data);
-	        	that.setObjects();
-	    	});
+            sess.prepare({userDefined: {project_id:dojo.config.corenkanConfig.projectId}, collab: true}).then(function(data) {
+                console.log("Prepare ok : ", data);
+                that.setObjects();
+            });
 
-	    	sess.onStatusChange = function(status) {
-	    	    console.log(status);
-	    	    that.onInternalStatusChange(status);
-	    	    if(typeof that.onStatusChange === "function") {
-	    	    	that.onStatusChange(status);
-	    	    }
-	    	};
-	    	    	
-    	});
-                
+            sess.onStatusChange = function(status) {
+                console.log(status);
+                that.onInternalStatusChange(status);
+                if(typeof that.onStatusChange === "function") {
+                    that.onStatusChange(status);
+                }
+            };
+
+        });
+
     };
-    
+
     proto.initCollab = function(id) {
-    	
-    	console.log("init collabbs objects " + "users_" + id + ", " + "renkan_" + id);
-    	
-    	this.users_collab = coweb.initCollab({id: "users_" + id});
-    	
-    	this.users_collab.subscribeReady(this, "onLocalJoin");        
+
+        console.log("init collabbs objects " + "users_" + id + ", " + "renkan_" + id);
+
+        this.users_collab = coweb.initCollab({id: "users_" + id});
+
+        this.users_collab.subscribeReady(this, "onLocalJoin");
         this.users_collab.subscribeSiteJoin(this, 'onRemoteJoin');
         this.users_collab.subscribeSiteLeave(this, 'onRemoteLeave');
-    	this.users_collab.subscribeSync("roster", this, "onRemoteRosterChange");
+        this.users_collab.subscribeSync("roster", this, "onRemoteRosterChange");
 
-    	this.users_collab.subscribeStateResponse(this, "onUsersStateResponse");
+        this.users_collab.subscribeStateResponse(this, "onUsersStateResponse");
+
+
+        this.collab = coweb.initCollab({id : "renkan_" + id});
 
-    	
-    	this.collab = coweb.initCollab({id : "renkan_" + id});    	    	    	
-    	
-    	this.collab.subscribeSync("project", this, "onRemoteProjectChange");
-    	this.collab.subscribeSync("user", this, "onRemoteUserChange");
-    	this.collab.subscribeSync("node", this, "onRemoteNodeChange");
-    	this.collab.subscribeSync("edge", this, "onRemoteEdgeChange");
+        this.collab.subscribeSync("project", this, "onRemoteProjectChange");
+        this.collab.subscribeSync("user", this, "onRemoteUserChange");
+        this.collab.subscribeSync("node", this, "onRemoteNodeChange");
+        this.collab.subscribeSync("edge", this, "onRemoteEdgeChange");
+        this.collab.subscribeSync("view", this, "onRemoteViewChange");
 
-    	this.collab.subscribeStateResponse(this, "onStateResponse");    	
-    	
+        this.collab.subscribeStateResponse(this, "onStateResponse");
+
     };
-    
+
     proto.onLocalJoin = function(params) {
-    	console.log("Local join", params);
-    	this.current_site = params.site;
-    	if(typeof this.renkan !== "undefined" && this.renkan != null && typeof this.renkan.current_user != "undefined") {
-    		this.renkan.current_user.set("site_id", params.site);
-    	}
+        console.log("Local join", params);
+        this.current_site = params.site;
+        if(typeof this.renkan !== "undefined" && this.renkan !== null && typeof this.renkan.current_user !== "undefined") {
+            this.renkan.current_user.set("site_id", params.site);
+        }
     };
-    
-    
+
+
     proto.onRemoteJoin = function(params) {
-    	console.log("Remote join", params);
-    	// do nothing
+        console.log("Remote join", params);
+        // do nothing
     };
-    
+
     proto.onRemoteLeave = function(params) {
-    	console.log("Remote leave", params);
-    	
-    	// remove remote site from current_user_list
-    	if(typeof this.renkan === "undefined" || this.renkan == null || typeof this.renkan.current_user_list === "undefined" || this.renkan.current_user_list == null) {
-    		return;
-    	}
-    	for ( var user in this.renkan.current_user_list.filter(function(u) { return u.get("site_id") == params.site; })) {
-			this.renkan.current_user_list.remove(user);
-		}
-    	
+        console.log("Remote leave", params);
+
+        // remove remote site from current_user_list
+        if(typeof this.renkan === "undefined" || this.renkan === null || typeof this.renkan.current_user_list === "undefined" || this.renkan.current_user_list === null) {
+            return;
+        }
+        var filtered_user_list = this.renkan.current_user_list.filter(function(u) { return u.get("site_id") == params.site; });
+        for ( var user in filtered_user_list) {
+            this.renkan.current_user_list.remove(user);
+        }
+
     };
-    
+
     proto.onUsersStateResponse = function(state) {
-    	
-    	user_list = json.parse(state);
-    	console.log("Users State response", user_list);
-    	_.each(user_list, function(user, i, l) {
-    		user['_id'] = user['id'];
-    	});    	
-    	this.renkan.current_user_list.reset(user_list, {silent: true});
+
+        var user_list = json.parse(state);
+        console.log("Users State response", user_list);
+        _.each(user_list, function(user, i, l) {
+            user._id = user.id;
+        });    	
+        this.renkan.current_user_list.reset(user_list, {silent: true});
     };
-    
+
     proto.onStateResponse = function(state) {
-    	obj = json.parse(state);
-    	console.log("State response", obj);
-    	obj['_id'] = obj['id'];
-    	this.project.set(obj, {validate: true});
-    	this.renkan.renderer.autoScale();
+        var obj = json.parse(state);
+        console.log("State response", obj);
+        obj._id = obj.id;
+        this.project.set(obj, {validate: true});
+        this.renkan.renderer.autoScale();
     };
-    
+
     proto.onInternalStatusChange = function(status) {
-    	if(status == "ready") {
-    	    this.renkan.read_only = false;
-    		this.renkan.onStatusChange();
-    	}
-    	else {
+        if(status == "ready") {
+            this.renkan.read_only = false;
+            this.renkan.onStatusChange();
+        }
+        else {
             this.renkan.read_only = true;
-            this.renkan.onStatusChange();                    		
-    	}
+            this.renkan.onStatusChange();
+        }
     };
-    
+
     function prepareValues(obj,c) {
-		values = {};
-		for(var fieldname in c.changes) {
-			if(c.changes[fieldname]) {
-				values[fieldname] = obj.get(fieldname);
-			}    			
-		}
-		return values;
+        var values = {};
+        for(var fieldname in c.changes) {
+            if(c.changes[fieldname]) {
+                values[fieldname] = obj.get(fieldname);
+            }    			
+        }
+        return values;
     }
-    
+
     proto.addObjectBind = function(type, obj, c, options, collab) {
-		console.log("add " + type,obj, c, options);
-		if(this.project == null) {
-			console.log("null project exiting");
-			return;
-		}
-		var values = obj.toJSON();
-		var new_values = {
-			id: obj.id,
-			_type: type,
-			_index: options.index,
-			_project_id : obj.get("project").get("_id"),
-			_user_id : (this.project.current_user!=null)?this.project.current_user.id:null
-		};
-		for(var k in new_values) {
-			values[k] = new_values[k];
-		}
-		console.log("add values : ", values);
-		collab.sendSync(type, values, "insert", options.index);
+        console.log("add " + type,obj, c, options);
+        if(this.project === null) {
+            console.log("null project exiting");
+            return;
+        }
+        var values = obj.toJSON();
+        var new_values = {
+                id: obj.id,
+                _type: type,
+                _index: options.index,
+                _project_id : obj.get("project").get("_id"),
+                _user_id : (this.project.current_user!==null && typeof this.project.current_user !== "undefined")?this.project.current_user.id:null
+        };
+        for(var k in new_values) {
+            values[k] = new_values[k];
+        }
+        var position = c.indexOf(obj);
+        console.log("add position, index, values: ", position, options.index, values);
+        collab.sendSync(type, values, "insert", position);
     };
-    
+
     proto.removeObjectBind = function(type, obj, c, options, collab) {
-		console.log("delete " + type,obj, c, options);
-		var values = {
-    	    id: obj.id,
-    	    _type: type,
-    	    _index: options.index,
-    	    _project_id : obj.get("project").id,
-    	    _user_id : (this.project.current_user!=null)?this.project.current_user.id:null
-    	};
-		collab.sendSync(type, values, "delete", options.index);    	
+        console.log("delete " + type,obj, c, options);
+        var values = {
+                id: obj.id,
+                _type: type,
+                _index: options.index,
+                _project_id : obj.get("project").id,
+                _user_id : (this.project.current_user!==null)?this.project.current_user.id:null
+        };
+        collab.sendSync(type, values, "delete", options.index);    	
     };
-    
+
     proto.updateObjectBind = function(type, obj, options, collab) {
-		console.log("change " + type,obj, options);
-		if(typeof obj != "undefined" && obj.hasChanged()) {
-			var values = {
-			    id: obj.id,
-	    	    _type: type,
-	    	    _project_id : obj.get("project").id,
-	    	    _user_id : (this.project.current_user!=null)?this.project.current_user.id:null
-	    	};
-			_.extend(values,obj.changed);		
-	    	collab.sendSync(type, values);
-		}
+        console.log("change " + type,obj, options);
+        if(typeof obj != "undefined" && obj.hasChanged()) {
+            var values = {
+                    id: obj.id,
+                    _type: type,
+                    _project_id : obj.get("project").id,
+                    _user_id : (this.project.current_user!==null)?this.project.current_user.id:null
+            };
+            _.extend(values,obj.changed);		
+            collab.sendSync(type, values);
+        }
     };
-    
+
     /**
      * Called when an abject is changed
      * 
      */
     proto.objectChange = function(event, model, collection, options) {
-    	
-		console.log("project change all ", event, model, collection, options);
-		// check that current user is in user list of the project
-		
-		if(this.project == null || this.project.current_user == null) {
-			return;
-		}
-		var current_user = this.project.current_user;
-		
-		if(this.project.get("users").get(current_user.id) == null) {
-			var props = current_user.toJSON();
-			this.project.addUser(props);
-		}
-		
-	};
+
+        console.log("project change all ", event, model, collection, options);
+        // check that current user is in user list of the project
+
+        if(this.project === null || this.project.current_user === null) {
+            return;
+        }
+        var current_user = this.project.current_user;
+
+        if(this.project.get("users").get(current_user.id) === null) {
+            var props = current_user.toJSON();
+            this.project.addUser(props);
+        }
+
+    };
+
+
+    proto.setObjects = function() {
+
+        console.log("Cookie BAYEUX_BROWSER : " + cookie("BAYEUX_BROWSER"));
+        var renkan = this.renkan;
+        var project = renkan.project;
+        this.setProject(project);
+        this.setRenkan(renkan);
+        this.setUser(renkan);
+
+    };
+
+    proto.setRenkan = function(renkan) {
+
+        console.log("Set Renkan");
 
-    
-    proto.setObjects = function() {
-    	
-    	console.log("Cookie BAYEUX_BROWSER : " + cookie("BAYEUX_BROWSER"));
-    	var renkan = this.renkan;
-    	var project = renkan.project;
-    	this.setProject(project);
-    	this.setRenkan(renkan);
-    	this.setUser(renkan);
+        var that = this;
+
+        renkan.current_user_list.bind("add", function(obj, c, options) {
+            that.addObjectBind("roster", obj, c, options, that.users_collab);
+        });
+        //renkan.current_user_list.bind("remove", function(obj, c, options) {
+        //	that.removeObjectBind("_roster", obj, c, options, that.users_collab);
+        //});
+        renkan.current_user_list.bind("change", function(obj, options) {
+            that.updateObjectBind("roster", obj, options, that.users_collab);
+        });
+
+        renkan.current_user_list.bind("change", function(obj, options) {
+            console.log("update roster",obj, options);
+            // get user in project
+            var project = obj.get("project");
+            if(project === null) {
+                console.log("null project return");
+                return;
+            }
+            var user = project.get("users").get(obj.id);
+            if(user === null) {
+                console.log("user " + obj.id + " not in project. return");
+                return;
+            }
+            var new_val;
+            for(var att in obj.changed) {
+                new_val = obj.changed[att];
+                if(user.get("att") != new_val) {
+                    user.set(att, new_val);
+                }
+            }
+        });
 
     };
-    
-    proto.setRenkan = function(renkan) {
-    	
-    	console.log("Set Renkan");
-    		
-    	var that = this;
-    	
-    	renkan.current_user_list.bind("add", function(obj, c, options) {
-    		that.addObjectBind("roster", obj, c, options, that.users_collab);
-    	});
-    	//renkan.current_user_list.bind("remove", function(obj, c, options) {
-    	//	that.removeObjectBind("_roster", obj, c, options, that.users_collab);
-    	//});
-    	renkan.current_user_list.bind("change", function(obj, options) {
-    		that.updateObjectBind("roster", obj, options, that.users_collab);
-    	});
-    	
-    	renkan.current_user_list.bind("change", function(obj, options) {
-    		console.log("update roster",obj, options);
-    		// get user in project
-    		project = obj.get("project");
-    		if(project == null) {
-    			console.log("null project return");
-    			return;
-    		}
-    		user = project.get("users").get(obj.id);
-    		if(user == null) {
-    			console.log("user " + obj.id + " not in project. return");
-    			return;
-    		}
-    		for(att in obj.changed) {
-    			new_val = obj.changed[att];
-    			if(user.get("att") != new_val) {
-    				user.set(att, new_val);
-    			}
-    		}
-    	});
-    	    	
-    };
-    
+
     proto.setUser = function(renkan) {
-    	console.log("set user : " + cookie("BAYEUX_BROWSER"));
-    	
-    	if(typeof renkan === "undefined" || typeof renkan.project === "undefined" || renkan.project == null) {
-    		return;
-    	}
-    	
-    	var user_id = cookie("BAYEUX_BROWSER");
-    	var project = renkan.project;
-    	
-    	var puser = project.get("users").get(user_id);
-    	var puser_def = null;
-    	if(puser == null) {
-    		color = new RColor();
-    		puser_def = {
-    		    id: user_id,
-                title: "anonymous",
-                project: project,
-                color: color.get(true, 0.5, 0.8),
-                site_id: this.current_site
+        console.log("set user : " + cookie("BAYEUX_BROWSER"));
+
+        if(typeof renkan === "undefined" || typeof renkan.project === "undefined" || renkan.project === null) {
+            return;
+        }
+
+        var user_id = cookie("BAYEUX_BROWSER");
+        var project = renkan.project;
+
+        var puser = project.get("users").get(user_id);
+        var puser_def = null;
+        if(puser === null || typeof puser === "undefined") {
+            var color = new RColor();
+            puser_def = {
+                    id: user_id,
+                    title: "anonymous",
+                    project: project,
+                    color: color.get(true, 0.5, 0.8),
+                    site_id: this.current_site
             };
-    	}
-    	else {
-    		puser_def = puser.toJSON();
-    		puser_def.project = project;
-    	}
-    	puser = renkan.current_user_list.push(puser_def);
-    	
-    	project.current_user = puser;
-    	renkan.current_user = puser.id;
-    	
-    	var that = this;
-    	
-    	project.once("all", function(event, model, collection, options){
-    		that.objectChange(event, model, collection, options);
-    	});
-    	project.get("nodes").once("all", function(event, model, collection, options){
-    		that.objectChange(event, model, collection, options);
-    	});
-    	project.get("edges").once("all", function(event, model, collection, options){
-    		that.objectChange(event, model, collection, options);
-    	});
+        }
+        else {
+            puser_def = puser.toJSON();
+            puser_def.project = project;
+        }
+        puser = renkan.current_user_list.push(puser_def);
+
+        project.current_user = puser;
+        renkan.current_user = puser.id;
+
+        var that = this;
 
-    	
+        project.once("all", function(event, model, collection, options){
+            that.objectChange(event, model, collection, options);
+        });
+        project.get("nodes").once("all", function(event, model, collection, options){
+            that.objectChange(event, model, collection, options);
+        });
+        project.get("edges").once("all", function(event, model, collection, options){
+            that.objectChange(event, model, collection, options);
+        });
+        project.get("views").once("all", function(event, model, collection, options){
+            that.objectChange(event, model, collection, options);
+        });
+
     };
-    
+
     proto.setProject = function(project) {
-    	
-    	console.log("project", project);
-    	    	
-    	var projectFields = ["title", "description", "uri"];
-    	var that = this;
-    	    	
-    	for(var fieldIndex in projectFields) {
-    		(function(fi){
-	    		var field = projectFields[fi];
-	        	project.bind("change:"+field, function(obj, c) {
-	        		console.log(c);
-	        		values = {
-	        		    id: obj.id,
-	        		    _type: "project",
-	        		    _project_id: obj.id,
-	        		    _user_id : (that.project.current_user!=null)?that.project.current_user.id:null
-	        		};
-	        		values[field] = c;
-	        		that.collab.sendSync("project", values);
-	        	});
-    		})(fieldIndex);
-    	}
-    	
-    	
-    	project.get("nodes").bind("add", function(obj, c, options) {    		
-    		that.addObjectBind("node", obj, c, options, that.collab);
-    	});
-    	
-    	project.get("nodes").bind("remove", function(obj, c, options) {
-    		that.removeObjectBind("node", obj, c, options, that.collab);
-    	});
-    	
-    	project.get("nodes").bind("change", function(obj, options) {
-    		that.updateObjectBind("node", obj, options, that.collab);
-    	});
+
+        console.log("project", project);
+
+        var projectFields = ["title", "description", "uri"];
+        var that = this;
+
+        var bind_field_index = function(fi){
+            var field = projectFields[fi];
+            project.bind("change:"+field, function(obj, c) {
+                console.log(c);
+                var values = {
+                        id: obj.id,
+                        _type: "project",
+                        _project_id: obj.id,
+                        _user_id : (that.project.current_user!==null)?that.project.current_user.id:null
+                };
+                values[field] = c;
+                that.collab.sendSync("project", values);
+            });
+        };
+        for(var fieldIndex in projectFields) {
+            bind_field_index(fieldIndex);
+        }
+
+
+        project.get("nodes").bind("add", function(obj, c, options) {
+            that.addObjectBind("node", obj, c, options, that.collab);
+        });
+
+        project.get("nodes").bind("remove", function(obj, c, options) {
+            that.removeObjectBind("node", obj, c, options, that.collab);
+        });
+
+        project.get("nodes").bind("change", function(obj, options) {
+            that.updateObjectBind("node", obj, options, that.collab);
+        });
 
-    	project.get("users").bind("add", function(obj, c, options) {    		
-    		that.addObjectBind("user", obj, c, options, that.collab);
-    	});
-    	
-    	project.get("users").bind("remove", function(obj, c, options) {
-    		that.removeObjectBind("user", obj, c, options, that.collab);
-    	});
-    	
-    	project.get("users").bind("change", function(obj, options) {
-    		that.updateObjectBind("user", obj, options, that.collab);
-    	});
+        project.get("users").bind("add", function(obj, c, options) {
+            that.addObjectBind("user", obj, c, options, that.collab);
+        });
+
+        project.get("users").bind("remove", function(obj, c, options) {
+            that.removeObjectBind("user", obj, c, options, that.collab);
+        });
+
+        project.get("users").bind("change", function(obj, options) {
+            that.updateObjectBind("user", obj, options, that.collab);
+        });
+
+        project.get("edges").bind("add", function(obj, c, options) {
+            that.addObjectBind("edge", obj, c, options, that.collab);
+        });
+
+        project.get("edges").bind("remove", function(obj, c, options) {
+            that.removeObjectBind("edge", obj, c, options, that.collab);
+        });
 
-    	project.get("edges").bind("add", function(obj, c, options) {    		
-    		that.addObjectBind("edge", obj, c, options, that.collab);
-    	});
-    	
-    	project.get("edges").bind("remove", function(obj, c, options) {
-    		that.removeObjectBind("edge", obj, c, options, that.collab);
-    	});
-    	
-    	project.get("edges").bind("change", function(obj, options) {
-    		that.updateObjectBind("edge", obj, options, that.collab);
-    	});
-    	
+        project.get("edges").bind("change", function(obj, options) {
+            that.updateObjectBind("edge", obj, options, that.collab);
+        });
+
+        project.get("views").bind("add", function(obj, c, options) {
+            that.addObjectBind("view", obj, c, options, that.collab);
+        });
+
+        project.get("views").bind("remove", function(obj, c, options) {
+            that.removeObjectBind("view", obj, c, options, that.collab);
+        });
+
+        project.get("views").bind("change", function(obj, options) {
+            that.updateObjectBind("view", obj, options, that.collab);
+        });
+
+        
     };
-    
-    
+
+
     /**
      * Called when a remote data store for project changes in some manner. Dispatches to
      * local methods for insert, update, delete handling.
@@ -376,12 +398,12 @@
      * @param args Cooperative web event
      */
     proto.onRemoteProjectChange = function(args) {    	
-    	console.log("Remote project change", args);
-    	if (args.type === "update") {
+        console.log("Remote project change", args);
+        if (args.type === "update") {
             this.onRemoteProjectUpdate(args.value, args.position);
-    	}
+        }
     };
-    
+
 
     /**
      * Called when a remote data store for nodes changes in some manner. Dispatches to
@@ -389,8 +411,8 @@
      * @param args Cooperative web event
      */
     proto.onRemoteObjectChange = function(field, args) {
-    	
-    	console.log("Remote "+ field +" change",args);
+
+        console.log("Remote "+ field +" change",args);
         if (args.type === "insert") {
             this.onRemoteObjectInsert(field, args.value, args.position);
         } else if (args.type === "update") {
@@ -400,14 +422,14 @@
         }
     };
 
-    
+
     /**
      * Called when a remote data store for nodes changes in some manner. Dispatches to
      * local methods for insert, update, delete handling.
      * @param args Cooperative web event
      */
     proto.onRemoteNodeChange = function(args) {
-    	this.onRemoteObjectChange("nodes", args);
+        this.onRemoteObjectChange("nodes", args);
     };
 
 
@@ -416,8 +438,8 @@
      * local methods for insert, update, delete handling.
      * @param args Cooperative web event
      */
-    proto.onRemoteUserChange = function(args) {    	
-    	this.onRemoteObjectChange("users", args);
+    proto.onRemoteUserChange = function(args) {
+        this.onRemoteObjectChange("users", args);
     };
 
     /**
@@ -425,9 +447,19 @@
      * local methods for insert, update, delete handling.
      * @param args Cooperative web event
      */
-    proto.onRemoteEdgeChange = function(args) {    	
-    	this.onRemoteObjectChange("edges", args);
+    proto.onRemoteEdgeChange = function(args) {
+        this.onRemoteObjectChange("edges", args);
     };
+
+    /**
+     * Called when a remote data store for views changes in some manner. Dispatches to
+     * local methods for insert, update, delete handling.
+     * @param args Cooperative web event
+     */
+    proto.onRemoteViewChange = function(args) {
+        this.onRemoteObjectChange("views", args);
+    };
+
     
     /**
      * Called when a remote data store for nodes changes in some manner. Dispatches to
@@ -435,9 +467,9 @@
      * @param args Cooperative web event
      */
     proto.onRemoteRosterChange = function(args) {    	
-    	this.onRemoteObjectChange(this.renkan.current_user_list, args);
+        this.onRemoteObjectChange(this.renkan.current_user_list, args);
     };
-    
+
     /**
      * Called when a project attribute changes value in a remote data store.
      * Updates the attribute value of the item with the same id in the local
@@ -447,19 +479,19 @@
      * @param position Which item to update.
      */
     proto.onRemoteProjectUpdate = function(values, position) {
-    	var project_id = values['id'];
-    	if(typeof(project_id) === "undefined") {
-    		return;
-		}
-    	
-    	if(this.project != null && project_id == this.project.id) {
-    		for(var fieldname in values) {
-    			if(fieldname != "id" && fieldname != "type") {
-    				this.project.set(fieldname, values[fieldname]);
-    			}
-    		}
-    	}
-    	
+        var project_id = values.id;
+        if(typeof(project_id) === "undefined") {
+            return;
+        }
+
+        if(this.project !== null && project_id === this.project.id) {
+            for(var fieldname in values) {
+                if(fieldname != "id" && fieldname != "type") {
+                    this.project.set(fieldname, values[fieldname]);
+                }
+            }
+        }
+
     };    
 
     /**
@@ -470,51 +502,54 @@
      * @param position Which item to update.
      */
     proto.onRemoteObjectInsert = function(field_coll, values, position) {
-    	
-    	console.log("Remote ", field_coll ," insert values ", values, "position", position);
-    	
-    	var coll = null;    	
-    	if(typeof field_coll === "string") {
-    		coll = this.project.get(field_coll);
-    	}
-    	else {
-    		coll = field_coll;
-    	}
+
+        console.log("Remote ", field_coll ," insert values ", values, "position", position);
+
+        var coll = null;    	
+        if(typeof field_coll === "string") {
+            coll = this.project.get(field_coll);
+        }
+        else {
+            coll = field_coll;
+        }
+
+        var object_id = values.id;    	
+
+        var obj = coll.get(object_id);
 
-    	var object_id = values['id'];    	
-    	
-    	var obj = coll.get(object_id);
-    	
-    	if(obj != null) {
-    		this.onRemoteObjectUpdate(field_coll, values, coll.indexOf(obj));
-    	}
-    	else {
-    		var add_values = {};
-    		for(var fieldname in values) {
-    			if(fieldname == "_id" || fieldname[0] !== '_' ) {
-    				add_values[fieldname] = values[fieldname];
-    			}
-    		}
-    		switch(field_coll) {
-    		case "nodes":
-    			this.project.addNode(add_values, {at:position});
-    			break;
-    		case "edges":
-    			this.project.addEdge(add_values, {at:position});
-    			break;
-    		case "users":
-    			this.project.addUser(add_values, {at:position});
-    			break;
-    		default:
-    			add_values.project = this.project;
-    			coll.push(add_values, {at:position});
-    			break;
-    		}
-    		
-    	}
-    	
+        if(obj !== null && typeof obj !== "undefined") {
+            this.onRemoteObjectUpdate(field_coll, values, coll.indexOf(obj));
+        }
+        else {
+            var add_values = {};
+            for(var fieldname in values) {
+                if(fieldname == "_id" || fieldname[0] !== '_' ) {
+                    add_values[fieldname] = values[fieldname];
+                }
+            }
+            switch(field_coll) {
+            case "nodes":
+                this.project.addNode(add_values, {at:position});
+                break;
+            case "edges":
+                this.project.addEdge(add_values, {at:position});
+                break;
+            case "users":
+                this.project.addUser(add_values, {at:position});
+                break;
+            case "views":
+                this.project.addView(add_values, {at:position});
+                break;
+            default:
+                add_values.project = this.project;
+            coll.push(add_values, {at:position});
+            break;
+            }
+
+        }
+
     };
-    
+
     /**
      * Called when a object attribute changes value in a remote data store.
      * Updates the attribute value of the item with the same id in the local
@@ -526,32 +561,32 @@
      */
     proto.onRemoteObjectUpdate = function(field_coll, values, position) {
 
-    	console.log("Remote ", field_coll ," update values ", values, "position", position);
+        console.log("Remote ", field_coll ," update values ", values, "position", position);
+
+        var coll = null;    	
+        if(typeof field_coll === "string") {
+            coll = this.project.get(field_coll);
+        }
+        else {
+            coll = field_coll;
+        }
+
+        var object_id = values.id;
 
-    	var coll = null;    	
-    	if(typeof field_coll === "string") {
-    		coll = this.project.get(field_coll);
-    	}
-    	else {
-    		coll = field_coll;
-    	}
-    	
-    	var object_id = values['id'];
-    	
-    	if(this.project != null) {
-    		var obj = coll.get(object_id);
-    		if(obj != null) {
-    			var changed_val = {};
-	    		for(var fieldname in values) {
-	    			if(fieldname != "id" && fieldname != "type") {
-	    				changed_val[fieldname] = values[fieldname];
-	    			}
-	    		}
-	    		obj.set(changed_val);
-    		}
-    	}
+        if(this.project !== null) {
+            var obj = coll.get(object_id);
+            if(obj !== null && typeof obj !== "undefined") {
+                var changed_val = {};
+                for(var fieldname in values) {
+                    if(fieldname != "id" && fieldname != "type" && fieldname != "_id") {
+                        changed_val[fieldname] = values[fieldname];
+                    }
+                }
+                obj.set(changed_val);
+            }
+        }
     };
-    
+
     /**
      * Called when a object is deleted in a remote data store.
      *
@@ -559,26 +594,26 @@
      * @param position Which item to update.
      */
     proto.onRemoteObjectDelete = function(field_coll, position) {
-    	console.log("Remote ", field_coll," delete position", position);
-    	var coll = null;    	
-    	if(typeof field_coll === "string") {
-    		coll = this.project.get(field);
-    	}
-    	else {
-    		coll = field_coll;
-    	}
+        console.log("Remote ", field_coll," delete position", position);
+        var coll = null;    	
+        if(typeof field_coll === "string") {
+            coll = this.project.get(field);
+        }
+        else {
+            coll = field_coll;
+        }
 
-    	coll.remove(coll.at(position));
+        coll.remove(coll.at(position));
     };
-    
-        
+
+
     var app = new CoRenkan();
     dojo.ready(function() {
         app.init();
     });
-    
-    
+
+
     return {
-    	app: app
+        app: app
     };
 });
\ No newline at end of file