first stage of undo/redo functionality -- there are still a number of actions that...
authorDave Stubbs <osm@randomjunk.co.uk>
Tue, 20 Apr 2010 21:27:15 +0000 (21:27 +0000)
committerDave Stubbs <osm@randomjunk.co.uk>
Tue, 20 Apr 2010 21:27:15 +0000 (21:27 +0000)
 * creates aren't undoable
 * numerous functions in Way/Relation still don't undo
 * lots of bits should be grouped actions that aren't

28 files changed:
net/systemeD/halcyon/connection/CompositeUndoableAction.as [new file with mode: 0644]
net/systemeD/halcyon/connection/Entity.as
net/systemeD/halcyon/connection/MainUndoStack.as [new file with mode: 0644]
net/systemeD/halcyon/connection/Node.as
net/systemeD/halcyon/connection/Relation.as
net/systemeD/halcyon/connection/Tag.as
net/systemeD/halcyon/connection/UndoableAction.as [new file with mode: 0644]
net/systemeD/halcyon/connection/UndoableEntityAction.as [new file with mode: 0644]
net/systemeD/halcyon/connection/Way.as
net/systemeD/halcyon/connection/actions/DeleteNodeAction.as [new file with mode: 0644]
net/systemeD/halcyon/connection/actions/DeleteRelationAction.as [new file with mode: 0644]
net/systemeD/halcyon/connection/actions/DeleteWayAction.as [new file with mode: 0644]
net/systemeD/halcyon/connection/actions/MergeWaysAction.as [new file with mode: 0644]
net/systemeD/halcyon/connection/actions/MoveNodeAction.as [new file with mode: 0644]
net/systemeD/halcyon/connection/actions/RemoveEntityFromRelationAction.as [new file with mode: 0644]
net/systemeD/halcyon/connection/actions/RemoveNodeFromWayAction.as [new file with mode: 0644]
net/systemeD/halcyon/connection/actions/SetTagAction.as [new file with mode: 0644]
net/systemeD/halcyon/connection/actions/SetTagKeyAction.as [new file with mode: 0644]
net/systemeD/potlatch2/TagViewer.mxml
net/systemeD/potlatch2/Toolbox.as
net/systemeD/potlatch2/controller/DrawWay.as
net/systemeD/potlatch2/controller/SelectedPOINode.as
net/systemeD/potlatch2/controller/SelectedWay.as
net/systemeD/potlatch2/controller/SelectedWayNode.as
net/systemeD/potlatch2/mapfeatures/editors/SingleTagEditor.as
net/systemeD/potlatch2/tools/Circularise.as
net/systemeD/potlatch2/tools/Straighten.as
potlatch2.mxml

