Merge pull request #49 from jabadia/jabadia-edits-store

jabadia: edits store
This commit is contained in:
Andy 2014-01-21 09:04:48 -08:00
commit 8f0da79911
3 changed files with 792 additions and 0 deletions

219
lib/edit/editsStore.js Normal file
View File

@ -0,0 +1,219 @@
"use strict"
define(["esri/graphic"],function(Graphic)
{
/* private consts */
var EDITS_QUEUE_KEY = "esriEditsQueue";
var REDO_STACK_KEY = "esriRedoStack";
var SEPARATOR = "|@|";
return {
//
// public interface
//
// enum
ADD: "add",
UPDATE: "update",
DELETE:"delete",
pushEdit: function(operation,layer,graphic)
{
var edit = {
operation: operation,
layer: layer,
graphic: this._serialize(graphic)
}
var edits = this._retrieveEditsQueue();
if( this._isEditDuplicated(edit,edits) )
{
// I still think that we shouldn't be concerned with duplicates:
// they just shouldn't exist, and if they do, then it is a bug in upper level code
console.log("duplicated", edit);
console.log("current store is", edits);
return false; // fail
}
else
{
edits.push(edit);
this._storeEditsQueue(edits);
this._storeRedoStack([]);
return true; // success
}
},
popFirstEdit: function()
{
var edits = this._retrieveEditsQueue();
if( edits )
{
var firstEdit = edits.shift();
this._storeEditsQueue(edits);
firstEdit.graphic = this._deserialize(firstEdit.graphic);
return firstEdit;
}
else
return null;
},
hasPendingEdits: function()
{
var storedValue = window.localStorage.getItem(EDITS_QUEUE_KEY) || "";
return ( storedValue != "" )
},
pendingEditsCount: function()
{
var storedValue = window.localStorage.getItem(EDITS_QUEUE_KEY) || "";
if( storedValue == "" )
return 0; // fast easy case
var editsArray = this._unpackArrayOfEdits(storedValue);
return editsArray.length;
},
resetEditsQueue: function()
{
window.localStorage.setItem(EDITS_QUEUE_KEY, "");
window.localStorage.setItem(REDO_STACK_KEY,"");
},
// undo / redo
canUndoEdit: function()
{
return this.hasPendingEdits();
},
undoEdit: function()
{
if(!this.canUndoEdit())
return false;
var edits = this._retrieveEditsQueue();
var redoStack = this._retrieveRedoStack();
var editToUndo = edits.pop();
redoStack.push(editToUndo);
this._storeEditsQueue(edits);
this._storeRedoStack(redoStack);
return true;
},
canRedoEdit: function()
{
var storedValue = window.localStorage.getItem(REDO_STACK_KEY) || "";
return ( storedValue != "" )
},
redoEdit: function()
{
if(!this.canRedoEdit())
return false;
var edits = this._retrieveEditsQueue();
var redoStack = this._retrieveRedoStack();
var editToRedo = redoStack.pop();
edits.push(editToRedo);
this._storeEditsQueue(edits);
this._storeRedoStack(redoStack);
return true;
},
//
// internal methods
//
//
// graphic serialization/deserialization
//
_serialize: function(graphic)
{
// keep only attributes and geometry, that are the values that get sent to the server by applyEdits()
// see http://resources.arcgis.com/en/help/arcgis-rest-api/index.html#/Apply_Edits_Feature_Service_Layer/02r3000000r6000000/
// use graphic's built-in serializing method
var json = graphic.toJson();
var jsonClean =
{
attributes: json.attributes,
geometry: json.geometry
}
return JSON.stringify(jsonClean);
},
_deserialize: function(json)
{
var graphic = new Graphic(JSON.parse(json));
return graphic;
},
_retrieveEditsQueue: function()
{
var storedValue = window.localStorage.getItem(EDITS_QUEUE_KEY) || "";
return this._unpackArrayOfEdits(storedValue);
},
_storeEditsQueue: function(edits)
{
var serializedEdits = this._packArrayOfEdits(edits);
window.localStorage.setItem(EDITS_QUEUE_KEY, serializedEdits);
},
_retrieveRedoStack: function()
{
var storedValue = window.localStorage.getItem(REDO_STACK_KEY) || "";
return this._unpackArrayOfEdits(storedValue);
},
_storeRedoStack: function(edits)
{
var serializedEdits = this._packArrayOfEdits(edits);
window.localStorage.setItem(REDO_STACK_KEY, serializedEdits);
},
_packArrayOfEdits: function(edits)
{
var serializedEdits = [];
edits.forEach(function(edit)
{
serializedEdits.push( JSON.stringify(edit) );
});
return serializedEdits.join(SEPARATOR);
},
_unpackArrayOfEdits: function(serializedEdits)
{
if( !serializedEdits )
return [];
var edits = [];
serializedEdits.split(SEPARATOR).forEach( function(serializedEdit)
{
edits.push( JSON.parse(serializedEdit) );
});
return edits;
},
_isEditDuplicated: function(newEdit,edits)
{
for(var i=0; i<edits.length; i++)
{
var edit = edits[i];
if( edit.operation == newEdit.operation &&
edit.layer == newEdit.layer &&
edit.graphic == newEdit.graphic )
{
return true;
}
}
return false;
}
}
});

