From e9d09e3b9a8889cf75f74f1939203bb82482884c Mon Sep 17 00:00:00 2001 From: Andy Gup Date: Mon, 27 Apr 2015 18:27:59 -0600 Subject: [PATCH] v2.7 --- CHANGELOG.md | 24 + Gruntfile.js | 6 +- dist/offline-edit-min.js | 5 +- dist/offline-edit-src.js | 510 ++++++++++++++----- dist/offline-tiles-advanced-min.js | 2 +- dist/offline-tiles-advanced-src.js | 2 +- dist/offline-tiles-basic-min.js | 2 +- dist/offline-tiles-basic-src.js | 2 +- dist/offline-tpk-min.js | 2 +- dist/offline-tpk-src.js | 2 +- doc/attachments.md | 45 +- doc/howtouseeditlibrary.md | 21 +- doc/offlinefeaturesmanager.md | 76 +-- lib/edit/attachmentsStore.js | 196 +++++--- lib/edit/offlineFeaturesManager.js | 322 +++++++++--- package.json | 2 +- samples/attachments-editor-secure.html | 365 ++++++++++++++ samples/attachments-editor.html | 17 +- samples/package.json | 2 +- test/SpecRunner.attachmentsStore.html | 228 +++++---- test/SpecRunner.offlineAttachments.html | 117 +++-- test/SpecRunner.offlineFeaturesManager.html | 25 +- test/images/blue-pin.png | Bin 0 -> 685 bytes test/images/red-pin.png | Bin 0 -> 480 bytes test/spec/attachmentsStoreSpec.js | 61 ++- test/spec/offlineAttachmentsSpec.js | 486 ++++++++++++------ test/spec/offlineFeaturesManagerSpec.js | 529 ++++++++++++++------ 27 files changed, 2277 insertions(+), 772 deletions(-) create mode 100644 samples/attachments-editor-secure.html create mode 100644 test/images/blue-pin.png create mode 100644 test/images/red-pin.png diff --git a/CHANGELOG.md b/CHANGELOG.md index e8dd192f..f7e5ffec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # offline-editor-js - Changelog +## Version 2.7 + +This release focused on improving the handling of attachments. Has breaking changes. + +** Enhancements ** +* Added a new sample attachments-editor-secure.html to demonstrate the pattern for working with secure feature services and attachments. +* Closes #286 - support for secure services (HTTPS) when working with attachments +* Closes #305 - Support both ADD and UPDATE attachment. +* Closes #306 - removes createObjectURL functionality from attachmentsStore. This allows for attachments to be used in full offline scenarios. +* Closes #318 - added OfflineFeaturesManager.ATTACHMENTS_DB_NAME and ATTACHMENTS_DB_OBJECSTORE_NAME. +* Closes #321 - switch offlineFeaturesManager unit test to a different feature service that's attachments enabled. +* Closes #322 - rewrite offlineAttachmentsSpec. +* Closes #324 - attachmentsStore._readFile() can indicate false positives. +* Closes #325 - support DELETE an existing attachment. +* Closes #328 - add layer.resestAttachmentsDatabase(). +* Closes #329 - add layer.getAttachmentsUsage(). + +** Breaking Changes ** +* attachmentsStore.DB_NAME has been renamed to attachmentsStore.dbName to be consistent with editStore. +* attachmentsStore.OBJECTSTORE_NAME has been renamed to attachmentsStore.objectStoreName to be consistent with editStore. +* Added use of the browser's [FormData() API](https://developer.mozilla.org/en-US/docs/Web/API/FormData) along with `FormData.append`. This may cause +attachment support to break in certain older browsers. This is a courtesy heads-up because as a rule this library only +supports the latest version of Chrome, Firefox and Safari. + ## Version 2.6.1 - April 13, 2015 Patch release. Recommended update. No breaking changes. diff --git a/Gruntfile.js b/Gruntfile.js index 2869294d..29030750 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -13,7 +13,7 @@ module.exports = function(grunt) { 'lib/tpk/*.js' ], - tasks: ['concat', 'uglify'], + tasks: ['jshint','concat', 'uglify'], options: { spawn: false } @@ -120,8 +120,8 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-contrib-jshint'); - grunt.registerTask('build',['concat','uglify']); + grunt.registerTask('build',['jshint','concat','uglify']); grunt.registerTask('test',['jshint']); - grunt.registerTask('buildAll',['jshint','concat','uglify']); + //grunt.registerTask('buildAll',['jshint','concat','uglify']); }; \ No newline at end of file diff --git a/dist/offline-edit-min.js b/dist/offline-edit-min.js index ca7112d5..d07f6953 100644 --- a/dist/offline-edit-min.js +++ b/dist/offline-edit-min.js @@ -1,4 +1,5 @@ -/*! offline-editor-js - v2.6.1 - 2015-04-13 +/*! offline-editor-js - v2.7.0 - 2015-04-27 * Copyright (c) 2015 Environmental Systems Research Institute, Inc. * Apache License*/ -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(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o){"use strict";return d("O.esri.Edit.OfflineFeaturesManager",[a],{_onlineStatus:"online",_featureLayers:{},_editStore:new O.esri.Edit.EditStore,ONLINE:"online",OFFLINE:"offline",RECONNECTING:"reconnecting",attachmentsStore:null,proxyPath:null,DB_NAME:"features_store",DB_OBJECTSTORE_NAME:"features",DB_UID:"objectid",events:{EDITS_SENT:"edits-sent",EDITS_ENQUEUED:"edits-enqueued",EDITS_ENQUEUED_ERROR:"edits-enqueued-error",EDITS_SENT_ERROR:"edits-sent-error",ALL_EDITS_SENT:"all-edits-sent",ATTACHMENT_ENQUEUED:"attachment-enqueued",ATTACHMENTS_SENT:"attachments-sent"},initAttachments:function(a){if(a=a||function(a){},!this._checkFileAPIs())return a(!1,"File APIs not supported");try{if(this.attachmentsStore=new O.esri.Edit.AttachmentsStore,!this.attachmentsStore.isSupported())return a(!1,"indexedDB not supported");this.attachmentsStore.init(a)}catch(b){}},extend:function(a,d,i){function l(){try{a._phantomLayer=new j({opacity:.8}),a._map.addLayer(a._phantomLayer)}catch(b){}}var m=this;a.objectIdField=this.DB_UID,this._initializeDB(i,d),this._featureLayers[a.url]=a,a._applyEdits=a.applyEdits,a._addAttachment=a.addAttachment,a._queryAttachmentInfos=a.queryAttachmentInfos,a._deleteAttachments=a.deleteAttachments,a.queryAttachmentInfos=function(a,c,d){if(m.getOnlineStatus()===m.ONLINE){var e=this._queryAttachmentInfos(a,function(){m.emit(m.events.ATTACHMENTS_INFO,arguments),c&&c.apply(this,arguments)},d);return e}if(m.attachmentsStore){var f=new b;return m.attachmentsStore.getAttachmentsByFeatureId(this.url,a,function(a){c&&c(a),f.resolve(a)}),f}},a.addAttachment=function(a,c,d,e){if(m.getOnlineStatus()===m.ONLINE)return this._addAttachment(a,c,function(){m.emit(m.events.ATTACHMENTS_SENT,arguments),d&&d.apply(this,arguments)},function(a){e&&e.apply(this,arguments)});if(m.attachmentsStore){var f=this._getFilesFromForm(c),g=f[0],i=new b,j=this._getNextTempId();return m.attachmentsStore.store(this.url,j,a,g,function(b,c){var f={attachmentId:j,objectId:a,success:b};if(b){m.emit(m.events.ATTACHMENT_ENQUEUED,f),d&&d(f),i.resolve(f);var g=this._url.path+"/"+a+"/attachments/"+j,k=h("[href="+g+"]");k.attr("href",c.url)}else f.error="can't store attachment",e&&e(f),i.reject(f)}.bind(this)),i}},a.deleteAttachments=function(a,d,e,f){if(m.getOnlineStatus()===m.ONLINE){var g=this._deleteAttachments(a,d,function(){e&&e.apply(this,arguments)},function(a){f&&f.apply(this,arguments)});return g}if(m.attachmentsStore){var h=[];d.forEach(function(c){c=parseInt(c,10);var d=new b;m.attachmentsStore["delete"](c,function(b){var e={objectId:a,attachmentId:c,success:b};d.resolve(e)}),h.push(d)},this);var i=c(h);return i.then(function(a){e&&e(a)}),i}},a.applyEdits=function(a,d,e,f,g){var h=[];if(m.getOnlineStatus()===m.ONLINE){var i=this._applyEdits(a,d,e,function(){m.emit(m.events.EDITS_SENT,arguments),f&&f.apply(this,arguments)},g);return i}var j=new b,k={addResults:[],updateResults:[],deleteResults:[]},l={};return a=a||[],a.forEach(function(a){var c=new b,d=this._getNextTempId();a.attributes[this.objectIdField]=d;var e=this;this._validateFeature(a,this.url,m._editStore.ADD).then(function(b){b.success?e._pushValidatedAddFeatureToDB(e,a,b.operation,k,d,c):c.resolve(!0)},function(a){c.reject(a)}),h.push(c)},this),d=d||[],d.forEach(function(a){var c=new b,d=a.attributes[this.objectIdField];l[d]=a;var e=this;this._validateFeature(a,this.url,m._editStore.UPDATE).then(function(b){b.success?e._pushValidatedUpdateFeatureToDB(e,a,b.operation,k,d,c):c.resolve(!0)},function(a){c.reject(a)}),h.push(c)},this),e=e||[],e.forEach(function(a){var c=new b,d=a.attributes[this.objectIdField],e=this;this._validateFeature(a,this.url,m._editStore.DELETE).then(function(b){b.success?e._pushValidatedDeleteFeatureToDB(e,a,b.operation,k,d,c):c.resolve(!0)},function(a){c.reject(a)}),h.push(c)},this),c(h).then(function(b){for(var c=!0,d=b.length,e=0;d>e;e++)b[e]===!1&&(c=!1);this._editHandler(k,a,l,f,g,j),c===!0?m.emit(m.events.EDITS_ENQUEUED,k):m.emit(m.events.EDITS_ENQUEUED_ERROR,k)}.bind(this)),j},a.convertGraphicLayerToJSON=function(a,b,c){var d={};d.objectIdFieldName=b.target.hasOwnProperty("objectIdField")?b.target.objectIdField:this.objectIdField,d.globalIdFieldName=b.target.globalIdField,d.geometryType=b.target.geometryType,d.spatialReference=b.target.spatialReference,d.fields=b.target.fields;for(var e=a.length,f=[],g=0;e>g;g++){var h=a[g].toJson();if(f.push(h),g==e-1){var i=JSON.stringify(f),j=JSON.stringify(d);c(i,j);break}}},a.getFeatureLayerJSON=function(a,b){require(["esri/request"],function(c){var d=c({url:a,content:{f:"json"},handleAs:"json",callbackParamName:"callback"});d.then(function(a){b(!0,a)},function(a){b(!1,a.message)})})},a.setFeatureLayerJSONDataStore=function(a,b){m._editStore.pushFeatureLayerJSON(a,function(a,c){b(a,c)})},a.getFeatureLayerJSONDataStore=function(a){m._editStore.getFeatureLayerJSON(function(b,c){a(b,c)})},a.setPhantomLayerGraphics=function(a){var b=a.length;if(b>0)for(var c=0;b>c;c++){var d=new k(a[c]);this._phantomLayer.add(d)}},a.getPhantomLayerGraphics=function(b){for(var c=a._phantomLayer.graphics,d=a._phantomLayer.graphics.length,e=[],f=0;d>f;f++){var g=c[f].toJson();if(e.push(g),f==d-1){var h=JSON.stringify(e);b(h);break}}},a.getPhantomGraphicsArray=function(a){m._editStore.getPhantomGraphicsArray(function(b,c){"end"==c?a(!0,b):a(!1,c)})},a.getUsage=function(a){m._editStore.getUsage(function(b,c){a(b,c)})},a.resetDatabase=function(a){m._editStore.resetEditsQueue(function(b,c){a(b,c)})},a.pendingEditsCount=function(a){m._editStore.pendingEditsCount(function(b){a(b)})},a.getFeatureDefinition=function(a,b,c,d){var e={layerDefinition:a,featureSet:{features:b,geometryType:c}};d(e)},a.getAllEditsArray=function(a){m._editStore.getAllEditsArray(function(b,c){"end"==c?a(!0,b):a(!1,c)})},a._pushValidatedDeleteFeatureToDB=function(a,b,c,d,e,h){m._editStore.pushEdit(c,a.url,b,function(c,i){if(c){d.deleteResults.push({success:!0,error:null,objectId:e});var j={};j[m.DB_UID]=e;var l=new k(b.geometry,m._getPhantomSymbol(b.geometry,m._editStore.DELETE),j);a._phantomLayer.add(l),m._editStore.pushPhantomGraphic(l,function(a){}),f.set(l.getNode(),"stroke-dasharray","4,4"),g.set(l.getNode(),"pointer-events","none"),m.attachmentsStore&&m.attachmentsStore.deleteAttachmentsByFeatureId(a.url,e,function(a){})}else d.deleteResults.push({success:!1,error:i,objectId:e});h.resolve(c)})},a._pushValidatedUpdateFeatureToDB=function(a,b,c,d,e,h){m._editStore.pushEdit(c,a.url,b,function(c,i){if(c){d.updateResults.push({success:!0,error:null,objectId:e});var j={};j[m.DB_UID]=e;var l=new k(b.geometry,m._getPhantomSymbol(b.geometry,m._editStore.UPDATE),j);a._phantomLayer.add(l),m._editStore.pushPhantomGraphic(l,function(a){}),f.set(l.getNode(),"stroke-dasharray","5,2"),g.set(l.getNode(),"pointer-events","none")}else d.updateResults.push({success:!1,error:i,objectId:e});h.resolve(c)})},a._pushValidatedAddFeatureToDB=function(a,b,c,d,e,h){m._editStore.pushEdit(c,a.url,b,function(c,i){if(c){d.addResults.push({success:!0,error:null,objectId:e});var j={};j[m.DB_UID]=e;var l=new k(b.geometry,m._getPhantomSymbol(b.geometry,m._editStore.ADD),j);a._phantomLayer.add(l),m._editStore.pushPhantomGraphic(l,function(a){}),f.set(l.getNode(),"stroke-dasharray","10,4"),g.set(l.getNode(),"pointer-events","none")}else d.addResults.push({success:!1,error:i,objectId:e});h.resolve(c)})},a._validateFeature=function(c,d,e){var f=new b,g=d+"/"+c.attributes.objectid;return m._editStore.getEdit(g,function(b,d){if(b)switch(e){case m._editStore.ADD:f.resolve({success:!0,graphic:c,operation:e});break;case m._editStore.UPDATE:d.operation==m._editStore.ADD&&(c.operation=m._editStore.ADD,e=m._editStore.ADD),f.resolve({success:!0,graphic:c,operation:e});break;case m._editStore.DELETE:var g=!0;d.operation==m._editStore.ADD&&a._deleteTemporaryFeature(c,function(a){a||(g=!1)}),f.resolve({success:g,graphic:c,operation:e})}else"Id not found"==d?f.resolve({success:!0,graphic:c,operation:e}):f.reject(c)}),f},a._deleteTemporaryFeature=function(d,e){function f(){var c=new b;return m._editStore["delete"](a.url,d,function(a,b){c.resolve(a?!0:!1)}),c.promise}function g(){var a=new b;return m._editStore.deletePhantomGraphic(h,function(b){a.resolve(b?!0:!1)}),a.promise}var h=m._editStore.PHANTOM_GRAPHIC_PREFIX+m._editStore._PHANTOM_PREFIX_TOKEN+d.attributes[m.DB_UID];c([f(),g()]).then(function(a){e(a)})},a._getFilesFromForm=function(a){var b=[],c=e.filter(a.elements,function(a){return"file"===a.type});return c.forEach(function(a){b.push.apply(b,a.files)},this),b},a._replaceFeatureIds=function(a,b,c){a.length||c(0);var d,e=a.length,f=e,g=0;for(d=0;e>d;d++)m.attachmentsStore.replaceFeatureId(this.url,a[d],b[d],function(a){--f,g+=a?1:0,0===f&&c(g)}.bind(this))},a._nextTempId=-1,a._getNextTempId=function(){return this._nextTempId--},l()},goOffline:function(){this._onlineStatus=this.OFFLINE},goOnline:function(a){this._onlineStatus=this.RECONNECTING,this._replayStoredEdits(function(b,c){var d={features:{success:b,responses:c}};null!=this.attachmentsStore?this._sendStoredAttachments(function(b,c){this._onlineStatus=this.ONLINE,d.attachments={success:b,responses:c},a&&a(d)}.bind(this)):(this._onlineStatus=this.ONLINE,a&&a(d))}.bind(this))},getOnlineStatus:function(){return this._onlineStatus},serializeFeatureGraphicsArray:function(a,b){for(var c=a.length,d=[],e=0;c>e;e++){var f=a[e].toJson();if(d.push(f),e==c-1){var g=JSON.stringify(d);b(g);break}}},_initializeDB:function(a,b){var c=this._editStore;c.dbName=this.DB_NAME,c.objectStoreName=this.DB_OBJECTSTORE_NAME,c.objectId=this.DB_UID,c.init(function(d,e){"object"==typeof a&&d===!0&&void 0!==a&&null!==a?c.pushFeatureLayerJSON(a,function(a,c){a?b(!0,null):b(!1,c)}):d?b(!0,null):b(!1,e)})},_checkFileAPIs:function(){return window.File&&window.FileReader&&window.FileList&&window.Blob?(XMLHttpRequest.prototype.sendAsBinary||(XMLHttpRequest.prototype.sendAsBinary=function(a){function b(a){return 255&a.charCodeAt(0)}var c=Array.prototype.map.call(a,b),d=new Uint8Array(c);this.send(d.buffer)}),!0):!1},_extendAjaxReq:function(a){a.sendAsBinary=XMLHttpRequest.prototype.sendAsBinary},_phantomSymbols:[],_getPhantomSymbol:function(a,b){if(0===this._phantomSymbols.length){var c=[0,255,0,255],d=1.5;this._phantomSymbols.point=[],this._phantomSymbols.point[this._editStore.ADD]=new l({type:"esriSMS",style:"esriSMSCross",xoffset:10,yoffset:10,color:[255,255,255,0],size:15,outline:{color:c,width:d,type:"esriSLS",style:"esriSLSSolid"}}),this._phantomSymbols.point[this._editStore.UPDATE]=new l({type:"esriSMS",style:"esriSMSCircle",xoffset:0,yoffset:0,color:[255,255,255,0],size:15,outline:{color:c,width:d,type:"esriSLS",style:"esriSLSSolid"}}),this._phantomSymbols.point[this._editStore.DELETE]=new l({type:"esriSMS",style:"esriSMSX",xoffset:0,yoffset:0,color:[255,255,255,0],size:15,outline:{color:c,width:d,type:"esriSLS",style:"esriSLSSolid"}}),this._phantomSymbols.multipoint=null,this._phantomSymbols.polyline=[],this._phantomSymbols.polyline[this._editStore.ADD]=new m({type:"esriSLS",style:"esriSLSSolid",color:c,width:d}),this._phantomSymbols.polyline[this._editStore.UPDATE]=new m({type:"esriSLS",style:"esriSLSSolid",color:c,width:d}),this._phantomSymbols.polyline[this._editStore.DELETE]=new m({type:"esriSLS",style:"esriSLSSolid",color:c,width:d}),this._phantomSymbols.polygon=[],this._phantomSymbols.polygon[this._editStore.ADD]=new n({type:"esriSFS",style:"esriSFSSolid",color:[255,255,255,0],outline:{type:"esriSLS",style:"esriSLSSolid",color:c,width:d}}),this._phantomSymbols.polygon[this._editStore.UPDATE]=new n({type:"esriSFS",style:"esriSFSSolid",color:[255,255,255,0],outline:{type:"esriSLS",style:"esriSLSDash",color:c,width:d}}),this._phantomSymbols.polygon[this._editStore.DELETE]=new n({type:"esriSFS",style:"esriSFSSolid",color:[255,255,255,0],outline:{type:"esriSLS",style:"esriSLSDot",color:c,width:d}})}return this._phantomSymbols[a.type][b]},_fieldSegment:function(a,b){return'Content-Disposition: form-data; name="'+a+'"\r\n\r\n'+b+"\r\n"},_fileSegment:function(a,b,c,d){return'Content-Disposition: form-data; name="'+a+'"; filename="'+b+'"\r\nContent-Type: '+c+"\r\n\r\n"+d+"\r\n"},_uploadAttachment:function(a){var c=new b,d=[];d.push(this._fieldSegment("f","json")),d.push(this._fileSegment("attachment",a.name,a.contentType,a.content));var e=new XMLHttpRequest;e.sendAsBinary||this._extendAjaxReq(e),e.onload=function(a){c.resolve(JSON.parse(a.target.response))},e.onerror=function(a){c.reject(a)};var f=this.proxyPath||i.defaults.io.proxyUrl||"";""!==f&&(f+="?"),e.open("post",f+a.featureId+"/addAttachment",!0);var g="---------------------------"+Date.now().toString(16);return e.setRequestHeader("Content-Type","multipart/form-data; boundary="+g),e.sendAsBinary("--"+g+"\r\n"+d.join("--"+g+"\r\n")+"--"+g+"--\r\n"),c},_deleteAttachment:function(a,c){var d=new b;return this.attachmentsStore["delete"](a,function(a){d.resolve(c)}),d},_sendStoredAttachments:function(a){this.attachmentsStore.getAllAttachments(function(b){var d=[];b.forEach(function(a){var b=this._uploadAttachment(a).then(function(b){return b.addAttachmentResult&&b.addAttachmentResult.success===!0?this._deleteAttachment(a.id,b):null}.bind(this),function(a){});d.push(b)},this);var e=c(d);e.then(function(b){a&&a(!0,b)},function(b){a&&a(!1,b)})}.bind(this))},_replayStoredEdits:function(a){var b,d={},e=this,f=[],g=[],h=[],i=[],j=[],l=this._featureLayers,m=this.attachmentsStore,n=this._editStore;this._editStore.getAllEditsArray(function(o,p){if(o.length>0){j=o;for(var q=j.length,r=0;q>r;r++){b=l[j[r].layer],null==m&&b.hasAttachments,b._attachmentsStore=m,b.__onEditsComplete=b.onEditsComplete,b.onEditsComplete=function(){},f=[],g=[],h=[],i=[];var s=new k(j[r].graphic);switch(j[r].operation){case n.ADD:for(var t=0;t0&&(g.updateResults[0].success?(h.layer=g.layer,h.id=g.updateResults[0].objectId,d.push(h)):e.push(g)),g.deleteResults.length>0&&(g.deleteResults[0].success?(h.layer=g.layer,h.id=g.deleteResults[0].objectId,d.push(h)):e.push(g)),g.addResults.length>0&&(g.addResults[0].success?(h.layer=g.layer,h.id=g.tempId,d.push(h)):e.push(g))}for(var i={},j=d.length,k=0;j>k;k++)i[k]=this._updateDatabase(d[k]);var l=c(i);l.then(function(a){e.length>0?b(!1,a):b(!0,a)},function(a){b(!1,a)})}else b(!0,{})},_updateDatabase:function(a){var c=new b,d={};return d.attributes={},d.attributes[this.DB_UID]=a.id,this._editStore["delete"](a.layer,d,function(a,b){a?c.resolve({success:!0,error:null}):c.reject({success:!1,error:b})}.bind(this)),c.promise},getFeatureLayerJSON:function(a,b){require(["esri/request"],function(c){var d=c({url:a,content:{f:"json"},handleAs:"json",callbackParamName:"callback"});d.then(function(a){b(!0,a)},function(a){b(!1,a.message)})})},_internalApplyEdits:function(a,c,d,e,f,g){var h=new b;return a._applyEdits(e,f,g,function(b,e,f){a._phantomLayer.clear();var g=b.map(function(a){return a.objectId});null!=a._attachmentsStore&&a.hasAttachments&&d.length>0?a._replaceFeatureIds(d,g,function(g){h.resolve({id:c,layer:a.url,tempId:d,addResults:b,updateResults:e,deleteResults:f})}):h.resolve({id:c,layer:a.url,tempId:d,addResults:b,updateResults:e,deleteResults:f})},function(b){a.onEditsComplete=a.__onEditsComplete,delete a.__onEditsComplete,h.reject(b)}),h.promise},_optimizeEditsQueue:function(){return"DEPRECATED at v2.5!"},getReadableEdit:function(a){return"DEPRECATED at v2.5!"}})}),"undefined"!=typeof O?O.esri.Edit={}:(O={},O.esri={Edit:{}}),O.esri.Edit.EditStore=function(){"use strict";this._db=null,this.dbName="features_store",this.objectStoreName="features",this.objectId="objectid";var a="featureId";this.ADD="add",this.UPDATE="update",this.DELETE="delete",this.FEATURE_LAYER_JSON_ID="feature-layer-object-1001",this.PHANTOM_GRAPHIC_PREFIX="phantom-layer",this._PHANTOM_PREFIX_TOKEN="|@|",this.isSupported=function(){return window.indexedDB?!0:!1},this.pushEdit=function(a,b,c,d){var e={id:b+"/"+c.attributes[this.objectId],operation:a,layer:b,type:c.geometry.type,graphic:c.toJson()};if("undefined"==typeof c.attributes[this.objectId])d(!1,"editsStore.pushEdit() - failed to insert undefined objectId into database. Did you set offlineFeaturesManager.DB_UID? "+JSON.stringify(c.attributes));else{var f=this._db.transaction([this.objectStoreName],"readwrite");f.oncomplete=function(a){d(!0)},f.onerror=function(a){d(!1,a.target.error.message)};var g=f.objectStore(this.objectStoreName);g.put(e)}},this.pushFeatureLayerJSON=function(a,b){"object"!=typeof a&&b(!1,"dataObject type is not an object.");var c=this._db;a.id=this.FEATURE_LAYER_JSON_ID,this.getFeatureLayerJSON(function(d,e){var f;if(d&&"undefined"!=typeof e){f=c.transaction([this.objectStoreName],"readwrite").objectStore(this.objectStoreName);for(var g in a)a.hasOwnProperty(g)&&(e[g]=a[g]);var h=f.put(e);h.onsuccess=function(){b(!0,null)},h.onerror=function(a){b(!1,a)}}else{var i=c.transaction([this.objectStoreName],"readwrite");i.oncomplete=function(a){b(!0,null)},i.onerror=function(a){b(!1,a.target.error.message)},f=i.objectStore(this.objectStoreName);try{f.put(a)}catch(j){b(!1,JSON.stringify(j))}}}.bind(this))},this.getFeatureLayerJSON=function(a){var b=this._db.transaction([this.objectStoreName],"readwrite").objectStore(this.objectStoreName),c=b.get(this.FEATURE_LAYER_JSON_ID);c.onsuccess=function(){var b=c.result;"undefined"!=typeof b?a(!0,b):a(!1,"nothing found")},c.onerror=function(b){a(!1,b)}},this.deleteFeatureLayerJSON=function(a){var b=this._db,c=null,d=this,e=this.FEATURE_LAYER_JSON_ID;require(["dojo/Deferred"],function(f){c=new f,c.then(function(b){d.editExists(e).then(function(b){b.success===!1?a(!0,{message:"id does not exist"}):a(!1,{message:null})},function(b){a(!0,{message:"id does not exist"})})},function(b){a(!1,{message:"id does not exist"})}),d.editExists(e).then(function(a){if(a&&a.success){var f=b.transaction([d.objectStoreName],"readwrite").objectStore(d.objectStoreName),g=f["delete"](e);g.onsuccess=function(){c.resolve(!0)},g.onerror=function(a){c.reject({success:!1,error:a})}}else c.reject({success:!1,message:"id does not exist"})},function(a){c.reject({success:!1,message:a})}.bind(this))})},this.pushPhantomGraphic=function(a,b){var c=this._db,d=this.PHANTOM_GRAPHIC_PREFIX+this._PHANTOM_PREFIX_TOKEN+a.attributes[this.objectId],e={id:d,graphic:a.toJson()},f=c.transaction([this.objectStoreName],"readwrite");f.oncomplete=function(a){b(!0,null)},f.onerror=function(a){b(!1,a.target.error.message)};var g=f.objectStore(this.objectStoreName);g.put(e)},this.getPhantomGraphicsArray=function(a){var b=[];if(null!==this._db){var c=this.PHANTOM_GRAPHIC_PREFIX,d=this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName).openCursor();d.onsuccess=function(d){var e=d.target.result;e&&e.value&&e.value.id?(-1!=e.value.id.indexOf(c)&&b.push(e.value),e["continue"]()):a(b,"end")}.bind(this),d.onerror=function(b){a(null,b)}}else a(null,"no db")},this._getPhantomGraphicsArraySimple=function(a){var b=[];if(null!==this._db){var c=this.PHANTOM_GRAPHIC_PREFIX,d=this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName).openCursor();d.onsuccess=function(d){var e=d.target.result;e&&e.value&&e.value.id?(-1!=e.value.id.indexOf(c)&&b.push(e.value.id),e["continue"]()):a(b,"end")}.bind(this),d.onerror=function(b){a(null,b)}}else a(null,"no db")},this.deletePhantomGraphic=function(a,b){var c=this._db,d=null,e=this;require(["dojo/Deferred"],function(f){d=new f,e.editExists(a).then(function(f){if(f.success){d.then(function(c){e.editExists(a).then(function(a){b(a.success===!1?!0:!1)},function(a){b(!0)})},function(a){b(!1,a)});var g=c.transaction([e.objectStoreName],"readwrite").objectStore(e.objectStoreName),h=g["delete"](a);h.onsuccess=function(){d.resolve(!0)},h.onerror=function(a){d.reject({success:!1,error:a})}}},function(a){b(!1)})})},this.resetLimitedPhantomGraphicsQueue=function(a,b){if(Object.keys(a).length>0){var c=this._db,d=0,e=c.transaction([this.objectStoreName],"readwrite"),f=e.objectStore(this.objectStoreName);f.onerror=function(){d++},e.oncomplete=function(){b(0===d?!0:!1)};for(var g in a)if(a.hasOwnProperty(g)){var h=a[g],i=this.PHANTOM_GRAPHIC_PREFIX+this._PHANTOM_PREFIX_TOKEN+h.id;h.updateResults.length>0&&h.updateResults[0].success&&f["delete"](i),h.deleteResults.length>0&&h.deleteResults[0].success&&f["delete"](i),h.addResults.length>0&&h.addResults[0].success&&f["delete"](i)}}else b(!0)},this.resetPhantomGraphicsQueue=function(a){var b=this._db;this._getPhantomGraphicsArraySimple(function(c){if(c!=[]){var d=0,e=b.transaction([this.objectStoreName],"readwrite"),f=e.objectStore(this.objectStoreName);f.onerror=function(){d++},e.oncomplete=function(){a(0===d?!0:!1)};for(var g=c.length,h=0;g>h;h++)f["delete"](c[h])}else a(!0)}.bind(this))},this.getEdit=function(a,b){var c=this._db.transaction([this.objectStoreName],"readwrite").objectStore(this.objectStoreName);require(["dojo/Deferred"],function(d){if("undefined"==typeof a)return void b(!1,"id is undefined.");var e=c.get(a);e.onsuccess=function(){var c=e.result;c&&c.id==a?b(!0,c):b(!1,"Id not found")},e.onerror=function(a){b(!1,a)}})},this.getAllEdits=function(a){if(null!==this._db){var b=this.FEATURE_LAYER_JSON_ID,c=this.PHANTOM_GRAPHIC_PREFIX,d=this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName).openCursor();d.onsuccess=function(d){var e=d.target.result;e&&e.hasOwnProperty("value")&&e.value.hasOwnProperty("id")?(e.value.id!==b&&-1==e.value.id.indexOf(c)&&a(e.value,null),e["continue"]()):a(null,"end")}.bind(this),d.onerror=function(b){a(null,b)}}else a(null,"no db")},this.getAllEditsArray=function(a){var b=[];if(null!==this._db){var c=this.FEATURE_LAYER_JSON_ID,d=this.PHANTOM_GRAPHIC_PREFIX,e=this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName).openCursor();e.onsuccess=function(e){var f=e.target.result;f&&f.value&&f.value.id?(f.value.id!==c&&-1==f.value.id.indexOf(d)&&b.push(f.value),f["continue"]()):a(b,"end")}.bind(this),e.onerror=function(b){a(null,b)}}else a(null,"no db")},this.updateExistingEdit=function(a,b,c,d){var e=this._db.transaction([this.objectStoreName],"readwrite").objectStore(this.objectStoreName),f=e.get(c.attributes[this.objectId]);f.onsuccess=function(){f.result;var g={id:b+"/"+c.attributes[this.objectId],operation:a,layer:b,graphic:c.toJson()},h=e.put(g);h.onsuccess=function(){d(!0)},h.onerror=function(a){d(!1,a)}}.bind(this)},this["delete"]=function(a,b,c){var d=this._db,e=null,f=this,g=a+"/"+b.attributes[this.objectId];require(["dojo/Deferred"],function(a){e=new a,f.editExists(g).then(function(a){if(a.success){e.then(function(a){f.editExists(g).then(function(a){c(a.success===!1?!0:!1)},function(a){c(!0)})},function(a){c(!1,a)});var b=d.transaction([f.objectStoreName],"readwrite").objectStore(f.objectStoreName),h=b["delete"](g);h.onsuccess=function(){e.resolve(!0)},h.onerror=function(a){e.reject({success:!1,error:a})}}},function(a){c(!1)})})},this.resetEditsQueue=function(a){var b=this._db.transaction([this.objectStoreName],"readwrite").objectStore(this.objectStoreName).clear();b.onsuccess=function(b){setTimeout(function(){a(!0)},0)},b.onerror=function(b){a(!1,b)}},this.pendingEditsCount=function(a){var b=0,c=this.FEATURE_LAYER_JSON_ID,d=this.PHANTOM_GRAPHIC_PREFIX,e=this._db.transaction([this.objectStoreName],"readwrite"),f=e.objectStore(this.objectStoreName);f.openCursor().onsuccess=function(e){var f=e.target.result;f&&f.value&&f.value.id&&-1==f.value.id.indexOf(d)?(f.value.id!==c&&b++,f["continue"]()):a(b)}},this.editExists=function(a){var b=this._db,c=null,d=this;return require(["dojo/Deferred"],function(e){c=new e;var f=b.transaction([d.objectStoreName],"readwrite").objectStore(d.objectStoreName),g=f.get(a);g.onsuccess=function(){var b=g.result;b&&b.id==a?c.resolve({success:!0,error:null}):c.reject({success:!1,error:"Layer id is not a match."})},g.onerror=function(a){c.reject({success:!1,error:a})}}),c},this.getUsage=function(a){var b=this.FEATURE_LAYER_JSON_ID,c=this.PHANTOM_GRAPHIC_PREFIX,d={sizeBytes:0,editCount:0},e=this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName).openCursor();e.onsuccess=function(e){var f=e.target.result;if(f&&f.value&&f.value.id){var g=f.value,h=JSON.stringify(g);d.sizeBytes+=h.length,-1==f.value.id.indexOf(c)&&f.value.id!==b&&(d.editCount+=1),f["continue"]()}else a(d,null)},e.onerror=function(b){a(null,b)}},this._serialize=function(a){var b=a.toJson(),c={attributes:b.attributes,geometry:b.geometry,infoTemplate:b.infoTemplate,symbol:b.symbol};return JSON.stringify(c)},this._deserialize=function(a){var b;return require(["esri/graphic"],function(c){b=new c(JSON.parse(a))}),b},this.init=function(b){var c=indexedDB.open(this.dbName,11);b=b||function(a){}.bind(this),c.onerror=function(a){b(!1,a.target.errorCode)}.bind(this),c.onupgradeneeded=function(b){var c=b.target.result;c.objectStoreNames.contains(this.objectStoreName)&&c.deleteObjectStore(this.objectStoreName);var d=c.createObjectStore(this.objectStoreName,{keyPath:"id"});d.createIndex(a,a,{unique:!1})}.bind(this),c.onsuccess=function(a){this._db=a.target.result,b(!0)}.bind(this)},this.hasPendingEdits=function(){return"DEPRECATED at v2.5!"},this._isEditDuplicated=function(a,b){return"DEPRECATED at v2.5!"},this._storeEditsQueue=function(a){return"DEPRECATED at v2.5!"},this._unpackArrayOfEdits=function(a){return"DEPRECATED at v2.5!"},this.getLocalStorageSizeBytes=function(){return"DEPRECATED at v2.5!"},this.peekFirstEdit=function(){return"DEPRECATED at v2.5!"},this.popFirstEdit=function(){return"DEPRECATED at v2.5!"}},O.esri.Edit.AttachmentsStore=function(){"use strict";this._db=null;var a="attachments_store",b="attachments";this.isSupported=function(){return window.indexedDB?!0:!1},this.store=function(a,c,d,e,f){try{this._readFile(e,function(g){var h={id:c,objectId:d,featureId:a+"/"+d,contentType:e.type,name:e.name,size:e.size,url:this._createLocalURL(e),content:g},i=this._db.transaction([b],"readwrite");i.oncomplete=function(a){f(!0,h)},i.onerror=function(a){f(!1,a.target.error.message)};var j=i.objectStore(b),k=j.put(h);k.onsuccess=function(a){}}.bind(this))}catch(g){f(!1,g.stack)}},this.retrieve=function(a,c){var d=this._db.transaction([b]).objectStore(b),e=d.get(a);e.onsuccess=function(a){var b=a.target.result;b?c(!0,b):c(!1,"not found")},e.onerror=function(a){c(!1,a)}},this.getAttachmentsByFeatureId=function(a,c,d){var e=a+"/"+c,f=[],g=this._db.transaction([b]).objectStore(b),h=g.index("featureId"),i=IDBKeyRange.only(e);h.openCursor(i).onsuccess=function(a){var b=a.target.result;b?(f.push(b.value),b["continue"]()):d(f)}},this.getAttachmentsByFeatureLayer=function(a,c){var d=[],e=this._db.transaction([b]).objectStore(b),f=e.index("featureId"),g=IDBKeyRange.bound(a+"/",a+"/A");f.openCursor(g).onsuccess=function(a){var b=a.target.result;b?(d.push(b.value),b["continue"]()):c(d)}},this.getAllAttachments=function(a){var c=[],d=this._db.transaction([b]).objectStore(b);d.openCursor().onsuccess=function(b){var d=b.target.result;d?(c.push(d.value),d["continue"]()):a(c)}},this.deleteAttachmentsByFeatureId=function(a,c,d){var e=a+"/"+c,f=this._db.transaction([b],"readwrite").objectStore(b),g=f.index("featureId"),h=IDBKeyRange.only(e),i=0;g.openCursor(h).onsuccess=function(a){var b=a.target.result;if(b){var c=b.value;this._revokeLocalURL(c),f["delete"](b.primaryKey),i++,b["continue"]()}else setTimeout(function(){d(i)},0)}.bind(this)},this["delete"]=function(a,c){this.retrieve(a,function(d,e){if(!d)return void c(!1,"attachment "+a+" not found");this._revokeLocalURL(e);var f=this._db.transaction([b],"readwrite").objectStore(b)["delete"](a);f.onsuccess=function(a){setTimeout(function(){c(!0)},0)},f.onerror=function(a){c(!1,a)}}.bind(this))},this.deleteAll=function(a){this.getAllAttachments(function(c){c.forEach(function(a){this._revokeLocalURL(a)},this);var d=this._db.transaction([b],"readwrite").objectStore(b).clear();d.onsuccess=function(b){setTimeout(function(){a(!0)},0)},d.onerror=function(b){a(!1,b)}}.bind(this))},this.replaceFeatureId=function(a,c,d,e){var f=a+"/"+c,g=this._db.transaction([b],"readwrite").objectStore(b),h=g.index("featureId"),i=IDBKeyRange.only(f),j=0;h.openCursor(i).onsuccess=function(b){var c=b.target.result;if(c){var f=a+"/"+d,h=c.value;h.featureId=f,h.objectId=d,g.put(h),j++,c["continue"]()}else setTimeout(function(){e(j)},1)}},this.getUsage=function(a){var c={sizeBytes:0,attachmentCount:0},d=this._db.transaction([b]).objectStore(b).openCursor();d.onsuccess=function(b){var d=b.target.result;if(d){var e=d.value,f=JSON.stringify(e);c.sizeBytes+=f.length,c.attachmentCount+=1,d["continue"]()}else a(c,null)}.bind(this),d.onerror=function(b){a(null,b)}},this._readFile=function(a,b){var c=new FileReader;c.onload=function(a){b(a.target.result)},c.readAsBinaryString(a)},this._createLocalURL=function(a){return window.URL.createObjectURL(a)},this._revokeLocalURL=function(a){window.URL.revokeObjectURL(a.url)},this.init=function(c){var d=indexedDB.open(a,11);c=c||function(a){}.bind(this),d.onerror=function(a){c(!1,a.target.errorCode)}.bind(this),d.onupgradeneeded=function(a){var c=a.target.result;c.objectStoreNames.contains(b)&&c.deleteObjectStore(b);var d=c.createObjectStore(b,{keyPath:"id"});d.createIndex("featureId","featureId",{unique:!1})}.bind(this),d.onsuccess=function(a){this._db=a.target.result,c(!0)}.bind(this)}}; \ No newline at end of file +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(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o){"use strict";return d("O.esri.Edit.OfflineFeaturesManager",[a],{_onlineStatus:"online",_featureLayers:{},_editStore:new O.esri.Edit.EditStore,ONLINE:"online",OFFLINE:"offline",RECONNECTING:"reconnecting",attachmentsStore:null,proxyPath:null,DB_NAME:"features_store",DB_OBJECTSTORE_NAME:"features",DB_UID:"objectid",ATTACHMENTS_DB_NAME:"attachments_store",ATTACHMENTS_DB_OBJECTSTORE_NAME:"attachments",events:{EDITS_SENT:"edits-sent",EDITS_ENQUEUED:"edits-enqueued",EDITS_ENQUEUED_ERROR:"edits-enqueued-error",EDITS_SENT_ERROR:"edits-sent-error",ALL_EDITS_SENT:"all-edits-sent",ATTACHMENT_ENQUEUED:"attachment-enqueued",ATTACHMENTS_SENT:"attachments-sent"},initAttachments:function(a){if(a=a||function(a){},!this._checkFileAPIs())return a(!1,"File APIs not supported");try{if(this.attachmentsStore=new O.esri.Edit.AttachmentsStore,this.attachmentsStore.dbName=this.ATTACHMENTS_DB_NAME,this.attachmentsStore.objectStoreName=this.ATTACHMENTS_DB_OBJECTSTORE_NAME,!this.attachmentsStore.isSupported())return a(!1,"indexedDB not supported");this.attachmentsStore.init(a)}catch(b){}},extend:function(a,d,i){function l(){try{a._phantomLayer=new j({opacity:.8}),a._map.addLayer(a._phantomLayer)}catch(b){}}var m=this;a.objectIdField=this.DB_UID,this._initializeDB(i,d),this._featureLayers[a.url]=a,a._applyEdits=a.applyEdits,a._addAttachment=a.addAttachment,a._queryAttachmentInfos=a.queryAttachmentInfos,a._deleteAttachments=a.deleteAttachments,a._updateAttachment=a.updateAttachment,a.queryAttachmentInfos=function(a,c,d){if(m.getOnlineStatus()===m.ONLINE){var e=this._queryAttachmentInfos(a,function(){m.emit(m.events.ATTACHMENTS_INFO,arguments),c&&c.apply(this,arguments)},d);return e}if(m.attachmentsStore){var f=new b;return m.attachmentsStore.getAttachmentsByFeatureId(this.url,a,function(a){c&&c(a),f.resolve(a)}),f}},a.addAttachment=function(a,c,d,e){if(m.getOnlineStatus()===m.ONLINE)return this._addAttachment(a,c,function(){m.emit(m.events.ATTACHMENTS_SENT,arguments),d&&d.apply(this,arguments)},function(a){e&&e.apply(this,arguments)});if(m.attachmentsStore){var f=this._getFilesFromForm(c),g=f[0],i=new b,j=this._getNextTempId();return m.attachmentsStore.store(this.url,j,a,g,m.attachmentsStore.TYPE.ADD,function(b,c){var f={attachmentId:j,objectId:a,success:b};if(b){m.emit(m.events.ATTACHMENT_ENQUEUED,f),d&&d(f),i.resolve(f);var g=this._url.path+"/"+a+"/attachments/"+j,k=h("[href="+g+"]");k.attr("href",c.url)}else f.error="can't store attachment",e&&e(f),i.reject(f)}.bind(this)),i}},a.updateAttachment=function(a,c,d,e,f){if(m.getOnlineStatus()===m.ONLINE)return this._updateAttachment(a,c,d,function(){e&&e.apply(this,arguments)},function(a){f&&f.apply(this,arguments)});if(m.attachmentsStore){var g=this._getFilesFromForm(d),i=g[0],j=new b;return m.attachmentsStore.store(this.url,c,a,i,m.attachmentsStore.TYPE.UPDATE,function(b,d){var g={attachmentId:c,objectId:a,success:b};if(b){m.emit(m.events.ATTACHMENT_ENQUEUED,g),e&&e(g),j.resolve(g);var i=this._url.path+"/"+a+"/attachments/"+c,k=h("[href="+i+"]");k.attr("href",d.url)}else g.error="layer.updateAttachment::attachmentStore can't store attachment",f&&f(g),j.reject(g)}.bind(this)),j}},a.deleteAttachments=function(a,d,e,f){if(m.getOnlineStatus()===m.ONLINE){var g=this._deleteAttachments(a,d,function(){e&&e.apply(this,arguments)},function(a){f&&f.apply(this,arguments)});return g}if(m.attachmentsStore){var h=[];d.forEach(function(c){c=parseInt(c,10);var d=new b;if(0>c)m.attachmentsStore["delete"](c,function(b){var e={objectId:a,attachmentId:c,success:b};d.resolve(e)});else{var e=new Blob([],{type:"image/png"});m.attachmentsStore.store(this.url,c,a,e,m.attachmentsStore.TYPE.DELETE,function(b,e){var f={attachmentId:c,objectId:a,success:b};b?d.resolve(f):d.reject(f)}.bind(this))}h.push(d)},this);var i=c(h);return i.then(function(a){e&&e(a)}),i}},a.applyEdits=function(a,d,e,f,g){var h=[];if(m.getOnlineStatus()===m.ONLINE){var i=this._applyEdits(a,d,e,function(){m.emit(m.events.EDITS_SENT,arguments),f&&f.apply(this,arguments)},g);return i}var j=new b,k={addResults:[],updateResults:[],deleteResults:[]},l={};return a=a||[],a.forEach(function(a){var c=new b,d=this._getNextTempId();a.attributes[this.objectIdField]=d;var e=this;this._validateFeature(a,this.url,m._editStore.ADD).then(function(b){b.success?e._pushValidatedAddFeatureToDB(e,a,b.operation,k,d,c):c.resolve(!0)},function(a){c.reject(a)}),h.push(c)},this),d=d||[],d.forEach(function(a){var c=new b,d=a.attributes[this.objectIdField];l[d]=a;var e=this;this._validateFeature(a,this.url,m._editStore.UPDATE).then(function(b){b.success?e._pushValidatedUpdateFeatureToDB(e,a,b.operation,k,d,c):c.resolve(!0)},function(a){c.reject(a)}),h.push(c)},this),e=e||[],e.forEach(function(a){var c=new b,d=a.attributes[this.objectIdField],e=this;this._validateFeature(a,this.url,m._editStore.DELETE).then(function(b){b.success?e._pushValidatedDeleteFeatureToDB(e,a,b.operation,k,d,c):c.resolve(!0)},function(a){c.reject(a)}),h.push(c)},this),c(h).then(function(b){for(var c=!0,d=b.length,e=0;d>e;e++)b[e]===!1&&(c=!1);this._editHandler(k,a,l,f,g,j),c===!0?m.emit(m.events.EDITS_ENQUEUED,k):m.emit(m.events.EDITS_ENQUEUED_ERROR,k)}.bind(this)),j},a.convertGraphicLayerToJSON=function(a,b,c){var d={};d.objectIdFieldName=b.target.hasOwnProperty("objectIdField")?b.target.objectIdField:this.objectIdField,d.globalIdFieldName=b.target.globalIdField,d.geometryType=b.target.geometryType,d.spatialReference=b.target.spatialReference,d.fields=b.target.fields;for(var e=a.length,f=[],g=0;e>g;g++){var h=a[g].toJson();if(f.push(h),g==e-1){var i=JSON.stringify(f),j=JSON.stringify(d);c(i,j);break}}},a.getFeatureLayerJSON=function(a,b){require(["esri/request"],function(c){var d=c({url:a,content:{f:"json"},handleAs:"json",callbackParamName:"callback"});d.then(function(a){b(!0,a)},function(a){b(!1,a.message)})})},a.setFeatureLayerJSONDataStore=function(a,b){m._editStore.pushFeatureLayerJSON(a,function(a,c){b(a,c)})},a.getFeatureLayerJSONDataStore=function(a){m._editStore.getFeatureLayerJSON(function(b,c){a(b,c)})},a.setPhantomLayerGraphics=function(a){var b=a.length;if(b>0)for(var c=0;b>c;c++){var d=new k(a[c]);this._phantomLayer.add(d)}},a.getPhantomLayerGraphics=function(b){for(var c=a._phantomLayer.graphics,d=a._phantomLayer.graphics.length,e=[],f=0;d>f;f++){var g=c[f].toJson();if(e.push(g),f==d-1){var h=JSON.stringify(e);b(h);break}}},a.getPhantomGraphicsArray=function(a){m._editStore.getPhantomGraphicsArray(function(b,c){"end"==c?a(!0,b):a(!1,c)})},a.getAttachmentsUsage=function(a){m.attachmentsStore.getUsage(function(b,c){a(b,c)})},a.resetAttachmentsDatabase=function(a){m.attachmentsStore.resetAttachmentsQueue(function(b,c){a(b,c)})},a.getUsage=function(a){m._editStore.getUsage(function(b,c){a(b,c)})},a.resetDatabase=function(a){m._editStore.resetEditsQueue(function(b,c){a(b,c)})},a.pendingEditsCount=function(a){m._editStore.pendingEditsCount(function(b){a(b)})},a.getFeatureDefinition=function(a,b,c,d){var e={layerDefinition:a,featureSet:{features:b,geometryType:c}};d(e)},a.getAllEditsArray=function(a){m._editStore.getAllEditsArray(function(b,c){"end"==c?a(!0,b):a(!1,c)})},a._pushValidatedDeleteFeatureToDB=function(a,b,c,d,e,h){m._editStore.pushEdit(c,a.url,b,function(c,i){if(c){d.deleteResults.push({success:!0,error:null,objectId:e});var j={};j[m.DB_UID]=e;var l=new k(b.geometry,m._getPhantomSymbol(b.geometry,m._editStore.DELETE),j);a._phantomLayer.add(l),m._editStore.pushPhantomGraphic(l,function(a){}),f.set(l.getNode(),"stroke-dasharray","4,4"),g.set(l.getNode(),"pointer-events","none"),m.attachmentsStore&&m.attachmentsStore.deleteAttachmentsByFeatureId(a.url,e,function(a){})}else d.deleteResults.push({success:!1,error:i,objectId:e});h.resolve(c)})},a._pushValidatedUpdateFeatureToDB=function(a,b,c,d,e,h){m._editStore.pushEdit(c,a.url,b,function(c,i){if(c){d.updateResults.push({success:!0,error:null,objectId:e});var j={};j[m.DB_UID]=e;var l=new k(b.geometry,m._getPhantomSymbol(b.geometry,m._editStore.UPDATE),j);a._phantomLayer.add(l),m._editStore.pushPhantomGraphic(l,function(a){}),f.set(l.getNode(),"stroke-dasharray","5,2"),g.set(l.getNode(),"pointer-events","none")}else d.updateResults.push({success:!1,error:i,objectId:e});h.resolve(c)})},a._pushValidatedAddFeatureToDB=function(a,b,c,d,e,h){m._editStore.pushEdit(c,a.url,b,function(c,i){if(c){d.addResults.push({success:!0,error:null,objectId:e});var j={};j[m.DB_UID]=e;var l=new k(b.geometry,m._getPhantomSymbol(b.geometry,m._editStore.ADD),j);a._phantomLayer.add(l),m._editStore.pushPhantomGraphic(l,function(a){}),f.set(l.getNode(),"stroke-dasharray","10,4"),g.set(l.getNode(),"pointer-events","none")}else d.addResults.push({success:!1,error:i,objectId:e});h.resolve(c)})},a._validateFeature=function(c,d,e){var f=new b,g=d+"/"+c.attributes.objectid;return m._editStore.getEdit(g,function(b,d){if(b)switch(e){case m._editStore.ADD:f.resolve({success:!0,graphic:c,operation:e});break;case m._editStore.UPDATE:d.operation==m._editStore.ADD&&(c.operation=m._editStore.ADD,e=m._editStore.ADD),f.resolve({success:!0,graphic:c,operation:e});break;case m._editStore.DELETE:var g=!0;d.operation==m._editStore.ADD&&a._deleteTemporaryFeature(c,function(a){a||(g=!1)}),f.resolve({success:g,graphic:c,operation:e})}else"Id not found"==d?f.resolve({success:!0,graphic:c,operation:e}):f.reject(c)}),f},a._deleteTemporaryFeature=function(d,e){function f(){var c=new b;return m._editStore["delete"](a.url,d,function(a,b){c.resolve(a?!0:!1)}),c.promise}function g(){var a=new b;return m._editStore.deletePhantomGraphic(h,function(b){a.resolve(b?!0:!1)}),a.promise}var h=m._editStore.PHANTOM_GRAPHIC_PREFIX+m._editStore._PHANTOM_PREFIX_TOKEN+d.attributes[m.DB_UID];c([f(),g()]).then(function(a){e(a)})},a._getFilesFromForm=function(a){var b=[],c=e.filter(a.elements,function(a){return"file"===a.type});return c.forEach(function(a){b.push.apply(b,a.files)},this),b},a._replaceFeatureIds=function(a,b,c){a.length||c(0);var d,e=a.length,f=e,g=0;for(d=0;e>d;d++)m.attachmentsStore.replaceFeatureId(this.url,a[d],b[d],function(a){--f,g+=a?1:0,0===f&&c(g)}.bind(this))},a._nextTempId=-1,a._getNextTempId=function(){return this._nextTempId--},l()},goOffline:function(){this._onlineStatus=this.OFFLINE},goOnline:function(a){this._onlineStatus=this.RECONNECTING,this._replayStoredEdits(function(b,c){var d={features:{success:b,responses:c}};this._onlineStatus=this.ONLINE,null!=this.attachmentsStore?this._sendStoredAttachments(function(b,c,e){this._onlineStatus=this.ONLINE,d.attachments={success:b,responses:c,dbResponses:e},a&&a(d)}.bind(this)):(this._onlineStatus=this.ONLINE,a&&a(d))}.bind(this))},getOnlineStatus:function(){return this._onlineStatus},serializeFeatureGraphicsArray:function(a,b){for(var c=a.length,d=[],e=0;c>e;e++){var f=a[e].toJson();if(d.push(f),e==c-1){var g=JSON.stringify(d);b(g);break}}},_initializeDB:function(a,b){var c=this._editStore;c.dbName=this.DB_NAME,c.objectStoreName=this.DB_OBJECTSTORE_NAME,c.objectId=this.DB_UID,c.init(function(d,e){"object"==typeof a&&d===!0&&void 0!==a&&null!==a?c.pushFeatureLayerJSON(a,function(a,c){a?b(!0,null):b(!1,c)}):d?b(!0,null):b(!1,e)})},_checkFileAPIs:function(){return window.File&&window.FileReader&&window.FileList&&window.Blob?(XMLHttpRequest.prototype.sendAsBinary||(XMLHttpRequest.prototype.sendAsBinary=function(a){function b(a){return 255&a.charCodeAt(0)}var c=Array.prototype.map.call(a,b),d=new Uint8Array(c);this.send(d.buffer)}),!0):!1},_extendAjaxReq:function(a){a.sendAsBinary=XMLHttpRequest.prototype.sendAsBinary},_phantomSymbols:[],_getPhantomSymbol:function(a,b){if(0===this._phantomSymbols.length){var c=[0,255,0,255],d=1.5;this._phantomSymbols.point=[],this._phantomSymbols.point[this._editStore.ADD]=new l({type:"esriSMS",style:"esriSMSCross",xoffset:10,yoffset:10,color:[255,255,255,0],size:15,outline:{color:c,width:d,type:"esriSLS",style:"esriSLSSolid"}}),this._phantomSymbols.point[this._editStore.UPDATE]=new l({type:"esriSMS",style:"esriSMSCircle",xoffset:0,yoffset:0,color:[255,255,255,0],size:15,outline:{color:c,width:d,type:"esriSLS",style:"esriSLSSolid"}}),this._phantomSymbols.point[this._editStore.DELETE]=new l({type:"esriSMS",style:"esriSMSX",xoffset:0,yoffset:0,color:[255,255,255,0],size:15,outline:{color:c,width:d,type:"esriSLS",style:"esriSLSSolid"}}),this._phantomSymbols.multipoint=null,this._phantomSymbols.polyline=[],this._phantomSymbols.polyline[this._editStore.ADD]=new m({type:"esriSLS",style:"esriSLSSolid",color:c,width:d}),this._phantomSymbols.polyline[this._editStore.UPDATE]=new m({type:"esriSLS",style:"esriSLSSolid",color:c,width:d}),this._phantomSymbols.polyline[this._editStore.DELETE]=new m({type:"esriSLS",style:"esriSLSSolid",color:c,width:d}),this._phantomSymbols.polygon=[],this._phantomSymbols.polygon[this._editStore.ADD]=new n({type:"esriSFS",style:"esriSFSSolid",color:[255,255,255,0],outline:{type:"esriSLS",style:"esriSLSSolid",color:c,width:d}}),this._phantomSymbols.polygon[this._editStore.UPDATE]=new n({type:"esriSFS",style:"esriSFSSolid",color:[255,255,255,0],outline:{type:"esriSLS",style:"esriSLSDash",color:c,width:d}}),this._phantomSymbols.polygon[this._editStore.DELETE]=new n({type:"esriSFS",style:"esriSFSSolid",color:[255,255,255,0],outline:{type:"esriSLS",style:"esriSLSDot",color:c,width:d}})}return this._phantomSymbols[a.type][b]},_fieldSegment:function(a,b){return'Content-Disposition: form-data; name="'+a+'"\r\n\r\n'+b+"\r\n"},_fileSegment:function(a,b,c,d){return'Content-Disposition: form-data; name="'+a+'"; filename="'+b+'"\r\nContent-Type: '+c+"\r\n\r\n"+d+"\r\n"},_uploadAttachment:function(a){var c=new b,d=this._featureLayers[a.featureLayerUrl],e=new FormData;switch(e.append("attachment",a.file),a.type){case this.attachmentsStore.TYPE.ADD:d.addAttachment(a.objectId,e,function(b){c.resolve({attachmentResult:b,id:a.id})},function(a){c.reject(a)});break;case this.attachmentsStore.TYPE.UPDATE:e.append("attachmentId",a.id),d._sendAttachment("update",a.objectId,e,function(b){c.resolve({attachmentResult:b,id:a.id})},function(a){c.reject(a)});break;case this.attachmentsStore.TYPE.DELETE:d.deleteAttachments(a.objectId,[a.id],function(b){c.resolve({attachmentResult:b,id:a.id})},function(a){c.reject(a)})}return c.promise},_deleteAttachmentFromDB:function(a,c){var d=new b;return this.attachmentsStore["delete"](a,function(a){d.resolve({success:a,result:c})}),d},_cleanAttachmentsDB:function(a,b){var d=this,e=[],f=0;a.forEach(function(a){"object"==typeof a.attachmentResult&&a.attachmentResult.success?e.push(d._deleteAttachmentFromDB(a.id,null)):a.attachmentResult instanceof Array?a.attachmentResult.forEach(function(b){b.success?e.push(d._deleteAttachmentFromDB(a.id,null)):f++}):f++});var g=c(e);g.then(function(c){b(f>0?{errors:!0,attachmentsDBResults:c,uploadResults:a}:{errors:!1,attachmentsDBResults:c,uploadResults:a})})},_sendStoredAttachments:function(a){this.attachmentsStore.getAllAttachments(function(b){var d=this,e=[];b.forEach(function(a){var b=this._uploadAttachment(a);e.push(b)},this);var f=c(e);f.then(function(b){d._cleanAttachmentsDB(b,function(c){c.errors?a&&a(!1,b,c):a&&a(!0,b,c)})},function(b){a&&a(!1,b)})}.bind(this))},_replayStoredEdits:function(a){var b,d={},e=this,f=[],g=[],h=[],i=[],j=[],l=this._featureLayers,m=this.attachmentsStore,n=this._editStore;this._editStore.getAllEditsArray(function(o,p){if(o.length>0){j=o;for(var q=j.length,r=0;q>r;r++){b=l[j[r].layer],null==m&&b.hasAttachments||b.hasAttachments===!1&&a(!1,"WARNING: Attachments not supported in layer: "+b.id),b._attachmentsStore=m,b.__onEditsComplete=b.onEditsComplete,b.onEditsComplete=function(){},f=[],g=[],h=[],i=[];var s=new k(j[r].graphic);switch(j[r].operation){case n.ADD:for(var t=0;t0&&(g.updateResults[0].success?(h.layer=g.layer,h.id=g.updateResults[0].objectId,d.push(h)):e.push(g)),g.deleteResults.length>0&&(g.deleteResults[0].success?(h.layer=g.layer,h.id=g.deleteResults[0].objectId,d.push(h)):e.push(g)),g.addResults.length>0&&(g.addResults[0].success?(h.layer=g.layer,h.id=g.tempId,d.push(h)):e.push(g))}for(var i={},j=d.length,k=0;j>k;k++)i[k]=this._updateDatabase(d[k]);var l=c(i);l.then(function(a){e.length>0?b(!1,a):b(!0,a)},function(a){b(!1,a)})}else b(!0,{})},_updateDatabase:function(a){var c=new b,d={};return d.attributes={},d.attributes[this.DB_UID]=a.id,this._editStore["delete"](a.layer,d,function(a,b){a?c.resolve({success:!0,error:null}):c.reject({success:!1,error:b})}.bind(this)),c.promise},getFeatureLayerJSON:function(a,b){require(["esri/request"],function(c){var d=c({url:a,content:{f:"json"},handleAs:"json",callbackParamName:"callback"});d.then(function(a){b(!0,a)},function(a){b(!1,a.message)})})},_internalApplyEdits:function(a,c,d,e,f,g){var h=new b;return a._applyEdits(e,f,g,function(b,e,f){a._phantomLayer.clear();var g=b.map(function(a){return a.objectId});null!=a._attachmentsStore&&a.hasAttachments&&d.length>0?a._replaceFeatureIds(d,g,function(g){h.resolve({id:c,layer:a.url,tempId:d,addResults:b,updateResults:e,deleteResults:f})}):h.resolve({id:c,layer:a.url,tempId:d,addResults:b,updateResults:e,deleteResults:f})},function(b){a.onEditsComplete=a.__onEditsComplete,delete a.__onEditsComplete,h.reject(b)}),h.promise},_optimizeEditsQueue:function(){return"DEPRECATED at v2.5!"},getReadableEdit:function(a){return"DEPRECATED at v2.5!"}})}),"undefined"!=typeof O?O.esri.Edit={}:(O={},O.esri={Edit:{}}),O.esri.Edit.EditStore=function(){"use strict";this._db=null,this.dbName="features_store",this.objectStoreName="features",this.objectId="objectid";var a="featureId";this.ADD="add",this.UPDATE="update",this.DELETE="delete",this.FEATURE_LAYER_JSON_ID="feature-layer-object-1001",this.PHANTOM_GRAPHIC_PREFIX="phantom-layer",this._PHANTOM_PREFIX_TOKEN="|@|",this.isSupported=function(){return window.indexedDB?!0:!1},this.pushEdit=function(a,b,c,d){var e={id:b+"/"+c.attributes[this.objectId],operation:a,layer:b,type:c.geometry.type,graphic:c.toJson()};if("undefined"==typeof c.attributes[this.objectId])d(!1,"editsStore.pushEdit() - failed to insert undefined objectId into database. Did you set offlineFeaturesManager.DB_UID? "+JSON.stringify(c.attributes));else{var f=this._db.transaction([this.objectStoreName],"readwrite");f.oncomplete=function(a){d(!0)},f.onerror=function(a){d(!1,a.target.error.message)};var g=f.objectStore(this.objectStoreName);g.put(e)}},this.pushFeatureLayerJSON=function(a,b){"object"!=typeof a&&b(!1,"dataObject type is not an object.");var c=this._db;a.id=this.FEATURE_LAYER_JSON_ID,this.getFeatureLayerJSON(function(d,e){var f;if(d&&"undefined"!=typeof e){f=c.transaction([this.objectStoreName],"readwrite").objectStore(this.objectStoreName);for(var g in a)a.hasOwnProperty(g)&&(e[g]=a[g]);var h=f.put(e);h.onsuccess=function(){b(!0,null)},h.onerror=function(a){b(!1,a)}}else{var i=c.transaction([this.objectStoreName],"readwrite");i.oncomplete=function(a){b(!0,null)},i.onerror=function(a){b(!1,a.target.error.message)},f=i.objectStore(this.objectStoreName);try{f.put(a)}catch(j){b(!1,JSON.stringify(j))}}}.bind(this))},this.getFeatureLayerJSON=function(a){var b=this._db.transaction([this.objectStoreName],"readwrite").objectStore(this.objectStoreName),c=b.get(this.FEATURE_LAYER_JSON_ID);c.onsuccess=function(){var b=c.result;"undefined"!=typeof b?a(!0,b):a(!1,"nothing found")},c.onerror=function(b){a(!1,b)}},this.deleteFeatureLayerJSON=function(a){var b=this._db,c=null,d=this,e=this.FEATURE_LAYER_JSON_ID;require(["dojo/Deferred"],function(f){c=new f,c.then(function(b){d.editExists(e).then(function(b){b.success===!1?a(!0,{message:"id does not exist"}):a(!1,{message:null})},function(b){a(!0,{message:"id does not exist"})})},function(b){a(!1,{message:"id does not exist"})}),d.editExists(e).then(function(a){if(a&&a.success){var f=b.transaction([d.objectStoreName],"readwrite").objectStore(d.objectStoreName),g=f["delete"](e);g.onsuccess=function(){c.resolve(!0)},g.onerror=function(a){c.reject({success:!1,error:a})}}else c.reject({success:!1,message:"id does not exist"})},function(a){c.reject({success:!1,message:a})}.bind(this))})},this.pushPhantomGraphic=function(a,b){var c=this._db,d=this.PHANTOM_GRAPHIC_PREFIX+this._PHANTOM_PREFIX_TOKEN+a.attributes[this.objectId],e={id:d,graphic:a.toJson()},f=c.transaction([this.objectStoreName],"readwrite");f.oncomplete=function(a){b(!0,null)},f.onerror=function(a){b(!1,a.target.error.message)};var g=f.objectStore(this.objectStoreName);g.put(e)},this.getPhantomGraphicsArray=function(a){var b=[];if(null!==this._db){var c=this.PHANTOM_GRAPHIC_PREFIX,d=this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName).openCursor();d.onsuccess=function(d){var e=d.target.result;e&&e.value&&e.value.id?(-1!=e.value.id.indexOf(c)&&b.push(e.value),e["continue"]()):a(b,"end")}.bind(this),d.onerror=function(b){a(null,b)}}else a(null,"no db")},this._getPhantomGraphicsArraySimple=function(a){var b=[];if(null!==this._db){var c=this.PHANTOM_GRAPHIC_PREFIX,d=this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName).openCursor();d.onsuccess=function(d){var e=d.target.result;e&&e.value&&e.value.id?(-1!=e.value.id.indexOf(c)&&b.push(e.value.id),e["continue"]()):a(b,"end")}.bind(this),d.onerror=function(b){a(null,b)}}else a(null,"no db")},this.deletePhantomGraphic=function(a,b){var c=this._db,d=null,e=this;require(["dojo/Deferred"],function(f){d=new f,e.editExists(a).then(function(f){if(f.success){d.then(function(c){e.editExists(a).then(function(a){b(a.success===!1?!0:!1)},function(a){b(!0)})},function(a){b(!1,a)});var g=c.transaction([e.objectStoreName],"readwrite").objectStore(e.objectStoreName),h=g["delete"](a);h.onsuccess=function(){d.resolve(!0)},h.onerror=function(a){d.reject({success:!1,error:a})}}},function(a){b(!1)})})},this.resetLimitedPhantomGraphicsQueue=function(a,b){if(Object.keys(a).length>0){var c=this._db,d=0,e=c.transaction([this.objectStoreName],"readwrite"),f=e.objectStore(this.objectStoreName);f.onerror=function(){d++},e.oncomplete=function(){b(0===d?!0:!1)};for(var g in a)if(a.hasOwnProperty(g)){var h=a[g],i=this.PHANTOM_GRAPHIC_PREFIX+this._PHANTOM_PREFIX_TOKEN+h.id;h.updateResults.length>0&&h.updateResults[0].success&&f["delete"](i),h.deleteResults.length>0&&h.deleteResults[0].success&&f["delete"](i),h.addResults.length>0&&h.addResults[0].success&&f["delete"](i)}}else b(!0)},this.resetPhantomGraphicsQueue=function(a){var b=this._db;this._getPhantomGraphicsArraySimple(function(c){if(c!=[]){var d=0,e=b.transaction([this.objectStoreName],"readwrite"),f=e.objectStore(this.objectStoreName);f.onerror=function(){d++},e.oncomplete=function(){a(0===d?!0:!1)};for(var g=c.length,h=0;g>h;h++)f["delete"](c[h])}else a(!0)}.bind(this))},this.getEdit=function(a,b){var c=this._db.transaction([this.objectStoreName],"readwrite").objectStore(this.objectStoreName);require(["dojo/Deferred"],function(d){if("undefined"==typeof a)return void b(!1,"id is undefined.");var e=c.get(a);e.onsuccess=function(){var c=e.result;c&&c.id==a?b(!0,c):b(!1,"Id not found")},e.onerror=function(a){b(!1,a)}})},this.getAllEdits=function(a){if(null!==this._db){var b=this.FEATURE_LAYER_JSON_ID,c=this.PHANTOM_GRAPHIC_PREFIX,d=this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName).openCursor();d.onsuccess=function(d){var e=d.target.result;e&&e.hasOwnProperty("value")&&e.value.hasOwnProperty("id")?(e.value.id!==b&&-1==e.value.id.indexOf(c)&&a(e.value,null),e["continue"]()):a(null,"end")}.bind(this),d.onerror=function(b){a(null,b)}}else a(null,"no db")},this.getAllEditsArray=function(a){var b=[];if(null!==this._db){var c=this.FEATURE_LAYER_JSON_ID,d=this.PHANTOM_GRAPHIC_PREFIX,e=this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName).openCursor();e.onsuccess=function(e){var f=e.target.result;f&&f.value&&f.value.id?(f.value.id!==c&&-1==f.value.id.indexOf(d)&&b.push(f.value),f["continue"]()):a(b,"end")}.bind(this),e.onerror=function(b){a(null,b)}}else a(null,"no db")},this.updateExistingEdit=function(a,b,c,d){var e=this._db.transaction([this.objectStoreName],"readwrite").objectStore(this.objectStoreName),f=e.get(c.attributes[this.objectId]);f.onsuccess=function(){f.result;var g={id:b+"/"+c.attributes[this.objectId],operation:a,layer:b,graphic:c.toJson()},h=e.put(g);h.onsuccess=function(){d(!0)},h.onerror=function(a){d(!1,a)}}.bind(this)},this["delete"]=function(a,b,c){var d=this._db,e=null,f=this,g=a+"/"+b.attributes[this.objectId];require(["dojo/Deferred"],function(a){e=new a,f.editExists(g).then(function(a){if(a.success){e.then(function(a){f.editExists(g).then(function(a){c(a.success===!1?!0:!1)},function(a){c(!0)})},function(a){c(!1,a)});var b=d.transaction([f.objectStoreName],"readwrite").objectStore(f.objectStoreName),h=b["delete"](g);h.onsuccess=function(){e.resolve(!0)},h.onerror=function(a){e.reject({success:!1,error:a})}}},function(a){c(!1)})})},this.resetEditsQueue=function(a){var b=this._db.transaction([this.objectStoreName],"readwrite").objectStore(this.objectStoreName).clear();b.onsuccess=function(b){setTimeout(function(){a(!0)},0)},b.onerror=function(b){a(!1,b)}},this.pendingEditsCount=function(a){var b=0,c=this.FEATURE_LAYER_JSON_ID,d=this.PHANTOM_GRAPHIC_PREFIX,e=this._db.transaction([this.objectStoreName],"readwrite"),f=e.objectStore(this.objectStoreName);f.openCursor().onsuccess=function(e){var f=e.target.result;f&&f.value&&f.value.id&&-1==f.value.id.indexOf(d)?(f.value.id!==c&&b++,f["continue"]()):a(b)}},this.editExists=function(a){var b=this._db,c=null,d=this;return require(["dojo/Deferred"],function(e){c=new e;var f=b.transaction([d.objectStoreName],"readwrite").objectStore(d.objectStoreName),g=f.get(a);g.onsuccess=function(){var b=g.result;b&&b.id==a?c.resolve({success:!0,error:null}):c.reject({success:!1,error:"Layer id is not a match."})},g.onerror=function(a){c.reject({success:!1,error:a})}}),c},this.getUsage=function(a){var b=this.FEATURE_LAYER_JSON_ID,c=this.PHANTOM_GRAPHIC_PREFIX,d={sizeBytes:0,editCount:0},e=this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName).openCursor();e.onsuccess=function(e){var f=e.target.result;if(f&&f.value&&f.value.id){var g=f.value,h=JSON.stringify(g);d.sizeBytes+=h.length,-1==f.value.id.indexOf(c)&&f.value.id!==b&&(d.editCount+=1),f["continue"]()}else a(d,null)},e.onerror=function(b){a(null,b)}},this._serialize=function(a){var b=a.toJson(),c={attributes:b.attributes,geometry:b.geometry,infoTemplate:b.infoTemplate,symbol:b.symbol};return JSON.stringify(c)},this._deserialize=function(a){var b;return require(["esri/graphic"],function(c){b=new c(JSON.parse(a))}),b},this.init=function(b){var c=indexedDB.open(this.dbName,11);b=b||function(a){}.bind(this),c.onerror=function(a){b(!1,a.target.errorCode)}.bind(this),c.onupgradeneeded=function(b){var c=b.target.result;c.objectStoreNames.contains(this.objectStoreName)&&c.deleteObjectStore(this.objectStoreName);var d=c.createObjectStore(this.objectStoreName,{keyPath:"id"});d.createIndex(a,a,{unique:!1})}.bind(this),c.onsuccess=function(a){this._db=a.target.result,b(!0)}.bind(this)},this.hasPendingEdits=function(){return"DEPRECATED at v2.5!"},this._isEditDuplicated=function(a,b){return"DEPRECATED at v2.5!"},this._storeEditsQueue=function(a){return"DEPRECATED at v2.5!"},this._unpackArrayOfEdits=function(a){return"DEPRECATED at v2.5!"},this.getLocalStorageSizeBytes=function(){return"DEPRECATED at v2.5!"},this.peekFirstEdit=function(){return"DEPRECATED at v2.5!"},this.popFirstEdit=function(){return"DEPRECATED at v2.5!"}},O.esri.Edit.AttachmentsStore=function(){"use strict";this._db=null,this.dbName="attachments_store",this.objectStoreName="attachments",this.TYPE={ADD:"add",UPDATE:"update",DELETE:"delete"},this.isSupported=function(){return window.indexedDB?!0:!1},this.store=function(a,b,c,d,e,f){try{e==this.TYPE.ADD||e==this.TYPE.UPDATE||e==this.TYPE.DELETE?this._readFile(d,function(g,h){if(g){var i={id:b,objectId:c,type:e,featureId:a+"/"+c,contentType:d.type,name:d.name,size:d.size,featureLayerUrl:a,content:h,file:d},j=this._db.transaction([this.objectStoreName],"readwrite");j.oncomplete=function(a){f(!0,i)},j.onerror=function(a){f(!1,a.target.error.message)};var k=j.objectStore(this.objectStoreName),l=k.put(i);l.onsuccess=function(a){}}else f(!1,h)}.bind(this)):f(!1,"attachmentsStore.store() Invalid type in the constructor!")}catch(g){f(!1,g.stack)}},this.retrieve=function(a,b){var c=this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName),d=c.get(a);d.onsuccess=function(a){var c=a.target.result;c?b(!0,c):b(!1,"not found")},d.onerror=function(a){b(!1,a)}},this.getAttachmentsByFeatureId=function(a,b,c){var d=a+"/"+b,e=[],f=this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName),g=f.index("featureId"),h=IDBKeyRange.only(d);g.openCursor(h).onsuccess=function(a){var b=a.target.result;b?(e.push(b.value),b["continue"]()):c(e)}},this.getAttachmentsByFeatureLayer=function(a,b){var c=[],d=this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName),e=d.index("featureLayerUrl"),f=IDBKeyRange.only(a);e.openCursor(f).onsuccess=function(a){var d=a.target.result;d?(c.push(d.value),d["continue"]()):b(c)}},this.getAllAttachments=function(a){var b=[],c=this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName);c.openCursor().onsuccess=function(c){var d=c.target.result;d?(b.push(d.value),d["continue"]()):a(b)}},this.deleteAttachmentsByFeatureId=function(a,b,c){var d=a+"/"+b,e=this._db.transaction([this.objectStoreName],"readwrite").objectStore(this.objectStoreName),f=e.index("featureId"),g=IDBKeyRange.only(d),h=0;f.openCursor(g).onsuccess=function(a){var b=a.target.result;b?(e["delete"](b.primaryKey),h++,b["continue"]()):setTimeout(function(){c(h)},0)}.bind(this)},this["delete"]=function(a,b){this.retrieve(a,function(c,d){if(!c)return void b(!1,"attachment "+a+" not found");var e=this._db.transaction([this.objectStoreName],"readwrite").objectStore(this.objectStoreName)["delete"](a);e.onsuccess=function(a){setTimeout(function(){b(!0)},0)},e.onerror=function(a){b(!1,a)}}.bind(this))},this.deleteAll=function(a){this.getAllAttachments(function(b){var c=this._db.transaction([this.objectStoreName],"readwrite").objectStore(this.objectStoreName).clear();c.onsuccess=function(b){setTimeout(function(){a(!0)},0)},c.onerror=function(b){a(!1,b)}}.bind(this))},this.replaceFeatureId=function(a,b,c,d){var e=a+"/"+b,f=this._db.transaction([this.objectStoreName],"readwrite").objectStore(this.objectStoreName),g=f.index("featureId"),h=IDBKeyRange.only(e),i=0;g.openCursor(h).onsuccess=function(b){var e=b.target.result;if(e){var g=a+"/"+c,h=e.value;h.featureId=g,h.objectId=c,f.put(h),i++,e["continue"]()}else setTimeout(function(){d(i)},1)}},this.getUsage=function(a){ +var b={sizeBytes:0,attachmentCount:0},c=this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName).openCursor();c.onsuccess=function(c){var d=c.target.result;if(d){var e=d.value,f=JSON.stringify(e);b.sizeBytes+=f.length,b.attachmentCount+=1,d["continue"]()}else a(b,null)}.bind(this),c.onerror=function(b){a(null,b)}},this.resetAttachmentsQueue=function(a){var b=this._db.transaction([this.objectStoreName],"readwrite").objectStore(this.objectStoreName).clear();b.onsuccess=function(b){setTimeout(function(){a(!0)},0)},b.onerror=function(b){a(!1,b)}},this._readFile=function(a,b){var c=new FileReader;c.onload=function(a){b(!0,a.target.result)},c.onerror=function(a){b(!1,a.target.result)},c.readAsBinaryString(a)},this.init=function(a){var b=indexedDB.open(this.dbName,12);a=a||function(a){}.bind(this),b.onerror=function(b){a(!1,b.target.errorCode)}.bind(this),b.onupgradeneeded=function(a){var b=a.target.result;b.objectStoreNames.contains(this.objectStoreName)&&b.deleteObjectStore(this.objectStoreName);var c=b.createObjectStore(this.objectStoreName,{keyPath:"id"});c.createIndex("featureId","featureId",{unique:!1}),c.createIndex("featureLayerUrl","featureLayerUrl",{unique:!1})}.bind(this),b.onsuccess=function(b){this._db=b.target.result,a(!0)}.bind(this)}}; \ No newline at end of file diff --git a/dist/offline-edit-src.js b/dist/offline-edit-src.js index 12d8cdc9..41d239e0 100644 --- a/dist/offline-edit-src.js +++ b/dist/offline-edit-src.js @@ -1,4 +1,4 @@ -/*! offline-editor-js - v2.6.1 - 2015-04-13 +/*! offline-editor-js - v2.7.0 - 2015-04-27 * Copyright (c) 2015 Environmental Systems Research Institute, Inc. * Apache License*/ /*jshint -W030 */ @@ -38,6 +38,13 @@ define([ DB_OBJECTSTORE_NAME: "features",// Represents an object store that allows access to a set of data in the IndexedDB database DB_UID: "objectid", // Set this based on the unique identifier is set up in the feature service + ATTACHMENTS_DB_NAME: "attachments_store", //Sets attachments database name + ATTACHMENTS_DB_OBJECTSTORE_NAME: "attachments", + // NOTE: attachments don't have the same issues as Graphics as related to UIDs. + // You can manually create a graphic, but it would be very rare for someone to + // manually create an attachment. So, we don't provide a public property for + // the attachments database UID. + // manager emits event when... events: { EDITS_SENT: "edits-sent", // ...whenever any edit is actually sent to the server @@ -66,6 +73,8 @@ define([ try { this.attachmentsStore = new O.esri.Edit.AttachmentsStore(); + this.attachmentsStore.dbName = this.ATTACHMENTS_DB_NAME; + this.attachmentsStore.objectStoreName = this.ATTACHMENTS_DB_OBJECTSTORE_NAME; if (/*false &&*/ this.attachmentsStore.isSupported()) { this.attachmentsStore.init(callback); @@ -81,8 +90,8 @@ define([ /** * Overrides a feature layer. Call this AFTER the FeatureLayer's 'update-end' event. - * IMPORTANT: If options are specified they will be saved to the database. Any complex - * objects such as [esri.Graphic] will need to be serialized or you will get an error. + * IMPORTANT: If dataStore is specified it will be saved to the database. Any complex + * objects such as [esri.Graphic] will need to be serialized or you will get an IndexedDB error. * @param layer * @param updateEndEvent The FeatureLayer's update-end event object * @param callback {true, null} or {false, errorString} Traps whether or not the database initialized @@ -115,6 +124,7 @@ define([ layer._addAttachment = layer.addAttachment; layer._queryAttachmentInfos = layer.queryAttachmentInfos; layer._deleteAttachments = layer.deleteAttachments; + layer._updateAttachment = layer.updateAttachment; /* operations supported offline: @@ -166,6 +176,7 @@ define([ }; layer.addAttachment = function (objectId, formNode, callback, errback) { + if (self.getOnlineStatus() === self.ONLINE) { return this._addAttachment(objectId, formNode, function () { @@ -181,7 +192,7 @@ define([ } if (!self.attachmentsStore) { - console.log("in order to support attachments you need to call initAttachments() method of offlineFeaturesManager"); + console.error("in order to support attachments you need to call initAttachments() method of offlineFeaturesManager"); return; } @@ -190,7 +201,7 @@ define([ var deferred = new Deferred(); var attachmentId = this._getNextTempId(); - self.attachmentsStore.store(this.url, attachmentId, objectId, file, function (success, newAttachment) { + self.attachmentsStore.store(this.url, attachmentId, objectId, file,self.attachmentsStore.TYPE.ADD, function (success, newAttachment) { var returnValue = {attachmentId: attachmentId, objectId: objectId, success: success}; if (success) { self.emit(self.events.ATTACHMENT_ENQUEUED, returnValue); @@ -212,6 +223,51 @@ define([ return deferred; }; + layer.updateAttachment = function(objectId, attachmentId, formNode, callback, errback) { + if (self.getOnlineStatus() === self.ONLINE) { + return this._updateAttachment(objectId, attachmentId, formNode, + function () { + callback && callback.apply(this, arguments); + }, + function (err) { + console.log("updateAttachment: " + err); + errback && errback.apply(this, arguments); + }); + //return def; + } + + if (!self.attachmentsStore) { + console.error("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(); + + self.attachmentsStore.store(this.url, attachmentId, objectId, file, self.attachmentsStore.TYPE.UPDATE, 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 = "layer.updateAttachment::attachmentStore 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, @@ -232,22 +288,42 @@ define([ // 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); - }); + + // IMPORTANT: If attachmentId < 0 then it's a local/new attachment + // and we can simply delete it from the attachmentsStore. + // However, if the attachmentId > 0 then we need to store the DELETE + // so that it can be processed and sync'd correctly during _uploadAttachments(). + if(attachmentId < 0) { + self.attachmentsStore.delete(attachmentId, function (success) { + var result = {objectId: objectId, attachmentId: attachmentId, success: success}; + deferred.resolve(result); + }); + } + else { + var dummyBlob = new Blob([],{type: "image/png"}); //TO-DO just a placeholder. Need to consider add a null check. + self.attachmentsStore.store(this.url, attachmentId, objectId, dummyBlob,self.attachmentsStore.TYPE.DELETE, function (success, newAttachment) { + var returnValue = {attachmentId: attachmentId, objectId: objectId, success: success}; + if (success) { + deferred.resolve(returnValue); + } + else { + deferred.reject(returnValue); + } + }.bind(this)); + } + //console.assert(attachmentId < 0, "we only support deleting local attachments"); promises.push(deferred); }, this); // call callback once all deletes have finished + // IMPORTANT: This returns an array!!! var allPromises = all(promises); allPromises.then(function (results) { callback && callback(results); @@ -543,7 +619,30 @@ define([ }; /** - * Returns the approximate size of the database in bytes + * Returns the approximate size of the attachments database in bytes + * @param callback callback({usage}, error) Whereas, the usage Object is {sizeBytes: number, attachmentCount: number} + */ + layer.getAttachmentsUsage = function(callback) { + self.attachmentsStore.getUsage(function(usage,error){ + callback(usage,error); + }); + }; + + /** + * Full attachments database reset. + * CAUTION! If some attachments weren't successfully sent, then their record + * will still exist in the database. If you use this function you + * will also delete those records. + * @param callback (boolean, error) + */ + layer.resetAttachmentsDatabase = function(callback){ + self.attachmentsStore.resetAttachmentsQueue(function(result,error){ + callback(result,error); + }); + }; + + /** + * Returns the approximate size of the edits database in bytes * @param callback callback({usage}, error) Whereas, the usage Object is {sizeBytes: number, editCount: number} */ layer.getUsage = function(callback){ @@ -553,7 +652,7 @@ define([ }; /** - * Full database reset. + * Full edits database reset. * CAUTION! If some edits weren't successfully sent, then their record * will still exist in the database. If you use this function you * will also delete those records. @@ -954,11 +1053,12 @@ define([ this._onlineStatus = this.RECONNECTING; this._replayStoredEdits(function (success, responses) { var result = {features: {success: success, responses: responses}}; + this._onlineStatus = this.ONLINE; if (this.attachmentsStore != null) { console.log("sending attachments"); - this._sendStoredAttachments(function (success, responses) { + this._sendStoredAttachments(function (success, uploadedResponses, dbResponses) { this._onlineStatus = this.ONLINE; - result.attachments = {success: success, responses: responses}; + result.attachments = {success: success, responses: uploadedResponses, dbResponses: dbResponses}; callback && callback(result); }.bind(this)); } @@ -1021,10 +1121,10 @@ define([ // Added @ v2.5 // // Configure database for offline restart - // Options object allows you to store data that you'll + // dataStore object allows you to store data that you'll // use after an offline browser restart. // - // If options Object is not defined then do nothing. + // If dataStore Object is not defined then do nothing. // //////////////////////////////////////////////////// @@ -1172,83 +1272,171 @@ define([ _uploadAttachment: function (attachment) { var dfd = new Deferred(); - var segments = []; - segments.push(this._fieldSegment("f", "json")); - segments.push(this._fileSegment("attachment", attachment.name, attachment.contentType, attachment.content)); + var layer = this._featureLayers[attachment.featureLayerUrl]; - var oAjaxReq = new XMLHttpRequest(); + var formData = new FormData(); + formData.append("attachment",attachment.file); - // surprisingly, sometimes the oAjaxReq object doesn't have the sendAsBinary() method, even if we added it to the XMLHttpRequest.prototype - if (!oAjaxReq.sendAsBinary) { - this._extendAjaxReq(oAjaxReq); - } - - oAjaxReq.onload = function (result) { - dfd.resolve(JSON.parse(result.target.response)); - }; - oAjaxReq.onerror = function (err) { - dfd.reject(err); - }; + switch(attachment.type){ + case this.attachmentsStore.TYPE.ADD: + layer.addAttachment(attachment.objectId,formData,function(evt){ + dfd.resolve({attachmentResult:evt,id:attachment.id}); + },function(err){ + dfd.reject(err); + }); + break; + case this.attachmentsStore.TYPE.UPDATE: + formData.append("attachmentId", attachment.id); + + // NOTE: + // We need to handle updates different from ADDS and DELETES because of how the JS API + // parses the DOM formNode property. + layer._sendAttachment("update",/* objectid */attachment.objectId, formData,function(evt){ + dfd.resolve({attachmentResult:evt,id:attachment.id}); + },function(err){ + dfd.reject(err); + }); - // IMPORTANT! - // Proxy path can be set to null if feature service is CORS enabled - // Refer to "Using the Proxy Page" for more information: https://developers.arcgis.com/en/javascript/jshelp/ags_proxy.html - var proxy = this.proxyPath || esriConfig.defaults.io.proxyUrl || ""; - if (proxy !== "") { - proxy += "?"; + break; + case this.attachmentsStore.TYPE.DELETE: + // IMPORTANT: This method returns attachmentResult as an Array. Whereas ADD and UPDATE do not!! + layer.deleteAttachments(attachment.objectId,[attachment.id],function(evt){ + dfd.resolve({attachmentResult:evt,id:attachment.id}); + },function(err){ + dfd.reject(err); + }); + break; } - console.log("proxy:", proxy); - oAjaxReq.open("post", proxy + attachment.featureId + "/addAttachment", true); - var sBoundary = "---------------------------" + Date.now().toString(16); - oAjaxReq.setRequestHeader("Content-Type", "multipart\/form-data; boundary=" + sBoundary); - oAjaxReq.sendAsBinary("--" + sBoundary + "\r\n" + segments.join("--" + sBoundary + "\r\n") + "--" + sBoundary + "--\r\n"); - return dfd; + return dfd.promise; }, - _deleteAttachment: function (attachmentId, uploadResult) { + _deleteAttachmentFromDB: function (attachmentId, uploadResult) { var dfd = new Deferred(); console.log("upload complete", uploadResult, attachmentId); this.attachmentsStore.delete(attachmentId, function (success) { console.assert(success === true, "can't delete attachment already uploaded"); console.log("delete complete", success); - dfd.resolve(uploadResult); + dfd.resolve({success:success,result:uploadResult}); }); return dfd; }, + /** + * Removes attachments from DB if they were successfully uploaded + * @param results promises.results + * @callback callback callback( {errors: boolean, attachmentsDBResults: results, uploadResults: results} ) + * @private + */ + _cleanAttachmentsDB: function(results,callback){ + + var self = this; + var promises = []; + var count = 0; + + results.forEach(function(value){ + + if(typeof value.attachmentResult == "object" && value.attachmentResult.success){ + // Delete an attachment from the database if it was successfully + // submitted to the server. + promises.push(self._deleteAttachmentFromDB(value.id,null)); + } + // NOTE: layer.deleteAttachments returns an array rather than an object + else if(value.attachmentResult instanceof Array){ + + // Because we get an array we have to cycle thru it to verify all results + value.attachmentResult.forEach(function(deleteValue){ + if(deleteValue.success){ + // Delete an attachment from the database if it was successfully + // submitted to the server. + promises.push(self._deleteAttachmentFromDB(value.id,null)); + } + else { + count++; + } + }); + } + else{ + // Do nothing. Don't delete attachments from DB if we can't upload them + count++; + } + }); + + var allPromises = all(promises); + allPromises.then(function(dbResults){ + if(count > 0){ + // If count is greater than zero then we have errors and need to set errors to true + callback({errors: true, attachmentsDBResults: dbResults, uploadResults: results}); + } + else{ + callback({errors: false, attachmentsDBResults: dbResults, uploadResults: results}); + } + }); + }, + + /** + * Attempts to upload stored attachments when the library goes back on line. + * @param callback callback({success: boolean, uploadResults: results, dbResults: results}) + * @private + */ _sendStoredAttachments: function (callback) { this.attachmentsStore.getAllAttachments(function (attachments) { + + var self = this; + console.log("we have", attachments.length, "attachments to upload"); var promises = []; attachments.forEach(function (attachment) { console.log("sending attachment", attachment.id, "to feature", attachment.featureId); - var deleteCompleted = - this._uploadAttachment(attachment) - .then(function (uploadResult) { - if (uploadResult.addAttachmentResult && uploadResult.addAttachmentResult.success === true) { - console.log("upload success", uploadResult.addAttachmentResult.success); - return this._deleteAttachment(attachment.id, uploadResult); - } - else { - console.log("upload failed", uploadResult); - return null; - } - }.bind(this), - function (err) { - console.log("failed uploading attachment", attachment); - } - ); - promises.push(deleteCompleted); + + var uploadAttachmentComplete = + this._uploadAttachment(attachment); + //.then(function (uploadResult) { + // if (uploadResult.addAttachmentResult && uploadResult.addAttachmentResult.success === true) { + // console.log("upload success", uploadResult.addAttachmentResult.success); + // return this._deleteAttachment(attachment.id, uploadResult); + // } + // else { + // console.log("upload failed", uploadResult); + // return null; + // } + //}.bind(this), + //function (err) { + // console.log("failed uploading attachment", attachment); + // return null; + //} + //); + promises.push(uploadAttachmentComplete); }, this); console.log("promises", promises.length); var allPromises = all(promises); - allPromises.then(function (results) { - console.log(results); - callback && callback(true, results); + allPromises.then(function (uploadResults) { + console.log(uploadResults); + self._cleanAttachmentsDB(uploadResults,function(dbResults){ + if(dbResults.errors){ + callback && callback(false, uploadResults,dbResults); + } + else{ + callback && callback(true, uploadResults,dbResults); + } + }); + //results.forEach(function(value){ + // if(value.attachmentResult.success){ + // // Delete an attachment from the database if it was successfully + // // submitted to the server. + // self._deleteAttachmentFromDB(value.id,null).then(function(result){ + // if(result.success){ + // callback && callback(true, results); + // } + // else{ + // callback && callback(false, results); + // } + // }); + // } + //}); }, function (err) { console.log("error!", err); @@ -1296,6 +1484,10 @@ define([ if (attachmentsStore == null && layer.hasAttachments) { console.log("NOTICE: you may need to run OfflineFeaturesManager.initAttachments(). Check the Attachments doc for more info. Layer id: " + layer.id + " accepts attachments"); } + else if(layer.hasAttachments === false){ + console.error("WARNING: Layer " + layer.id + "doesn't seem to accept attachments. Recheck the layer permissions."); + callback(false,"WARNING: Attachments not supported in layer: " + layer.id); + } // Assign the attachmentsStore to the layer as a private var so we can access it from // the promises applyEdits() method. @@ -2638,8 +2830,14 @@ O.esri.Edit.AttachmentsStore = function () { this._db = null; - var DB_NAME = "attachments_store"; - var OBJECT_STORE_NAME = "attachments"; + this.dbName = "attachments_store"; + this.objectStoreName = "attachments"; + + this.TYPE = { + "ADD" : "add", + "UPDATE" : "update", + "DELETE" : "delete" + }; this.isSupported = function () { if (!window.indexedDB) { @@ -2648,40 +2846,70 @@ O.esri.Edit.AttachmentsStore = function () { return true; }; - this.store = function (featureLayerUrl, attachmentId, objectId, attachmentFile, callback) { + /** + * Stores an attachment in the database. + * In theory, this abides by the query-attachment-infos-complete Object which can be found here: + * https://developers.arcgis.com/javascript/jsapi/featurelayer-amd.html#event-query-attachment-infos-complete + * @param featureLayerUrl + * @param attachmentId The temporary or actual attachmentId issued by the feature service + * @param objectId The actual ObjectId issues by the feature service + * @param attachmentFile + * @param type Type of operation: "add", "update" or "delete" + * @param callback + */ + this.store = function (featureLayerUrl, attachmentId, objectId, attachmentFile, type, callback) { try { - // first of all, read file content - this._readFile(attachmentFile, function (fileContent) { - // now, store it in the db - var newAttachment = - { - id: attachmentId, - objectId: objectId, - featureId: featureLayerUrl + "/" + objectId, - contentType: attachmentFile.type, - name: attachmentFile.name, - size: attachmentFile.size, - url: this._createLocalURL(attachmentFile), - content: fileContent - }; + // Avoid allowing the wrong type to be stored + if(type == this.TYPE.ADD || type == this.TYPE.UPDATE || type == this.TYPE.DELETE) { - var transaction = this._db.transaction([OBJECT_STORE_NAME], "readwrite"); + // first of all, read file content + this._readFile(attachmentFile, function (success, fileContent) { - transaction.oncomplete = function (event) { - callback(true, newAttachment); - }; + if (success) { + // now, store it in the db + var newAttachment = + { + id: attachmentId, + objectId: objectId, + type: type, + + // Unique ID - don't use the ObjectId + // multiple features services could have an a feature with the same ObjectId + featureId: featureLayerUrl + "/" + objectId, + contentType: attachmentFile.type, + name: attachmentFile.name, + size: attachmentFile.size, + featureLayerUrl: featureLayerUrl, + content: fileContent, + file: attachmentFile + }; - transaction.onerror = function (event) { - callback(false, event.target.error.message); - }; + var transaction = this._db.transaction([this.objectStoreName], "readwrite"); - var objectStore = transaction.objectStore(OBJECT_STORE_NAME); - var request = objectStore.put(newAttachment); - request.onsuccess = function (event) { - //console.log("item added to db " + event.target.result); - }; + transaction.oncomplete = function (event) { + callback(true, newAttachment); + }; - }.bind(this)); + transaction.onerror = function (event) { + callback(false, event.target.error.message); + }; + + var objectStore = transaction.objectStore(this.objectStoreName); + var request = objectStore.put(newAttachment); + request.onsuccess = function (event) { + //console.log("item added to db " + event.target.result); + }; + + } + else { + callback(false, fileContent); + } + }.bind(this)); + } + else{ + console.error("attachmentsStore.store() Invalid type in the constructor!"); + callback(false,"attachmentsStore.store() Invalid type in the constructor!"); + } } catch (err) { console.log("AttachmentsStore: " + err.stack); @@ -2692,7 +2920,7 @@ O.esri.Edit.AttachmentsStore = function () { this.retrieve = function (attachmentId, callback) { console.assert(this._db !== null, "indexeddb not initialized"); - var objectStore = this._db.transaction([OBJECT_STORE_NAME]).objectStore(OBJECT_STORE_NAME); + var objectStore = this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName); var request = objectStore.get(attachmentId); request.onsuccess = function (event) { var result = event.target.result; @@ -2715,7 +2943,7 @@ O.esri.Edit.AttachmentsStore = function () { var featureId = featureLayerUrl + "/" + objectId; var attachments = []; - var objectStore = this._db.transaction([OBJECT_STORE_NAME]).objectStore(OBJECT_STORE_NAME); + var objectStore = this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName); var index = objectStore.index("featureId"); var keyRange = IDBKeyRange.only(featureId); index.openCursor(keyRange).onsuccess = function (evt) { @@ -2735,9 +2963,9 @@ O.esri.Edit.AttachmentsStore = function () { var attachments = []; - var objectStore = this._db.transaction([OBJECT_STORE_NAME]).objectStore(OBJECT_STORE_NAME); - var index = objectStore.index("featureId"); - var keyRange = IDBKeyRange.bound(featureLayerUrl + "/", featureLayerUrl + "/A"); + var objectStore = this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName); + var index = objectStore.index("featureLayerUrl"); + var keyRange = IDBKeyRange.only(featureLayerUrl); index.openCursor(keyRange).onsuccess = function (evt) { var cursor = evt.target.result; if (cursor) { @@ -2755,7 +2983,7 @@ O.esri.Edit.AttachmentsStore = function () { var attachments = []; - var objectStore = this._db.transaction([OBJECT_STORE_NAME]).objectStore(OBJECT_STORE_NAME); + var objectStore = this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName); objectStore.openCursor().onsuccess = function (evt) { var cursor = evt.target.result; if (cursor) { @@ -2773,15 +3001,15 @@ O.esri.Edit.AttachmentsStore = function () { var featureId = featureLayerUrl + "/" + objectId; - var objectStore = this._db.transaction([OBJECT_STORE_NAME], "readwrite").objectStore(OBJECT_STORE_NAME); + var objectStore = this._db.transaction([this.objectStoreName], "readwrite").objectStore(this.objectStoreName); var index = objectStore.index("featureId"); var keyRange = IDBKeyRange.only(featureId); var deletedCount = 0; index.openCursor(keyRange).onsuccess = function (evt) { var cursor = evt.target.result; if (cursor) { - var attachment = cursor.value; - this._revokeLocalURL(attachment); + //var attachment = cursor.value; + //this._revokeLocalURL(attachment); objectStore.delete(cursor.primaryKey); deletedCount++; cursor.continue(); @@ -2805,10 +3033,10 @@ O.esri.Edit.AttachmentsStore = function () { return; } - this._revokeLocalURL(attachment); + //this._revokeLocalURL(attachment); - var request = this._db.transaction([OBJECT_STORE_NAME], "readwrite") - .objectStore(OBJECT_STORE_NAME) + var request = this._db.transaction([this.objectStoreName], "readwrite") + .objectStore(this.objectStoreName) .delete(attachmentId); request.onsuccess = function (event) { setTimeout(function () { @@ -2825,12 +3053,12 @@ O.esri.Edit.AttachmentsStore = function () { console.assert(this._db !== null, "indexeddb not initialized"); this.getAllAttachments(function (attachments) { - attachments.forEach(function (attachment) { - this._revokeLocalURL(attachment); - }, this); + //attachments.forEach(function (attachment) { + // this._revokeLocalURL(attachment); + //}, this); - var request = this._db.transaction([OBJECT_STORE_NAME], "readwrite") - .objectStore(OBJECT_STORE_NAME) + var request = this._db.transaction([this.objectStoreName], "readwrite") + .objectStore(this.objectStoreName) .clear(); request.onsuccess = function (event) { setTimeout(function () { @@ -2848,7 +3076,7 @@ O.esri.Edit.AttachmentsStore = function () { var featureId = featureLayerUrl + "/" + oldId; - var objectStore = this._db.transaction([OBJECT_STORE_NAME], "readwrite").objectStore(OBJECT_STORE_NAME); + var objectStore = this._db.transaction([this.objectStoreName], "readwrite").objectStore(this.objectStoreName); var index = objectStore.index("featureId"); var keyRange = IDBKeyRange.only(featureId); var replacedCount = 0; @@ -2877,8 +3105,8 @@ O.esri.Edit.AttachmentsStore = function () { var usage = {sizeBytes: 0, attachmentCount: 0}; - var transaction = this._db.transaction([OBJECT_STORE_NAME]) - .objectStore(OBJECT_STORE_NAME) + var transaction = this._db.transaction([this.objectStoreName]) + .objectStore(this.objectStoreName) .openCursor(); console.log("dumping keys"); @@ -2902,28 +3130,55 @@ O.esri.Edit.AttachmentsStore = function () { }; }; + /** + * Full attachments database reset. + * CAUTION! If some attachments weren't successfully sent, then their record + * will still exist in the database. If you use this function you + * will also delete those records. + * @param callback boolean + */ + this.resetAttachmentsQueue = function (callback) { + console.assert(this._db !== null, "indexeddb not initialized"); + + var request = this._db.transaction([this.objectStoreName], "readwrite") + .objectStore(this.objectStoreName) + .clear(); + request.onsuccess = function (event) { + setTimeout(function () { + callback(true); + }, 0); + }; + request.onerror = function (err) { + callback(false, err); + }; + }; + // internal methods this._readFile = function (attachmentFile, callback) { var reader = new FileReader(); reader.onload = function (evt) { - callback(evt.target.result); + callback(true,evt.target.result); + }; + reader.onerror = function (evt) { + callback(false,evt.target.result); }; reader.readAsBinaryString(attachmentFile); }; - this._createLocalURL = function (attachmentFile) { - return window.URL.createObjectURL(attachmentFile); - }; + // Deprecated @ v2.7 + //this._createLocalURL = function (attachmentFile) { + // return window.URL.createObjectURL(attachmentFile); + //}; - this._revokeLocalURL = function (attachment) { - window.URL.revokeObjectURL(attachment.url); - }; + //this._revokeLocalURL = function (attachment) { + // window.URL.revokeObjectURL(attachment.url); + //}; this.init = function (callback) { console.log("init AttachmentStore"); - var request = indexedDB.open(DB_NAME, 11); + var request = indexedDB.open(this.dbName, 12); callback = callback || function (success) { console.log("AttachmentsStore::init() success:", success); }.bind(this); @@ -2936,12 +3191,13 @@ O.esri.Edit.AttachmentsStore = function () { request.onupgradeneeded = function (event) { var db = event.target.result; - if (db.objectStoreNames.contains(OBJECT_STORE_NAME)) { - db.deleteObjectStore(OBJECT_STORE_NAME); + if (db.objectStoreNames.contains(this.objectStoreName)) { + db.deleteObjectStore(this.objectStoreName); } - var objectStore = db.createObjectStore(OBJECT_STORE_NAME, {keyPath: "id"}); + var objectStore = db.createObjectStore(this.objectStoreName, {keyPath: "id"}); objectStore.createIndex("featureId", "featureId", {unique: false}); + objectStore.createIndex("featureLayerUrl", "featureLayerUrl", {unique: false}); }.bind(this); request.onsuccess = function (event) { diff --git a/dist/offline-tiles-advanced-min.js b/dist/offline-tiles-advanced-min.js index ef473b7e..3ebdd81a 100644 --- a/dist/offline-tiles-advanced-min.js +++ b/dist/offline-tiles-advanced-min.js @@ -1,4 +1,4 @@ -/*! offline-editor-js - v2.6.1 - 2015-04-13 +/*! offline-editor-js - v2.7.0 - 2015-04-27 * Copyright (c) 2015 Environmental Systems Research Institute, Inc. * Apache License*/ define(["dojo/query","dojo/request","dojo/_base/declare","esri/layers/LOD","esri/geometry/Point","esri/geometry/Extent","esri/layers/TileInfo","esri/SpatialReference","esri/geometry/Polygon","esri/layers/TiledMapServiceLayer"],function(a,b,c,d,e,f,g,h,i,j){"use strict";return c("O.esri.Tiles.OfflineTileEnablerLayer",[j],{tileInfo:null,_imageType:"",_level:null,_minZoom:null,_maxZoom:null,_tilesCore:null,constructor:function(a,b,c){this._isLocalStorage()===!1&&(alert("OfflineTiles Library not supported on this browser."),b(!1)),this._tilesCore=new O.esri.Tiles.TilesCore,Array.prototype.sortNumber=function(){return this.sort(function(a,b){return a-b})},this._self=this,this._lastTileUrl="",this._imageType="",this._getTileUrl=this.getTileUrl;var d=!0;return("undefined"!=typeof c||null!=c)&&(d=c),this.offline={online:d,store:new O.esri.Tiles.TilesStore,proxyPath:null},this.offline.store.isSupported()?void this.offline.store.init(function(c){c&&this._getTileInfoPrivate(a,function(a){void 0==localStorage.__offlineTileInfo&&0!=a&&(localStorage.__offlineTileInfo=a),0==this.offline.online&&0==a&&void 0!=localStorage.__offlineTileInfo?a=localStorage.__offlineTileInfo:0==this.offline.online&&0==a&&void 0==localStorage.__offlineTileInfo&&alert("There was a problem retrieving tiled map info in OfflineTilesEnablerLayer."),this._tilesCore._parseGetTileInfo(a,function(a){this.layerInfos=a.resultObj.layers,this.minScale=a.resultObj.minScale,this.maxScale=a.resultObj.maxScale,this.tileInfo=a.tileInfo,this._imageType=this.tileInfo.format.toLowerCase(),this.fullExtent=a.fullExtent,this.spatialReference=this.tileInfo.spatialReference,this.initialExtent=a.initExtent,this.loaded=!0,this.onLoad(this),b(!0)}.bind(this._self))}.bind(this._self))}.bind(this._self)):b(!1,"indexedDB not supported")},getTileUrl:function(b,c,d){this._level=b;var e=this.url+"/tile/"+b+"/"+c+"/"+d;if(this.offline.online)return this._lastTileUrl=e,e;e=e.split("?")[0];var f="void:/"+b+"/"+c+"/"+d,g=null;return this._tilesCore._getTiles(g,this._imageType,e,f,this.offline.store,a),f},getBasemapLayer:function(a){var b=a.layerIds[0];return a.getLayer(b)},getLevelEstimation:function(a,b,c){var d=new O.esri.Tiles.TilingScheme(this),e=d.getAllCellIdsInExtent(a,b),f={level:b,tileCount:e.length,sizeBytes:e.length*c};return f},getLevel:function(){return this._level},getMaxZoom:function(a){null==this._maxZoom&&(this._maxZoom=this.tileInfo.lods[this.tileInfo.lods.length-1].level),a(this._maxZoom)},getMinZoom:function(a){null==this._minZoom&&(this._minZoom=this.tileInfo.lods[0].level),a(this._minZoom)},getMinMaxLOD:function(a,b){var c={},d=this.getMap(),e=d.getLevel()+a,f=d.getLevel()+b;return null!=this._maxZoom&&null!=this._minZoom?(c.max=Math.min(this._maxZoom,f),c.min=Math.max(this._minZoom,e)):(this.getMinZoom(function(a){c.min=Math.max(a,e)}),this.getMaxZoom(function(a){c.max=Math.min(a,f)})),c},prepareForOffline:function(a,b,c,d){this._tilesCore._createCellsForOffline(this,a,b,c,function(a){this._doNextTile(0,a,d)}.bind(this))},goOffline:function(){this.offline.online=!1},goOnline:function(){this.offline.online=!0,this.refresh()},isOnline:function(){return this.offline.online},deleteAllTiles:function(a){var b=this.offline.store;b.deleteAll(a)},getOfflineUsage:function(a){var b=this.offline.store;b.usedSpace(a)},getTilePolygons:function(a){this._tilesCore._getTilePolygons(this.offline.store,this.url,this,a)},saveToFile:function(a,b){this._tilesCore._saveToFile(a,this.offline.store,b)},loadFromFile:function(a,b){this._tilesCore._loadFromFile(a,this.offline.store,b)},estimateTileSize:function(a){this._tilesCore._estimateTileSize(b,this._lastTileUrl,this.offline.proxyPath,a)},getExtentBuffer:function(a,b){return b.xmin-=a,b.ymin-=a,b.xmax+=a,b.ymax+=a,b},getTileUrlsByExtent:function(a,b){var c=new O.esri.Tiles.TilingScheme(this),d=c.getAllCellIdsInExtent(a,b),e=[];return d.forEach(function(a){e.push(this.url+"/"+b+"/"+a[1]+"/"+a[0])}.bind(this)),e},_doNextTile:function(a,b,c){var d=b[a],e=this._getTileUrl(d.level,d.row,d.col);this._tilesCore._storeTile(e,this.offline.proxyPath,this.offline.store,function(e,f){e||(f={cell:d,msg:f});var g=c({countNow:a,countMax:b.length,cell:d,error:f,finishedDownloading:!1});g||a===b.length-1?c({finishedDownloading:!0,cancelRequested:g}):this._doNextTile(a+1,b,c)}.bind(this))},_isLocalStorage:function(){var a="test";try{return localStorage.setItem(a,a),localStorage.removeItem(a),!0}catch(b){return!1}},_getTileInfoPrivate:function(a,b){var c=new XMLHttpRequest,a=null!=this.offline.proxyPath?this.offline.proxyPath+"?"+a+"?f=pjson":a+"?f=pjson";c.open("GET",a,!0),c.onload=function(){b(200===c.status&&""!==c.responseText?this.response:!1)},c.onerror=function(a){b(!1)},c.send(null)}})}),"undefined"!=typeof O?O.esri.Tiles={}:(O={},O.esri={Tiles:{}}),O.esri.Tiles.Base64Utils={},O.esri.Tiles.Base64Utils.outputTypes={Base64:0,Hex:1,String:2,Raw:3},O.esri.Tiles.Base64Utils.addWords=function(a,b){var c=(65535&a)+(65535&b),d=(a>>16)+(b>>16)+(c>>16);return d<<16|65535&c},O.esri.Tiles.Base64Utils.stringToWord=function(a){for(var b=8,c=(1<e;e+=b)d[e>>5]|=(a.charCodeAt(e/b)&c)<e;e+=b)d.push(String.fromCharCode(a[e>>5]>>>e%32&c));return d.join("")},O.esri.Tiles.Base64Utils.wordToHex=function(a){for(var b="0123456789abcdef",c=[],d=0,e=4*a.length;e>d;d++)c.push(b.charAt(a[d>>2]>>d%4*8+4&15)+b.charAt(a[d>>2]>>d%4*8&15));return c.join("")},O.esri.Tiles.Base64Utils.wordToBase64=function(a){for(var b="=",c="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",d=[],e=0,f=4*a.length;f>e;e+=3)for(var g=(a[e>>2]>>8*(e%4)&255)<<16|(a[e+1>>2]>>8*((e+1)%4)&255)<<8|a[e+2>>2]>>8*((e+2)%4)&255,h=0;4>h;h++)d.push(8*e+6*h>32*a.length?b:c.charAt(g>>6*(3-h)&63));return d.join("")},/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ diff --git a/dist/offline-tiles-advanced-src.js b/dist/offline-tiles-advanced-src.js index 5819247a..7193b9dc 100644 --- a/dist/offline-tiles-advanced-src.js +++ b/dist/offline-tiles-advanced-src.js @@ -1,4 +1,4 @@ -/*! offline-editor-js - v2.6.1 - 2015-04-13 +/*! offline-editor-js - v2.7.0 - 2015-04-27 * Copyright (c) 2015 Environmental Systems Research Institute, Inc. * Apache License*/ define([ diff --git a/dist/offline-tiles-basic-min.js b/dist/offline-tiles-basic-min.js index 187d9e1d..cd2a01b9 100644 --- a/dist/offline-tiles-basic-min.js +++ b/dist/offline-tiles-basic-min.js @@ -1,4 +1,4 @@ -/*! offline-editor-js - v2.6.1 - 2015-04-13 +/*! offline-editor-js - v2.7.0 - 2015-04-27 * Copyright (c) 2015 Environmental Systems Research Institute, Inc. * Apache License*/ define(["dojo/query","dojo/request","esri/geometry/Polygon","dojo/_base/declare"],function(a,b,c,d){"use strict";return d("O.esri.Tiles.OfflineTilesEnabler",[],{getBasemapLayer:function(a){var b=a.layerIds[0];return a.getLayer(b)},extend:function(c,d,e){c._tilesCore=new O.esri.Tiles.TilesCore,c._lastTileUrl="",c._imageType="",c._minZoom=null,c._maxZoom=null,c._getTileUrl=c.getTileUrl;var f=!0;return"undefined"!=typeof e&&(f=e),c.offline={online:f,store:new O.esri.Tiles.TilesStore,proxyPath:null},c.offline.store.isSupported()?(c.offline.store.init(function(b){b&&(c.resampling=!1,c.getTileUrl=function(b,d,e){var f=this._getTileUrl(b,d,e);if(this.offline.online)return""==c._imageType&&(c._imageType=this.tileInfo.format.toLowerCase()),c._lastTileUrl=f,f;f=f.split("?")[0];var g="void:/"+b+"/"+d+"/"+e,h=null;return c._tilesCore._getTiles(h,this._imageType,f,g,this.offline.store,a),g},d&&d(!0))}.bind(this)),c.getLevelEstimation=function(a,b,c){var d=new O.esri.Tiles.TilingScheme(this),e=d.getAllCellIdsInExtent(a,b),f={level:b,tileCount:e.length,sizeBytes:e.length*c};return f},c.prepareForOffline=function(a,b,d,e){c._tilesCore._createCellsForOffline(this,a,b,d,function(a){this._doNextTile(0,a,e)}.bind(this))},c.goOffline=function(){this.offline.online=!1},c.goOnline=function(){this.offline.online=!0,this.refresh()},c.isOnline=function(){return this.offline.online},c.deleteAllTiles=function(a){var b=this.offline.store;b.deleteAll(a)},c.getOfflineUsage=function(a){var b=this.offline.store;b.usedSpace(a)},c.getTilePolygons=function(a){c._tilesCore._getTilePolygons(this.offline.store,c.url,this,a)},c.saveToFile=function(a,b){c._tilesCore._saveToFile(a,this.offline.store,b)},c.loadFromFile=function(a,b){c._tilesCore._loadFromFile(a,this.offline.store,b)},c.getMaxZoom=function(a){null==this._maxZoom&&(this._maxZoom=c.tileInfo.lods[c.tileInfo.lods.length-1].level),a(this._maxZoom)},c.getMinZoom=function(a){null==this._minZoom&&(this._minZoom=c.tileInfo.lods[0].level),a(this._minZoom)},c.getMinMaxLOD=function(a,b){var d={},e=c.getMap(),f=e.getLevel()+a,g=e.getLevel()+b;return null!=this._maxZoom&&null!=this._minZoom?(d.max=Math.min(this._maxZoom,g),d.min=Math.max(this._minZoom,f)):(c.getMinZoom(function(a){d.min=Math.max(a,f)}),c.getMaxZoom(function(a){d.max=Math.min(a,g)})),d},c.estimateTileSize=function(a){c._tilesCore._estimateTileSize(b,this._lastTileUrl,this.offline.proxyPath,a)},c.getExtentBuffer=function(a,b){return b.xmin-=a,b.ymin-=a,b.xmax+=a,b.ymax+=a,b},c.getTileUrlsByExtent=function(a,b){var d=new O.esri.Tiles.TilingScheme(c),e=d.getAllCellIdsInExtent(a,b),f=[];return e.forEach(function(a){f.push(c.url+"/"+b+"/"+a[1]+"/"+a[0])}.bind(this)),f},void(c._doNextTile=function(a,b,d){var e=b[a],f=this._getTileUrl(e.level,e.row,e.col);c._tilesCore._storeTile(f,this.offline.proxyPath,this.offline.store,function(c,f){c||(f={cell:e,msg:f});var g=d({countNow:a,countMax:b.length,cell:e,error:f,finishedDownloading:!1});g||a===b.length-1?d({finishedDownloading:!0,cancelRequested:g}):this._doNextTile(a+1,b,d)}.bind(this))})):d(!1,"indexedDB not supported")}})}),"undefined"!=typeof O?O.esri.Tiles={}:(O={},O.esri={Tiles:{}}),O.esri.Tiles.Base64Utils={},O.esri.Tiles.Base64Utils.outputTypes={Base64:0,Hex:1,String:2,Raw:3},O.esri.Tiles.Base64Utils.addWords=function(a,b){var c=(65535&a)+(65535&b),d=(a>>16)+(b>>16)+(c>>16);return d<<16|65535&c},O.esri.Tiles.Base64Utils.stringToWord=function(a){for(var b=8,c=(1<e;e+=b)d[e>>5]|=(a.charCodeAt(e/b)&c)<e;e+=b)d.push(String.fromCharCode(a[e>>5]>>>e%32&c));return d.join("")},O.esri.Tiles.Base64Utils.wordToHex=function(a){for(var b="0123456789abcdef",c=[],d=0,e=4*a.length;e>d;d++)c.push(b.charAt(a[d>>2]>>d%4*8+4&15)+b.charAt(a[d>>2]>>d%4*8&15));return c.join("")},O.esri.Tiles.Base64Utils.wordToBase64=function(a){for(var b="=",c="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",d=[],e=0,f=4*a.length;f>e;e+=3)for(var g=(a[e>>2]>>8*(e%4)&255)<<16|(a[e+1>>2]>>8*((e+1)%4)&255)<<8|a[e+2>>2]>>8*((e+2)%4)&255,h=0;4>h;h++)d.push(8*e+6*h>32*a.length?b:c.charAt(g>>6*(3-h)&63));return d.join("")},/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ diff --git a/dist/offline-tiles-basic-src.js b/dist/offline-tiles-basic-src.js index b799ffbb..630c0524 100644 --- a/dist/offline-tiles-basic-src.js +++ b/dist/offline-tiles-basic-src.js @@ -1,4 +1,4 @@ -/*! offline-editor-js - v2.6.1 - 2015-04-13 +/*! offline-editor-js - v2.7.0 - 2015-04-27 * Copyright (c) 2015 Environmental Systems Research Institute, Inc. * Apache License*/ define([ diff --git a/dist/offline-tpk-min.js b/dist/offline-tpk-min.js index fdd54bcc..a3dc43fe 100644 --- a/dist/offline-tpk-min.js +++ b/dist/offline-tpk-min.js @@ -1,4 +1,4 @@ -/*! offline-editor-js - v2.6.1 - 2015-04-13 +/*! offline-editor-js - v2.7.0 - 2015-04-27 * Copyright (c) 2015 Environmental Systems Research Institute, Inc. * Apache License*/ define(["dojo/_base/declare","esri/geometry/Extent","dojo/query","esri/SpatialReference","esri/layers/TileInfo","esri/layers/TiledMapServiceLayer","dojo/Deferred","dojo/promise/all","dojo/Evented"],function(a,b,c,d,e,f,g,h,i){return a("O.esri.TPK.TPKLayer",[f,i],{map:null,store:null,MAX_DB_SIZE:75,TILE_PATH:"",RECENTER_DELAY:350,PARSING_ERROR:"parsingError",DB_INIT_ERROR:"dbInitError",DB_FULL_ERROR:"dbFullError",NO_SUPPORT_ERROR:"libNotSupportedError",PROGRESS_START:"start",PROGRESS_END:"end",WINDOW_VALIDATED:"windowValidated",DB_VALIDATED:"dbValidated",DATABASE_ERROR_EVENT:"databaseErrorEvent",VALIDATION_EVENT:"validationEvent",PROGRESS_EVENT:"progress",_maxDBSize:75,_isDBWriteable:!0,_isDBValid:!1,_autoCenter:null,_fileEntriesLength:0,_inMemTilesObject:null,_inMemTilesObjectLength:0,_zeroLengthFileCounter:0,constructor:function(){this._self=this,this._inMemTilesIndex=[],this._inMemTilesObject={},this.store=new O.esri.Tiles.TilesStore,this._validate()},extend:function(a){this._fileEntriesLength=a.length,this.emit(this.PROGRESS_EVENT,this.PROGRESS_START),this._parseInMemFiles(a,function(){this._parseConfCdi(function(a){this.initialExtent=this.fullExtent=a,this._parseConfXml(function(a){this.tileInfo=new e(a),this.spatialReference=new d({wkid:this.tileInfo.spatialReference.wkid}),this.loaded=!0,this.onLoad(this),this.emit(this.PROGRESS_EVENT,this.PROGRESS_END)}.bind(this._self))}.bind(this._self))}.bind(this._self))},getTileUrl:function(a,b,d){this.emit(this.PROGRESS_EVENT,this.PROGRESS_START);var e=this._self.TILE_PATH+"_alllayers",f=this._getCacheFilePath(e,a,b,d);if(this._inMemTilesObject!={}){var g="void:/"+a+"/"+b+"/"+d;return null==this.map&&(this.map=this.getMap()),null==this._autoCenter&&(this._autoCenter=new O.esri.TPK.autoCenterMap(this.map,this.RECENTER_DELAY),this._autoCenter.init()),this._getInMemTiles(f,e,a,b,d,g,function(a,b,d){var e=c("img[src="+b+"]")[0];"undefined"==typeof e&&(e=new Image);var f;if(a){var g="data:image/png;base64,";switch(this.tileInfo.format){case"JPEG":f="data:image/jpg;base64,"+a;break;case"PNG":f=g+a;break;case"PNG8":f=g+a;break;case"PNG24":f=g+a;break;case"PNG32":f=g+a;break;default:f="data:image/jpg;base64,"+a}e.style.borderColor="blue"}else e.style.borderColor="green",f="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAABQdJREFUeNrs2yFv6mocwOH/ualYRUVJRrKKCRATCCZqJ/mOfKQJBGaiYkcguoSJigoQTc4VN222Mdhu7l0ysudJjqFAD13669u37a/lcvkngB8piYhYLBa2BPxAf9kEIACAAAACAAgAIACAAAACAAgAIACAAAACAAgAIACAAAACAAgAIACAAAACAAgAIACAAAACAAgAIACAAAACAAgAIACAAAACAAgAIACAAAACAAgACAAgAIAAAAIACAAgAIAAAAIACAAgAIAAAAIACAAgAIAAAAIACAAgAIAAAAIACAAgAIAAAAIACAAgAIAAAAIACAAgAIAAAAIACAAgAIAAAAIACAAIACAAgAAAAgAIACAAgAAAAgAIACAAgAAAAgAIACAAgAAAAgAIACAAgAAAAgAIACAAgAAAAgAIACAAgAAAAgAIACAAgAAAAgAIACAAgAAAAgAIAPxsiU3wfbRtG1mWnVzedV3kef7q9a7rYrvdxm63i4iILMtiNBpFkiQfftdnZFkWbdtGRAzr7j+fZdnR9Xy0jiRJTv5eBOBHqaoqsiyLm5ubo8ubponFYjG8Vtd1VFV1sKMlSRI3NzdRFMXJ7/qMsixjtVpFRAzr7j9fluVBkD67jjzPoyxLf3gBoLfZbGI8Hh/dqV6q6zoeHh4iSZKYTCYxGo0iImK73Q7Luq6L6+vrg88WRfFqHfv9Puq6jjRN4+rq6tV7Ly4u/tNvKori3e9I09QfXAB4a71ex93d3ckhfNd1UVXVcIR+OZTO8zyKooj7+/uoqiouLy8Pdra3I4OmaaKu67i4uIjpdPq//p63seH7MAn4DXVdF+v1+sOjf390f+88Osuy4ci/2WxsVATgXEwmk2ia5uSOu91uIyJiPB4ffU+/rJ/AA6cAZ2A6ncbz83NUVRV5nr97hO8n104Nrftln53s+ypVVR2czpj8MwLghPl8HkmSDBN556xt22ia5tU/jAA4IU3TmE6nUVVVVFUVs9nsbH/LqUuFGAFwxPX1deR5HnVdD+f8LwPx0fl9f2OQy20IwJm6vb0dTgX2+/3wej8vcCoA/VDb3XYIwLmeoyVJzGaz6LpuOKJHRFxeXkbEP5cDj+mX9e8FAThD4/H44HJfURSRpmk0TROPj48Hn3l4eIimaSJN06O3A4NJwDMxm82ibdtXo4D5fB6r1Sp+//4dz8/Pw5H+6ekpdrtdJEkS8/n8S/9f713ie3vaceo9x557QAB451Sgfyin34HKshweunk5HzAej2MymXz5+f9nbjJyI9L39Wu5XP55+XQZ39uxR4Z3u90wSXjqEV0wAjhjx47oaZq63Me/ZhIQBAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEAAbAJQAAAAQAEABAAQAAAAQAEABAAQAAAAQAEABAAQAAAAQAEABAAQAAAAQAEABAAQAAAAQAEABAAQAAAAQAEABAAQAAAAQAEABAAQAAAAQAEABAAEABAAAABAAQAEABAAAABAAQAEABAAAABAAQAEABAAAABAAQAEABAAAABAAQAEABAAAABAAQAEABAAAABAAQAEABAAAABAAQAEABAAAABAAQAEAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEAvqe/BwCeKjUweoA8pQAAAABJRU5ErkJggg==";return e.style.visibility="visible",e.src=f,this.emit(this.PROGRESS_EVENT,this.PROGRESS_END),""}.bind(this._self)),g}},setMaxDBSize:function(a){var b=/^\d+$/;b.test(a)&&a<=this.MAX_DB_SIZE&&(this._maxDBSize=a)},getDBSize:function(a){this.store.usedSpace(function(b,c){a(b,c)}.bind(this))},setDBWriteable:function(a){this._isDBWriteable=a},isDBValid:function(){return this._validate(),this._isDBValid},loadFromURL:function(a,b){this.isDBValid()?this.store.store(a,function(a,c){a?b(!0,""):b(!1,c)}):b(!1,"not supported")},_validate:function(){window.File||window.FileReader||window.Blob||window.btoa||window.DataView?this.emit(this.VALIDATION_EVENT,{msg:this.WINDOW_VALIDATED,err:null}):this.emit(this.VALIDATION_EVENT,{msg:this.NO_SUPPORT_ERROR,err:null}),this.store.isSupported()?this.store.init(function(a){0==a?this.emit(this.DATABASE_ERROR_EVENT,{msg:this.DB_INIT_ERROR,err:null}):this.store.usedSpace(function(a,b){var c=this._bytes2MBs(a.sizeBytes);c>this.MAX_DB_SIZE&&this.emit(this.DATABASE_ERROR_EVENT,{msg:this.DB_FULL_ERROR,err:b}),this.emit(this.VALIDATION_EVENT,{msg:this.DB_VALIDATED,err:null}),this._isDBValid=!0}.bind(this))}.bind(this)):this.emit(this.VALIDATION_EVENT,{msg:this.NO_SUPPORT_ERROR,err:null})},_parseInMemFiles:function(a,b){var c=this._fileEntriesLength;this._zeroLengthFileCounter=0;for(var d=[],e=0;c>e;e++){var f=new g,i=a[e].filename.toLocaleUpperCase(),j=i.indexOf("_ALLLAYERS",0);-1!=j&&(this.TILE_PATH=i.slice(0,j)),0==a[e].compressedSize&&this._zeroLengthFileCounter++;var k=i.indexOf("CONF.CDI",0),l=i.indexOf("CONF.XML",0),m=i.indexOf("BUNDLE",0),n=i.indexOf("BUNDLX",0);-1!=k||-1!=l?this._unzipConfFiles(a,e,f,function(a,b){a.resolve(b)}):-1!=m||-1!=n?this._unzipTileFiles(a,e,f,function(a,b){a.resolve(b)}):f.resolve(e),d.push(f)}h(d).then(function(a){b&&b(a)})},ObjectSize:function(a){var b,c=0;for(b in a)a.hasOwnProperty(b)&&c++;return c},_unzipConfFiles:function(a,b,c,d){a[b].getData(new O.esri.zip.TextWriter(b),function(b){this._inMemTilesIndex.push("blank");var e=a[b.token].filename.toLocaleUpperCase();this._inMemTilesObject[e]=b.string;var f=this.ObjectSize(this._inMemTilesObject);f>0&&d(c,b.token)}.bind(this))},_unzipTileFiles:function(a,b,c,d){var e=this;a[b].getData(new O.esri.zip.BlobWriter(b),function(b){if(0!=b.size){var f=new FileReader;f.token=b.token,f.onerror=function(a){e.emit(e.PARSING_ERROR,{msg:"Error parsing file: ",err:a.target.error})},f.addEventListener("loadend",function(f){if(void 0!=this.token){e._inMemTilesIndex.push("blank");var g=a[this.token].filename.toLocaleUpperCase();e._inMemTilesObject[g]=this.result;var h=e.ObjectSize(e._inMemTilesObject);h>0&&d(c,b.token)}}),f.readAsArrayBuffer(b)}})},_parseConfCdi:function(a){var c=this._inMemTilesObject[this.TILE_PATH+"CONF.CDI"],e=new O.esri.TPK.X2JS,f=e.xml_str2json(c),g=f.EnvelopeN,h=parseFloat(g.XMin),i=parseFloat(g.YMin),j=parseFloat(g.XMax),k=parseFloat(g.YMax),l=parseInt(g.SpatialReference.WKID),m=new b(h,i,j,k,new d({wkid:l}));a(m)},_parseConfXml:function(a){var b=this._inMemTilesObject[this.TILE_PATH+"CONF.XML"],c=new O.esri.TPK.X2JS,d=c.xml_str2json(b),e=d.CacheInfo,f={};f.rows=parseInt(e.TileCacheInfo.TileRows),f.cols=parseInt(e.TileCacheInfo.TileCols),f.dpi=parseInt(e.TileCacheInfo.DPI),f.format=e.TileImageInfo.CacheTileFormat,f.compressionQuality=parseInt(e.TileImageInfo.CompressionQuality),f.origin={x:parseInt(e.TileCacheInfo.TileOrigin.X),y:parseInt(e.TileCacheInfo.TileOrigin.Y)},f.spatialReference={wkid:parseInt(e.TileCacheInfo.SpatialReference.WKID)};for(var g=e.TileCacheInfo.LODInfos.LODInfo,h=[],i=0;im;m+=3)f=i[m]<<16|i[m+1]<<8|i[m+2],b=(16515072&f)>>18,c=(258048&f)>>12,d=(4032&f)>>6,e=63&f,g+=h[b]+h[c]+h[d]+h[e];return 1==k?(f=i[l],b=(252&f)>>2,c=(3&f)<<4,g+=h[b]+h[c]+"=="):2==k&&(f=i[l]<<8|i[l+1],b=(64512&f)>>10,c=(1008&f)>>4,d=(15&f)<<2,g+=h[b]+h[c]+h[d]+"="),g},_buffer2Base64:function(a,b,c){var d=new DataView(a,b),e=d.getInt32(0,!0),f=d.buffer.slice(b+4,b+4+e),g=this._base64ArrayBuffer(f);c(g)},_int2HexString:function(a){var b=a.toString(16).toUpperCase();return 1===b.length?"000"+b:2===b.length?"00"+b:3===b.length?"0"+b:b.substr(0,b.length)},_getOffset:function(a,b,c,d,e){var f=128*(c-e)+(b-d);return 16+5*f},_getCacheFilePath:function(a,b,c,d){var e=[];return e.push(a),e.push("/"),e.push("L"),e.push(10>b?"0"+b:b),e.push("/"),e.push("R"),e.push(this._int2HexString(c)),e.push("C"),e.push(this._int2HexString(d)),e.join("")},_bytes2MBs:function(a){return(a>>>20)+"."+(2046&a)}})}),"undefined"!=typeof O?O.esri.TPK={}:(O={},O.esri={TPK:{},Tiles:{}}),O.esri.Tiles.TilesStore=function(){this._db=null;var a="offline_tile_store";this.isSupported=function(){return window.indexedDB||window.openDatabase?!0:!1},this.store=function(a,b){try{var c=this._db.transaction(["tilepath"],"readwrite");c.oncomplete=function(){b(!0)},c.onerror=function(a){b(!1,a.target.error.message)};var d=c.objectStore("tilepath"),e=d.put(a);e.onsuccess=function(){}}catch(f){b(!1,f.stack)}},this.retrieve=function(a,b){if(null!==this._db){var c=this._db.transaction(["tilepath"]).objectStore("tilepath"),d=c.get(a);d.onsuccess=function(a){var c=a.target.result;void 0==c?b(!1,"not found"):b(!0,c)},d.onerror=function(a){b(!1,a)}}},this.deleteAll=function(a){if(null!==this._db){var b=this._db.transaction(["tilepath"],"readwrite").objectStore("tilepath").clear();b.onsuccess=function(){a(!0)},b.onerror=function(b){a(!1,b)}}else a(!1,null)},this["delete"]=function(a,b){if(null!==this._db){var c=this._db.transaction(["tilepath"],"readwrite").objectStore("tilepath")["delete"](a);c.onsuccess=function(){b(!0)},c.onerror=function(a){b(!1,a)}}else b(!1,null)},this.getAllTiles=function(a){if(null!==this._db){var b=this._db.transaction(["tilepath"]).objectStore("tilepath").openCursor();b.onsuccess=function(b){var c=b.target.result;if(c){var d=c.value.url,e=c.value.img;a(d,e,null),c["continue"]()}else a(null,null,"end")}.bind(this),b.onerror=function(b){a(null,null,b)}}else a(null,null,"no db")},this.usedSpace=function(a){if(null!==this._db){var b={sizeBytes:0,tileCount:0},c=this._db.transaction(["tilepath"]).objectStore("tilepath").openCursor();c.onsuccess=function(c){var d=c.target.result;if(d){var e=d.value,f=JSON.stringify(e);b.sizeBytes+=this._stringBytes(f),b.tileCount+=1,d["continue"]()}else a(b,null)}.bind(this),c.onerror=function(b){a(null,b)}}else a(null,null)},this._stringBytes=function(a){return a.length},this.init=function(b){var c=indexedDB.open(a,4);b=b||function(a){}.bind(this),c.onerror=function(a){b(!1,a.target.errorCode)}.bind(this),c.onupgradeneeded=function(a){var b=a.target.result;b.objectStoreNames.contains("tilepath")&&b.deleteObjectStore("tilepath"),b.createObjectStore("tilepath",{keyPath:"url"})}.bind(this),c.onsuccess=function(a){this._db=a.target.result,b(!0)}.bind(this)}},function(a){function b(){var a=-1,b=this;b.append=function(c){var d,e=b.table;for(d=0;d>>8^e[255&(a^c[d])]},b.get=function(){return~a}}function c(a,b,c){return a.slice?a.slice(b,b+c):a.webkitSlice?a.webkitSlice(b,b+c):a.mozSlice?a.mozSlice(b,b+c):a.msSlice?a.msSlice(b,b+c):void 0}function d(a,b){var c,d;return c=new ArrayBuffer(a),d=new Uint8Array(c),b&&d.set(b,0),{buffer:c,array:d,view:new DataView(c)}}function e(){}function f(a){function b(b,c){var f=new Blob([a],{type:M});d=new h(f),d.init(function(){e.size=d.size,b()},c)}function c(a,b,c,e){d.readUint8Array(a,b,c,e)}var d,e=this;e.size=0,e.init=b,e.readUint8Array=c}function g(b){function c(a){for(var c=b.length;"="==b.charAt(c-1);)c--;f=b.indexOf(",")+1,g.size=Math.floor(.75*(c-f)),a()}function e(c,e,g){var h,i=d(e),j=4*Math.floor(c/3),k=4*Math.ceil((c+e)/3),l=a.atob(b.substring(j+f,k+f)),m=c-3*Math.floor(j/4);for(h=m;m+e>h;h++)i.array[h-m]=l.charCodeAt(h);g(i.array)}var f,g=this;g.size=0,g.init=c,g.readUint8Array=e}function h(a){function b(b){this.size=a.size,b()}function d(b,d,e,f){var g=new FileReader;g.onload=function(a){e(new Uint8Array(a.target.result))},g.onerror=f,g.readAsArrayBuffer(c(a,b,d))}var e=this;e.size=0,e.init=b,e.readUint8Array=d}function i(){}function j(a,b){function c(a){f=new Blob([],{type:M}),a()}function d(a,b){f=new Blob([f,A?a:a.buffer],{type:M}),b()}function e(c,d){var e=new FileReader;e.onload=function(b){var d={string:b.target.result,token:a};c(d)},e.onerror=d,e.readAsText(f,b)}var f,g=this;g.init=c,g.writeUint8Array=d,g.getData=e}function k(b){function c(a){g+="data:"+(b||"")+";base64,",a()}function d(b,c){var d,e=h.length,f=h;for(h="",d=0;d<3*Math.floor((e+b.length)/3)-e;d++)f+=String.fromCharCode(b[d]);for(;d2?g+=a.btoa(f):h=f,c()}function e(b){b(g+a.btoa(h))}var f=this,g="",h="";f.init=c,f.writeUint8Array=d,f.getData=e}function l(a,b){function c(a){f=new Blob([],{type:b}),a()}function d(c,d){f=new Blob([f,A?c:c.buffer],{type:b}),f.token=a,d()}function e(a){a(f)}var f,g=this;g.init=c,g.writeUint8Array=d,g.getData=e}function m(a,b,c,d,e,f,g,h,i,j){function k(){a.removeEventListener(N,l,!1),h(o)}function l(a){var b=a.data,d=b.data;b.onappend&&(o+=d.length,c.writeUint8Array(d,function(){f(!1,d),m()},j)),b.onflush&&(d?(o+=d.length,c.writeUint8Array(d,function(){f(!1,d),k()},j)):k()),b.progress&&g&&g(n+b.current,e)}function m(){n=p*J,e>n?b.readUint8Array(d+n,Math.min(J,e-n),function(b){a.postMessage({append:!0,data:b}),p++,g&&g(n,e),f(!0,b)},i):a.postMessage({flush:!0})}var n,o,p=0;o=0,a.addEventListener(N,l,!1),m()}function n(a,b,c,d,e,f,g,h,i,j){function k(){var o;l=m*J,e>l?b.readUint8Array(d+l,Math.min(J,e-l),function(b){var h=a.append(b,function(){g&&g(d+l,e)});n+=h.length,f(!0,b),c.writeUint8Array(h,function(){f(!1,h),m++,setTimeout(k,1)},j),g&&g(l,e)},i):(o=a.flush(),o?(n+=o.length,c.writeUint8Array(o,function(){f(!1,o),h(n)},j)):h(n))}var l,m=0,n=0;k()}function o(c,d,e,f,g,h,i,j,k){function l(a,b){g&&!a&&q.append(b)}function o(a){h(a,q.get())}var p,q=new b;return a.zip.useWebWorkers?(p=new Worker(a.zip.workerScriptsPath+K),m(p,c,d,e,f,l,i,o,j,k)):n(new a.zip.Inflater,c,d,e,f,l,i,o,j,k),p}function p(c,d,e,f,g,h,i){function j(a,b){a&&p.append(b)}function k(a){f(a,p.get())}function l(){o.removeEventListener(N,l,!1),m(o,c,d,0,c.size,j,g,k,h,i)}var o,p=new b;return a.zip.useWebWorkers?(o=new Worker(a.zip.workerScriptsPath+L),o.addEventListener(N,l,!1),o.postMessage({init:!0,level:e})):n(new a.zip.Deflater,c,d,0,c.size,j,g,k,h,i),o}function q(a,c,d,e,f,g,h,i,j){function k(){var b=l*J;e>b?a.readUint8Array(d+b,Math.min(J,e-b),function(a){f&&m.append(a),h&&h(b,e,a),c.writeUint8Array(a,function(){l++,k()},j)},i):g(e,m.get())}var l=0,m=new b;k()}function r(a){var b,c,d="",e=["Ç","ü","é","â","ä","à","å","ç","ê","ë","è","ï","î","ì","Ä","Å","É","æ","Æ","ô","ö","ò","û","ù","ÿ","Ö","Ü","ø","£","Ø","×","ƒ","á","í","ó","ú","ñ","Ñ","ª","º","¿","®","¬","½","¼","¡","«","»","_","_","_","¦","¦","Á","Â","À","©","¦","¦","+","+","¢","¥","+","+","-","-","+","-","+","ã","Ã","+","+","-","-","¦","-","+","¤","ð","Ð","Ê","Ë","È","i","Í","Î","Ï","+","+","_","_","¦","Ì","_","Ó","ß","Ô","Ò","õ","Õ","µ","þ","Þ","Ú","Û","Ù","ý","Ý","¯","´","­","±","_","¾","¶","§","÷","¸","°","¨","·","¹","³","²","_"," "];for(b=0;b127?e[c-128]:String.fromCharCode(c);return d}function s(a){return decodeURIComponent(escape(a))}function t(a){var b,c="";for(b=0;b>16,c=65535&a;try{return new Date(1980+((65024&b)>>9),((480&b)>>5)-1,31&b,(63488&c)>>11,(2016&c)>>5,2*(31&c),0)}catch(d){}}function v(a,b,c,d,e){return a.version=b.view.getUint16(c,!0),a.bitFlag=b.view.getUint16(c+2,!0),a.compressionMethod=b.view.getUint16(c+4,!0),a.lastModDateRaw=b.view.getUint32(c+6,!0),a.lastModDate=u(a.lastModDateRaw),1===(1&a.bitFlag)?void e(C):((d||8!=(8&a.bitFlag))&&(a.crc32=b.view.getUint32(c+10,!0),a.compressedSize=b.view.getUint32(c+14,!0),a.uncompressedSize=b.view.getUint32(c+18,!0)),4294967295===a.compressedSize||4294967295===a.uncompressedSize?void e(D):(a.filenameLength=b.view.getUint16(c+22,!0),void(a.extraFieldLength=b.view.getUint16(c+24,!0))))}function w(a,b){function c(){}function e(c,f){a.readUint8Array(a.size-c,c,function(a){var b=d(a.length,a).view;1347093766!=b.getUint32(0)?e(c+1,f):f(b)},function(){b(E)})}return c.prototype.getData=function(c,e,f,g){function h(a,b){m&&m.terminate(),m=null,a&&a(b)}function i(a){var b=d(4);return b.view.setUint32(0,a),n.crc32==b.view.getUint32(0)}function j(a,b){g&&!i(b)?k():c.getData(function(a){h(e,a)})}function k(){h(b,H)}function l(){h(b,G)}var m,n=this;a.readUint8Array(n.offset,30,function(e){var h,i=d(e.length,e);return 1347093252!=i.view.getUint32(0)?void b(B):(v(n,i,4,!1,b),h=n.offset+30+n.filenameLength+n.extraFieldLength,void c.init(function(){0===n.compressionMethod?q(a,c,h,n.compressedSize,g,j,f,k,l):m=o(a,c,h,n.compressedSize,g,j,f,k,l)},l))},k)},{getEntries:function(f){return a.size<22?void b(B):void e(22,function(e){var g,h;g=e.getUint32(16,!0),h=e.getUint16(8,!0),a.readUint8Array(g,a.size-g,function(a){var e,g,i,j,k=0,l=[],m=d(a.length,a);for(e=0;h>e;e++){if(g=new c,1347092738!=m.view.getUint32(k))return void b(B);v(g,m,k+6,!0,b),g.commentLength=m.view.getUint16(k+32,!0),g.directory=16==(16&m.view.getUint8(k+38)),g.offset=m.view.getUint32(k+42,!0),i=t(m.array.subarray(k+46,k+46+g.filenameLength)),g.filename=2048===(2048&g.bitFlag)?s(i):r(i),g.directory||"/"!=g.filename.charAt(g.filename.length-1)||(g.directory=!0),j=t(m.array.subarray(k+46+g.filenameLength+g.extraFieldLength,k+46+g.filenameLength+g.extraFieldLength+g.commentLength)),g.comment=2048===(2048&g.bitFlag)?s(j):r(j),l.push(g),k+=46+g.filenameLength+g.extraFieldLength+g.commentLength}f(l)},function(){b(E)})})},close:function(a){a&&a()}}}function x(a){return unescape(encodeURIComponent(a))}function y(a){var b,c=[];for(b=0;ba;a++){for(c=a,b=0;8>b;b++)1&c?c=c>>>1^3988292384:c>>>=1;d[a]=c}return d}(),f.prototype=new e,f.prototype.constructor=f,g.prototype=new e,g.prototype.constructor=g,h.prototype=new e,h.prototype.constructor=h,i.prototype.getData=function(a){a(this.data)},j.prototype=new i,j.prototype.constructor=j,k.prototype=new i,k.prototype.constructor=k,l.prototype=new i,l.prototype.constructor=l,a.zip={Reader:e,Writer:i,BlobReader:h,Data64URIReader:g,TextReader:f,BlobWriter:l,Data64URIWriter:k,TextWriter:j,createReader:function(a,b,c){a.init(function(){b(w(a,c))},c)},createWriter:function(a,b,c,d){a.init(function(){b(z(a,c,d))},c)},workerScriptsPath:"",useWebWorkers:!0}}(O.esri),O.esri.TPK.autoCenterMap=function(a,b){function c(a){var b="onorientationchange"in window,c=b?"orientationchange":"resize";window.addEventListener(c,e(function(){d()},a))}function d(){var b=i().split(","),c=a.spatialReference.wkid,d=null;4326==c?d=new esri.geometry.Point(b[1],b[0]):(c=102100)&&(d=new esri.geometry.Point(b[0],b[1],new esri.SpatialReference({wkid:c}))),a.centerAt(d)}function e(a,b,c){var d;return function(){var e=this,f=arguments;clearTimeout(d),d=setTimeout(function(){d=null,c||a.apply(e,f)},b),c&&!d&&a.apply(e,f)}}function f(){a.on("pan-end",function(){var b=a.extent.getCenter();h(b.x,b.y,a.spatialReference.wkid)})}function g(){a.on("zoom-end",function(){var b=a.extent.getCenter();h(b.x,b.y,a.spatialReference.wkid),a.setZoom(a.getZoom())}.bind(self))}function h(a,b,c){localStorage.setItem("_centerPtX",a),localStorage.setItem("_centerPtY",b),localStorage.setItem("_spatialReference",c)}function i(){var a=null;try{a=localStorage.getItem("_centerPtX")+","+localStorage.getItem("_centerPtY")+","+localStorage.getItem("_spatialReference")}catch(b){}return a}this.init=function(){f(),g(),c(b);var d=a.extent.getCenter();h(d.x,d.y,a.spatialReference.wkid)}},O.esri.TPK.inflate=function(a){function b(){function a(a,b,c,d,j,k,l,n,p,r,s){var t,u,v,w,x,y,z,A,C,D,E,F,G,H,I;D=0,x=c;do e[a[b+D]]++,D++,x--;while(0!==x);if(e[0]==c)return l[0]=-1,n[0]=0,i;for(A=n[0],y=1;B>=y&&0===e[y];y++);for(z=y,y>A&&(A=y),x=B;0!==x&&0===e[x];x--);for(v=x,A>x&&(A=x),n[0]=A,H=1<y;y++,H<<=1)if((H-=e[y])<0)return m;if((H-=e[x])<0)return m;for(e[x]+=H,h[1]=y=0,D=1,G=2;0!==--x;)h[G]=y+=e[D],G++,D++;x=0,D=0;do 0!==(y=a[b+D])&&(s[h[y]++]=x),D++;while(++x=z;z++)for(t=e[z];0!==t--;){for(;z>F+A;){if(w++,F+=A,I=v-F,I=I>A?A:I,(u=1<<(y=z-F))>t+1&&(u-=t+1,G=z,I>y))for(;++yq)return m;g[w]=E=r[0],r[0]+=I,0!==w?(h[w]=x,f[0]=y,f[1]=A,y=x>>>F-A,f[2]=E-g[w-1]-y,p.set(f,3*(g[w-1]+y))):l[0]=E}for(f[1]=z-F,D>=c?f[0]=192:s[D]>>F;I>y;y+=u)p.set(f,3*(E+y));for(y=1<>>=1)x^=y;for(x^=y,C=(1<b;b++)d[b]=0;for(b=0;B+1>b;b++)e[b]=0;for(b=0;3>b;b++)f[b]=0;g.set(e.subarray(0,B),0),h.set(e.subarray(0,B+1),0)}var c,d,e,f,g,h,j=this;j.inflate_trees_bits=function(e,f,g,h,i){var j;return b(19),c[0]=0,j=a(e,0,19,19,null,null,g,f,h,c,d),j==m?i.msg="oversubscribed dynamic bit lengths tree":(j==o||0===f[0])&&(i.msg="incomplete dynamic bit lengths tree",j=m),j},j.inflate_trees_dynamic=function(e,f,g,h,j,k,l,p,q){var r;return b(288),c[0]=0,r=a(g,0,e,257,x,y,k,h,p,c,d),r!=i||0===h[0]?(r==m?q.msg="oversubscribed literal/length tree":r!=n&&(q.msg="incomplete literal/length tree",r=m),r):(b(288),r=a(g,e,f,0,z,A,l,j,p,c,d),r!=i||0===j[0]&&e>257?(r==m?q.msg="oversubscribed distance tree":r==o?(q.msg="incomplete distance tree",r=m):r!=n&&(q.msg="empty distance tree with lengths",r=m),r):i)}}function c(){function a(a,b,c,d,e,f,g,h){var k,l,n,o,q,r,s,t,u,v,w,x,y,z,A,B;s=h.next_in_index,t=h.avail_in,q=g.bitb,r=g.bitk,u=g.write,v=ur;)t--,q|=(255&h.read_byte(s++))<>=l[B+1],r-=l[B+1],0!==(16&o)){for(o&=15,y=l[B+2]+(q&p[o]),q>>=o,r-=o;15>r;)t--,q|=(255&h.read_byte(s++))<>=l[B+1],r-=l[B+1],0!==(16&o)){for(o&=15;o>r;)t--,q|=(255&h.read_byte(s++))<>=o,r-=o,v-=y,u>=z)A=u-z,u-A>0&&2>u-A?(g.window[u++]=g.window[A++],g.window[u++]=g.window[A++],y-=2):(g.window.set(g.window.subarray(A,A+2),u),u+=2,A+=2,y-=2);else{A=u-z;do A+=g.end;while(0>A);if(o=g.end-A,y>o){if(y-=o,u-A>0&&o>u-A){do g.window[u++]=g.window[A++];while(0!==--o)}else g.window.set(g.window.subarray(A,A+o),u),u+=o,A+=o,o=0;A=0}}if(u-A>0&&y>u-A){do g.window[u++]=g.window[A++];while(0!==--y)}else g.window.set(g.window.subarray(A,A+y),u),u+=y,A+=y,y=0;break}if(0!==(64&o))return h.msg="invalid distance code",y=h.avail_in-t,y=y>r>>3?r>>3:y,t+=y,s-=y,r-=y<<3,g.bitb=q,g.bitk=r,h.avail_in=t,h.total_in+=s-h.next_in_index,h.next_in_index=s,g.write=u,m;k+=l[B+2],k+=q&p[o],B=3*(n+k),o=l[B]}break}if(0!==(64&o))return 0!==(32&o)?(y=h.avail_in-t,y=y>r>>3?r>>3:y,t+=y,s-=y,r-=y<<3,g.bitb=q,g.bitk=r,h.avail_in=t,h.total_in+=s-h.next_in_index,h.next_in_index=s,g.write=u,j):(h.msg="invalid literal/length code",y=h.avail_in-t,y=y>r>>3?r>>3:y,t+=y,s-=y,r-=y<<3,g.bitb=q,g.bitk=r,h.avail_in=t,h.total_in+=s-h.next_in_index,h.next_in_index=s,g.write=u,m);if(k+=l[B+2],k+=q&p[o],B=3*(n+k),0===(o=l[B])){q>>=l[B+1],r-=l[B+1],g.window[u++]=l[B+2],v--;break}}else q>>=l[B+1],r-=l[B+1],g.window[u++]=l[B+2],v--}while(v>=258&&t>=10);return y=h.avail_in-t,y=y>r>>3?r>>3:y,t+=y,s-=y,r-=y<<3,g.bitb=q,g.bitk=r,h.avail_in=t,h.total_in+=s-h.next_in_index,h.next_in_index=s,g.write=u,i}var b,c,d,e,f=this,g=0,h=0,k=0,n=0,o=0,q=0,r=0,s=0,t=0,u=0;f.init=function(a,f,g,h,i,j){b=C,r=a,s=f,d=g,t=h,e=i,u=j,c=null},f.proc=function(f,v,w){var x,y,z,A,B,M,N,O=0,P=0,Q=0;for(Q=v.next_in_index,A=v.avail_in,O=f.bitb,P=f.bitk,B=f.write,M=B=258&&A>=10&&(f.bitb=O,f.bitk=P,v.avail_in=A,v.total_in+=Q-v.next_in_index,v.next_in_index=Q,f.write=B,w=a(r,s,d,t,e,u,f,v),Q=v.next_in_index,A=v.avail_in,O=f.bitb,P=f.bitk,B=f.write,M=BP;){if(0===A)return f.bitb=O,f.bitk=P,v.avail_in=A,v.total_in+=Q-v.next_in_index,v.next_in_index=Q,f.write=B,f.inflate_flush(v,w);w=i,A--,O|=(255&v.read_byte(Q++))<>>=c[y+1],P-=c[y+1],z=c[y],0===z){n=c[y+2],b=I;break}if(0!==(16&z)){o=15&z,g=c[y+2],b=E;break}if(0===(64&z)){k=z,h=y/3+c[y+2];break}if(0!==(32&z)){b=J;break}return b=L,v.msg="invalid literal/length code",w=m,f.bitb=O,f.bitk=P,v.avail_in=A,v.total_in+=Q-v.next_in_index, diff --git a/dist/offline-tpk-src.js b/dist/offline-tpk-src.js index 1e4ea1e0..06cd3ab9 100644 --- a/dist/offline-tpk-src.js +++ b/dist/offline-tpk-src.js @@ -1,4 +1,4 @@ -/*! offline-editor-js - v2.6.1 - 2015-04-13 +/*! offline-editor-js - v2.7.0 - 2015-04-27 * Copyright (c) 2015 Environmental Systems Research Institute, Inc. * Apache License*/ /** diff --git a/doc/attachments.md b/doc/attachments.md index 2948b4a4..7c2f3953 100644 --- a/doc/attachments.md +++ b/doc/attachments.md @@ -5,36 +5,61 @@ The __offline-edit-min.js__ has support for attachments in offline mode. See [at While your application is in `OFFLINE` mode, you can: * add attachments to any feature, either a feature that already exists in the server or a newly added feature. -* remove attachments from features. It only works for attachments that have been added while offline. +* delete attachments from features if you have pre-cached the attachments or if you have added a feature while offline you can delete it from the local database. * query for attachments of a particular feature. It will only return attachments that have been added while offline. * view the attached files (see __limitations__ below) -* when the app goes to `ONLINE` mode, all attachments are sent back to the server and removed from local browser storage +* when the app goes to `ONLINE` mode, all attachments are sent back to the server and removed from the local database. -##How you do that: -You can either use the ArcGIS FeatureLayer API _(esri.layers.FeatureLayer)_ directly or use the built-in [AttachmentEditor](https://developers.arcgis.com/javascript/jsapi/attachmenteditor-amd.html) widget that support feature attachment editing. Both approaches work well, and the code you write works the same either if you are on `ONLINE` or `OFFLINE` modes. +##How you do use it: +You can either use the ArcGIS FeatureLayer API _(esri.layers.FeatureLayer)_ directly or use the [AttachmentEditor](https://developers.arcgis.com/javascript/jsapi/attachmenteditor-amd.html) widget that supports feature attachment editing. Both approaches work well, and the code you write works the same either if you are on `ONLINE` or `OFFLINE` modes. The only differences in your code are: -* create an offlineFeaturesManager enabled for attachment support: +* create an offlineFeaturesManager enabled for attachment support. Make sure you initialize the attachments database: var offlineFeaturesManager = new esri.OfflineFeaturesManager(); offlineFeaturesManager.initAttachments(); * extend your featureLayers with offline editing functionality: - offlineFeaturesManager.extend(featureLayer, function(success) + offlineFeaturesManager.extend(featureLayer, function(success, error) { console.log("layer extended", success? "success" : "failed"); }); +You can also modified the database's name and object store name. This functionality is typically used for advanced +users that have a requirement to run multiple databases: + + var offlineFeaturesManager = new esri.OfflineFeaturesManager(); + offlineFeaturesManager.ATTACHMENTS_DB_NAME = "attachment-store-two"; + offlineFeaturesManager.ATTACHMENTS_DB_OBJECTSTORE_NAME = "attachments-two"; + + offlineFeaturesManager.initAttachments(); + ###Using the FeatureLayer API -The FeatureLayer API for handling attachments consists primarily of three methods: +The FeatureLayer API for handling attachments consists primarily of four methods. In general you should let `OfflineFeaturesManager` +handle interactions with attachments and it's not recommended to interact with the attachments database directly. * `layer.queryAttachmentInfos(objectId,callback,errback)` [doc](https://developers.arcgis.com/javascript/jsapi/featurelayer.html#queryattachmentinfos) * `layer.addAttachment(objectId, formNode, callback, errback)` [doc](https://developers.arcgis.com/javascript/jsapi/featurelayer.html#addattachment) +* `layer.updateAttachment(objectId, attachmentId, formNode, callback, errback)` - as of April 2015 the ArcGIS API for JavaScript document has this functionality but it's not documented. That should hopefully be fixed in the next release of the JS API. * `layer.deleteAttachments(objectId, attachmentIds, callback, errback)` [doc](https://developers.arcgis.com/javascript/jsapi/featurelayer.html#deleteattachments) -They work the same both in ONLINE and OFFLINE mode. In OFFLINE mode, attachments will be kept in the local browser storage (indexeddb) and sent back to the server when you call `offlineFeaturesManager.goOnline()` +They work the same both in ONLINE and OFFLINE mode. In OFFLINE mode, attachments will be kept in the local database (indexeddb) and sent back to the server when you call `offlineFeaturesManager.goOnline()` + +##Getting database usage +Once a feature layer is extended you can find out how big the database and how many attachments are stored by using the following pattern: + + layer.getAttachmentsUsage(function(usage, error) { + console.log("Size: " + usage.sizeBytes + ", attachmentCount: " + usage.attachmentCount); + }); + +##Resetting the database +Under certain circumstances you may want to force the database to delete everything. + + layer.resetAttachmentsDatabase(function(result, error) { + console.log("Reset succes: " + result); // result is a boolean + }); ###Using the AttachmentEditor widget The [AttachmentEditor](https://developers.arcgis.com/javascript/jsapi/attachmenteditor-amd.html) is not very fancy, but it's easy to work with: @@ -59,5 +84,5 @@ The widget internally uses the FeatureLayer API, and it works well in OFFLINE mo ##Limitations Attachment support in OFFLINE mode has some limitations: -* while in OFFLINE mode, features in a featureLayer don't know whether they have any attachments in the server or any other information about attachments. Therefore queryAttachmentInfos() and deleteAttachments() can't take those attachments into account. Calling queryAttachmentInfos() will only return attachments that are stored in local storage and deleteAttachments() can only remove local attachments. -* in order to see local attachments, the library uses [window.URL.createObjectURL() API](https://developer.mozilla.org/en-US/docs/Web/API/URL.createObjectURL). This API generates an opaque URL that represents the content of the File passed as parameter. This allows the user of the app to see and download attachments that are still in local browser storage as if they were actual files accessible through a URL. However, the lifetime of this URL is tied to the page where it is created. This means that if the user reloads (while still offline) the page after adding some local attachments, then these URLs will be invalid. \ No newline at end of file +* while in OFFLINE mode, features in a featureLayer don't know whether they have any attachments in the server or any other +information about attachments unless you specifically build out that functionality. Therefore queryAttachmentInfos() and deleteAttachments() can't take those attachments into account. Calling queryAttachmentInfos() will only return attachments that are stored in local storage and deleteAttachments() can only remove local attachments. \ No newline at end of file diff --git a/doc/howtouseeditlibrary.md b/doc/howtouseeditlibrary.md index 68632471..a83c6880 100644 --- a/doc/howtouseeditlibrary.md +++ b/doc/howtouseeditlibrary.md @@ -154,7 +154,7 @@ Force the library to return to an online condition. If there are pending edits, ```js function goOnline() { - offlineFeaturesManager.goOnline(function(success,errors) + offlineFeaturesManager.goOnline(function(success,results) { if(success){ //Modify user inteface depending on success/failure @@ -163,6 +163,25 @@ Force the library to return to an online condition. If there are pending edits, } ``` +It's important to note that the `results` object contains all the necessary information about successes and failures that may have occurred during the online resync process. Here is a description of what's inside. The `features.responses` object contains information on features sync. The `attachments.uploadResponses` contain information on attachments sync. And, the `attachments.dbResponses` contains information on whether or not any attachment that was successfully sync'd was deleted from the local database. + +```js + +resultsObject = { + features:{ + success : boolean, + responses : responses + }, + attachments:{ + success : boolean, + uploadResponses : uploadResponses, + dbResponses : dbResponses + } +} + +``` + + ####offlineFeaturesManager.getOnlineStatus() Within your application you can manually check online status and then update your user interface. By using a switch/case statement you can check against three enums that indicate if the library thinks it is offline, online or in the process of reconnecting. diff --git a/doc/offlinefeaturesmanager.md b/doc/offlinefeaturesmanager.md index a6cfab13..e8350a71 100644 --- a/doc/offlinefeaturesmanager.md +++ b/doc/offlinefeaturesmanager.md @@ -13,9 +13,11 @@ Constructor | Description ###Properties Property | Value | Description --- | --- | --- -`DB_NAME` | "features_store" | **New @ v2.5** Sets the database name. You can instantiate multiple databases within the same application by creating seperate instances of OfflineFeaturesManager. -`DB_OBJECTSTORE_NAME` | "features" | **New @ v2.5** Represents an object store that allows access to a set of data in the database. -`DB_UID` | "objectid" | **New @ v2.5 IMPORTANT!** This tells the database what id to use as a unique identifier. This depends on how your feature service was created. ArcGIS Online services may use something different such as `GlobalID`. +`DB_NAME` | "features_store" | Sets the database name. You can instantiate multiple databases within the same application by creating seperate instances of OfflineFeaturesManager. +`DB_OBJECTSTORE_NAME` | "features" | Represents an object store that allows access to a set of data in the database. +`DB_UID` | "objectid" | IMPORTANT!** This tells the database what id to use as a unique identifier. This depends on how your feature service was created. ArcGIS Online services may use something different such as `GlobalID`. +`ATTACHMENTS_DB_NAME` | "attachments_store" | **New @ v2.7** Sets the attachments database name. +`ATTACHMENTS_DB_OBJECTSTORE_NAME` | "attachments" | **New @ v2.7** Sets the attachments database object store name. `proxyPath` | null | Default is `null`. If you are using a Feature Service that is not CORS-enabled then you will need to set this path. `attachmentsStore` | null | Default is `null`. If you are using attachments, this property gives you access to the associated database. @@ -36,9 +38,9 @@ OfflineFeaturesManager provides the following functionality. Methods | Returns | Description --- | --- | --- -`extend(layer,callback,dataStore)`|`callback( boolean, errors )`| **Updated @ v2.5** Overrides a feature layer, by replacing the `applyEdits()` method of the layer. You can use the FeatureLayer as always, but it's behaviour will be enhanced according to the online status of the manager and the capabilities included in this library. `Callback` is related to initialization the library. `dataStore` is an optional Object that contains any information you need when reconsistuting the layer after an offline browser restart. Refer to the [How to use the edit library doc](howtouseeditlibrary.md) for addition information. +`extend(layer,callback,dataStore)`|`callback( boolean, errors )`| Overrides a feature layer, by replacing the `applyEdits()` method of the layer. You can use the FeatureLayer as always, but it's behaviour will be enhanced according to the online status of the manager and the capabilities included in this library. `Callback` is related to initialization the library.

