define([ "dojo/Evented", "dojo/_base/Deferred", "dojo/promise/all", "dojo/_base/declare", "dojo/_base/array", "dojo/dom-attr", "dojo/dom-style", "dojo/query", "esri/config", "esri/layers/GraphicsLayer", "esri/graphic", "esri/symbols/SimpleMarkerSymbol", "esri/symbols/SimpleLineSymbol", "esri/symbols/SimpleFillSymbol", "esri/urlUtils"], function( Evented,Deferred,all,declare,array,domAttr,domStyle,query, esriConfig,GraphicsLayer,Graphic,SimpleMarkerSymbol,SimpleLineSymbol,SimpleFillSymbol,urlUtils) { "use strict"; return declare("O.esri.Edit.OfflineFeaturesManager",[Evented], { _onlineStatus: "online", _featureLayers: {}, _editStore: new O.esri.Edit.EditStore(Graphic), ONLINE: "online", // all edits will directly go to the server OFFLINE: "offline", // edits will be enqueued RECONNECTING: "reconnecting", // sending stored edits to the server attachmentsStore: null, // indexedDB for storing attachments proxyPath: null, // by default we use CORS and therefore proxyPath is null // manager emits event when... events: { EDITS_SENT: "edits-sent", // ...whenever any edit is actually sent to the server EDITS_ENQUEUED: "edits-enqueued", // ...when an edit is enqueued (and not sent to the server) ALL_EDITS_SENT: "all-edits-sent", // ...after going online and there are no pending edits in the queue ATTACHMENT_ENQUEUED: "attachment-enqueued", ATTACHMENTS_SENT: "attachments-sent" }, /** * Need to call this method only if you want to support offline attachments * it is optional * @param callback(success) * @returns void */ initAttachments: function(callback) { callback = callback || function(success) { console.log("attachments inited ", success? "ok" : "failed"); }; if( !this._checkFileAPIs()) { return callback(false, "File APIs not supported"); } try { this.attachmentsStore = new O.esri.Edit.AttachmentsStore(); if( /*false &&*/ this.attachmentsStore.isSupported() ) { this.attachmentsStore.init(callback); } else { return callback(false, "indexedDB not supported"); } } catch(err) { console.log("problem! " + err.toString()); } }, /** * internal method that checks if this browser supports everything that is needed to handle offline attachments * it also extends XMLHttpRequest with sendAsBinary() method, needed in Chrome */ _checkFileAPIs: function() { if( window.File && window.FileReader && window.FileList && window.Blob ) { console.log("All APIs supported!"); if(!XMLHttpRequest.prototype.sendAsBinary ) { // https://code.google.com/p/chromium/issues/detail?id=35705#c40 XMLHttpRequest.prototype.sendAsBinary = function(datastr) { function byteValue(x) { return x.charCodeAt(0) & 0xff; } var ords = Array.prototype.map.call(datastr, byteValue); var ui8a = new Uint8Array(ords); this.send(ui8a.buffer); }; console.log("extending XMLHttpRequest"); } return true; } console.log("The File APIs are not fully supported in this browser."); return false; }, /** * internal method that extends an object with sendAsBinary() method * sometimes extending XMLHttpRequest.prototype is not enough, as ArcGIS JS lib seems to mess with this object too * @param oAjaxReq object to extend */ _extendAjaxReq: function(oAjaxReq) { oAjaxReq.sendAsBinary = XMLHttpRequest.prototype.sendAsBinary; console.log("extending XMLHttpRequest"); }, /** * Overrides a feature layer. * @param layer * @returns deferred */ extend: function(layer) { var self = this; // we keep track of the FeatureLayer object this._featureLayers[ layer.url ] = layer; // replace the applyEdits() method layer._applyEdits = layer.applyEdits; // attachments layer._addAttachment = layer.addAttachment; layer._queryAttachmentInfos = layer.queryAttachmentInfos; layer._deleteAttachments = layer.deleteAttachments; /* operations supported offline: 1. add a new attachment to an existing feature (DONE) 2. add a new attachment to a new feature (DONE) 3. remove an attachment that is already in the server... (NOT YET) 4. remove an attachment that is not in the server yet (DONE) 5. update an existing attachment to an existing feature (NOT YET) 6. update a new attachment (NOT YET) concerns: - manage the relationship between offline features and attachments: what if the user wants to add an attachment to a feature that is still offline? we need to keep track of objectids so that when the feature is sent to the server and receives a final objectid we replace the temporary negative id by its final objectid (DONE) - what if the user deletes an offline feature that had offline attachments? we need to discard the attachment (DONE) pending tasks: - delete attachment (DONE) - send attachments to server when reconnecting (DONE) - check for hasAttachments attribute in the FeatureLayer (NOT YET) */ // // attachments // layer.queryAttachmentInfos = function(objectId,callback,errback) { if( self.getOnlineStatus() === self.ONLINE) { var def = this._queryAttachmentInfos(objectId, function() { console.log(arguments); self.emit(self.events.ATTACHMENTS_INFO,arguments); callback && callback.apply(this,arguments); }, errback); return def; } if( !self.attachmentsStore ) { console.log("in order to support attachments you need to call initAttachments() method of offlineFeaturesManager"); return; } // will only return LOCAL attachments var deferred = new Deferred(); self.attachmentsStore.getAttachmentsByFeatureId(this.url, objectId, function(attachments) { callback && callback(attachments); deferred.resolve(attachments); }); return deferred; }; layer.addAttachment = function(objectId,formNode,callback,errback) { if( self.getOnlineStatus() === self.ONLINE) { return this._addAttachment(objectId,formNode, function() { console.log(arguments); self.emit(self.events.ATTACHMENTS_SENT,arguments); callback && callback.apply(this,arguments); }, function(err) { console.log("addAttachment: " + err); errback && errback.apply(this,arguments); } ); } if( !self.attachmentsStore ) { console.log("in order to support attachments you need to call initAttachments() method of offlineFeaturesManager"); return; } var files = this._getFilesFromForm(formNode); var file = files[0]; // addAttachment can only add one file, so the rest -if any- are ignored var deferred = new Deferred(); var attachmentId = this._getNextTempId(); self.attachmentsStore.store(this.url, attachmentId, objectId, file, function(success, newAttachment) { var returnValue = { attachmentId: attachmentId, objectId:objectId, success:success }; if( success ) { self.emit(self.events.ATTACHMENT_ENQUEUED,returnValue); callback && callback(returnValue); deferred.resolve(returnValue); // replace the default URL that is set by attachmentEditor with the local file URL var attachmentUrl = this._url.path + "/" + objectId + "/attachments/" + attachmentId; var attachmentElement = query("[href=" + attachmentUrl + "]"); attachmentElement.attr("href", newAttachment.url); } else { returnValue.error = "can't store attachment"; errback && errback(returnValue); deferred.reject(returnValue); } }.bind(this)); return deferred; }; layer.deleteAttachments = function(objectId,attachmentsIds,callback,errback) { if( self.getOnlineStatus() === self.ONLINE) { var def = this._deleteAttachments(objectId,attachmentsIds, function() { callback && callback.apply(this,arguments); }, function(err) { console.log("deleteAttachments: " + err); errback && errback.apply(this,arguments); }); return def; } if( !self.attachmentsStore ) { console.log("in order to support attachments you need to call initAttachments() method of offlineFeaturesManager"); return; } // case 1.- it is a new attachment // case 2.- it is an already existing attachment // only case 1 is supported right now // asynchronously delete each of the attachments var promises = []; attachmentsIds.forEach(function(attachmentId) { attachmentId = parseInt(attachmentId,10); // to number console.assert( attachmentId<0 , "we only support deleting local attachments"); var deferred = new Deferred(); self.attachmentsStore.delete(attachmentId, function(success) { var result = { objectId: objectId, attachmentId: attachmentId, success: success }; deferred.resolve(result); }); promises.push(deferred); }, this); // call callback once all deletes have finished var allPromises = all(promises); allPromises.then( function(results) { callback && callback(results); }); return allPromises; }; // // other functions // /** * Overrides the ArcGIS API for JavaSccript applyEdits() method. * @param adds Creates a new edit entry. * @param updates Updates an existing entry. * @param deletes Deletes an existing entry. * @param callback Called when the operation is complete. * @param errback An error object is returned if an error occurs * @returns {*} deferred */ layer.applyEdits = function(adds,updates,deletes,callback,errback) { // inside this method, 'this' will be the FeatureLayer // and 'self' will be the offlineFeatureLayer object if( self.getOnlineStatus() === self.ONLINE) { var def = this._applyEdits(adds,updates,deletes, function() { self.emit(self.events.EDITS_SENT,arguments); callback && callback.apply(this,arguments); }, errback); return def; } var deferred = new Deferred(); var results = { addResults:[],updateResults:[], deleteResults:[] }; var updatesMap = {}; this.onBeforeApplyEdits(adds, updates, deletes); adds = adds || []; adds.forEach(function(addEdit) { var objectId = this._getNextTempId(); addEdit.attributes[ this.objectIdField ] = objectId; var result = self._editStore.pushEdit(self._editStore.ADD, this.url, addEdit); results.addResults.push({ success:result.success, error: result.error, objectId: objectId}); if(result.success) { var phantomAdd = new Graphic( addEdit.geometry, self._getPhantomSymbol(addEdit.geometry, self._editStore.ADD), { objectId: objectId }); this._phantomLayer.add(phantomAdd); domAttr.set(phantomAdd.getNode(),"stroke-dasharray","10,4"); domStyle.set(phantomAdd.getNode(), "pointer-events","none"); } },this); updates = updates || []; updates.forEach(function(updateEdit) { var objectId = updateEdit.attributes[ this.objectIdField ]; var result = self._editStore.pushEdit(self._editStore.UPDATE, this.url, updateEdit); results.updateResults.push({success:result.success, error: result.error, objectId: objectId}); updatesMap[ objectId ] = updateEdit; if(result.success) { var phantomUpdate = new Graphic( updateEdit.geometry, self._getPhantomSymbol(updateEdit.geometry, self._editStore.UPDATE), { objectId: objectId }); this._phantomLayer.add(phantomUpdate); domAttr.set(phantomUpdate.getNode(),"stroke-dasharray","5,2"); domStyle.set(phantomUpdate.getNode(), "pointer-events","none"); } },this); deletes = deletes || []; deletes.forEach(function(deleteEdit) { var objectId = deleteEdit.attributes[ this.objectIdField ]; var result = self._editStore.pushEdit(self._editStore.DELETE, this.url, deleteEdit); results.deleteResults.push({success:result.success, error: result.error, objectId: objectId}); if(result.success) { var phantomDelete = new Graphic( deleteEdit.geometry, self._getPhantomSymbol(deleteEdit.geometry, self._editStore.DELETE), { objectId: objectId }); this._phantomLayer.add(phantomDelete); domAttr.set(phantomDelete.getNode(),"stroke-dasharray","4,4"); domStyle.set(phantomDelete.getNode(), "pointer-events","none"); } if( self.attachmentsStore ) { // delete local attachments of this feature, if any... we just launch the delete and don't wait for it to complete self.attachmentsStore.deleteAttachmentsByFeatureId(this.url, objectId, function(deletedCount) { console.log("deleted",deletedCount,"attachments of feature",objectId); }); } },this); /* we already pushed the edits into the local store, now we let the FeatureLayer to do the local updating of the layer graphics */ setTimeout(function() { this._editHandler(results, adds, updatesMap, callback, errback, deferred); self.emit(self.events.EDITS_ENQUEUED, results); }.bind(this),0); return deferred; }; // layer.applyEdits() /** * Converts an array of graphics/features into JSON * @param features * @param updateEndEvent * @param callback */ layer.convertGraphicLayerToJSON = function(features,updateEndEvent,callback){ var layerDefinition = {}; layerDefinition.objectIdFieldName = updateEndEvent.target.objectIdField; layerDefinition.globalIdFieldName = updateEndEvent.target.globalIdField; layerDefinition.geometryType = updateEndEvent.target.geometryType; layerDefinition.spatialReference = updateEndEvent.target.spatialReference; layerDefinition.fields = updateEndEvent.target.fields; var length = features.length; var jsonArray = []; for(var i=0; i < length; i++){ var jsonGraphic = features[i].toJson(); jsonArray.push(jsonGraphic); if(i == (length - 1)) { var featureJSON = JSON.stringify(jsonArray); var layerDefJSON = JSON.stringify(layerDefinition); callback(featureJSON,layerDefJSON); break; } } } /** * Create a featureDefinition * @param featureLayer * @param featuresArr * @param geometryType * @param callback */ layer.getFeatureDefinition = function(/* Object */ featureLayer,/* Array */ featuresArr,/* String */ geometryType,callback){ var featureDefinition = { "layerDefinition":featureLayer, "featureSet":{ "features": featuresArr, "geometryType": geometryType } } callback(featureDefinition); } /* internal methods */ layer._getFilesFromForm = function(formNode) { var files = []; var inputNodes = array.filter(formNode.elements, function(node) { return node.type === "file"; }); inputNodes.forEach(function(inputNode) { files.push.apply(files,inputNode.files); },this); return files; }; layer._replaceFeatureIds = function(tempObjectIds,newObjectIds,callback) { console.log("replacing ids of attachments",tempObjectIds, newObjectIds); console.assert( tempObjectIds.length === newObjectIds.length, "arrays must be the same length"); if(!tempObjectIds.length) { console.log("no ids to replace!"); callback(0); } var i, n = tempObjectIds.length; var count = n; var successCount = 0; for(i=0; i 0) { layer._replaceFeatureIds(tempObjectIds,newObjectIds,function(success) { dfd.resolve({addResults:addResults,updateResults:updateResults,deleteResults:deleteResults}); // wrap three arguments in a single object }); } else { dfd.resolve({addResults:addResults,updateResults:updateResults,deleteResults:deleteResults}); // wrap three arguments in a single object } }, function(error) { layer.onEditsComplete = layer.__onEditsComplete; delete layer.__onEditsComplete; layer.onBeforeApplyEdits = layer.__onBeforeApplyEdits; delete layer.__onBeforeApplyEdits; dfd.reject(error); } ); return dfd; }(layer,tempObjectIds)); } } // // wait for all requests to finish // var allPromises = all(promises); allPromises.then( function(responses) { console.log("all responses are back"); this.emit(this.events.EDITS_SENT); this.emit(this.events.ALL_EDITS_SENT); callback && callback(true,responses); }.bind(this), function(errors) { console.log("ERROR!!"); console.log(errors); callback && callback(false,errors); }.bind(this)); } // hasPendingEdits() else { this.emit(this.events.ALL_EDITS_SENT); callback && callback(true, {}); } } }); // declare }); // define