View File

@ -0,0 +1,142 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Jasmine Spec Runner - Graphics Store</title>
<link rel="shortcut icon" type="image/png" href="../vendor/jasmine-1.3.1/jasmine_favicon.png">
<link rel="stylesheet" type="text/css" href="../vendor/jasmine-1.3.1/jasmine.css">
<script type="text/javascript" src="../vendor/jasmine-1.3.1/jasmine.js"></script>
<script type="text/javascript" src="../vendor/jasmine-1.3.1/jasmine-html.js"></script>
<script type="text/javascript" src="../vendor/jasmine.async/lib/jasmine.async.js"></script>
<script>
var dojoConfig = {
paths: { edit: location.pathname.replace(/\/[^/]+$/, "") + "../../lib/edit" }
}
</script>
<link rel="stylesheet" href="http://js.arcgis.com/3.6/js/dojo/dijit/themes/claro/claro.css">
<link rel="stylesheet" href="http://js.arcgis.com/3.6/js/esri/css/esri.css">
<script src="http://js.arcgis.com/3.6/"></script>
<!-- include spec files here... -->
<script type="text/javascript" src="spec/editsStoreSpec.js"></script>
<script type="text/javascript">
"use strict"
var g_map;
var g_test = {};
var g_editsStore;
require(["esri/map",
"esri/layers/GraphicsLayer", "esri/graphic", "esri/symbols/SimpleFillSymbol", "esri/symbols/SimpleMarkerSymbol", "esri/symbols/SimpleLineSymbol",
"esri/SpatialReference","esri/geometry",
"edit/editsStore",
"dojo/dom", "dojo/on", "dojo/query",
"dojo/dom-construct", "dojo/domReady!"],
function(Map,
GraphicsLayer, Graphic, SimpleFillSymbol, SimpleMarkerSymbol, SimpleLineSymbol,
SpatialReference, geometry,
editsStore,
dom, on, query,
domConstruct)
{
/*
g_map = new Map("map", {
basemap: "gray",
center: [-3.695, 40.412], // Madrid center
zoom: 14,
sliderStyle: "small"
});
g_map.on('load', test);
*/
g_editsStore = editsStore;
test();
function initTestData()
{
// some geometry
g_test.point = new esri.geometry.Point(11,-11, new SpatialReference({"wkid":4326 }));
g_test.line = new esri.geometry.Polyline({
"paths":[[[-122.68,45.53], [-122.58,45.55],[-122.57,45.58],[-122.53,45.6]]],
"spatialReference":{"wkid":4326}});
g_test.polygon = new esri.geometry.Polygon({
"rings":[[[-122.63,45.52],[-122.57,45.53],[-122.52,45.50],[-122.49,45.48],[-122.64,45.49],[-122.63,45.52],[-122.63,45.52]]],
"spatialReference":{"wkid":4326 }
});
// some symbols
g_test.pointSymbol = new SimpleMarkerSymbol({"color": [255,255,255,64],
"size": 12, "angle": -30,
"xoffset": 0, "yoffset": 0,
"type": "esriSMS", "style": "esriSMSCircle",
"outline": { "color": [0,0,0,255], "width": 1, "type": "esriSLS", "style": "esriSLSSolid"}
});
g_test.lineSymbol = new SimpleLineSymbol({
"type": "esriSLS", "style": "esriSLSDot",
"color": [115,76,0,255],"width": 1
});
g_test.polygonSymbol = new SimpleFillSymbol({
"type": "esriSFS",
"style": "esriSFSSolid",
"color": [115,76,0,255],
"outline": { "type": "esriSLS", "style": "esriSLSSolid", "color": [110,110,110,255], "width": 1 }
});
g_test.pointFeature = new Graphic( g_test.point, g_test.pointSymbol, {"name": "the name of the feature", "objectid":2});
g_test.lineFeature = new Graphic( g_test.line, g_test.lineSymbol, {"nombre": "España","objectid":5});
g_test.polygonFeature = new Graphic( g_test.polygon, g_test.polygonSymbol, {"nombre": "España","timestamp": new Date().getTime(), "objectid":5});
console.log(g_test);
}
function test()
{
try
{
initTestData();
console.log("everything ok!");
var jasmineEnv = jasmine.getEnv();
jasmineEnv.updateInterval = 1000;
jasmineEnv.defaultTimeoutInterval = 10000; // 10 sec
var htmlReporter = new jasmine.HtmlReporter();
jasmineEnv.addReporter(htmlReporter);
jasmineEnv.specFilter = function(spec) {
return htmlReporter.specFilter(spec);
};
var currentWindowOnload = window.onload;
window.onload = function() {
if (currentWindowOnload) {
currentWindowOnload();
}
execJasmine();
};
}
catch(err)
{
alert(err);
}
function execJasmine() {
jasmineEnv.execute();
}
}; // test()
}); // require()
</script>
</head>
<body>
<div id="map" style="position: absolute; bottom: 0; right: 0; height:200px; width: 200px;"></div>
<img src="" alt="" id="fakeTile" style="display:none;">
</body>
</html>