`dataStore` is an optional Object that contains any information you need when reconsistuting the layer after an offline browser restart. Refer to the [How to use the edit library doc](howtouseeditlibrary.md) for addition information. `goOffline()` | nothing | Forces library into an offline state. Any edits applied to extended FeatureLayers during this condition will be stored locally. -`goOnline(callback)` | `callback( boolean, errors )` | Forces library to return to an online state. If there are pending edits, an attempt will be made to sync them with the remote feature server. Callback function will be called when resync process is done. +`goOnline(callback)` | `callback( boolean, results )` | Forces library to return to an online state. If there are pending edits, an attempt will be made to sync them with the remote feature server. Callback function will be called when resync process is done.

Refer to the [How to use the edit library doc](howtouseeditlibrary.md) for addition information on the `results` object. `getOnlineStatus()` | `ONLINE`, `OFFLINE` or `RECONNECTING`| Determines the current state of the manager. Please, note that this library doesn't detect actual browser offline/online condition. You need to use the `offline.min.js` library included in `vendor\offline` directory to detect connection status and connect events to goOffline() and goOnline() methods. See `military-offline.html` sample. `getReadableEdit()` | String | **DEPRECATED** @ v2.5. A string value representing human readable information on pending edits. Use `featureLayer.getAllEditsArray()`. @@ -58,10 +60,10 @@ Application code can subscribe to offlineFeaturesManager events to be notified o Event | Value | Returns | Description --- | --- | --- | --- -`events.EDITS_SENT` | "edits-sent" | nothing | **Updated @ v2.5** When any edit is actually sent to the server while online-only. -`events.EDITS_SENT_ERROR` | "edits-sent-error" | {msg:error} | **New @ v2.5** There was a problem while sending errors to the server. +`events.EDITS_SENT` | "edits-sent" | nothing | When any edit is actually sent to the server while online-only. +`events.EDITS_SENT_ERROR` | "edits-sent-error" | {msg:error} | There was a problem while sending errors to the server. `events.EDITS_ENQUEUED` | "edits-enqueued" | nothing | When an edit is enqueued and not sent to the server. -`events.EDITS_ENQUEUED_ERROR` | "edits-enqueued-error" | {msg:error} | **New @ v2.5** An error occurred while trying to store the edit. In your app it is recommended to verify if the edit is in the database or not. +`events.EDITS_ENQUEUED_ERROR` | "edits-enqueued-error" | {msg:error} | An error occurred while trying to store the edit. In your app it is recommended to verify if the edit is in the database or not. `events.ALL_EDITS_SENT` | "all-edits-sent" | {[addResults] ,[updateResults], [deleteResults]} | After going online and there are no pending edits remaining in the queue. Be sure to also check for `EDITS_SENT_ERROR`. `events.ATTACHMENT_ENQUEUED` | "attachment-enqueued" | nothing | An attachment is in the queue to be sent to the server. `events.ATTACHMENTS_SENT` | "attachments-sent" | nothing | When any attachment is actually sent to the server. @@ -86,19 +88,24 @@ A FeatureLayer that has been extended using OfflineFeaturesManager.extend() will Methods | Returns | Description --- | --- | --- `applyEdits(` `adds, updates, deletes,` `callback, errback)` | `deferred` | applyEdits() method is replaced by this library. It's behaviour depends upon online state of the manager. You need to pass the same arguments as to the original applyEdits() method and it returns a deferred object, that will be resolved in the same way as the original, as well as the callbacks will be called under the same conditions. This method looks the same as the original to calling code, the only difference is internal. Listen for `EDITS_ENQUEUED` or `EDITS_ENQUEUED_ERROR`. +`addAttachment( objectId, formNode,` `callback,errback)` | `deferred` | Adds a single attachment. +`updateAttachment( objectId, attachmentId,` `formNode, callback, errback)` | `deferred` | **New @ v2.7** Updates an existing attachment. +`deleteAttachments( objectId, attachmentsIds,` `callback, errback)`| `deferred` | Deletes existing attachments as well as attachments that were created while offline. +`getAttachmentsUsage(callback)` | `callback(usageObject,error)` | **New @ v2.7** Returns the approximate size of the attachments database. The usage Object is {sizeBytes: number, attachmentCount: number}. +`resetAttachmentsDatabase( callback)` | `callback(boolean, error)` | **New @ v2.7** Resets the entire attachments database -- use with **caution**. `convertGraphicLayerToJSON(` `features, updateEndEvent, callback)` | `callback( featureJSON, layerDefJSON)` | Not really needed @ v2.5 when you can store the entire feature layer's JSON using the `dataStore` property in the `OfflineFeatureManager` contructor. Used with offline browser restarts. In order to reconstitute the feature layer and map you'll need to store the featureJSON and layerDefJSON in local storage and then it read back upon an offline restart. The `updateEndEvent` is the Feature Layer's `update-end` event object. The appcache-features.html sample demonstrates this pattern. -`getFeatureDefinition(` `featureLayer, featuresArr` `geometryType, callback)` | Object | Used with offline browser restarts. Not really needed @ v2.5 when you can store the entire feature layer's JSON using the `dataStore` property in the `OfflineFeatureManager` contructor. Pass it a FeatureLayer instance, an array of features and specify the Esri geometry type. It will return a FeatureLayer Definition object that can be used to reconstitute a Feature Layer from scratch. The appcache-features.html sample demonstrates this pattern. Go here for more info on the ArcGIS REST API [layerDefinition](http://resources.arcgis.com/en/help/arcgis-rest-api/index.html#//02r30000004v000000), and [Layer](http://resources.arcgis.com/en/help/arcgis-rest-api/index.html#/Layer/02r30000004q000000/). +`getFeatureDefinition(` `featureLayer, featuresArr` `geometryType, callback)` | `Object` | Used with offline browser restarts. Not really needed @ v2.5 when you can store the entire feature layer's JSON using the `dataStore` property in the `OfflineFeatureManager` contructor. Pass it a FeatureLayer instance, an array of features and specify the Esri geometry type. It will return a FeatureLayer Definition object that can be used to reconstitute a Feature Layer from scratch. The appcache-features.html sample demonstrates this pattern. Go here for more info on the ArcGIS REST API [layerDefinition](http://resources.arcgis.com/en/help/arcgis-rest-api/index.html#//02r30000004v000000), and [Layer](http://resources.arcgis.com/en/help/arcgis-rest-api/index.html#/Layer/02r30000004q000000/). `setPhantomLayerGraphics( graphicsArray) ` | nothing | Used with offline browser restarts. Adds the graphics in the `graphicsArray` to the internal phantom graphics layer. This layer is designed to indicate to the user any graphic that has been modified while offline. The appcache-features.html sample demonstrates this pattern. `getPhantomLayerGraphics( callback) ` | `callback( graphicsLayerJSON)` | Used with offline browser restarts. Returns a JSON representation of the internal phantom graphics layer. This layer is designed to indicate to the user any graphic that has been modified while offline. The appcache-features.html sample demonstrates this pattern. -`resetDatabase(callback)` | `callback( boolean, error)` | **New @ v2.5** Full database reset -- use with **caution**. If some edits weren't successfully sent, then the record will still exist in the database. If you use this function then those pending records will also be deleted. -`pendingEditsCount(callback)` | `callback( int )` | **New @ v2.5** Returns the number of pending edits in the database. -`getUsage(callback)`| `callback({usage}, error)` | **New @ v2.5** Returns the approximate size of the database in bytes. the usage Object is {sizeBytes: number, editCount: number}. -`getPhantomGraphicsArray( callback)` | `callback(boolean, array)` | **New @ v2.5** Used with offline browser restarts. Returns an array of phantom graphics from the database. -`getAllEditsArray(callback)` | `callback(boolean, array)` | **New @ v2.5** Returns an array of all edits stored in the database. Each item in array is an object that contains: {"id":"internalID", "operation":"add, update, delete","layer":"layerURL","type":"esriGeometryType","graphic":"esri.Graphic JSON"} -`getFeatureLayerJSON(url,callback)` | `callback( boolean, JSON )` | **New @ v2.5.** Helper function that retrieves the feature layer's JSON using `f=json` parameter. -`setFeatureLayerJSONDataStore( jsonObject, callback)` | `callback( boolean, error)` | **New @ v2.5** Sets the optional feature layer storage object. Can be used instead of the `OfflineFeatureManager` constructor's `dataStore` property or to update it. `jsonObject` can be any Object. However, they key name `id` is reserved. This data store object is used for full offline browser restarts. -`getFeatureLayerJSONDataStore(callback)` | `callback( true, object )` or `callback( false, errorString)` | **New @ v2.5** Retrieves the optional feature layer storage object. This data store object is used for full offline browser restarts. -`convertFeatureGraphicsToJSON(` `[features],callback)` | `callback( jsonString )` | **New @ v2.5.** Helper function that converts an array of feature layer graphics to a JSON string. +`resetDatabase(callback)` | `callback( boolean, error)` | Full edits database reset -- use with **caution**. If some edits weren't successfully sent, then the record will still exist in the database. If you use this function then those pending records will also be deleted. +`pendingEditsCount(callback)` | `callback( int )` | Returns the number of pending edits in the database. +`getUsage(callback)`| `callback({usage}, error)` | Returns the approximate size of the edits database in bytes. The usage Object is {sizeBytes: number, editCount: number}. +`getPhantomGraphicsArray( callback)` | `callback(boolean, array)` | Used with offline browser restarts. Returns an array of phantom graphics from the database. +`getAllEditsArray(callback)` | `callback(boolean, array)` | Returns an array of all edits stored in the database. Each item in array is an object that contains: {"id":"internalID", "operation":"add, update, delete","layer":"layerURL","type":"esriGeometryType","graphic":"esri.Graphic JSON"} +`getFeatureLayerJSON(url,callback)` | `callback( boolean, JSON )` | Helper function that retrieves the feature layer's JSON using `f=json` parameter. +`setFeatureLayerJSONDataStore( jsonObject, callback)` | `callback( boolean, error)` | Sets the optional feature layer storage object. Can be used instead of the `OfflineFeatureManager` constructor's `dataStore` property or to update it. `jsonObject` can be any Object. However, they key name `id` is reserved. This data store object is used for full offline browser restarts. +`getFeatureLayerJSONDataStore(callback)` | `callback( true, object )` or `callback( false, errorString)` | Retrieves the optional feature layer storage object. This data store object is used for full offline browser restarts. +`convertFeatureGraphicsToJSON(` `[features],callback)` | `callback( jsonString )` | Helper function that converts an array of feature layer graphics to a JSON string. ##O.esri.Edit.EditStore @@ -123,29 +130,29 @@ Property | Value | Description Property | Value | Description --- | --- | --- -`dbName` | "features_store" | **New @ v2.5.** Defines the database name. You can have multiple databases within the same application. -`objectStoreName` | "features" | **New @ v2.5.** Represents an object store that allows access to a set of data in the IndexedDB database, looked up via primary key. +`dbName` | "features_store" | Defines the database name. You can have multiple databases within the same application. +`objectStoreName` | "features" | Represents an object store that allows access to a set of data in the IndexedDB database, looked up via primary key. ###Public Methods Methods | Returns | Description --- | --- | --- `isSupported()` | boolean | Determines if local storage is available. If it is not available then the storage cache will not work. It's a best practice to verify this before attempting to write to the local cache. `pushEdit(` `operation, layer, graphic, callback)` | `callback(` `true, edit)` or `callback(` `false, message)`| Pushes an edit into storage. Operation is the corresponding enum. Layer is a reference to the feature layer, and the graphic is the graphic object associated with the edit. -`resetEditsQueue(callback)` | `callback( boolean, error)` | **Updated @ v2.5.** Use with **caution**, initiates a complete database reset. If some edits weren't sent when your app goes online, then you will delete those records as well. -`pendingEditsCount( callback )` | `callback( int )` | **Updated @ v2.5.** The total number of edits that are queued in the database. -`getAllEditsArray( callback)` | `callback()` | **New @ v2.5.** Returns all edits in an iterable array. -`getFeatureLayerJSON( callback)` | `callback( boolean, Object)` | **New @ v2.5.** Returns the feature layer JSON object. -`deleteFeatureLayerJSON( callback)` | `callback( boolean, {message:String)` | **New @ v2.5.** Delete the feature layer JSON object from the database. -`pushFeatureLayerJSON( dataObject, callback)` | `callback( boolean, error)` | **New @ v2.5.** Use this to store any static FeatureLayer or related JSON data related to your app that will assist in restoring the layer after an offline restart. Supports adds and updates, will not overwrite entire object. -`getUsage( callback)` | `callback( int, errorString)` | **New @ v2.5.** Returns the approximate size of the database in bytes. -`hasPendingEdits()` | boolean | **Deprecated @ v2.5.** Determines if there are any queued edits in the local cache. Use `pendingEditsCount()` instead. -`retrieveEditsQueue()` | Array | **Deprecated @ v2.5.** Returns an array of all pending edits. -`getEditsStoreSizeBytes()` | Number | **Deprecated @ v2.5.** Returns the total size of all pending edits in bytes. Use `getUsage()` instead. -`getLocalStorageSizeBytes()` | Number | **Deprecated @ v2.5.** Returns the total size in bytes of all items for local storage cached using the current domain name. Use `getUsage()` instead. +`resetEditsQueue(callback)` | `callback( boolean, error)` | Use with **caution**, initiates a complete database reset. If some edits weren't sent when your app goes online, then you will delete those records as well. +`pendingEditsCount( callback )` | `callback( int )` | The total number of edits that are queued in the database. +`getAllEditsArray( callback)` | `callback()` | Returns all edits in an iterable array. +`getFeatureLayerJSON( callback)` | `callback( boolean, Object)` | Returns the feature layer JSON object. +`deleteFeatureLayerJSON( callback)` | `callback( boolean, {message:String)` | Delete the feature layer JSON object from the database. +`pushFeatureLayerJSON( dataObject, callback)` | `callback( boolean, error)` | Use this to store any static FeatureLayer or related JSON data related to your app that will assist in restoring the layer after an offline restart. Supports adds and updates, will not overwrite entire object. +`getUsage( callback)` | `callback( int, errorString)` | Returns the approximate size of the database in bytes. +`hasPendingEdits()` | boolean | Determines if there are any queued edits in the local cache. Use `pendingEditsCount()` instead. +`retrieveEditsQueue()` | Array | Returns an array of all pending edits. +`getEditsStoreSizeBytes()` | Number | Returns the total size of all pending edits in bytes. Use `getUsage()` instead. +`getLocalStorageSizeBytes()` | Number | Returns the total size in bytes of all items for local storage cached using the current domain name. Use `getUsage()` instead. ##O.esri.Edit.AttachmentsStore -Provides a number of public methods that are used by `OfflineFeaturesManager` library for storing attachments in the browser. Instiantiate this library using a `new` statement. Instiantiate this library using a `new` statement. +Provides a number of public methods that are used by `OfflineFeaturesManager` library for storing attachments in the browser. Instiantiate this library using a `new` statement. Instiantiate this library using a `new` statement. In general, you shouldn't be adding, updating or deleting data directly. You should be using functionality extended thru the feature layer. ###Constructor Constructor | Description @@ -156,8 +163,9 @@ Constructor | Description Property | Value | Description --- | --- | --- -`DB_NAME` | "attachments_store" | Represents a FeatureLayer.add() operation. -`OBJECT_STORE_NAME` | "attachments" | Represents a FeatureLayer.update() operation. +`dbName` | "attachments_store" | **Updated @ v2.7** Represents a FeatureLayer.add() operation. +`objectStoreName` | "attachments" | **Updated @ v2.7** Represents a FeatureLayer.update() operation. +`TYPE` | "ADD", "UPDATE" or "DELETE" | **New @ v2.7** Specifies the type of operation against an attachment. ###Public Methods Methods | Returns | Description diff --git a/lib/edit/attachmentsStore.js b/lib/edit/attachmentsStore.js index 69e69388..0e99a07c 100644 --- a/lib/edit/attachmentsStore.js +++ b/lib/edit/attachmentsStore.js @@ -5,8 +5,14 @@ O.esri.Edit.AttachmentsStore = function () { this._db = null; - var DB_NAME = "attachments_store"; - var OBJECT_STORE_NAME = "attachments"; + this.dbName = "attachments_store"; + this.objectStoreName = "attachments"; + + this.TYPE = { + "ADD" : "add", + "UPDATE" : "update", + "DELETE" : "delete" + }; this.isSupported = function () { if (!window.indexedDB) { @@ -15,40 +21,70 @@ O.esri.Edit.AttachmentsStore = function () { return true; }; - this.store = function (featureLayerUrl, attachmentId, objectId, attachmentFile, callback) { + /** + * Stores an attachment in the database. + * In theory, this abides by the query-attachment-infos-complete Object which can be found here: + * https://developers.arcgis.com/javascript/jsapi/featurelayer-amd.html#event-query-attachment-infos-complete + * @param featureLayerUrl + * @param attachmentId The temporary or actual attachmentId issued by the feature service + * @param objectId The actual ObjectId issues by the feature service + * @param attachmentFile + * @param type Type of operation: "add", "update" or "delete" + * @param callback + */ + this.store = function (featureLayerUrl, attachmentId, objectId, attachmentFile, type, callback) { try { - // first of all, read file content - this._readFile(attachmentFile, function (fileContent) { - // now, store it in the db - var newAttachment = - { - id: attachmentId, - objectId: objectId, - featureId: featureLayerUrl + "/" + objectId, - contentType: attachmentFile.type, - name: attachmentFile.name, - size: attachmentFile.size, - url: this._createLocalURL(attachmentFile), - content: fileContent - }; - - var transaction = this._db.transaction([OBJECT_STORE_NAME], "readwrite"); - - transaction.oncomplete = function (event) { - callback(true, newAttachment); - }; - - transaction.onerror = function (event) { - callback(false, event.target.error.message); - }; - - var objectStore = transaction.objectStore(OBJECT_STORE_NAME); - var request = objectStore.put(newAttachment); - request.onsuccess = function (event) { - //console.log("item added to db " + event.target.result); - }; - - }.bind(this)); + // Avoid allowing the wrong type to be stored + if(type == this.TYPE.ADD || type == this.TYPE.UPDATE || type == this.TYPE.DELETE) { + + // first of all, read file content + this._readFile(attachmentFile, function (success, fileContent) { + + if (success) { + // now, store it in the db + var newAttachment = + { + id: attachmentId, + objectId: objectId, + type: type, + + // Unique ID - don't use the ObjectId + // multiple features services could have an a feature with the same ObjectId + featureId: featureLayerUrl + "/" + objectId, + contentType: attachmentFile.type, + name: attachmentFile.name, + size: attachmentFile.size, + featureLayerUrl: featureLayerUrl, + content: fileContent, + file: attachmentFile + }; + + var transaction = this._db.transaction([this.objectStoreName], "readwrite"); + + transaction.oncomplete = function (event) { + callback(true, newAttachment); + }; + + transaction.onerror = function (event) { + callback(false, event.target.error.message); + }; + + var objectStore = transaction.objectStore(this.objectStoreName); + var request = objectStore.put(newAttachment); + request.onsuccess = function (event) { + //console.log("item added to db " + event.target.result); + }; + + } + else { + callback(false, fileContent); + } + }.bind(this)); + } + else{ + console.error("attachmentsStore.store() Invalid type in the constructor!"); + callback(false,"attachmentsStore.store() Invalid type in the constructor!"); + } } catch (err) { console.log("AttachmentsStore: " + err.stack); @@ -59,7 +95,7 @@ O.esri.Edit.AttachmentsStore = function () { this.retrieve = function (attachmentId, callback) { console.assert(this._db !== null, "indexeddb not initialized"); - var objectStore = this._db.transaction([OBJECT_STORE_NAME]).objectStore(OBJECT_STORE_NAME); + var objectStore = this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName); var request = objectStore.get(attachmentId); request.onsuccess = function (event) { var result = event.target.result; @@ -82,7 +118,7 @@ O.esri.Edit.AttachmentsStore = function () { var featureId = featureLayerUrl + "/" + objectId; var attachments = []; - var objectStore = this._db.transaction([OBJECT_STORE_NAME]).objectStore(OBJECT_STORE_NAME); + var objectStore = this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName); var index = objectStore.index("featureId"); var keyRange = IDBKeyRange.only(featureId); index.openCursor(keyRange).onsuccess = function (evt) { @@ -102,9 +138,9 @@ O.esri.Edit.AttachmentsStore = function () { var attachments = []; - var objectStore = this._db.transaction([OBJECT_STORE_NAME]).objectStore(OBJECT_STORE_NAME); - var index = objectStore.index("featureId"); - var keyRange = IDBKeyRange.bound(featureLayerUrl + "/", featureLayerUrl + "/A"); + var objectStore = this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName); + var index = objectStore.index("featureLayerUrl"); + var keyRange = IDBKeyRange.only(featureLayerUrl); index.openCursor(keyRange).onsuccess = function (evt) { var cursor = evt.target.result; if (cursor) { @@ -122,7 +158,7 @@ O.esri.Edit.AttachmentsStore = function () { var attachments = []; - var objectStore = this._db.transaction([OBJECT_STORE_NAME]).objectStore(OBJECT_STORE_NAME); + var objectStore = this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName); objectStore.openCursor().onsuccess = function (evt) { var cursor = evt.target.result; if (cursor) { @@ -140,15 +176,15 @@ O.esri.Edit.AttachmentsStore = function () { var featureId = featureLayerUrl + "/" + objectId; - var objectStore = this._db.transaction([OBJECT_STORE_NAME], "readwrite").objectStore(OBJECT_STORE_NAME); + var objectStore = this._db.transaction([this.objectStoreName], "readwrite").objectStore(this.objectStoreName); var index = objectStore.index("featureId"); var keyRange = IDBKeyRange.only(featureId); var deletedCount = 0; index.openCursor(keyRange).onsuccess = function (evt) { var cursor = evt.target.result; if (cursor) { - var attachment = cursor.value; - this._revokeLocalURL(attachment); + //var attachment = cursor.value; + //this._revokeLocalURL(attachment); objectStore.delete(cursor.primaryKey); deletedCount++; cursor.continue(); @@ -172,10 +208,10 @@ O.esri.Edit.AttachmentsStore = function () { return; } - this._revokeLocalURL(attachment); + //this._revokeLocalURL(attachment); - var request = this._db.transaction([OBJECT_STORE_NAME], "readwrite") - .objectStore(OBJECT_STORE_NAME) + var request = this._db.transaction([this.objectStoreName], "readwrite") + .objectStore(this.objectStoreName) .delete(attachmentId); request.onsuccess = function (event) { setTimeout(function () { @@ -192,12 +228,12 @@ O.esri.Edit.AttachmentsStore = function () { console.assert(this._db !== null, "indexeddb not initialized"); this.getAllAttachments(function (attachments) { - attachments.forEach(function (attachment) { - this._revokeLocalURL(attachment); - }, this); + //attachments.forEach(function (attachment) { + // this._revokeLocalURL(attachment); + //}, this); - var request = this._db.transaction([OBJECT_STORE_NAME], "readwrite") - .objectStore(OBJECT_STORE_NAME) + var request = this._db.transaction([this.objectStoreName], "readwrite") + .objectStore(this.objectStoreName) .clear(); request.onsuccess = function (event) { setTimeout(function () { @@ -215,7 +251,7 @@ O.esri.Edit.AttachmentsStore = function () { var featureId = featureLayerUrl + "/" + oldId; - var objectStore = this._db.transaction([OBJECT_STORE_NAME], "readwrite").objectStore(OBJECT_STORE_NAME); + var objectStore = this._db.transaction([this.objectStoreName], "readwrite").objectStore(this.objectStoreName); var index = objectStore.index("featureId"); var keyRange = IDBKeyRange.only(featureId); var replacedCount = 0; @@ -244,8 +280,8 @@ O.esri.Edit.AttachmentsStore = function () { var usage = {sizeBytes: 0, attachmentCount: 0}; - var transaction = this._db.transaction([OBJECT_STORE_NAME]) - .objectStore(OBJECT_STORE_NAME) + var transaction = this._db.transaction([this.objectStoreName]) + .objectStore(this.objectStoreName) .openCursor(); console.log("dumping keys"); @@ -269,28 +305,55 @@ O.esri.Edit.AttachmentsStore = function () { }; }; + /** + * Full attachments database reset. + * CAUTION! If some attachments weren't successfully sent, then their record + * will still exist in the database. If you use this function you + * will also delete those records. + * @param callback boolean + */ + this.resetAttachmentsQueue = function (callback) { + console.assert(this._db !== null, "indexeddb not initialized"); + + var request = this._db.transaction([this.objectStoreName], "readwrite") + .objectStore(this.objectStoreName) + .clear(); + request.onsuccess = function (event) { + setTimeout(function () { + callback(true); + }, 0); + }; + request.onerror = function (err) { + callback(false, err); + }; + }; + // internal methods this._readFile = function (attachmentFile, callback) { var reader = new FileReader(); reader.onload = function (evt) { - callback(evt.target.result); + callback(true,evt.target.result); + }; + reader.onerror = function (evt) { + callback(false,evt.target.result); }; reader.readAsBinaryString(attachmentFile); }; - this._createLocalURL = function (attachmentFile) { - return window.URL.createObjectURL(attachmentFile); - }; + // Deprecated @ v2.7 + //this._createLocalURL = function (attachmentFile) { + // return window.URL.createObjectURL(attachmentFile); + //}; - this._revokeLocalURL = function (attachment) { - window.URL.revokeObjectURL(attachment.url); - }; + //this._revokeLocalURL = function (attachment) { + // window.URL.revokeObjectURL(attachment.url); + //}; this.init = function (callback) { console.log("init AttachmentStore"); - var request = indexedDB.open(DB_NAME, 11); + var request = indexedDB.open(this.dbName, 12); callback = callback || function (success) { console.log("AttachmentsStore::init() success:", success); }.bind(this); @@ -303,12 +366,13 @@ O.esri.Edit.AttachmentsStore = function () { request.onupgradeneeded = function (event) { var db = event.target.result; - if (db.objectStoreNames.contains(OBJECT_STORE_NAME)) { - db.deleteObjectStore(OBJECT_STORE_NAME); + if (db.objectStoreNames.contains(this.objectStoreName)) { + db.deleteObjectStore(this.objectStoreName); } - var objectStore = db.createObjectStore(OBJECT_STORE_NAME, {keyPath: "id"}); + var objectStore = db.createObjectStore(this.objectStoreName, {keyPath: "id"}); objectStore.createIndex("featureId", "featureId", {unique: false}); + objectStore.createIndex("featureLayerUrl", "featureLayerUrl", {unique: false}); }.bind(this); request.onsuccess = function (event) { diff --git a/lib/edit/offlineFeaturesManager.js b/lib/edit/offlineFeaturesManager.js index 97e8db55..4e7ce28f 100644 --- a/lib/edit/offlineFeaturesManager.js +++ b/lib/edit/offlineFeaturesManager.js @@ -35,6 +35,13 @@ define([ DB_OBJECTSTORE_NAME: "features",// Represents an object store that allows access to a set of data in the IndexedDB database DB_UID: "objectid", // Set this based on the unique identifier is set up in the feature service + ATTACHMENTS_DB_NAME: "attachments_store", //Sets attachments database name + ATTACHMENTS_DB_OBJECTSTORE_NAME: "attachments", + // NOTE: attachments don't have the same issues as Graphics as related to UIDs. + // You can manually create a graphic, but it would be very rare for someone to + // manually create an attachment. So, we don't provide a public property for + // the attachments database UID. + // manager emits event when... events: { EDITS_SENT: "edits-sent", // ...whenever any edit is actually sent to the server @@ -63,6 +70,8 @@ define([ try { this.attachmentsStore = new O.esri.Edit.AttachmentsStore(); + this.attachmentsStore.dbName = this.ATTACHMENTS_DB_NAME; + this.attachmentsStore.objectStoreName = this.ATTACHMENTS_DB_OBJECTSTORE_NAME; if (/*false &&*/ this.attachmentsStore.isSupported()) { this.attachmentsStore.init(callback); @@ -78,8 +87,8 @@ define([ /** * Overrides a feature layer. Call this AFTER the FeatureLayer's 'update-end' event. - * IMPORTANT: If options are specified they will be saved to the database. Any complex - * objects such as [esri.Graphic] will need to be serialized or you will get an error. + * IMPORTANT: If dataStore is specified it will be saved to the database. Any complex + * objects such as [esri.Graphic] will need to be serialized or you will get an IndexedDB error. * @param layer * @param updateEndEvent The FeatureLayer's update-end event object * @param callback {true, null} or {false, errorString} Traps whether or not the database initialized @@ -112,6 +121,7 @@ define([ layer._addAttachment = layer.addAttachment; layer._queryAttachmentInfos = layer.queryAttachmentInfos; layer._deleteAttachments = layer.deleteAttachments; + layer._updateAttachment = layer.updateAttachment; /* operations supported offline: @@ -163,6 +173,7 @@ define([ }; layer.addAttachment = function (objectId, formNode, callback, errback) { + if (self.getOnlineStatus() === self.ONLINE) { return this._addAttachment(objectId, formNode, function () { @@ -178,7 +189,7 @@ define([ } if (!self.attachmentsStore) { - console.log("in order to support attachments you need to call initAttachments() method of offlineFeaturesManager"); + console.error("in order to support attachments you need to call initAttachments() method of offlineFeaturesManager"); return; } @@ -187,7 +198,7 @@ define([ var deferred = new Deferred(); var attachmentId = this._getNextTempId(); - self.attachmentsStore.store(this.url, attachmentId, objectId, file, function (success, newAttachment) { + self.attachmentsStore.store(this.url, attachmentId, objectId, file,self.attachmentsStore.TYPE.ADD, function (success, newAttachment) { var returnValue = {attachmentId: attachmentId, objectId: objectId, success: success}; if (success) { self.emit(self.events.ATTACHMENT_ENQUEUED, returnValue); @@ -209,6 +220,51 @@ define([ return deferred; }; + layer.updateAttachment = function(objectId, attachmentId, formNode, callback, errback) { + if (self.getOnlineStatus() === self.ONLINE) { + return this._updateAttachment(objectId, attachmentId, formNode, + function () { + callback && callback.apply(this, arguments); + }, + function (err) { + console.log("updateAttachment: " + err); + errback && errback.apply(this, arguments); + }); + //return def; + } + + if (!self.attachmentsStore) { + console.error("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(); + + self.attachmentsStore.store(this.url, attachmentId, objectId, file, self.attachmentsStore.TYPE.UPDATE, 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 = "layer.updateAttachment::attachmentStore 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, @@ -229,22 +285,42 @@ define([ // 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); - }); + + // IMPORTANT: If attachmentId < 0 then it's a local/new attachment + // and we can simply delete it from the attachmentsStore. + // However, if the attachmentId > 0 then we need to store the DELETE + // so that it can be processed and sync'd correctly during _uploadAttachments(). + if(attachmentId < 0) { + self.attachmentsStore.delete(attachmentId, function (success) { + var result = {objectId: objectId, attachmentId: attachmentId, success: success}; + deferred.resolve(result); + }); + } + else { + var dummyBlob = new Blob([],{type: "image/png"}); //TO-DO just a placeholder. Need to consider add a null check. + self.attachmentsStore.store(this.url, attachmentId, objectId, dummyBlob,self.attachmentsStore.TYPE.DELETE, function (success, newAttachment) { + var returnValue = {attachmentId: attachmentId, objectId: objectId, success: success}; + if (success) { + deferred.resolve(returnValue); + } + else { + deferred.reject(returnValue); + } + }.bind(this)); + } + //console.assert(attachmentId < 0, "we only support deleting local attachments"); promises.push(deferred); }, this); // call callback once all deletes have finished + // IMPORTANT: This returns an array!!! var allPromises = all(promises); allPromises.then(function (results) { callback && callback(results); @@ -540,7 +616,30 @@ define([ }; /** - * Returns the approximate size of the database in bytes + * Returns the approximate size of the attachments database in bytes + * @param callback callback({usage}, error) Whereas, the usage Object is {sizeBytes: number, attachmentCount: number} + */ + layer.getAttachmentsUsage = function(callback) { + self.attachmentsStore.getUsage(function(usage,error){ + callback(usage,error); + }); + }; + + /** + * Full attachments database reset. + * CAUTION! If some attachments weren't successfully sent, then their record + * will still exist in the database. If you use this function you + * will also delete those records. + * @param callback (boolean, error) + */ + layer.resetAttachmentsDatabase = function(callback){ + self.attachmentsStore.resetAttachmentsQueue(function(result,error){ + callback(result,error); + }); + }; + + /** + * Returns the approximate size of the edits database in bytes * @param callback callback({usage}, error) Whereas, the usage Object is {sizeBytes: number, editCount: number} */ layer.getUsage = function(callback){ @@ -550,7 +649,7 @@ define([ }; /** - * Full database reset. + * Full edits database reset. * CAUTION! If some edits weren't successfully sent, then their record * will still exist in the database. If you use this function you * will also delete those records. @@ -951,11 +1050,12 @@ define([ this._onlineStatus = this.RECONNECTING; this._replayStoredEdits(function (success, responses) { var result = {features: {success: success, responses: responses}}; + this._onlineStatus = this.ONLINE; if (this.attachmentsStore != null) { console.log("sending attachments"); - this._sendStoredAttachments(function (success, responses) { + this._sendStoredAttachments(function (success, uploadedResponses, dbResponses) { this._onlineStatus = this.ONLINE; - result.attachments = {success: success, responses: responses}; + result.attachments = {success: success, responses: uploadedResponses, dbResponses: dbResponses}; callback && callback(result); }.bind(this)); } @@ -1018,10 +1118,10 @@ define([ // Added @ v2.5 // // Configure database for offline restart - // Options object allows you to store data that you'll + // dataStore object allows you to store data that you'll // use after an offline browser restart. // - // If options Object is not defined then do nothing. + // If dataStore Object is not defined then do nothing. // //////////////////////////////////////////////////// @@ -1169,83 +1269,171 @@ define([ _uploadAttachment: function (attachment) { var dfd = new Deferred(); - var segments = []; - segments.push(this._fieldSegment("f", "json")); - segments.push(this._fileSegment("attachment", attachment.name, attachment.contentType, attachment.content)); - - var oAjaxReq = new XMLHttpRequest(); + var layer = this._featureLayers[attachment.featureLayerUrl]; - // surprisingly, sometimes the oAjaxReq object doesn't have the sendAsBinary() method, even if we added it to the XMLHttpRequest.prototype - if (!oAjaxReq.sendAsBinary) { - this._extendAjaxReq(oAjaxReq); - } + var formData = new FormData(); + formData.append("attachment",attachment.file); - oAjaxReq.onload = function (result) { - dfd.resolve(JSON.parse(result.target.response)); - }; - oAjaxReq.onerror = function (err) { - dfd.reject(err); - }; + switch(attachment.type){ + case this.attachmentsStore.TYPE.ADD: + layer.addAttachment(attachment.objectId,formData,function(evt){ + dfd.resolve({attachmentResult:evt,id:attachment.id}); + },function(err){ + dfd.reject(err); + }); + break; + case this.attachmentsStore.TYPE.UPDATE: + formData.append("attachmentId", attachment.id); + + // NOTE: + // We need to handle updates different from ADDS and DELETES because of how the JS API + // parses the DOM formNode property. + layer._sendAttachment("update",/* objectid */attachment.objectId, formData,function(evt){ + dfd.resolve({attachmentResult:evt,id:attachment.id}); + },function(err){ + dfd.reject(err); + }); - // IMPORTANT! - // Proxy path can be set to null if feature service is CORS enabled - // Refer to "Using the Proxy Page" for more information: https://developers.arcgis.com/en/javascript/jshelp/ags_proxy.html - var proxy = this.proxyPath || esriConfig.defaults.io.proxyUrl || ""; - if (proxy !== "") { - proxy += "?"; + break; + case this.attachmentsStore.TYPE.DELETE: + // IMPORTANT: This method returns attachmentResult as an Array. Whereas ADD and UPDATE do not!! + layer.deleteAttachments(attachment.objectId,[attachment.id],function(evt){ + dfd.resolve({attachmentResult:evt,id:attachment.id}); + },function(err){ + dfd.reject(err); + }); + break; } - console.log("proxy:", proxy); - oAjaxReq.open("post", proxy + attachment.featureId + "/addAttachment", true); - var sBoundary = "---------------------------" + Date.now().toString(16); - oAjaxReq.setRequestHeader("Content-Type", "multipart\/form-data; boundary=" + sBoundary); - oAjaxReq.sendAsBinary("--" + sBoundary + "\r\n" + segments.join("--" + sBoundary + "\r\n") + "--" + sBoundary + "--\r\n"); - return dfd; + return dfd.promise; }, - _deleteAttachment: function (attachmentId, uploadResult) { + _deleteAttachmentFromDB: function (attachmentId, uploadResult) { var dfd = new Deferred(); console.log("upload complete", uploadResult, attachmentId); this.attachmentsStore.delete(attachmentId, function (success) { console.assert(success === true, "can't delete attachment already uploaded"); console.log("delete complete", success); - dfd.resolve(uploadResult); + dfd.resolve({success:success,result:uploadResult}); }); return dfd; }, + /** + * Removes attachments from DB if they were successfully uploaded + * @param results promises.results + * @callback callback callback( {errors: boolean, attachmentsDBResults: results, uploadResults: results} ) + * @private + */ + _cleanAttachmentsDB: function(results,callback){ + + var self = this; + var promises = []; + var count = 0; + + results.forEach(function(value){ + + if(typeof value.attachmentResult == "object" && value.attachmentResult.success){ + // Delete an attachment from the database if it was successfully + // submitted to the server. + promises.push(self._deleteAttachmentFromDB(value.id,null)); + } + // NOTE: layer.deleteAttachments returns an array rather than an object + else if(value.attachmentResult instanceof Array){ + + // Because we get an array we have to cycle thru it to verify all results + value.attachmentResult.forEach(function(deleteValue){ + if(deleteValue.success){ + // Delete an attachment from the database if it was successfully + // submitted to the server. + promises.push(self._deleteAttachmentFromDB(value.id,null)); + } + else { + count++; + } + }); + } + else{ + // Do nothing. Don't delete attachments from DB if we can't upload them + count++; + } + }); + + var allPromises = all(promises); + allPromises.then(function(dbResults){ + if(count > 0){ + // If count is greater than zero then we have errors and need to set errors to true + callback({errors: true, attachmentsDBResults: dbResults, uploadResults: results}); + } + else{ + callback({errors: false, attachmentsDBResults: dbResults, uploadResults: results}); + } + }); + }, + + /** + * Attempts to upload stored attachments when the library goes back on line. + * @param callback callback({success: boolean, uploadResults: results, dbResults: results}) + * @private + */ _sendStoredAttachments: function (callback) { this.attachmentsStore.getAllAttachments(function (attachments) { + + var self = this; + console.log("we have", attachments.length, "attachments to upload"); var promises = []; attachments.forEach(function (attachment) { console.log("sending attachment", attachment.id, "to feature", attachment.featureId); - var deleteCompleted = - this._uploadAttachment(attachment) - .then(function (uploadResult) { - if (uploadResult.addAttachmentResult && uploadResult.addAttachmentResult.success === true) { - console.log("upload success", uploadResult.addAttachmentResult.success); - return this._deleteAttachment(attachment.id, uploadResult); - } - else { - console.log("upload failed", uploadResult); - return null; - } - }.bind(this), - function (err) { - console.log("failed uploading attachment", attachment); - } - ); - promises.push(deleteCompleted); + + var uploadAttachmentComplete = + this._uploadAttachment(attachment); + //.then(function (uploadResult) { + // if (uploadResult.addAttachmentResult && uploadResult.addAttachmentResult.success === true) { + // console.log("upload success", uploadResult.addAttachmentResult.success); + // return this._deleteAttachment(attachment.id, uploadResult); + // } + // else { + // console.log("upload failed", uploadResult); + // return null; + // } + //}.bind(this), + //function (err) { + // console.log("failed uploading attachment", attachment); + // return null; + //} + //); + promises.push(uploadAttachmentComplete); }, this); console.log("promises", promises.length); var allPromises = all(promises); - allPromises.then(function (results) { - console.log(results); - callback && callback(true, results); + allPromises.then(function (uploadResults) { + console.log(uploadResults); + self._cleanAttachmentsDB(uploadResults,function(dbResults){ + if(dbResults.errors){ + callback && callback(false, uploadResults,dbResults); + } + else{ + callback && callback(true, uploadResults,dbResults); + } + }); + //results.forEach(function(value){ + // if(value.attachmentResult.success){ + // // Delete an attachment from the database if it was successfully + // // submitted to the server. + // self._deleteAttachmentFromDB(value.id,null).then(function(result){ + // if(result.success){ + // callback && callback(true, results); + // } + // else{ + // callback && callback(false, results); + // } + // }); + // } + //}); }, function (err) { console.log("error!", err); @@ -1293,6 +1481,10 @@ define([ if (attachmentsStore == null && layer.hasAttachments) { console.log("NOTICE: you may need to run OfflineFeaturesManager.initAttachments(). Check the Attachments doc for more info. Layer id: " + layer.id + " accepts attachments"); } + else if(layer.hasAttachments === false){ + console.error("WARNING: Layer " + layer.id + "doesn't seem to accept attachments. Recheck the layer permissions."); + callback(false,"WARNING: Attachments not supported in layer: " + layer.id); + } // Assign the attachmentsStore to the layer as a private var so we can access it from // the promises applyEdits() method. diff --git a/package.json b/package.json index 3d51d143..095b34f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "offline-editor-js", - "version": "2.6.1", + "version": "2.7.0", "description": "Lightweight set of libraries for working offline with map tiles and ArcGIS feature services", "author": "Andy Gup (http://blog.andygup.net)", "license": "Apache 2", diff --git a/samples/attachments-editor-secure.html b/samples/attachments-editor-secure.html new file mode 100644 index 00000000..2c6f3cd3 --- /dev/null +++ b/samples/attachments-editor-secure.html @@ -0,0 +1,365 @@ + + + + + + + + Secure Attachments + + + + + + + + + + + + + + +
+ +
+ + + + + +
+
unknown
+
+
+
Storage used: 0 MBs
+
+
+ + + + +
    +
    +
    + +
    + + +
    + + + + + diff --git a/samples/attachments-editor.html b/samples/attachments-editor.html index 59d0de4a..ac1c9cad 100644 --- a/samples/attachments-editor.html +++ b/samples/attachments-editor.html @@ -5,10 +5,10 @@ - SanFrancisco311 - Incidents + Basic Attachments - + - + - - - - - - - - - - - - + + + + + + + + + + + + -
    -
    - Select any file (.png, .jpg,...) to launch unit testing -
    + + + + diff --git a/test/SpecRunner.offlineAttachments.html b/test/SpecRunner.offlineAttachments.html index b5244639..ce3ba27d 100644 --- a/test/SpecRunner.offlineAttachments.html +++ b/test/SpecRunner.offlineAttachments.html @@ -19,9 +19,9 @@ } - - - + + + @@ -30,10 +30,11 @@ var g_map; var g_featureLayers = []; + var g_featureLayer = null; var g_offlineFeaturesManager; var g_modules = {}; var g_editsStore; - var g_formNode; + var g_formNode, g_formData, g_formData2, g_formNode2; require(["esri/map", "esri/layers/GraphicsLayer", "esri/graphic", @@ -51,6 +52,8 @@ g_modules.esriRequest = esriRequest; g_modules.Graphic = Graphic; g_offlineFeaturesManager = new O.esri.Edit.OfflineFeaturesManager(); + g_offlineFeaturesManager.DB_UID = "OBJECTID"; + g_editsStore = new O.esri.Edit.EditStore(); g_offlineFeaturesManager.initAttachments(function(success) @@ -58,7 +61,9 @@ console.log("attachments inited", success); }); - g_formNode = dom.byId('theForm'); + var form = dom.byId('theForm'); + + on(dom.byId('theFile'),'change',test); g_map = new Map("map", { basemap: "topo", @@ -67,46 +72,36 @@ sliderStyle: "small" }); - var fsUrl = "http://services2.arcgis.com/CQWCKwrSm5dkM28A/arcgis/rest/services/Military/FeatureServer/"; + var fsUrl = "http://services2.arcgis.com/CQWCKwrSm5dkM28A/arcgis/rest/services/Military/FeatureServer/3"; + var fsUrl2 = "http://services1.arcgis.com/M8KJPUwAXP8jhtnM/arcgis/rest/services/Simple_Point_Service/FeatureServer/0"; // var layersIds = [0,1,2,3,4,5,6]; - var layersIds = [1,2,3,6]; +// var layersIds = [1,2,3,6]; - layersIds.forEach(function(layerId) - { - var layer = new FeatureLayer(fsUrl + layerId, { - mode: FeatureLayer.MODE_SNAPSHOT, - outFields: ['*'] - }); - g_featureLayers.push(layer); - }) + g_featureLayer = new FeatureLayer(fsUrl2, { + mode: FeatureLayer.MODE_SNAPSHOT, + outFields: ['*'] + }); - g_map.addLayers(g_featureLayers); + g_map.on('layers-add-result', function(evt){ + console.log("layers ready"); + }); - g_map.on('layers-add-result', test); + g_map.addLayers([g_featureLayer]); function test() { - try - { - g_featureLayers.forEach(function(layer) - { - g_offlineFeaturesManager.extend(layer,function(success,message){ - if(!success){ - alert("There was a problem extending the layer: " + layer); - } - }); - }); - } - catch(err) - { - console.log(err); - } + + g_offlineFeaturesManager.extend(g_featureLayer,function(success,message){ + if(!success){ + alert("There was a problem extending the layer: " + layer); + } + }); try { var jasmineEnv = jasmine.getEnv(); jasmineEnv.updateInterval = 1000; - jasmineEnv.defaultTimeoutInterval = 5000; // 10 sec + jasmineEnv.defaultTimeoutInterval = 10000; // 10 sec var htmlReporter = new jasmine.HtmlReporter(); jasmineEnv.addReporter(htmlReporter); @@ -130,13 +125,65 @@ } // execJasmine(); - on(dom.byId('theFile'),'change',execJasmine); +// on(dom.byId('theFile'),'change',execJasmine); + + function retrieveFile(){ + + var xhr = new XMLHttpRequest(); + xhr.open("GET","images/blue-pin.png",true); + xhr.responseType = "blob"; + + xhr.onload = function() + { + if( xhr.status === 200) + { + var blob = new Blob([this.response],{type: this.response.type}); + var parts = [blob,"test", new ArrayBuffer(blob.size)]; + var files = []; + var file = new File(parts,"blue-pin.png",{ + lastModified: new Date(0), + type: this.response.type + }); + + files.push(file); + + // Fake a form node for our custom parser in offlineFeaturesManager.js + g_formNode = { + elements:[ + {type:"file", + files:files} + ] + }; + + g_formData = new FormData(); + g_formData.append("attachment",file); + + + g_formData2 = form; + +// retrieveFile2(); + execJasmine(); + + } + else + { + console.log("Retrieve file failed"); + } + }; + xhr.onerror = function(e) + { + console.log("Retrieved file failed: " + JSON.stringify(e)); + }; + + xhr.send(null); + } } catch(err) { console.log(err); } + retrieveFile(); }; // test() @@ -146,7 +193,7 @@
    -
    +
    Select any file (.png, .jpg,...) to launch unit testing
    diff --git a/test/SpecRunner.offlineFeaturesManager.html b/test/SpecRunner.offlineFeaturesManager.html index c64dac21..67af0bfb 100644 --- a/test/SpecRunner.offlineFeaturesManager.html +++ b/test/SpecRunner.offlineFeaturesManager.html @@ -18,10 +18,9 @@ } } - - - - + + + @@ -31,8 +30,10 @@ var g_map; var g_featureLayers = []; var g_offlineFeaturesManager; + var g_offlineFeaturesManagerAttach; var g_modules = {}; var g_editsStore; + var g_layersIds = []; require(["esri/map", "esri/layers/GraphicsLayer", "esri/graphic", @@ -52,6 +53,13 @@ g_modules.Graphic = Graphic; g_offlineFeaturesManager = new O.esri.Edit.OfflineFeaturesManager(); g_offlineFeaturesManager.DB_NAME = "FEATURES_TEST"; + g_offlineFeaturesManager.DB_UID = "OBJECTID"; //VERY IMPORTANT! We get this from the Service Directory "Fields" section. + + // For testing attachments + g_offlineFeaturesManagerAttach = new O.esri.Edit.OfflineFeaturesManager(); + g_offlineFeaturesManagerAttach.DB_NAME = "ATTACH_TEST"; + + // We are also validating the OfflineFeatureManager directly against the database g_editsStore = new O.esri.Edit.EditStore(); g_editsStore.dbName = "FEATURES_TEST"; @@ -63,19 +71,20 @@ }); var fsUrl = "http://services2.arcgis.com/CQWCKwrSm5dkM28A/arcgis/rest/services/Military/FeatureServer/"; + var fsUrlAttachmentsEnabled = "http://services1.arcgis.com/M8KJPUwAXP8jhtnM/arcgis/rest/services/Simple_Point_Service/FeatureServer/"; // Layer 1 = points // Layer 2 = lines // Layer 3 = polygons - var layersIds = [1,2,3]; + g_layersIds = [0]; - layersIds.forEach(function(layerId) + g_layersIds.forEach(function(layerId) { - var layer = new FeatureLayer(fsUrl + layerId, { + var layer = new FeatureLayer(fsUrlAttachmentsEnabled + layerId, { mode: FeatureLayer.MODE_SNAPSHOT, outFields: ['*'] }); g_featureLayers.push(layer); - }) + }); g_map.addLayers(g_featureLayers); diff --git a/test/images/blue-pin.png b/test/images/blue-pin.png new file mode 100644 index 0000000000000000000000000000000000000000..e3331a7122767b46f6831f11b181fa04c8418617 GIT binary patch literal 685 zcmV;e0#f~nP)O2S9NM__^&Eio~XK|sS~h@D8-bZ~JohJoFUfv~wa z;3^Y_I2pCFkeDcy0TL7WGJzOr+FCd&M_Zn!w$1gpyZ2g2TzHZrlwL08*WF7{YPqxKi-HrBUKOT*B;r2iX!Fn&!nvVBh_wama1AB4JJ$CO# zE1r(`7LZZ|0}Afn>c*Yx9hg~I!|Roub9s_gMP|qtj80EnLq{ke#&~FUkP&=5-Y>>@ zj@$$fSBQzKGFB(V(#NN+FkW3Dw!9v9h4D5KVlUqA3ajI)c)tm;{X_Dtzxn(VUp7-0^8}PCe75k-@&8OX%zueX>_1?V?5)6J$?esep}GbY#rzm!qsQ^x zs=*SJ+KGYl*^%f?oU9hKg4ij{Sv8;?RIvbVKqu(jYC3@;sNzUvI-~Kq>_dI1E3Ebm zmfze?knKLRs!#;kR%Wh{PDtVGiRK7#$WGYrt(8nu@6fA!R&S%rNhgToOn?CZ-aRlW Tr@4{&00000NkvXXu0mjfQgl2C literal 0 HcmV?d00001 diff --git a/test/images/red-pin.png b/test/images/red-pin.png new file mode 100644 index 0000000000000000000000000000000000000000..7a4ed2eddceb84fcaa5124eed5f67224007759e5 GIT binary patch literal 480 zcmV<60U!Q}P)eO&75<{OBsi7Ag@TwJPw zv_&bjcn(1V*n2BPH%@YLOWX7HoW?!C$VY0qO5xg#!;6>3hUS1$-XBa{wm9gRt@tV(Z|^g!Z;@IhEZDxO@@X69Z3RS5LS*3{V<}o>k>Gi>Z_w}R|xUKr?yus zpn7shu;@Rk8z0zeT5l0%V4JnEIF2E$HzfGOXs`xrum)?e|G<>oK}4^d$$VZL(lrgz zdWW#HmmsJNenZY;0aQ