diff --git a/net/systemeD/halcyon/connection/CompositeUndoableAction.as b/net/systemeD/halcyon/connection/CompositeUndoableAction.as
new file mode 100644 (file)
index 0000000..966e159
--- /dev/null
@@ -0,0 +1,66 @@
+package net.systemeD.halcyon.connection {
+
+    public class CompositeUndoableAction extends UndoableAction {
+        
+        private var name:String;
+        private var actions:Array = [];
+        private var actionsDone:Boolean = false;
+    
+        public function CompositeUndoableAction(name:String) {
+            this.name = name;
+        }
+        
+        public function push(action:UndoableAction):void {
+            actions.push(action);
+        }
+        
+        public override function doAction():uint {
+            if ( actionsDone )
+                return UndoableAction.FAIL;
+                
+            var somethingDone:Boolean = false;
+            for ( var i:int = 0; i < actions.length; i++ ) {
+                var action:UndoableAction = actions[i];
+                
+                var result:uint = action.doAction();
+                if ( result == UndoableAction.NO_CHANGE ) {
+                    // splice this one out as it doesn't do anything
+                    actions.splice(i, 1)
+                    i --;
+                } else if ( result == UndoableAction.FAIL ) {
+                    undoFrom(i);
+                    return UndoableAction.FAIL;
+                } else {
+                    somethingDone = true;
+                }
+            }
+            actionsDone = true;
+            return somethingDone ? UndoableAction.SUCCESS : UndoableAction.NO_CHANGE;
+        }
+        
+        public override function undoAction():uint {
+            if ( !actionsDone )
+                return UndoableAction.FAIL;
+                
+            undoFrom(actions.length);
+            return UndoableAction.SUCCESS;
+        }
+        
+        public function undoFrom(index:int):void {
+            for ( var i:int = index - 1; i >= 0; i-- ) {
+                var action:UndoableAction = actions[i];
+                trace("going to do "+action);
+                
+                action.undoAction();
+            }
+            actionsDone = false;
+        }
+        
+        public function toString():String {
+            var str:String = " {" + actions.join(",") + "}";
+            return name + str;
+        }
+    }
+
+}
+
index 8039cb7..8f9d5ac 100644 (file)
@@ -2,6 +2,8 @@ package net.systemeD.halcyon.connection {
 
     import flash.events.EventDispatcher;
     import flash.utils.Dictionary;
+    
+    import net.systemeD.halcyon.connection.actions.*;
 
     public class Entity extends EventDispatcher {
         private var _id:Number;
@@ -50,27 +52,13 @@ package net.systemeD.halcyon.connection {
         public function getTag(key:String):String {
             return tags[key];
         }
-
-        public function setTag(key:String, value:String):void {
-            var old:String = tags[key];
-            if ( old != value ) {
-                if ( value == null || value == "" )
-                    delete tags[key];
-                else
-                    tags[key] = value;
-                markDirty();
-                dispatchEvent(new TagEvent(Connection.TAG_CHANGED, this, key, key, old, value));
-            }
+        
+        public function setTag(key:String, value:String, performAction:Function):void {
+            performAction(new SetTagAction(this, key, value));
         }
 
-        public function renameTag(oldKey:String, newKey:String):void {
-            var value:String = tags[oldKey];
-            if ( oldKey != newKey ) {
-                delete tags[oldKey];
-                tags[newKey] = value;
-                markDirty();
-                dispatchEvent(new TagEvent(Connection.TAG_CHANGED, this, oldKey, newKey, value, value));
-            }
+        public function renameTag(oldKey:String, newKey:String, performAction:Function):void {
+            performAction(new SetTagKeyAction(this, oldKey, newKey));
         }
 
         public function getTagList():TagList {
@@ -109,25 +97,33 @@ package net.systemeD.halcyon.connection {
             modified = false;
         }
 
-        protected function markDirty():void {
+        internal function markDirty():void {
             modified = true;
         }
 
                // Delete entity
                
-               public function remove():void {
+               public function remove(performAction:Function):void {
                        // to be overridden
                }
                
+               public function isDeleted():Boolean {
+                   return deleted;
+               }
+               
+               protected function setDeletedState(isDeleted:Boolean):void {
+                   deleted = isDeleted;
+               }
+               
                internal function isEmpty():Boolean {
                        return false;   // to be overridden
                }
 
-               protected function removeFromParents():void {
+               public function removeFromParents(performAction:Function):void {
                        for (var o:Object in parents) {
-                               if (o is Relation) { Relation(o).removeMember(this); }
-                               else if (o is Way) { Way(o).removeNode(Node(this)); }
-                               if (o.isEmpty()) { o.remove(); }
+                               if (o is Relation) { Relation(o).removeMember(this, performAction); }
+                               else if (o is Way) { Way(o).removeNode(Node(this), performAction); }
+                               if (o.isEmpty()) { o.remove(performAction); }
                        }
                }
 
@@ -212,3 +208,4 @@ package net.systemeD.halcyon.connection {
     }
 
 }
+
diff --git a/net/systemeD/halcyon/connection/MainUndoStack.as b/net/systemeD/halcyon/connection/MainUndoStack.as
new file mode 100644 (file)
index 0000000..5f9790d
--- /dev/null
@@ -0,0 +1,89 @@
+package net.systemeD.halcyon.connection {
+
+    import flash.events.*;
+
+    public class MainUndoStack extends EventDispatcher {
+        private static const GLOBAL_INSTANCE:MainUndoStack = new MainUndoStack();
+        
+        public static function getGlobalStack():MainUndoStack {
+            return GLOBAL_INSTANCE;
+        }
+        
+        private var undoActions:Array = [];
+        private var redoActions:Array = [];
+
+        /*
+         * Performs the action, then puts it on the undo stack.
+         *
+         * If you want to delay execution don't put it on this
+         * stack -- find another one.
+         */
+        public function addAction(action:UndoableAction):void {
+            trace("doing "+action);
+            var result:uint = action.doAction();
+            
+            switch ( result ) {
+            
+            case UndoableAction.FAIL:
+                throw new Error("Failure performing "+action);
+                
+            case UndoableAction.NO_CHANGE:
+                // nothing to do, and don't add to stack
+                break;
+                
+            case UndoableAction.SUCCESS:
+            default:
+                if ( undoActions.length > 0 ) {
+                    var previous:UndoableAction = undoActions[undoActions.length - 1];
+                    var isMerged:Boolean = action.mergePrevious(previous);
+                    if ( isMerged )
+                        undoActions.pop();
+                }
+                undoActions.push(action);
+                redoActions = [];
+                dispatchEvent(new Event("new_undo_item"));
+                dispatchEvent(new Event("new_redo_item"));
+                break;
+                
+            }
+        }
+        
+        /*
+         * Call to kill the undo queue -- the user will not be able to undo
+         * anything they previously did after this is called.
+         */
+        public function breakUndo():void {
+            undoActions = [];
+            redoActions = [];
+        }
+        
+        [Bindable(event="new_undo_item")]
+        public function canUndo():Boolean {
+            return undoActions.length > 0;
+        }
+        
+        [Bindable(event="new_redo_item")]
+        public function canRedo():Boolean {
+            return redoActions.length > 0;
+        }
+        
+        public function undo():void {
+            var action:UndoableAction = undoActions.pop();
+            trace("undoing "+action);
+            action.undoAction();
+            redoActions.push(action);
+            dispatchEvent(new Event("new_undo_item"));
+            dispatchEvent(new Event("new_redo_item"));
+        }
+        
+        public function redo():void {
+            var action:UndoableAction = redoActions.pop();
+            trace("redoing "+action);
+            action.doAction();
+            undoActions.push(action);
+            dispatchEvent(new Event("new_undo_item"));
+            dispatchEvent(new Event("new_redo_item"));
+        }
+       
+    }
+}
index 88f7f64..7050a22 100644 (file)
@@ -1,5 +1,7 @@
 package net.systemeD.halcyon.connection {
 
+    import net.systemeD.halcyon.connection.actions.*;
+
     public class Node extends Entity {
         private var _lat:Number;
         private var _latproj:Number;
@@ -28,50 +30,40 @@ package net.systemeD.halcyon.connection {
             return _lon;
         }
 
-        public function set lat(lat:Number):void {
-            var oldLat:Number = this._lat;
+        private function setLatLonImmediate(lat:Number, lon:Number):void {
             this._lat = lat;
             this._latproj = lat2latp(lat);
-            markDirty();
-            dispatchEvent(new NodeMovedEvent(Connection.NODE_MOVED, this, oldLat, _lon));
+            this._lon = lon;
+        }
+        
+        public function set lat(lat:Number):void {
+            MainUndoStack.getGlobalStack().addAction(new MoveNodeAction(this, lat, _lon, setLatLonImmediate));
         }
 
         public function set latp(latproj:Number):void {
-            var oldLat:Number = this._lat;
-            this._latproj = latproj;
-            this._lat = latp2lat(latproj);
-            markDirty();
-            dispatchEvent(new NodeMovedEvent(Connection.NODE_MOVED, this, oldLat, _lon));
-         }
+            MainUndoStack.getGlobalStack().addAction(new MoveNodeAction(this, latp2lat(latproj), _lon, setLatLonImmediate));
+        }
 
         public function set lon(lon:Number):void {
-            var oldLon:Number = this._lon;
-            this._lon = lon;
-            markDirty();
-            dispatchEvent(new NodeMovedEvent(Connection.NODE_MOVED, this, _lat, oldLon));
-         }
-
-               public function setLonLatp(lon:Number,latproj:Number):void {
-                       // move both lon and latp but only fire one event
-                       var oldLon:Number = this._lon;
-                       var oldLat:Number = this._lat;
-                       this._latproj = latproj;
-                       this._lat = latp2lat(latproj);
-                       this._lon = lon;
-                       markDirty();
-            dispatchEvent(new NodeMovedEvent(Connection.NODE_MOVED, this, oldLat, oldLon));
+            MainUndoStack.getGlobalStack().addAction(new MoveNodeAction(this, _lat, lon, setLatLonImmediate));
+        }
+        
+        public function setLatLon(lat:Number, lon:Number, performAction:Function):void {
+            performAction(new MoveNodeAction(this, lat, lon, setLatLonImmediate));
+        } 
+
+               public function setLonLatp(lon:Number,latproj:Number, performAction:Function):void {
+                   performAction(new MoveNodeAction(this, latp2lat(latproj), lon, setLatLonImmediate));
                }
 
         public override function toString():String {
             return "Node("+id+"@"+version+"): "+lat+","+lon+" "+getTagList();
         }
 
-               public override function remove():void {
-                       removeFromParents();
-                       deleted=true;
-            dispatchEvent(new EntityEvent(Connection.NODE_DELETED, this));
+               public override function remove(performAction:Function):void {
+                       performAction(new DeleteNodeAction(this, setDeletedState));
                }
-
+               
                internal override function isEmpty():Boolean {
                        return deleted;
                }
index a9743b9..dfa6b45 100644 (file)
@@ -1,5 +1,7 @@
 package net.systemeD.halcyon.connection {
 
+       import net.systemeD.halcyon.connection.actions.*;
+
     public class Relation extends Entity {
         private var members:Array;
                public static var entity_type:String = 'relation';
@@ -75,18 +77,8 @@ package net.systemeD.halcyon.connection {
             return members.length;
         }
 
-               public function removeMember(entity:Entity):void {
-                       var i:int;
-                       var lastRemoved:int = -1;
-                       while ((i=findEntityMemberIndex(entity))>-1) {
-                               members.splice(i, 1);
-                               lastRemoved = i;
-                       }
-                       entity.removeParent(this);
-                       markDirty();
-                       
-                       if ( lastRemoved >= 0 )
-                           dispatchEvent(new RelationMemberEvent(Connection.RELATION_MEMBER_REMOVED, entity, this, lastRemoved));
+               public function removeMember(entity:Entity, performAction:Function):void {
+                       performAction(new RemoveEntityFromRelationAction(this, entity, members));
                }
 
         public function removeMemberByIndex(index:uint):void {
@@ -101,12 +93,8 @@ package net.systemeD.halcyon.connection {
             dispatchEvent(new RelationMemberEvent(Connection.RELATION_MEMBER_REMOVED, entity, this, index));
         }
 
-               public override function remove():void {
-                       removeFromParents();
-                       for each (var member:RelationMember in members) { member.entity.removeParent(this); }
-                       members=[];
-                       deleted=true;
-            dispatchEvent(new EntityEvent(Connection.RELATION_DELETED, this));
+               public override function remove(performAction:Function):void {
+                       performAction(new DeleteRelationAction(this, setDeletedState, members));
                }
 
                internal override function isEmpty():Boolean {
index 9f45e0a..2f9289b 100644 (file)
@@ -20,11 +20,11 @@ package net.systemeD.halcyon.connection {
             var realVal:String = entity.getTag(oldKey);
             _key = key;
             if ( oldKey != null && realVal != null && realVal != "" )
-                entity.renameTag(oldKey, key);
+                entity.renameTag(oldKey, key, MainUndoStack.getGlobalStack().addAction);
         }
 
         public function set value(value:String):void {
-            entity.setTag(_key, value);
+            entity.setTag(_key, value, MainUndoStack.getGlobalStack().addAction);
         }
 
         private function tagChanged(event:TagEvent):void {
diff --git a/net/systemeD/halcyon/connection/UndoableAction.as b/net/systemeD/halcyon/connection/UndoableAction.as
new file mode 100644 (file)
index 0000000..774967b
--- /dev/null
@@ -0,0 +1,18 @@
+package net.systemeD.halcyon.connection {
+
+    public class UndoableAction {
+    
+        public static const FAIL:uint = 0;
+        public static const SUCCESS:uint = 1;
+        public static const NO_CHANGE:uint = 2; 
+    
+        public function doAction():uint { return FAIL; }
+        
+        public function undoAction():uint { return FAIL; }
+        
+        public function mergePrevious(previous:UndoableAction):Boolean {
+            return false;
+        }
+    }
+    
+}
diff --git a/net/systemeD/halcyon/connection/UndoableEntityAction.as b/net/systemeD/halcyon/connection/UndoableEntityAction.as
new file mode 100644 (file)
index 0000000..0dc9cfc
--- /dev/null
@@ -0,0 +1,29 @@
+package net.systemeD.halcyon.connection {
+
+    public class UndoableEntityAction extends UndoableAction {
+        private var wasDirty:Boolean;
+        protected var name:String;
+        protected var entity:Entity;
+            
+        public function UndoableEntityAction(entity:Entity, name:String) {
+            this.entity = entity;
+            this.name = name;
+        }
+            
+        protected function markDirty():void {
+            wasDirty = entity.isDirty;
+            if ( !wasDirty )
+                entity.markDirty();
+        }
+            
+        protected function markClean():void {
+            if ( !wasDirty )
+                entity.markClean(entity.id, entity.version);
+        }
+            
+        public function toString():String {
+            return name + " " + entity.getType() + " " + entity.id;
+        }
+    }
+}
+
index 0e0a543..b587702 100644 (file)
@@ -1,6 +1,7 @@
 package net.systemeD.halcyon.connection {
     import flash.geom.Point;
        import net.systemeD.halcyon.Globals;
+       import net.systemeD.halcyon.connection.actions.*;
 
     public class Way extends Entity {
         private var nodes:Array;
@@ -58,14 +59,8 @@ package net.systemeD.halcyon.connection {
             return nodes.indexOf(node);
         }
 
-               public function removeNode(node:Node):void {
-                       var i:int;
-                       while ((i=nodes.indexOf(node))>-1) {
-                               nodes.splice(i,1);
-               dispatchEvent(new WayNodeEvent(Connection.WAY_NODE_REMOVED, node, this, i));
-                       }
-                       node.removeParent(this);
-                       markDirty();
+               public function removeNode(node:Node, performAction:Function):void {
+                       performAction(new RemoveNodeFromWayAction(this, node, nodes));
                }
 
         public function removeNodeByIndex(index:uint):void {
@@ -87,39 +82,11 @@ package net.systemeD.halcyon.connection {
                        markDirty();
                }
 
-               public function mergeWith(way:Way,topos:int,frompos:int):void {
-                       var i:int;
-                       suspend();
-
-                       // merge relations
-                       for each (var r:Relation in way.parentRelations) {
-                               // ** needs to copy roles as well
-                               if (r.findEntityMemberIndex(this)==-1) {
-                                       r.appendMember(new RelationMember(this, ''));
-                               }
-                       }
-
-                       // merge tags
-                       var t:Object=way.getTagsHash();
-                       for (var k:String in t) {
-                               if (getTag(k) && getTag(k)!=t[k]) {
-                                       setTag(k,getTag(k)+"; "+t[k]);
-                                       // ** send a warning about tags not matching
-                               } else {
-                                       setTag(k,t[k]);
-                               }
-                       }
-
-                       // merge nodes
-                       if (frompos==0) { for (i=0; i<way.length;    i++) { addToEnd(topos,way.getNode(i)); } }
-                                          else { for (i=way.length-1; i>=0; i--) { addToEnd(topos,way.getNode(i)); } }
-
-                       // delete way
-                       way.remove();
-                       resume();
+               public function mergeWith(way:Way,topos:int,frompos:int, performAction:Function):void {
+                       performAction(new MergeWaysAction(this, way, topos, frompos));
                }
                
-               private function addToEnd(topos:int,node:Node):void {
+               public function addToEnd(topos:int,node:Node):void {
                        if (topos==0) {
                                if (nodes[0]==node) { return; }
                                prependNode(node);
@@ -185,6 +152,10 @@ package net.systemeD.halcyon.connection {
                        return (nodes[0].id==nodes[nodes.length-1].id && nodes.length>2);
                }
                
+               public override function remove(performAction:Function):void {
+                       performAction(new DeleteWayAction(this, setDeletedState, nodes));
+               }
+
                public function get clockwise():Boolean {
                        var lowest:uint=0;
                        var xmin:Number=-999999; var ymin:Number=-999999;
@@ -208,21 +179,6 @@ package net.systemeD.halcyon.connection {
                        return left;
                }
 
-               public override function remove():void {
-                       var node:Node;
-                       suspend();
-                       removeFromParents();
-                       while (nodes.length) { 
-                               node=nodes.pop();
-                               dispatchEvent(new WayNodeEvent(Connection.WAY_NODE_REMOVED, node, this, 0));
-                               node.removeParent(this);
-                               if (!node.hasParents) { node.remove(); }
-                       }
-                       deleted=true;
-            dispatchEvent(new EntityEvent(Connection.WAY_DELETED, this));
-                       resume();
-               }
-
                internal override function isEmpty():Boolean {
                        return (deleted || (nodes.length==0));
                }
diff --git a/net/systemeD/halcyon/connection/actions/DeleteNodeAction.as b/net/systemeD/halcyon/connection/actions/DeleteNodeAction.as
new file mode 100644 (file)
index 0000000..3a08001
--- /dev/null
@@ -0,0 +1,38 @@
+package net.systemeD.halcyon.connection.actions {
+
+    import net.systemeD.halcyon.connection.*;
+    
+    public class DeleteNodeAction extends UndoableEntityAction {
+        private var setDeleted:Function;
+        private var effects:CompositeUndoableAction;
+        
+        public function DeleteNodeAction(node:Node, setDeleted:Function) {
+            super(node, "Delete");
+            this.setDeleted = setDeleted;
+        }
+            
+        public override function doAction():uint {
+            var node:Node = entity as Node;
+            if ( node.isDeleted() )
+                return NO_CHANGE;
+
+            effects = new CompositeUndoableAction("Delete refs");            
+            node.removeFromParents(effects.push);
+            effects.doAction();
+            setDeleted(true);
+            markDirty();
+            node.dispatchEvent(new EntityEvent(Connection.NODE_DELETED, node));
+            
+            return SUCCESS;
+        }
+            
+        public override function undoAction():uint {
+            setDeleted(false);
+            markClean();
+            entity.dispatchEvent(new EntityEvent(Connection.NEW_NODE, entity));
+            effects.undoAction();
+            return SUCCESS;
+        }
+    }
+}
+
diff --git a/net/systemeD/halcyon/connection/actions/DeleteRelationAction.as b/net/systemeD/halcyon/connection/actions/DeleteRelationAction.as
new file mode 100644 (file)
index 0000000..3043ecb
--- /dev/null
@@ -0,0 +1,53 @@
+package net.systemeD.halcyon.connection.actions {
+
+    import net.systemeD.halcyon.connection.*;
+    
+    public class DeleteRelationAction extends UndoableEntityAction {
+        private var setDeleted:Function;
+        private var effects:CompositeUndoableAction;
+        private var memberList:Array;
+        private var oldMemberList:Array;
+        
+        public function DeleteRelationAction(relation:Relation, setDeleted:Function, memberList:Array) {
+            super(relation, "Delete");
+            this.setDeleted = setDeleted;
+            this.memberList = memberList;
+        }
+            
+        public override function doAction():uint {
+            var relation:Relation = entity as Relation;
+            if ( relation.isDeleted() )
+                return NO_CHANGE;
+
+            effects = new CompositeUndoableAction("Delete refs");
+                       relation.removeFromParents(effects.push);
+                       oldMemberList = memberList.slice();
+                       for each (var member:RelationMember in memberList) {
+                           member.entity.removeParent(relation);
+                       }
+                       memberList.splice(0, memberList.length);
+                       effects.doAction();
+                       setDeleted(true);
+                       markDirty();
+            relation.dispatchEvent(new EntityEvent(Connection.RELATION_DELETED, relation));
+
+            return SUCCESS;
+        }
+            
+        public override function undoAction():uint {
+            var relation:Relation = entity as Relation;
+            setDeleted(false);
+            markClean();
+            relation.dispatchEvent(new EntityEvent(Connection.NEW_RELATION, relation));
+            effects.undoAction();
+            for each(var member:RelationMember in oldMemberList) {
+                memberList.push(member);
+                relation.dispatchEvent(new RelationMemberEvent(
+                        Connection.RELATION_MEMBER_ADDED, member.entity, relation, 0));
+            }
+            
+            return SUCCESS;
+        }
+    }
+}
+
diff --git a/net/systemeD/halcyon/connection/actions/DeleteWayAction.as b/net/systemeD/halcyon/connection/actions/DeleteWayAction.as
new file mode 100644 (file)
index 0000000..00725ff
--- /dev/null
@@ -0,0 +1,58 @@
+package net.systemeD.halcyon.connection.actions {
+
+    import net.systemeD.halcyon.connection.*;
+    
+    public class DeleteWayAction extends UndoableEntityAction {
+        private var setDeleted:Function;
+        private var effects:CompositeUndoableAction;
+        private var nodeList:Array;
+        private var oldNodeList:Array;
+        
+        public function DeleteWayAction(way:Way, setDeleted:Function, nodeList:Array) {
+            super(way, "Delete");
+            this.setDeleted = setDeleted;
+            this.nodeList = nodeList;
+        }
+            
+        public override function doAction():uint {
+            var way:Way = entity as Way;
+            if ( way.isDeleted() )
+                return NO_CHANGE;
+
+            effects = new CompositeUndoableAction("Delete refs");            
+                       var node:Node;
+                       way.suspend();
+                       way.removeFromParents(effects.push);
+                       oldNodeList = nodeList.slice();
+                       while (nodeList.length > 0) {
+                               node=nodeList.pop();
+                               way.dispatchEvent(new WayNodeEvent(Connection.WAY_NODE_REMOVED, node, way, 0));
+                               node.removeParent(way);
+                               if (!node.hasParents) { node.remove(effects.push); }
+                       }
+                       effects.doAction();
+                       setDeleted(true);
+                       markDirty();
+            way.dispatchEvent(new EntityEvent(Connection.WAY_DELETED, way));
+                       way.resume();
+
+            return SUCCESS;
+        }
+            
+        public override function undoAction():uint {
+            var way:Way = entity as Way;
+                       way.suspend();
+            setDeleted(false);
+            markClean();
+            way.dispatchEvent(new EntityEvent(Connection.NEW_WAY, way));
+            effects.undoAction();
+            for each(var node:Node in oldNodeList) {
+                nodeList.push(node);
+                way.dispatchEvent(new WayNodeEvent(Connection.WAY_NODE_ADDED, node, way, 0));
+            }
+            way.resume();
+            return SUCCESS;
+        }
+    }
+}
+
diff --git a/net/systemeD/halcyon/connection/actions/MergeWaysAction.as b/net/systemeD/halcyon/connection/actions/MergeWaysAction.as
new file mode 100644 (file)
index 0000000..416b0f9
--- /dev/null
@@ -0,0 +1,81 @@
+package net.systemeD.halcyon.connection.actions {
+
+    import net.systemeD.halcyon.connection.*;
+    
+    public class MergeWaysAction extends CompositeUndoableAction {
+        private var way1:Way;
+        private var way2:Way;
+        private var toPos:uint;
+        private var fromPos:uint;
+    
+        public function MergeWaysAction(way1:Way, way2:Way, toPos:uint, fromPos:uint) {
+            super("Merge ways "+way1.id+" "+way2.id);
+            this.way1 = way1;
+            this.way2 = way2;
+            this.toPos = toPos;
+            this.fromPos = fromPos;
+        }
+        
+        public override function doAction():uint {
+            // subactions are added to the composite list -- we then
+            // execute them all at the bottom. Doing it this way gives
+            // us an automatic undo
+            
+                       way1.suspend();
+
+            mergeRelations();
+               mergeTags();
+               mergeNodes();
+                       way2.remove(push);
+
+            super.doAction();
+                       way1.resume();
+            
+            return SUCCESS;
+        }
+
+        public override function undoAction():uint {
+            way1.suspend();
+            super.undoAction();
+            way1.resume();
+            
+            return SUCCESS;
+        }
+        
+        public function mergeRelations():void {
+                       for each (var r:Relation in way2.parentRelations) {
+                               // ** needs to copy roles as well
+                               if (r.findEntityMemberIndex(way1)==-1) {
+                                       r.appendMember(new RelationMember(way1, ''));
+                               }
+                       }
+        }
+        
+        public function mergeTags():void {
+               var way1Tags:Object = way1.getTagsHash();
+                       var way2Tags:Object = way2.getTagsHash();
+                       for (var k:String in way2Tags) {
+                           var v1:String = way1Tags[k];
+                           var v2:String = way2Tags[k];
+                               if ( v1 && v1 != v2) {
+                                       way1.setTag(k, v1+"; "+v2, push);
+                                       // ** send a warning about tags not matching
+                               } else {
+                                       way1.setTag(k, v2, push);
+                               }
+                       }
+        }
+        
+        public function mergeNodes():void {
+            var i:int;
+               if (fromPos==0) {
+                   for (i = 0; i < way2.length; i++)
+                       way1.addToEnd(toPos, way2.getNode(i));
+               } else {
+                   for (i = way2.length-1; i >= 0; i--)
+                       way1.addToEnd(toPos, way2.getNode(i));
+               }
+        }   
+    }
+}
+
diff --git a/net/systemeD/halcyon/connection/actions/MoveNodeAction.as b/net/systemeD/halcyon/connection/actions/MoveNodeAction.as
new file mode 100644 (file)
index 0000000..63768d1
--- /dev/null
@@ -0,0 +1,59 @@
+package net.systemeD.halcyon.connection.actions {
+
+    import flash.utils.getTimer;
+
+    import net.systemeD.halcyon.connection.*;
+    
+    public class MoveNodeAction extends UndoableEntityAction {
+        private var createTime:uint;
+        private var oldLat:Number;
+        private var oldLon:Number;
+        private var newLat:Number;
+        private var newLon:Number;        
+        private var setLatLon:Function;
+        
+        public function MoveNodeAction(node:Node, newLat:Number, newLon:Number, setLatLon:Function) {
+            super(node, "Move to "+newLon+","+newLat);
+            this.newLat = newLat;
+            this.newLon = newLon;
+            this.setLatLon = setLatLon;
+            createTime = getTimer();
+        }
+            
+        public override function doAction():uint {
+            var node:Node = entity as Node;
+            oldLat = node.lat;
+            oldLon = node.lon;
+            if ( oldLat == newLat && oldLon == newLon )
+                return NO_CHANGE;
+            
+            setLatLon(newLat, newLon);
+            markDirty();
+            entity.dispatchEvent(new NodeMovedEvent(Connection.NODE_MOVED, node, oldLat, oldLon));
+            
+            return SUCCESS;
+        }
+            
+        public override function undoAction():uint {
+            setLatLon(oldLat, oldLon);
+            markDirty();
+            entity.dispatchEvent(new NodeMovedEvent(Connection.NODE_MOVED, Node(entity), newLat, newLon));
+            return SUCCESS;
+        }
+        
+        public override function mergePrevious(prev:UndoableAction):Boolean {
+            if ( !(prev is MoveNodeAction) )
+                return false;
+                
+            var prevMove:MoveNodeAction = prev as MoveNodeAction;
+            if ( prevMove.entity == entity && prevMove.createTime + 1000 > createTime ) {
+                oldLat = prevMove.oldLat;
+                oldLon = prevMove.oldLon;
+                return true;
+            }
+            
+            return false;
+        }
+    }
+}
+
diff --git a/net/systemeD/halcyon/connection/actions/RemoveEntityFromRelationAction.as b/net/systemeD/halcyon/connection/actions/RemoveEntityFromRelationAction.as
new file mode 100644 (file)
index 0000000..fc41dea
--- /dev/null
@@ -0,0 +1,60 @@
+package net.systemeD.halcyon.connection.actions {
+
+    import net.systemeD.halcyon.connection.*;
+    
+    public class RemoveEntityFromRelationAction extends UndoableEntityAction {
+        private var member:Entity;
+        private var memberList:Array;
+        private var memberRemovedFrom:Array;
+        private var removedMembers:Array;
+        
+        public function RemoveEntityFromRelationAction(rel:Relation, member:Entity, memberList:Array) {
+            super(rel, "Remove "+member.getType()+" "+member.id+" from ");
+            this.member = member;
+            this.memberList = memberList;
+        }
+            
+        public override function doAction():uint {
+            memberRemovedFrom = [];
+            removedMembers = [];
+            
+            var rel:Relation = entity as Relation;
+                       var i:int;
+                       while ((i=rel.findEntityMemberIndex(member))>-1) {
+                               var removed:RelationMember = memberList.splice(i, 1)[0];
+                               memberRemovedFrom.push(i);
+                               removedMembers.push(removed);
+                       }
+                       
+                       if ( removedMembers.length > 0 ) {
+                           member.removeParent(rel);
+                           markDirty();
+                           rel.dispatchEvent(new RelationMemberEvent(
+                               Connection.RELATION_MEMBER_REMOVED, member, rel, memberRemovedFrom[0]));
+                           return SUCCESS;
+                       }
+            
+            return NO_CHANGE;
+        }
+            
+        public override function undoAction():uint {
+            member.addParent(entity);
+            
+            var last:int = 0;
+            for (var i:uint = removedMembers.length - 1; i >= 0; i++) {
+                var removed:RelationMember = removedMembers[i];
+                var index:int = memberRemovedFrom[i];
+                memberList.splice(index, 0, removed);
+                last = index;
+            }
+
+            markClean();
+            
+                       entity.dispatchEvent(new RelationMemberEvent(
+                               Connection.RELATION_MEMBER_ADDED, member, Relation(entity), memberRemovedFrom[0]));
+            
+            return SUCCESS;
+        }
+    }
+}
+
diff --git a/net/systemeD/halcyon/connection/actions/RemoveNodeFromWayAction.as b/net/systemeD/halcyon/connection/actions/RemoveNodeFromWayAction.as
new file mode 100644 (file)
index 0000000..2e29db0
--- /dev/null
@@ -0,0 +1,50 @@
+package net.systemeD.halcyon.connection.actions {
+
+    import net.systemeD.halcyon.connection.*;
+    
+    public class RemoveNodeFromWayAction extends UndoableEntityAction {
+        private var node:Node;
+        private var nodeList:Array;
+        private var nodeRemovedFrom:Array;
+        
+        public function RemoveNodeFromWayAction(way:Way, node:Node, nodeList:Array) {
+            super(way, "Remove node "+node.id+" from ");
+            this.node = node;
+            this.nodeList = nodeList;
+        }
+            
+        public override function doAction():uint {
+            nodeRemovedFrom = [];
+                       var i:int;
+                       while ((i=nodeList.indexOf(node))>-1) {
+                               nodeList.splice(i,1);
+                               nodeRemovedFrom.push(i);
+               entity.dispatchEvent(new WayNodeEvent(Connection.WAY_NODE_REMOVED, node, Way(entity), i));
+                       }
+                       
+                       if ( nodeRemovedFrom.length > 0 ) {
+                           node.removeParent(entity);
+                           markDirty();
+                           return SUCCESS;
+                       }
+            
+            return NO_CHANGE;
+        }
+            
+        public override function undoAction():uint {
+            node.addParent(entity);
+            
+            for (var i:int = nodeRemovedFrom.length - 1; i >= 0; i--) {
+                var index:int = nodeRemovedFrom[i];
+                trace("node being added "+index+" "+i+" "+node);
+                nodeList.splice(index, 0, node);
+               entity.dispatchEvent(new WayNodeEvent(Connection.WAY_NODE_ADDED, node, Way(entity), index));
+            }
+            
+            markClean();
+            
+            return SUCCESS;
+        }
+    }
+}
+
diff --git a/net/systemeD/halcyon/connection/actions/SetTagAction.as b/net/systemeD/halcyon/connection/actions/SetTagAction.as
new file mode 100644 (file)
index 0000000..5f5a445
--- /dev/null
@@ -0,0 +1,57 @@
+package net.systemeD.halcyon.connection.actions {
+
+    import net.systemeD.halcyon.connection.*;
+    
+    public class SetTagAction extends UndoableEntityAction {
+        private var oldValue:String;
+        private var key:String;
+        private var value:String;
+        
+        public function SetTagAction(entity:Entity, key:String, value:String) {
+            super(entity, "Set "+key+"="+value);
+            this.key = key;
+            this.value = value;
+        }
+            
+        public override function doAction():uint {
+            var tags:Object = entity.getTagsHash();
+            oldValue = tags[key];
+            if ( oldValue != value ) {
+                if ( value == null || value == "" )
+                    delete tags[key];
+                else
+                    tags[key] = value;
+                markDirty();
+                entity.dispatchEvent(new TagEvent(Connection.TAG_CHANGED, entity, key, key, oldValue, value));
+                return SUCCESS;
+            } else {
+                return NO_CHANGE;
+            }
+        }
+            
+        public override function undoAction():uint {
+            var tags:Object = entity.getTagsHash();
+            if ( oldValue == null || oldValue == "" )
+                delete tags[key];
+            else
+                tags[key] = oldValue;
+            markClean();
+            entity.dispatchEvent(new TagEvent(Connection.TAG_CHANGED, entity, key, key, value, oldValue));
+            
+            return SUCCESS;
+        }
+        
+        public override function mergePrevious(prev:UndoableAction):Boolean {
+            if ( !(prev is SetTagAction) )
+                return false;
+                
+            var prevSet:SetTagAction = prev as SetTagAction;
+            if ( prevSet.entity == entity && prevSet.key == key ) {
+                oldValue = prevSet.oldValue;
+                return true;
+            }
+            return false;
+        }
+    }
+}
+
diff --git a/net/systemeD/halcyon/connection/actions/SetTagKeyAction.as b/net/systemeD/halcyon/connection/actions/SetTagKeyAction.as
new file mode 100644 (file)
index 0000000..ac486e9
--- /dev/null
@@ -0,0 +1,41 @@
+package net.systemeD.halcyon.connection.actions {
+
+    import net.systemeD.halcyon.connection.*;
+    
+    public class SetTagKeyAction extends UndoableEntityAction {
+        private var oldKey:String;
+        private var newKey:String;
+        
+        public function SetTagKeyAction(entity:Entity, oldKey:String, newKey:String) {
+            super(entity, "Rename tag "+oldKey+"->"+newKey);
+            this.oldKey = oldKey;
+            this.newKey = newKey;
+        }
+            
+        public override function doAction():uint {
+            var tags:Object = entity.getTagsHash();
+            var value:String = tags[oldKey];
+            if ( oldKey != newKey ) {
+                delete tags[oldKey];
+                tags[newKey] = value;
+                markDirty();
+                entity.dispatchEvent(new TagEvent(Connection.TAG_CHANGED, entity, oldKey, newKey, value, value));
+                return SUCCESS;
+            } else {
+                return NO_CHANGE;
+            }
+        }
+            
+        public override function undoAction():uint {
+            var tags:Object = entity.getTagsHash();
+            var value:String = tags[newKey];
+            delete tags[newKey];
+            tags[oldKey] = value;
+            markClean();
+            entity.dispatchEvent(new TagEvent(Connection.TAG_CHANGED, entity, newKey, oldKey, value, value));
+            
+            return SUCCESS;
+        }
+    }
+}
+
index 92941c0..8184853 100644 (file)
 
       public function removeTag():void {
           var k:String = advancedTagGrid.selectedItem.key;
-          selectedEntity.setTag(k, null);
+          selectedEntity.setTag(k, null, MainUndoStack.getGlobalStack().addAction);
       }
       
       public function addToRelation():void {
               return;
 
           var newFeature:Feature = tw.selectedType;
-          
+          var undoStack:Function = MainUndoStack.getGlobalStack().addAction;
+          var action:CompositeUndoableAction = new CompositeUndoableAction(
+                  "Set "+selectedEntity.getType()+" "+selectedEntity.id+" to "+feature.name);
+
           // remove tags from the current feature
           if ( feature != null ) {
               for each( var oldtag:Object in feature.tags ) {
-                  selectedEntity.setTag(oldtag["k"], null);
+                  selectedEntity.setTag(oldtag["k"], null, action.push);
               }
           }
           
           // set tags for new feature
           if ( newFeature != null ) {
               for each( var newtag:Object in newFeature.tags ) {
-                  selectedEntity.setTag(newtag["k"], newtag["v"]);
+                  selectedEntity.setTag(newtag["k"], newtag["v"], action.push);
               }
           }
           
+          undoStack(action);
           popupChange.close();
       }
       
index f684dab..a1252de 100644 (file)
@@ -56,7 +56,7 @@ package net.systemeD.potlatch2 {
 
                public function doDelete():void {
                        if (entity is Node) { controller.connection.unregisterPOI(Node(entity)); }
-                       entity.remove();
+                       entity.remove(MainUndoStack.getGlobalStack().addAction);
 
                        if (controller.state is SelectedWayNode) {
                                controller.setState(new SelectedWay(SelectedWayNode(controller.state).selectedWay));
@@ -84,4 +84,4 @@ package net.systemeD.potlatch2 {
                }
 
        }
-}
\ No newline at end of file
+}
index 351c8c4..6b2198f 100644 (file)
@@ -99,7 +99,7 @@ package net.systemeD.potlatch2.controller {
                protected function stopDrawing():ControllerState {
                        if ( selectedWay.length<2) {
                                controller.map.setHighlight(selectedWay, { showNodes: false });
-                               selectedWay.remove();
+                               selectedWay.remove(MainUndoStack.getGlobalStack().addAction);
                                // delete controller.map.ways[selectedWay.id];
                                return new NoSelection();
                        } else if ( leaveNodeSelected ) {
index 22210da..571e4fd 100644 (file)
@@ -64,7 +64,7 @@ package net.systemeD.potlatch2.controller {
                
                public function deletePOI():ControllerState {
                        controller.connection.unregisterPOI(selectedNode);
-                       selectedNode.remove();
+                       selectedNode.remove(MainUndoStack.getGlobalStack().addAction);
                        return new NoSelection();
                }
                
index 3d09f38..affae2d 100644 (file)
@@ -90,19 +90,30 @@ package net.systemeD.potlatch2.controller {
                        return node;
         }
 
-               protected function mergeWith(way:Way):Boolean {
-                       // ** needs to prefer positive to negative IDs
+               protected function mergeWith(otherWay:Way):Boolean {
+                       var way1:Way;
+                       var way2:Way;
+                       if ( selectedWay.id < otherWay.id && selectedWay.id >= 0 ) {
+                           way1 = selectedWay;
+                           way2 = otherWay;
+                       } else {
+                           way1 = otherWay;
+                           way2 = selectedWay;
+                       }
+                       
+                       var undo:Function = MainUndoStack.getGlobalStack().addAction;
+                       
                        // find common point
-                       if (way==selectedWay) { return false; }
-                       if      (selectedWay.getNode(0)   ==way.getNode(0)   ) { selectedWay.mergeWith(way,0,0); }
-                       else if (selectedWay.getNode(0)   ==way.getLastNode()) { selectedWay.mergeWith(way,0,way.length-1); }
-                       else if (selectedWay.getLastNode()==way.getNode(0)   ) { selectedWay.mergeWith(way,selectedWay.length-1,0); }
-                       else if (selectedWay.getLastNode()==way.getLastNode()) { selectedWay.mergeWith(way,selectedWay.length-1,way.length-1); }
+                       if (way1 == way2) { return false; }
+                       if      (way1.getNode(0)   ==way2.getNode(0)   ) { way1.mergeWith(way2,0,0,undo); }
+                       else if (way1.getNode(0)   ==way2.getLastNode()) { way1.mergeWith(way2,0,way2.length-1,undo); }
+                       else if (way1.getLastNode()==way2.getNode(0)   ) { way1.mergeWith(way2,way1.length-1,0,undo); }
+                       else if (way1.getLastNode()==way2.getLastNode()) { way1.mergeWith(way2,way1.length-1,way2.length-1,undo); }
                        return true;
                }
         
                public function deleteWay():ControllerState {
-                       selectedWay.remove();
+                       selectedWay.remove(MainUndoStack.getGlobalStack().addAction);
                        return new NoSelection();
                }
 
index 340a112..149e349 100644 (file)
@@ -126,7 +126,7 @@ package net.systemeD.potlatch2.controller {
                }
                
                public function deleteNode():ControllerState {
-                       selectedNode.remove();
+                       selectedNode.remove(MainUndoStack.getGlobalStack().addAction);
                        return new SelectedWay(selectedWay);
                }
 
index 0417654..c600745 100644 (file)
@@ -27,7 +27,7 @@ package net.systemeD.potlatch2.mapfeatures.editors {
       
       public function set value(val:String):void {
           if ( _entity != null )
-              _entity.setTag(_factory.key, val);
+              _entity.setTag(_factory.key, val, MainUndoStack.getGlobalStack().addAction);
       }
 
       public function set factory(factory:SingleTagEditorFactory):void {
index 4283d5a..38a83f3 100644 (file)
@@ -1,7 +1,9 @@
 package net.systemeD.potlatch2.tools {
        import net.systemeD.halcyon.Map;
+       import net.systemeD.halcyon.connection.CompositeUndoableAction;
        import net.systemeD.halcyon.connection.Way;
        import net.systemeD.halcyon.connection.Node;
+       import net.systemeD.halcyon.connection.MainUndoStack;
 
        public class Circularise {
 
@@ -38,12 +40,15 @@ package net.systemeD.potlatch2.tools {
                        }
                        d=d/way.length;
                        
+
+                       var action:CompositeUndoableAction = new CompositeUndoableAction("Straighten");
+
                        // Move each node
                        for (i=0; i<way.length-1; i++) {
                                n=way.getNode(i);
                                var c:Number=Math.sqrt(Math.pow(n.lon-cx,2)+Math.pow(n.latp-cy,2));
                                n.setLonLatp(cx+(n.lon -cx)/c*d,
-                                            cy+(n.latp-cy)/c*d);
+                                            cy+(n.latp-cy)/c*d, action.push);
                        }
 
                        // Insert extra nodes to make circle
@@ -83,4 +88,4 @@ package net.systemeD.potlatch2.tools {
                        }
                }
        }
-}
\ No newline at end of file
+}
index b171c7d..7cf40e0 100644 (file)
@@ -1,7 +1,9 @@
 package net.systemeD.potlatch2.tools {
        import net.systemeD.halcyon.Map;
+       import net.systemeD.halcyon.connection.CompositeUndoableAction;
        import net.systemeD.halcyon.connection.Way;
        import net.systemeD.halcyon.connection.Node;
+       import net.systemeD.halcyon.connection.MainUndoStack;
 
        public class Straighten {
 
@@ -14,6 +16,8 @@ package net.systemeD.potlatch2.tools {
                        
                        // ** could potentially do the 'too bendy?' check here as per Potlatch 1
                        
+                       var action:CompositeUndoableAction = new CompositeUndoableAction("Straighten");
+                       
                        var todelete:Array=[];
                        var n:Node;
                
@@ -28,14 +32,16 @@ package net.systemeD.potlatch2.tools {
                                                      (n.latp-a.latp)*(b.latp-a.latp))/
                                                     (Math.pow(b.lon-a.lon,2)+Math.pow(b.latp-a.latp,2));
                                        n.setLonLatp(a.lon +u*(b.lon -a.lon ),
-                                                    a.latp+u*(b.latp-a.latp));
+                                                    a.latp+u*(b.latp-a.latp), action.push);
                                        
                                } else {
                                        // safe to delete
                                        if (todelete.indexOf(n)==-1) { todelete.push(n); }
                                }
                        }
-                       for each (n in todelete) { n.remove(); }
+                       for each (n in todelete) { n.remove(action.push); }
+                       
+                       MainUndoStack.getGlobalStack().addAction(action);
                }
        }
 }
index ea1eef3..d5dfc98 100755 (executable)
         <mx:PopUpButton id="styleButton" label="Map Style" openAlways="true"
             creationComplete="styleButton.popUp = new StyleSelector();"/>
         <mx:Spacer width="100%"/>
+        <mx:Button label="Undo" click="MainUndoStack.getGlobalStack().undo();"
+            enabled="{MainUndoStack.getGlobalStack().canUndo()}"/>
+        <mx:Button label="Redo" click="MainUndoStack.getGlobalStack().redo();"
+            enabled="{MainUndoStack.getGlobalStack().canRedo()}"/>
+        <mx:Spacer width="100%"/>
         <mx:Button label="Help" click="new HelpDialog().init();" />
         <mx:Button label="Options" click="new OptionsDialog().init();" />
         <mx:Button label="Save" icon="@Embed('embedded/save.svg')" click="SaveManager.saveChanges();"/>
             var mapLoc:Point = Globals.vars.root.globalToLocal(new Point(event.stageX, event.stageY));
             var lat:Number = Globals.vars.root.coord2lat(mapLoc.y);
             var lon:Number = Globals.vars.root.coord2lon(mapLoc.x);
+            
+            var createAction:CompositeUndoableAction = new CompositeUndoableAction("Create POI");
+            
             var node:Node = Connection.getConnectionInstance().createNode({}, lat, lon);
             for each ( var tag:Object in tags ) {
-              node.setTag(tag.k, tag.v);
+                node.setTag(tag.k, tag.v, createAction.push);
             }
                        Connection.getConnectionInstance().registerPOI(node);
+                       MainUndoStack.getGlobalStack().addAction(createAction);
             theController.setState(new SelectedPOINode(node));
         }