431
test/spec/editsStoreSpec.js Normal file
View File

@ -0,0 +1,431 @@
"use strict"
describe("Internal Methods", function()
{
describe("Serialize/Deserialize Graphics", function()
{
describe("Sanity Check", function()
{
it("validate geometry objects", function()
{
// sanity checks on test data
expect(typeof(g_test)).toBe("object");
// geometry
expect(typeof(g_test.point)).toBe("object");
expect(g_test.point.declaredClass).toBe("esri.geometry.Point");
expect(g_test.point.type).toBe("point");
expect(g_test.point.spatialReference.wkid).toEqual(4326);
expect(typeof(g_test.line)).toBe("object");
expect(g_test.line.declaredClass).toBe("esri.geometry.Polyline");
expect(g_test.line.type).toBe("polyline");
expect(g_test.line.spatialReference.wkid).toEqual(4326);
expect(typeof(g_test.polygon)).toBe("object");
expect(g_test.polygon.declaredClass).toBe("esri.geometry.Polygon");
expect(g_test.polygon.type).toBe("polygon");
expect(g_test.polygon.spatialReference.wkid).toEqual(4326);
});
it("validate symbols", function()
{
// symbols
expect(typeof(g_test.pointSymbol)).toBe("object");
expect(g_test.pointSymbol.declaredClass).toBe("esri.symbol.SimpleMarkerSymbol");
expect(g_test.pointSymbol.style).toBe("circle");
expect(typeof(g_test.lineSymbol)).toBe("object");
expect(g_test.lineSymbol.declaredClass).toBe("esri.symbol.SimpleLineSymbol");
expect(g_test.lineSymbol.style).toBe("dot");
expect(typeof(g_test.polygonSymbol)).toBe("object");
expect(g_test.polygonSymbol.declaredClass).toBe("esri.symbol.SimpleFillSymbol");
expect(g_test.polygonSymbol.style).toBe("solid");
});
it("validate features", function()
{
// features
expect(typeof(g_test.pointFeature)).toBe("object");
expect(g_test.pointFeature.declaredClass).toBe("esri.Graphic");
expect(g_test.pointFeature.geometry).toEqual(g_test.point);
expect(g_test.pointFeature.symbol).toEqual(g_test.pointSymbol);
expect(typeof(g_test.pointFeature.attributes)).toBe("object");
expect(typeof(g_test.lineFeature)).toBe("object");
expect(g_test.lineFeature.declaredClass).toBe("esri.Graphic");
expect(g_test.lineFeature.geometry).toEqual(g_test.line);
expect(g_test.lineFeature.symbol).toEqual(g_test.lineSymbol);
expect(typeof(g_test.lineFeature.attributes)).toBe("object");
expect(typeof(g_test.polygonFeature)).toBe("object");
expect(g_test.polygonFeature.declaredClass).toBe("esri.Graphic");
expect(g_test.polygonFeature.geometry).toEqual(g_test.polygon);
expect(g_test.polygonFeature.symbol).toEqual(g_test.polygonSymbol);
expect(typeof(g_test.polygonFeature.attributes)).toBe("object");
});
});
describe("Serialize/Deserialize Point", function()
{
var str, graphic;
it("serialize", function()
{
str = g_editsStore._serialize(g_test.pointFeature);
expect(typeof(str)).toBe("string");
});
it("deserialize", function()
{
graphic = g_editsStore._deserialize(str);
expect(typeof(graphic)).toBe("object");
expect(graphic.declaredClass).toEqual("esri.Graphic");
});
it("deserialize - attributes", function()
{
expect(graphic.attributes).toEqual(g_test.pointFeature.attributes);
});
it("deserialize - geometry", function()
{
expect(graphic.geometry).toEqual(g_test.pointFeature.geometry);
});
it("deserialize - symbol should be null", function()
{
expect(graphic.symbol).toBeNull();
});
it("deserialize - infoTemplate should be null", function()
{
expect(graphic.infoTemplate).toBeNull();
});
});
describe("Serialize/Deserialize Polyline", function()
{
var str, graphic;
it("serialize", function()
{
str = g_editsStore._serialize(g_test.lineFeature);
expect(typeof(str)).toBe("string");
});
it("deserialize", function()
{
graphic = g_editsStore._deserialize(str);
expect(typeof(graphic)).toBe("object");
expect(graphic.declaredClass).toEqual("esri.Graphic");
});
it("deserialize - attributes", function()
{
expect(graphic.attributes).toEqual(g_test.lineFeature.attributes);
});
it("deserialize - geometry", function()
{
expect(graphic.geometry).toEqual(g_test.lineFeature.geometry);
});
it("deserialize - symbol should be null", function()
{
expect(graphic.symbol).toBeNull();
});
it("deserialize - infoTemplate should be null", function()
{
expect(graphic.infoTemplate).toBeNull();
});
});
describe("Serialize/Deserialize Polygon", function()
{
var str, graphic;
it("serialize", function()
{
str = g_editsStore._serialize(g_test.polygonFeature);
expect(typeof(str)).toBe("string");
});
it("deserialize", function()
{
graphic = g_editsStore._deserialize(str);
expect(typeof(graphic)).toBe("object");
expect(graphic.declaredClass).toEqual("esri.Graphic");
});
it("deserialize - attributes", function()
{
expect(graphic.attributes).toEqual(g_test.polygonFeature.attributes);
});
it("deserialize - geometry", function()
{
expect(graphic.geometry).toEqual(g_test.polygonFeature.geometry);
});
it("deserialize - symbol should be null", function()
{
expect(graphic.symbol).toBeNull();
});
it("deserialize - infoTemplate should be null", function()
{
expect(graphic.infoTemplate).toBeNull();
});
});
});
describe("Pack/Unpack array of edits",function()
{
// TODO
});
});
describe("Public Interface", function()
{
describe("Edit queue management", function()
{
describe("Normal edits", function()
{
it("reset edits queue", function()
{
g_editsStore.resetEditsQueue();
expect(g_editsStore.pendingEditsCount()).toBe(0);
});
it("add edits to edits queue", function()
{
var success;
success = g_editsStore.pushEdit(g_editsStore.ADD, 6, g_test.pointFeature);
expect(success).toBeTruthy();
expect(g_editsStore.pendingEditsCount()).toBe(1);
success = g_editsStore.pushEdit(g_editsStore.UPDATE, 3, g_test.polygonFeature);
expect(success).toBeTruthy();
expect(g_editsStore.pendingEditsCount()).toBe(2);
success = g_editsStore.pushEdit(g_editsStore.DELETE, 2, g_test.lineFeature);
expect(success).toBeTruthy();
expect(g_editsStore.pendingEditsCount()).toBe(3);
});
it("pending edits", function()
{
expect(g_editsStore.hasPendingEdits()).toBeTruthy();
});
it("pop edit from edits queue - 1", function()
{
var firstEdit = g_editsStore.popFirstEdit();
expect(g_editsStore.pendingEditsCount()).toBe(2);
expect(typeof(firstEdit)).toBe("object");
expect(firstEdit.operation).toBe(g_editsStore.ADD);
expect(firstEdit.layer).toBe(6);
expect(firstEdit.graphic.attributes).toEqual(g_test.pointFeature.attributes);
expect(firstEdit.graphic.geometry).toEqual(g_test.pointFeature.geometry);
expect(firstEdit.graphic.symbol).toEqual(null);
});
it("pop edit from edits queue - 2", function()
{
var secondEdit = g_editsStore.popFirstEdit();
expect(g_editsStore.pendingEditsCount()).toBe(1);
expect(typeof(secondEdit)).toBe("object");
expect(secondEdit.operation).toBe(g_editsStore.UPDATE);
expect(secondEdit.layer).toBe(3);
expect(secondEdit.graphic.attributes).toEqual(g_test.polygonFeature.attributes);
expect(secondEdit.graphic.geometry).toEqual(g_test.polygonFeature.geometry);
expect(secondEdit.graphic.symbol).toEqual(null);
});
it("pop edit from edits queue - 3", function()
{
var thirdEdit = g_editsStore.popFirstEdit();
expect(g_editsStore.pendingEditsCount()).toBe(0);
expect(typeof(thirdEdit)).toBe("object");
expect(thirdEdit.operation).toBe(g_editsStore.DELETE);
expect(thirdEdit.layer).toBe(2);
expect(thirdEdit.graphic.attributes).toEqual(g_test.lineFeature.attributes);
expect(thirdEdit.graphic.geometry).toEqual(g_test.lineFeature.geometry);
expect(thirdEdit.graphic.symbol).toEqual(null);
});
it("pending edits", function()
{
expect(g_editsStore.hasPendingEdits()).toBeFalsy();
});
});
describe("Duplicate edit detection", function()
{
it("reset edits queue", function()
{
g_editsStore.resetEditsQueue();
expect(g_editsStore.pendingEditsCount()).toBe(0);
});
it("try to add duplicate edits to edits queue", function()
{
var success;
success = g_editsStore.pushEdit(g_editsStore.ADD, 6, g_test.pointFeature);
expect(g_editsStore.pendingEditsCount()).toBe(1);
expect(success).toBeTruthy();
success = g_editsStore.pushEdit(g_editsStore.UPDATE, 3, g_test.polygonFeature);
expect(success).toBeTruthy();
expect(g_editsStore.pendingEditsCount()).toBe(2);
success = g_editsStore.pushEdit(g_editsStore.ADD, 6, g_test.pointFeature);
expect(g_editsStore.pendingEditsCount()).toBe(2);
expect(success).toBeFalsy();
});
});
describe("Undo/Redo management", function()
{
it("reset edits queue", function()
{
g_editsStore.resetEditsQueue();
expect(g_editsStore.pendingEditsCount()).toBe(0);
});
it("can undo? - no", function()
{
expect(g_editsStore.canUndoEdit()).toBeFalsy();
});
it("can redo? - no", function()
{
expect(g_editsStore.canRedoEdit()).toBeFalsy();
});
it("add edits to edits queue", function()
{
var success;
success = g_editsStore.pushEdit(g_editsStore.ADD, 6, g_test.pointFeature);
expect(success).toBeTruthy();
expect(g_editsStore.pendingEditsCount()).toBe(1);
success = g_editsStore.pushEdit(g_editsStore.UPDATE, 3, g_test.polygonFeature);
expect(success).toBeTruthy();
expect(g_editsStore.pendingEditsCount()).toBe(2);
success = g_editsStore.pushEdit(g_editsStore.DELETE, 2, g_test.lineFeature);
expect(success).toBeTruthy();
expect(g_editsStore.pendingEditsCount()).toBe(3);
});
it("pending edits", function()
{
expect(g_editsStore.hasPendingEdits()).toBeTruthy();
});
it("can undo? - yes", function()
{
expect(g_editsStore.canUndoEdit()).toBeTruthy();
});
it("can redo? - no", function()
{
expect(g_editsStore.canRedoEdit()).toBeFalsy();
});
it("undo", function()
{
expect(g_editsStore.pendingEditsCount()).toBe(3);
g_editsStore.undoEdit();
expect(g_editsStore.pendingEditsCount()).toBe(2);
});
it("can undo? - yes", function()
{
expect(g_editsStore.canUndoEdit()).toBeTruthy();
});
it("can redo? - yes", function()
{
expect(g_editsStore.canRedoEdit()).toBeTruthy();
});
it("redo", function()
{
expect(g_editsStore.pendingEditsCount()).toBe(2);
g_editsStore.redoEdit();
expect(g_editsStore.pendingEditsCount()).toBe(3);
});
it("can undo? - yes", function()
{
expect(g_editsStore.canUndoEdit()).toBeTruthy();
});
it("can redo? - no", function()
{
expect(g_editsStore.canRedoEdit()).toBeFalsy();
});
it("undo x 3", function()
{
expect(g_editsStore.pendingEditsCount()).toBe(3);
g_editsStore.undoEdit();
expect(g_editsStore.pendingEditsCount()).toBe(2);
g_editsStore.undoEdit();
expect(g_editsStore.pendingEditsCount()).toBe(1);
g_editsStore.undoEdit();
expect(g_editsStore.pendingEditsCount()).toBe(0);
});
it("can undo? - no", function()
{
expect(g_editsStore.canUndoEdit()).toBeFalsy();
});
it("can redo? - yes", function()
{
expect(g_editsStore.canRedoEdit()).toBeTruthy();
});
it("redo x 2", function()
{
expect(g_editsStore.pendingEditsCount()).toBe(0);
g_editsStore.redoEdit();
expect(g_editsStore.pendingEditsCount()).toBe(1);
g_editsStore.redoEdit();
expect(g_editsStore.pendingEditsCount()).toBe(2);
});
it("can undo? - yes", function()
{
expect(g_editsStore.canUndoEdit()).toBeTruthy();
});
it("can redo? - yes", function()
{
expect(g_editsStore.canRedoEdit()).toBeTruthy();
});
it("add new edit", function()
{
var success;
success = g_editsStore.pushEdit(g_editsStore.ADD, 10, g_test.pointFeature);
expect(success).toBeTruthy();
expect(g_editsStore.pendingEditsCount()).toBe(3);
});
it("can redo? - no", function()
{
expect(g_editsStore.canRedoEdit()).toBeFalsy();
});
});
});
});
describe("Reset store", function()
{
it("reset the store", function()
{
g_editsStore.resetEditsQueue();
})
});