Select to view content in your preferred language

Undo Move Graphic

2565
10
07-19-2011 08:47 AM
MarkHoover
Frequent Contributor
Hello,

I have been successfully implementing the UndoManager for a variety of tasks, but I have gotten stuck and could use some help.

My UndoManager needs to be able to handle edits applied via the Edit Toolbar.  It works great for Scale and Rotate, but Move doesn't undo.

To implement this, my Undo and Redo actions have references to the old and new graphic (before and after the edit) and use Graphic.setGeometry(geometry) to undo/redo the actions.  Like I said, this works beautifully for Scale and Rotate, but my Moves don't Undo.

So my question is, is the graphic's location not stored in the geometry of a graphic?  Or is something else, unique to the positioning of a graphic, causing this to fail?

If anyone could provide any help, that would be great.

Thanks,
Mark
0 Kudos
10 Replies
MarkHoover
Frequent Contributor
For some clarity...My editor Toolbar listens for the following events:

onGraphicFirstMove
onScaleFirstMove
onRotateFirstMove

When fired, a function runs that creates a new graphic (not on the map, just in a variable) that grabs the state of the graphic.  There are also corresponding stop listeners (i.e. onGraphicMoveStop) that grab and store the new state of the graphic.  My UndoManager simply tries to set the geometry of the graphic to the old state of the graphic's geometry.  This goes off without a hitch for scale and rotate, but not move.

Is the onGraphicFirstMove a bit different from the other two listeners I'm using?  Can anyone help?

How about an alternate approach?

onGraphicMoveStop returns a transform Object.  Nothing seems to have an applyTransformation() method where I could make use of this object.  The object has properties such as dx, dy, xx, xy, etc.  Would anyone know how to use this transform Object to reverse the position of the graphic?
0 Kudos
MarkHoover
Frequent Contributor
handleGraphicEdit: function (graphic) {
    oldGraphic = new esri.Graphic(graphic.geometry, graphic.symbol);
}

This is my handler for each of the three events I listen for, onGraphicFirstMove, onScaleFirstMove, onRotateFirstMove.  As I've said, this works for scale and rotate (when I undo, I set the geometry of the desired graphic to the geometry of my oldGraphic variable.

However, this oldGraphic's geometry gets changed to the new location of the graphic by the time the edit finishes (when the listener for onGraphicMoveStop fires). 

What makes this even stranger is that if I perform a scale, then a move, then hit undo twice...the first undo causes nothing (the failing move undo), but the second undo successfully undoes both the scale (as expected) AND the move.  So the oldGraphic variable successfully maintains the original geometry in the onScaleFirstMove and onRotateFirstMove events, but not after the onGraphicFirstMove event.

I noticed in the API, that the "graphic" argument that each of these listeners provides access to, is slightly different for Move than for Scale and Rotate.  For Move, this graphic is described as "The graphic associated with the toolbar.".  For Rotate, this graphic is described as "The rotated graphic." (Likewise Scale is "The scaled graphic.")

Is the Move event listener's callback wired up differently, and should that be the case?  Why is my variable oldGraphic, a new graphic object that accepts the geometry and symbol of the graphic at the start of the edit, getting altered to become the updated geometry when the edit is finished in the case of Move?

All my workaround attempts have failed, getting pretty disheartening.  If someone from Esri could explain this or provide a sample showing how to undo a Move performed by the Edit toolbar, that would be a huge help.

Thanks
0 Kudos
derekswingley1
Deactivated User
Hi Mark,

The editor can manage undo/redo for you. I tweaked the Edit Points of Interest sample to show this (remember to update the proxy):

