diff --git a/lib/edit/editsStore.js b/lib/edit/editsStore.js new file mode 100644 index 0000000..82fb0a9 --- /dev/null +++ b/lib/edit/editsStore.js @@ -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 + + + + Jasmine Spec Runner - Graphics Store + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/test/spec/editsStoreSpec.js b/test/spec/editsStoreSpec.js new file mode 100644 index 0000000..c851483 --- /dev/null +++ b/test/spec/editsStoreSpec.js @@ -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(); + }) +});