<!doctype html>
<html lang="en">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=7, IE=9" />
    <!--The viewport meta tag is used to improve the presentation and behavior of the samples 
      on iOS devices-->
    <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no"/>
    <title>Editing Points with Undo/Redo</title>

    <!-- include dojo theme -->
    <link rel="stylesheet" type="text/css" href="http://serverapi.arcgisonline.com/jsapi/arcgis/2.4/js/dojo/dijit/themes/claro/claro.css">
    <style type="text/css">
      html, body {
        height: 100%;
        width: 100%;
        margin: 0; 
        padding: 0;
        overflow:hidden;
        background:#fff;
      }
      #header{
        border: solid 1px #232416;
        color:#453823;
        font-weight:600;
        font-size:14px;
        height:40px;
      }

      #map{
        border: solid 1px #232416;
        padding:0;
      }
      #leftPane{
        width:200px;
        border:none;
      }
      .templatePicker {
        border:solid 2px #232416 !important;
      }
 
      .dj_ie .infowindow .window .top .right .user .content { position: relative; }
      .dj_ie .simpleInfoWindow .content {position: relative;}  
      .esriAttributeInspector .atiRichTextField .dijitEditorIFrameContainer{
        height:60px;
      }
  
    </style>

    <script type="text/javascript">
      dojoConfig = {
        parseOnLoad:true
      };
    </script>

    <!-- reference ArcGIS JavaScript API -->
    <script type="text/javascript" src="http://serverapi.arcgisonline.com/jsapi/arcgis/?v=2.4"></script>
    <script type="text/javascript">
      dojo.require("dijit.layout.ContentPane");
      dojo.require("dijit.layout.BorderContainer");
      dojo.require("esri.map");
      dojo.require("esri.dijit.editing.Editor-all");
      dojo.require("esri.dijit.editing.editOperation");
      dojo.require("esri.undoManager");


      var map, resizeTimer, myEditor;
      
      function init() {
        // Specify your proxy URL
        esri.config.defaults.io.proxyUrl = "/arcgisserver/apis/javascript/proxy/proxy.ashx";
        esri.config.defaults.geometryService = new esri.tasks.GeometryService("http://sampleserver3.arcgisonline.com/arcgis/rest/services/Geometry/GeometryServer");
        
        var initialExtent = new esri.geometry.Extent({"xmin":-13114358,"ymin":4069927,"xmax":-13097981,"ymax":4077246,"spatialReference":{"wkid":102100}});
        map = new esri.Map("map", { extent: initialExtent });
        
        dojo.connect(map, "onLoad", function() {
          dojo.connect(dijit.byId('map'), 'resize', function() {
            resizeMap();
          });
        });
        
        dojo.connect(map, "onLayersAddResult", initEditor);
        
        var basemap = new esri.layers.ArcGISTiledMapServiceLayer("http://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer");
        map.addLayer(basemap);

        var pointsOfInterest = new esri.layers.FeatureLayer("http://sampleserver3.arcgisonline.com/ArcGIS/rest/services/Fire/Sheep/FeatureServer/0",{
          mode: esri.layers.FeatureLayer.MODE_ONDEMAND, 
          outFields: ['*']
        });

        dojo.connect(pointsOfInterest, 'onLoad', function(poi) {
          poi.setSelectionSymbol(new esri.symbol.SimpleMarkerSymbol('circle', 32, null, new dojo.Color([0, 0, 0, 0.25])));
        });

        map.addLayers([pointsOfInterest]);
      }

      function initEditor(results) {
        var layers = dojo.map(results, function(result) {
          var fieldInfos= dojo.map(result.layer.fields,function(field){
            if(field.name === 'description'){
              return {'fieldName': field.name,'label':'Details',stringFieldOption:esri.dijit.AttributeInspector.STRING_FIELD_OPTION_RICHTEXT,richTextPlugins:['undo','redo','bold', 'italic']}
            }
            else{
              return {'fieldName': field.name,'label':field.alias}
            }
          });
          return {featureLayer:result.layer,'fieldInfos':fieldInfos}
        });
        var templateLayers = dojo.map(results,function(result){
          return result.layer;
        });
        var templatePicker = new esri.dijit.editing.TemplatePicker({
          featureLayers: templateLayers,
          rows: 'auto',
          columns: 'auto',
          style:'height:98%;width:98%;'
        },'templatePickerDiv');
        
        templatePicker.startup();

        // Create Editor and specify an undoManager
        var undoManager = new esri.UndoManager();
        var settings = {
          templatePicker: templatePicker,
          map: map,
          layerInfos:layers,
          toolbarVisible: false,
          enableUndoRedo: true,
          undoManager: undoManager
        }
        var params = {settings: settings};
        myEditor = new esri.dijit.editing.Editor(params);
        myEditor.startup();
        map.infoWindow.resize(275,220);

        dojo.connect(dojo.byId('undo'), 'onclick', function(click) {
          if ( myEditor.undoManager.canUndo ) {
            myEditor.undoManager.undo();
            console.log('called undo');
          } else {
            alert('Nothing to undo.');
          }
        });
        dojo.connect(dojo.byId('redo'), 'onclick', function(click) {
          if ( myEditor.undoManager.canRedo ) {
            myEditor.undoManager.redo();
            console.log('called redo');
          } else {
            alert('Nothing to redo.');
          }
        });
      }
      //Handle resize of browser
      function resizeMap(){
        clearTimeout(resizeTimer);
        resizeTimer = setTimeout(function(){
          map.resize();
          map.reposition();
        }, 500);
      }

      dojo.addOnLoad(init);
    </script>
  </head>
  <body class="claro">    
    <div data-dojo-type="dijit.layout.BorderContainer" data-dojo-props="gutters:true, design:'sidebar'" style="width:100%;height:100%;">
      <div data-dojo-type="dijit.layout.ContentPane" id="header" data-dojo-props="region:'top'">
        Fire Map
        <div class="undoButtons">
          <button id="undo"  data-dojo-type="dijit.form.Button">
            Undo
          </button>
          <button id="redo"  data-dojo-type="dijit.form.Button">
            Redo
          </button>
        </div>
      </div>
      <div id="map" data-dojo-type="dijit.layout.ContentPane" data-dojo-props="region:'center'"></div>
      <div id="leftPane"  data-dojo-type="dijit.layout.ContentPane"  data-dojo-props="region:'left'">
        <div id="templatePickerDiv"></div>
      </div>
    </div>
  </body>
</html>

0 Kudos
MarkHoover
Frequent Contributor
Hmm, well that is interesting, and I didn't know that widget had those capabilities...but I'm using the Edit Toolbar to handle editing of graphics (and actions thereof such as Scale, Rotate, and Move), not editing of features from a feature service.
0 Kudos
derekswingley1
Deactivated User
Can you post a more complete code sample?
0 Kudos
MarkHoover
Frequent Contributor
When I have some time I'll try and build a quick and dirty sample that shows what's happening.  Hopefully tomorrow or early next week.

Thanks for the assistance.
0 Kudos
JianHuang
Deactivated User
Mark,

One thing you may want to look at is the geometry passed into the method setGeometry. In order to avoid referring to the new geometry that you moved, it should be serialized to json to store the old and new geometry. Then, when redo/undo the operation, deserialize it by calling esri.geometry.fromJson(geometryJson).
Hope this helps.
0 Kudos
MarkHoover
Frequent Contributor
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> 
<html>   
  <head> 
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
    <meta http-equiv="X-UA-Compatible" content="IE=7, IE=9" /> 
    <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no"/> 
    <title> 
      Maps Toolbar 
    </title> 
    <link rel="stylesheet" type="text/css" href="http://serverapi.arcgisonline.com/jsapi/arcgis/2.4/js/dojo/dijit/themes/claro/claro.css"> 
    <style type="text/css"> 
      html, body {  
        height: 100%; width: 100%; margin: 0; padding: 0;  
      }  
    </style> 
    <script type="text/javascript"> 
      dojoConfig = { 
        parseOnLoad: true 
      } 
    </script> 
    <script type="text/javascript" src="http://serverapi.arcgisonline.com/jsapi/arcgis/?v=2.4"> 
    </script> 
    <script type="text/javascript"> 
      dojo.require("esri.map"); 
      dojo.require("dijit.dijit"); // optimize: load dijit layer 
      dojo.require("dijit.layout.BorderContainer"); 
      dojo.require("dijit.layout.ContentPane"); 
      dojo.require("esri.toolbars.edit"); 
                  dojo.require("dijit.form.Button"); 
      dojo.require("dijit.Menu");
 dojo.require("esri.undoManager"); 

      var map, editToolbar, undoManager; 
 var oldGraphic, currentGraphic;
 
      function init() { 
        var startExtent = new esri.geometry.Extent({"xmin":-10527519,"ymin":3459600,"xmax":11408073,"ymax":13047861,"spatialReference":{"wkid":102100}}); 
        map = new esri.Map("map", { 
          extent: startExtent, 
          wrapAround180:true 
        }); 
        dojo.connect(map, "onLoad", createToolbar); 
        var basemap = new esri.layers.ArcGISTiledMapServiceLayer("http://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer"); 
        map.addLayer(basemap); 
 undoManager = new esri.UndoManager();
 buildOperations();
      } 

 function buildOperations(){
  dojo.declare('operations.graphics.applyEdit', esri.OperationBase, {
            label: 'Apply Edit',
            constructor: function (parameters) {
            },
            performUndo: function () {
  var test = map.graphics.graphics;
                for (var i = 0; i < test.length; i++) {
                    if (test.geometry == currentGraphic.geometry) {
                        test.setGeometry(oldGraphic.geometry);
                        editToolbar.refresh();
                    }
                }
            },
            performRedo: function () {
                for (var i = 0; i < map.graphics.length; i++) {
                    if (map.graphics.geometry == oldGraphic.geometry) {
                        map.graphics.setGeometry(currentGraphic.geometry);
                        editToolbar.refresh();
                    }
                }
            }
        });
 }
 
      function createToolbar(map) { 
        dojo.connect(dijit.byId('map'), 'resize', resizeMap); 
        addGraphics(); 
        editToolbar = new esri.toolbars.Edit(map); 
 
        //Activate the toolbar when you click on a graphic 
        dojo.connect(map.graphics, "onClick", function(evt) { 
          dojo.stopEvent(evt); 
          activateToolbar(evt.graphic); 
        }); 
        //deactivate the toolbar when you click outside a graphic 
        dojo.connect(map,"onClick", function(evt){ 
          editToolbar.deactivate(); 
        }); 
      } 
      function addGraphics() { 
        //add pre-defined geometries to map 
        var polygonSymbol = new esri.symbol.SimpleFillSymbol(esri.symbol.SimpleFillSymbol.STYLE_SOLID, new esri.symbol.SimpleLineSymbol(esri.symbol.SimpleLineSymbol.STYLE_DASHDOT, new 

dojo.Color([255, 0, 0]), 2), new dojo.Color([255, 255, 0, 0.25])); 
        var polylineSymbol = new esri.symbol.SimpleLineSymbol(esri.symbol.SimpleLineSymbol.STYLE_SOLID, new dojo.Color([255, 0, 0]), 3); 
        var polyline = new 

esri.geometry.Polyline({"paths":[[[-12484306.95575803,7244028.536719631],[-7318386.8361340165,10061803.147423638],[-3013453.403114006,10727111.04161764]]],"spatialReference":{"wkid":102100}}); 
        var polygon = new esri.geometry.Polygon({ 
          "rings": [ 
            [ 
              [-4226661.916056009, 8496372.808143634], 
              [-3835304.3312360067, 8731187.359035634], 
              [-2269873.991956003, 9005137.668409634], 
              [-1213208.5129420012, 8613780.083589634], 
              [-1017529.7205320001, 8065879.464841632], 
              [-1213208.5129420012, 7478843.087611631], 
              [-2230738.233474003, 6891806.710381631], 
              [-2935181.8861500043, 6735263.6764536295], 
              [-3522218.263380006, 6891806.710381631], 
              [-3952711.606682008, 7165757.01975563], 
              [-4265797.674538009, 7283164.295201631], 
              [-4304933.433020009, 7635386.121539632], 
              [-4304933.433020009, 7674521.880021632], 
              [-4226661.916056009, 8496372.808143634] 
            ] 
          ], 
          "spatialReference": { 
            "wkid": 102100 
          } 
        }); 
        var triangle = new esri.geometry.Polygon({ 
          "rings": [ 
            [ 
              [2426417.02588401, 8535508.566625634], 
              [4304933.433020014, 12292541.380897645], 
              [6183449.840156019, 8535508.566625634], 
              [2426417.02588401, 8535508.566625634] 
            ] 
          ], 
          "spatialReference": { 
            "wkid": 102100 
          } 
        }); 
 
        map.graphics.add(new esri.Graphic(polyline, polylineSymbol)); 
        map.graphics.add(new esri.Graphic(polygon, polygonSymbol)); 
        map.graphics.add(new esri.Graphic(triangle, polygonSymbol)); 
      } 
 
      function activateToolbar(graphic) { 
        var tool = 0; 
         
        if (dijit.byId("tool_move").checked) { 
          tool = tool | esri.toolbars.Edit.MOVE;  
        } 
        if (dijit.byId("tool_scale").checked) { 
          tool = tool | esri.toolbars.Edit.SCALE;  
        } 
        if (dijit.byId("tool_rotate").checked) { 
          tool = tool | esri.toolbars.Edit.ROTATE;  
        } 
        editToolbar.activate(tool, graphic);
 dojo.connect(map, 'onClick', dojo.hitch(this, this.handleMapClick));
        dojo.connect(editToolbar, 'onGraphicFirstMove', handleGraphicEdit);
        dojo.connect(editToolbar, 'onScaleFirstMove', handleGraphicEdit);
        dojo.connect(editToolbar, 'onRotateFirstMove', handleGraphicEdit);
        dojo.connect(editToolbar, 'onGraphicMoveStop', handleGraphicEditStop);
        dojo.connect(editToolbar, 'onScaleStop', handleGraphicEditStop);
        dojo.connect(editToolbar, 'onRotateStop', handleGraphicEditStop);
 
      } 

 function handleGraphicEdit(graphic){
  oldGraphic = new esri.Graphic(graphic.geometry, graphic.symbol);
 }

 function handleGraphicEditStop(graphic){
  currentGraphic = new esri.Graphic(graphic.geometry, graphic.symbol);
  
  var operation = new operations.graphics.applyEdit({
          graphics: map.graphics,
          currentGraphic: currentGraphic,
          oldGraphic: oldGraphic
      });
      undoManager.add(operation);
 }

      function resizeMap() { 
        //resize the map when the browser resizes - view the 'Resizing and repositioning the map' section in  
        //the following help topic for more details http://help.esri.com/EN/webapi/javascript/arcgis/help/jshelp_start.htm#jshelp/inside_guidelines.htm 
        var resizeTimer; 
        clearTimeout(resizeTimer); 
        resizeTimer = setTimeout(function() { 
          map.resize(); 
          map.reposition(); 
        }, 500); 
      } 
      dojo.addOnLoad(init); 
    </script> 
  </head> 
   
  <body class="claro"> 
  <div id="mainWindow" data-dojo-type="dijit.layout.BorderContainer" data-dojo-props="design:'headline', gutters:'false'" style="width:100%; height:100%;"> 
    <div id="header" data-dojo-type="dijit.layout.ContentPane" data-dojo-props="region:'top'" style="height:58px;text-align:left;font-weight:bold;font-size:14px;color:#400D12;"> 
      <span>Specify editing options then click a graphic to edit the shape. Click outside the graphic to deactivate the toolbar.</span><br /> 
      <div id="tool_move" data-dojo-type="dijit.form.ToggleButton" data-dojo-props="checked:'true', iconClass:'dijitCheckBoxIcon'">Move</div> 
      <div id="tool_scale" data-dojo-type="dijit.form.ToggleButton" data-dojo-props="checked:'true', iconClass:'dijitCheckBoxIcon'">Scale</div> 
      <div id="tool_rotate" data-dojo-type="dijit.form.ToggleButton" data-dojo-props="checked:'true', iconClass:'dijitCheckBoxIcon'">Rotate</div> 
 <button data-dojo-type="dijit.form.Button" data-dojo-props="onClick:function(){undoManager.undo()}">Undo</button>
 <button data-dojo-type="dijit.form.Button" data-dojo-props="onClick:function(){undoManager.redo()}">Redo</button>
    </div> 
    <div id="map" data-dojo-type="dijit.layout.ContentPane" style="border:solid 2px cornflowerblue;margin:5px;" data-dojo-props="region:'center'"> 
    </div> 
    </div> 
  </body> 
 
</html>
0 Kudos
MarkHoover
Frequent Contributor
Jian,
I played with converting the graphics to Json first, but not exactly in the way you described.  I will give that a spin tomorrow.

In the meantime, the above block of code is my sample that shows the behavior I'm describing.  It is edited from the Edit Toolbar sample provided in the JavaScript API.

To view the behvaior I've been describing, try the following:

1. Click a graphic (i.e. the large polygon)
2. Scale the graphic
3. Click Undo (success)
4. Rotate the graphic
5. Click Undo (success)
6. Move the graphic
7. Click Undo (Failure)

It's a very quick and dirty sample but at least you can see what I'm describing.
0 Kudos