mirror of
https://github.com/Esri/offline-editor-js.git
synced 2025-12-15 15:20:05 +00:00
## Version 3.0.6 - March 30, 2016 No breaking changes **Bug Fixes** * Closes #448 - OfflineEditAdvanced - after multiple offline restarts, UID begins at -1 again for Adds. **Enhancements** * Closes #451 - OfflineTiles - Option for offline_id_manager localStorage key name * Updates to howtousetiles.md documentation for the offlineIdManager parameter.
3495 lines
157 KiB
JavaScript
3495 lines
157 KiB
JavaScript
/*! esri-offline-maps - v3.0.6 - 2016-03-30
|
|
* Copyright (c) 2016 Environmental Systems Research Institute, Inc.
|
|
* Apache License*/
|
|
// Configure offline/online detection
|
|
// Requires: http://github.hubspot.com/offline/docs/welcome/
|
|
|
|
Offline.options = { // jshint ignore:line
|
|
checks: {
|
|
image: {
|
|
url: function() {
|
|
return 'http://esri.github.io/offline-editor-js/tiny-image.png?_=' + (Math.floor(Math.random() * 1000000000));
|
|
}
|
|
},
|
|
active: 'image'
|
|
}
|
|
};
|
|
/*jshint -W030 */
|
|
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/request",
|
|
"esri/symbols/SimpleMarkerSymbol",
|
|
"esri/symbols/SimpleLineSymbol",
|
|
"esri/symbols/SimpleFillSymbol",
|
|
"esri/urlUtils"],
|
|
function (Evented, Deferred, all, declare, array, domAttr, domStyle, query,
|
|
esriConfig, GraphicsLayer, Graphic, esriRequest, SimpleMarkerSymbol, SimpleLineSymbol, SimpleFillSymbol, urlUtils) {
|
|
"use strict";
|
|
return declare("O.esri.Edit.OfflineEditAdvanced", [Evented],
|
|
{
|
|
_onlineStatus: "online",
|
|
_featureLayers: {},
|
|
_featureCollectionUsageFlag: false, // if a feature collection was used to create the feature layer.
|
|
_editStore: new O.esri.Edit.EditStore(),
|
|
_defaultXhrTimeout: 15000, // ms
|
|
|
|
ONLINE: "online", // all edits will directly go to the server
|
|
OFFLINE: "offline", // edits will be enqueued
|
|
RECONNECTING: "reconnecting", // sending stored edits to the server
|
|
attachmentsStore: null, // indexedDB for storing attachments
|
|
proxyPath: null, // by default we use CORS and therefore proxyPath is null
|
|
|
|
ENABLE_FEATURECOLLECTION: false, // Set this to true for full offline use if you want to use the
|
|
// getFeatureCollections() pattern of reconstituting a feature layer.
|
|
|
|
// Database properties
|
|
DB_NAME: "features_store", // Sets the database name.
|
|
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 (e.g. the need for DB_UID).
|
|
// 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
|
|
EDITS_ENQUEUED: "edits-enqueued", // ...when an edit is enqueued (and not sent to the server)
|
|
EDITS_ENQUEUED_ERROR: "edits-enqueued-error", // ...when there is an error during the queing process
|
|
EDITS_SENT_ERROR: "edits-sent-error", // ...there was a problem with one or more edits!
|
|
ALL_EDITS_SENT: "all-edits-sent", // ...after going online and there are no pending edits in the queue
|
|
ATTACHMENT_ENQUEUED: "attachment-enqueued",
|
|
ATTACHMENTS_SENT: "attachments-sent",
|
|
},
|
|
|
|
/**
|
|
* Need to call this method only if you want to support offline attachments
|
|
* it is optional
|
|
* @param callback(success)
|
|
* @returns void
|
|
*/
|
|
initAttachments: function (callback) {
|
|
callback = callback || function (success) {
|
|
console.log("attachments inited ", success ? "ok" : "failed");
|
|
};
|
|
|
|
if (!this._checkFileAPIs()) {
|
|
return callback(false, "File APIs not supported");
|
|
}
|
|
|
|
try {
|
|
this.attachmentsStore = new O.esri.Edit.AttachmentsStore();
|
|
this.attachmentsStore.dbName = this.ATTACHMENTS_DB_NAME;
|
|
this.attachmentsStore.objectStoreName = this.ATTACHMENTS_DB_OBJECTSTORE_NAME;
|
|
|
|
if (/*false &&*/ this.attachmentsStore.isSupported()) {
|
|
this.attachmentsStore.init(callback);
|
|
}
|
|
else {
|
|
return callback(false, "indexedDB not supported");
|
|
}
|
|
}
|
|
catch (err) {
|
|
console.log("problem! " + err.toString());
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Overrides a feature layer. Call this AFTER the FeatureLayer's 'update-end' event.
|
|
* 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
|
|
* @param dataStore Optional configuration Object. Added @ v2.5. There is only one reserved object key and that is "id".
|
|
* Use this option to store featureLayerJSON and any other configuration information you'll need access to after
|
|
* a full offline browser restart.
|
|
* @returns deferred
|
|
*/
|
|
extend: function (layer, callback, dataStore) {
|
|
|
|
var extendPromises = []; // deferred promises related to initializing this method
|
|
|
|
var self = this;
|
|
layer.offlineExtended = true; // to identify layer has been extended
|
|
|
|
if(!layer.loaded) {
|
|
console.error("Make sure to initialize OfflineEditAdvanced after layer loaded and feature layer update-end event.");
|
|
}
|
|
|
|
// NOTE: At v2.6.1 we've discovered that not all feature layers support objectIdField.
|
|
// However, to try to be consistent here with how the library is managing Ids
|
|
// we force the layer.objectIdField to DB_UID. This should be consistent with
|
|
// how esri.Graphics assign a unique ID to a graphic. If it is not, then this
|
|
// library will break and we'll have to re-architect how it manages UIDs.
|
|
layer.objectIdField = this.DB_UID;
|
|
|
|
var url = null;
|
|
|
|
// There have been reproducible use cases showing when a browser is restarted offline that
|
|
// for some reason the layer.url may be undefined.
|
|
// This is an attempt to minimize the possibility of that situation causing errors.
|
|
if(layer.url) {
|
|
url = layer.url;
|
|
// we keep track of the FeatureLayer object
|
|
this._featureLayers[layer.url] = layer;
|
|
}
|
|
|
|
// This is a potentially brittle solution to detecting if a feature layer collection
|
|
// was used to create the feature layer.
|
|
// Is there a better way??
|
|
if(layer._mode.featureLayer.hasOwnProperty("_collection")){
|
|
// This means a feature collection was used to create the feature layer and it will
|
|
// require different handling when running applyEdit()
|
|
this._featureCollectionUsageFlag = true;
|
|
}
|
|
|
|
// Initialize the database as well as set offline data.
|
|
if(!this._editStore._isDBInit) {
|
|
extendPromises.push(this._initializeDB(dataStore, url));
|
|
}
|
|
|
|
// replace the applyEdits() method
|
|
layer._applyEdits = layer.applyEdits;
|
|
|
|
|
|
// attachments
|
|
layer._addAttachment = layer.addAttachment;
|
|
layer._queryAttachmentInfos = layer.queryAttachmentInfos;
|
|
layer._deleteAttachments = layer.deleteAttachments;
|
|
layer._updateAttachment = layer.updateAttachment;
|
|
|
|
/*
|
|
operations supported offline:
|
|
1. add a new attachment to an existing feature (DONE)
|
|
2. add a new attachment to a new feature (DONE)
|
|
3. remove an attachment that is already in the server... (DONE)
|
|
4. remove an attachment that is not in the server yet (DONE)
|
|
5. update an existing attachment to an existing feature (DONE)
|
|
6. update a new attachment (DONE)
|
|
|
|
concerns:
|
|
- manage the relationship between offline features and attachments: what if the user wants to add
|
|
an attachment to a feature that is still offline? we need to keep track of objectids so that when
|
|
the feature is sent to the server and receives a final objectid we replace the temporary negative id
|
|
by its final objectid (DONE)
|
|
- what if the user deletes an offline feature that had offline attachments? we need to discard the attachment (DONE)
|
|
*/
|
|
|
|
//
|
|
// attachments
|
|
//
|
|
layer.queryAttachmentInfos = function (objectId, callback, errback) {
|
|
if (self.getOnlineStatus() === self.ONLINE) {
|
|
var def = this._queryAttachmentInfos(objectId,
|
|
function () {
|
|
console.log(arguments);
|
|
self.emit(self.events.ATTACHMENTS_INFO, arguments);
|
|
callback && callback.apply(this, arguments);
|
|
},
|
|
errback);
|
|
return def;
|
|
}
|
|
|
|
if (!self.attachmentsStore) {
|
|
console.log("in order to support attachments you need to call initAttachments() method of OfflineEditAdvanced");
|
|
return;
|
|
}
|
|
|
|
// will only return LOCAL attachments
|
|
var deferred = new Deferred();
|
|
self.attachmentsStore.getAttachmentsByFeatureId(this.url, objectId, function (attachments) {
|
|
callback && callback(attachments);
|
|
deferred.resolve(attachments);
|
|
});
|
|
return deferred;
|
|
};
|
|
|
|
layer.addAttachment = function (objectId, formNode, callback, errback) {
|
|
|
|
if (self.getOnlineStatus() === self.ONLINE) {
|
|
return this._addAttachment(objectId, formNode,
|
|
function () {
|
|
console.log(arguments);
|
|
self.emit(self.events.ATTACHMENTS_SENT, arguments);
|
|
callback && callback.apply(this, arguments);
|
|
},
|
|
function (err) {
|
|
console.log("addAttachment: " + err);
|
|
errback && errback.apply(this, arguments);
|
|
}
|
|
);
|
|
}
|
|
|
|
if (!self.attachmentsStore) {
|
|
console.error("in order to support attachments you need to call initAttachments() method of OfflineEditAdvanced");
|
|
return;
|
|
}
|
|
|
|
var files = this._getFilesFromForm(formNode);
|
|
var file = files[0]; // addAttachment can only add one file, so the rest -if any- are ignored
|
|
|
|
var deferred = new Deferred();
|
|
var attachmentId = this._getNextTempId();
|
|
self.attachmentsStore.store(this.url, attachmentId, objectId, file,self.attachmentsStore.TYPE.ADD, function (success, newAttachment) {
|
|
var returnValue = {attachmentId: attachmentId, objectId: objectId, success: success};
|
|
if (success) {
|
|
self.emit(self.events.ATTACHMENT_ENQUEUED, returnValue);
|
|
callback && callback(returnValue);
|
|
deferred.resolve(returnValue);
|
|
|
|
// replace the default URL that is set by attachmentEditor with the local file URL
|
|
var attachmentUrl = this._url.path + "/" + objectId + "/attachments/" + attachmentId;
|
|
var attachmentElement = query("[href=" + attachmentUrl + "]");
|
|
attachmentElement.attr("href", newAttachment.url);
|
|
}
|
|
else {
|
|
returnValue.error = "can't store attachment";
|
|
errback && errback(returnValue);
|
|
deferred.reject(returnValue);
|
|
}
|
|
}.bind(this));
|
|
|
|
return deferred;
|
|
};
|
|
|
|
layer.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 OfflineEditAdvanced");
|
|
return;
|
|
}
|
|
|
|
var files = this._getFilesFromForm(formNode);
|
|
var file = files[0]; // addAttachment can only add one file, so the rest -if any- are ignored
|
|
var action = self.attachmentsStore.TYPE.UPDATE; // Is this an ADD or an UPDATE?
|
|
|
|
var deferred = new Deferred();
|
|
|
|
// If the attachment has a temporary ID we want to keep it's action as an ADD.
|
|
// Otherwise we'll get an error when we try to UPDATE an ObjectId that doesn't exist in ArcGIS Online or Server.
|
|
if(attachmentId < 0) {
|
|
action = self.attachmentsStore.TYPE.ADD;
|
|
}
|
|
|
|
self.attachmentsStore.store(this.url, attachmentId, objectId, file, action, 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,
|
|
function () {
|
|
callback && callback.apply(this, arguments);
|
|
},
|
|
function (err) {
|
|
console.log("deleteAttachments: " + 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 OfflineEditAdvanced");
|
|
return;
|
|
}
|
|
|
|
// case 1.- it is a new attachment
|
|
// case 2.- it is an already existing attachment
|
|
|
|
// asynchronously delete each of the attachments
|
|
var promises = [];
|
|
attachmentsIds.forEach(function (attachmentId) {
|
|
attachmentId = parseInt(attachmentId, 10); // to number
|
|
|
|
var deferred = new Deferred();
|
|
|
|
// 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);
|
|
});
|
|
|
|
return allPromises;
|
|
};
|
|
|
|
//
|
|
// other functions
|
|
//
|
|
|
|
/**
|
|
* Overrides the ArcGIS API for JavaSccript applyEdits() method.
|
|
* @param adds Creates a new edit entry.
|
|
* @param updates Updates an existing entry.
|
|
* @param deletes Deletes an existing entry.
|
|
* @param callback Called when the operation is complete.
|
|
* @param errback An error object is returned if an error occurs
|
|
* @returns {*} deferred
|
|
* @event EDITS_ENQUEUED boolean if all edits successfully stored while offline
|
|
* @event EDITS_ENQUEUED_ERROR string message if there was an error while storing an edit while offline
|
|
*/
|
|
layer.applyEdits = function (adds, updates, deletes, callback, errback) {
|
|
// inside this method, 'this' will be the FeatureLayer
|
|
// and 'self' will be the offlineFeatureLayer object
|
|
var promises = [];
|
|
|
|
if (self.getOnlineStatus() === self.ONLINE) {
|
|
var def = this._applyEdits(adds, updates, deletes,
|
|
function () {
|
|
self.emit(self.events.EDITS_SENT, arguments);
|
|
callback && callback.apply(this, arguments);
|
|
},
|
|
errback);
|
|
return def;
|
|
}
|
|
|
|
var deferred1 = new Deferred();
|
|
var results = {addResults: [], updateResults: [], deleteResults: []};
|
|
var updatesMap = {};
|
|
|
|
var _adds = adds || [];
|
|
_adds.forEach(function (addEdit) {
|
|
var deferred = new Deferred();
|
|
|
|
var objectId = this._getNextTempId();
|
|
|
|
addEdit.attributes[this.objectIdField] = objectId;
|
|
|
|
var thisLayer = this;
|
|
|
|
// We need to run some validation tests against each feature being added.
|
|
// Adding the same feature multiple times results in the last edit wins. LIFO.
|
|
this._validateFeature(addEdit,this.url,self._editStore.ADD).then(function(result){
|
|
console.log("EDIT ADD IS BACK!!! " );
|
|
|
|
if(result.success){
|
|
thisLayer._pushValidatedAddFeatureToDB(thisLayer,addEdit,result.operation,results,objectId,deferred);
|
|
}
|
|
else{
|
|
// If we get here then we deleted an edit that was added offline.
|
|
// We also have deleted the phantom graphic.
|
|
deferred.resolve(true);
|
|
}
|
|
|
|
},function(error){
|
|
console.log("_validateFeature: Unable to validate!");
|
|
deferred.reject(error);
|
|
});
|
|
|
|
promises.push(deferred);
|
|
}, this);
|
|
|
|
updates = updates || [];
|
|
updates.forEach(function (updateEdit) {
|
|
var deferred = new Deferred();
|
|
|
|
var objectId = updateEdit.attributes[this.objectIdField];
|
|
updatesMap[objectId] = updateEdit;
|
|
|
|
var thisLayer = this;
|
|
|
|
// We need to run some validation tests against each feature being updated.
|
|
// If we have added a feature and we need to update it then we change it's operation type to "add"
|
|
// and the last edits wins. LIFO.
|
|
this._validateFeature(updateEdit,this.url,self._editStore.UPDATE).then(function(result){
|
|
console.log("EDIT UPDATE IS BACK!!! " );
|
|
|
|
if(result.success){
|
|
thisLayer._pushValidatedUpdateFeatureToDB(thisLayer,updateEdit,result.operation,results,objectId,deferred);
|
|
}
|
|
else{
|
|
// If we get here then we deleted an edit that was added offline.
|
|
// We also have deleted the phantom graphic.
|
|
deferred.resolve(true);
|
|
}
|
|
|
|
},function(error){
|
|
console.log("_validateFeature: Unable to validate!");
|
|
deferred.reject(error);
|
|
});
|
|
|
|
promises.push(deferred);
|
|
}, this);
|
|
|
|
deletes = deletes || [];
|
|
deletes.forEach(function (deleteEdit) {
|
|
var deferred = new Deferred();
|
|
|
|
var objectId = deleteEdit.attributes[this.objectIdField];
|
|
|
|
var thisLayer = this;
|
|
|
|
// We need to run some validation tests against each feature being deleted.
|
|
// If we have added a feature and then deleted it in the app then we go ahead
|
|
// and delete it and its phantom graphic from the database.
|
|
// NOTE: at this time we don't handle attachments automatically
|
|
this._validateFeature(deleteEdit,this.url,self._editStore.DELETE).then(function(result){
|
|
console.log("EDIT DELETE IS BACK!!! " );
|
|
|
|
if(result.success){
|
|
thisLayer._pushValidatedDeleteFeatureToDB(thisLayer,deleteEdit,result.operation,results,objectId,deferred);
|
|
}
|
|
else{
|
|
// If we get here then we deleted an edit that was added offline.
|
|
// We also have deleted the phantom graphic.
|
|
deferred.resolve(true);
|
|
}
|
|
|
|
},function(error){
|
|
console.log("_validateFeature: Unable to validate!");
|
|
deferred.reject(error);
|
|
});
|
|
|
|
promises.push(deferred);
|
|
}, this);
|
|
|
|
all(promises).then(function (r) {
|
|
// Make sure all edits were successful. If not throw an error.
|
|
var promisesSuccess = true;
|
|
for (var v = 0; v < r.length; v++) {
|
|
if (r[v] === false) {
|
|
promisesSuccess = false;
|
|
}
|
|
}
|
|
|
|
layer._pushFeatureCollections(function(success){
|
|
console.log("All edits done");
|
|
|
|
if(success && promisesSuccess){
|
|
self.emit(self.events.EDITS_ENQUEUED, results);
|
|
}
|
|
else {
|
|
if(!success){
|
|
console.log("applyEdits() there was a problem with _pushFeatureCollections.");
|
|
}
|
|
self.emit(self.events.EDITS_ENQUEUED_ERROR, results);
|
|
}
|
|
|
|
//promisesSuccess === true ? self.emit(self.events.EDITS_ENQUEUED, results) : self.emit(self.events.EDITS_ENQUEUED_ERROR, results);
|
|
|
|
// we already pushed the edits into the database, now we let the FeatureLayer to do the local updating of the layer graphics
|
|
this._editHandler(results, _adds, updatesMap, callback, errback, deferred1);
|
|
}.bind(this));
|
|
|
|
//success === true ? self.emit(self.events.EDITS_ENQUEUED, results) : self.emit(self.events.EDITS_ENQUEUED_ERROR, results);
|
|
// EDITS_ENQUEUED = callback(true, edit), and EDITS_ENQUEUED_ERROR = callback(false, /*String */ error)
|
|
//this._editHandler(results, _adds, updatesMap, callback, errback, deferred1);
|
|
}.bind(this));
|
|
|
|
return deferred1;
|
|
|
|
}; // layer.applyEdits()
|
|
|
|
/**
|
|
* Converts an array of graphics/features into JSON
|
|
* @param features
|
|
* @param updateEndEvent The layer's 'update-end' event
|
|
* @param callback
|
|
*/
|
|
layer.convertGraphicLayerToJSON = function (features, updateEndEvent, callback) {
|
|
var layerDefinition = {};
|
|
|
|
// We want to be consistent, but we've discovered that not all feature layers have an objectIdField
|
|
if(updateEndEvent.target.hasOwnProperty("objectIdField"))
|
|
{
|
|
layerDefinition.objectIdFieldName = updateEndEvent.target.objectIdField;
|
|
}else {
|
|
layerDefinition.objectIdFieldName = this.objectIdField;
|
|
}
|
|
|
|
layerDefinition.globalIdFieldName = updateEndEvent.target.globalIdField;
|
|
layerDefinition.geometryType = updateEndEvent.target.geometryType;
|
|
layerDefinition.spatialReference = updateEndEvent.target.spatialReference;
|
|
layerDefinition.fields = updateEndEvent.target.fields;
|
|
|
|
var length = features.length;
|
|
var jsonArray = [];
|
|
for (var i = 0; i < length; i++) {
|
|
var jsonGraphic = features[i].toJson();
|
|
jsonArray.push(jsonGraphic);
|
|
if (i == (length - 1)) {
|
|
var featureJSON = JSON.stringify(jsonArray);
|
|
var layerDefJSON = JSON.stringify(layerDefinition);
|
|
callback(featureJSON, layerDefJSON);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Retrieves f=json from the feature layer
|
|
* @param url FeatureLayer's URL
|
|
* @param callback
|
|
* @private
|
|
*/
|
|
layer.getFeatureLayerJSON = function (url, callback) {
|
|
require(["esri/request"], function (esriRequest) {
|
|
var request = esriRequest({
|
|
url: url,
|
|
content: {f: "json"},
|
|
handleAs: "json",
|
|
callbackParamName: "callback"
|
|
});
|
|
|
|
request.then(function (response) {
|
|
console.log("Success: ", response);
|
|
callback(true, response);
|
|
}, function (error) {
|
|
console.log("Error: ", error.message);
|
|
callback(false, error.message);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Sets the optional feature layer storage object
|
|
* @param jsonObject
|
|
* @param callback
|
|
*/
|
|
layer.setFeatureLayerJSONDataStore = function(jsonObject, callback){
|
|
self._editStore.pushFeatureLayerJSON(jsonObject,function(success,error){
|
|
callback(success,error);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Retrieves the optional feature layer storage object
|
|
* @param callback callback(true, object) || callback(false, error)
|
|
*/
|
|
layer.getFeatureLayerJSONDataStore = function(callback){
|
|
self._editStore.getFeatureLayerJSON(function(success,message){
|
|
callback(success,message);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Sets the phantom layer with new features.
|
|
* Used to restore PhantomGraphicsLayer after offline restart
|
|
* @param graphicsArray an array of Graphics
|
|
*/
|
|
layer.setPhantomLayerGraphics = function (graphicsArray) {
|
|
var length = graphicsArray.length;
|
|
|
|
if (length > 0) {
|
|
for (var i = 0; i < length; i++) {
|
|
var graphic = new Graphic(graphicsArray[i]);
|
|
this._phantomLayer.add(graphic);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns the array of graphics from the phantom graphics layer.
|
|
* This layer identifies features that have been modified
|
|
* while offline.
|
|
* @returns {array}
|
|
*/
|
|
layer.getPhantomLayerGraphics = function (callback) {
|
|
//return layer._phantomLayer.graphics;
|
|
var graphics = layer._phantomLayer.graphics;
|
|
var length = layer._phantomLayer.graphics.length;
|
|
var jsonArray = [];
|
|
for (var i = 0; i < length; i++) {
|
|
var jsonGraphic = graphics[i].toJson();
|
|
jsonArray.push(jsonGraphic);
|
|
if (i == (length - 1)) {
|
|
var graphicsJSON = JSON.stringify(jsonArray);
|
|
callback(graphicsJSON);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns an array of phantom graphics from the database.
|
|
* @param callback callback (true, array) or (false, errorString)
|
|
*/
|
|
layer.getPhantomGraphicsArray = function(callback){
|
|
self._editStore.getPhantomGraphicsArray(function(array,message){
|
|
if(message == "end"){
|
|
callback(true,array);
|
|
}
|
|
else{
|
|
callback(false,message);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 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){
|
|
self._editStore.getUsage(function(usage,error){
|
|
callback(usage,error);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 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.
|
|
* @param callback (boolean, error)
|
|
*/
|
|
layer.resetDatabase = function(callback){
|
|
self._editStore.resetEditsQueue(function(result,error){
|
|
callback(result,error);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Returns the number of edits pending in the database.
|
|
* @param callback callback( int )
|
|
*/
|
|
layer.pendingEditsCount = function(callback){
|
|
self._editStore.pendingEditsCount(function(count){
|
|
callback(count);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Create a featureDefinition
|
|
* @param featureLayer
|
|
* @param featuresArr
|
|
* @param geometryType
|
|
* @param callback
|
|
*/
|
|
layer.getFeatureDefinition = function (/* Object */ featureLayer, /* Array */ featuresArr, /* String */ geometryType, callback) {
|
|
|
|
var featureDefinition = {
|
|
"layerDefinition": featureLayer,
|
|
"featureSet": {
|
|
"features": featuresArr,
|
|
"geometryType": geometryType
|
|
}
|
|
|
|
};
|
|
|
|
callback(featureDefinition);
|
|
};
|
|
|
|
/**
|
|
* Returns an iterable array of all edits stored in the database
|
|
* Each item in the array is an object and contains:
|
|
* {
|
|
* id: "internal ID",
|
|
* operation: "add, update or delete",
|
|
* layer: "layerURL",
|
|
* type: "esri Geometry Type",
|
|
* graphic: "esri.Graphic converted to JSON then serialized"
|
|
* }
|
|
* @param callback (true, array) or (false, errorString)
|
|
*/
|
|
layer.getAllEditsArray = function(callback){
|
|
self._editStore.getAllEditsArray(function(array,message){
|
|
if(message == "end"){
|
|
callback(true,array);
|
|
}
|
|
else{
|
|
callback(false,message);
|
|
}
|
|
});
|
|
};
|
|
|
|
/* internal methods */
|
|
|
|
/**
|
|
* Automatically creates a set of featureLayerCollections. This is specifically for
|
|
* use with offline browser restarts. You can retrieve the collections and use them
|
|
* to reconstitute a featureLayer and then redisplay all the associated features.
|
|
*
|
|
* To retrieve use OfflineEditAdvanced.getFeatureCollections().
|
|
* @param callback (boolean)
|
|
* @private
|
|
*/
|
|
layer._pushFeatureCollections = function(callback){
|
|
|
|
// First let's see if any collections exists
|
|
self._editStore._getFeatureCollections(function(success, result) {
|
|
|
|
var featureCollection =
|
|
{
|
|
featureLayerUrl: layer.url,
|
|
featureLayerCollection: layer.toJson()
|
|
};
|
|
|
|
// An array of feature collections, of course :-)
|
|
var featureCollectionsArray = [
|
|
featureCollection
|
|
];
|
|
|
|
// An object for storing multiple feature collections
|
|
var featureCollectionsObject = {
|
|
// The id is required because the editsStore keypath
|
|
// uses it as a UID for all entries in the database
|
|
id: self._editStore.FEATURE_COLLECTION_ID,
|
|
featureCollections: featureCollectionsArray
|
|
};
|
|
|
|
// THIS IS A HACK.
|
|
// There is a bug in JS API 3.11+ when you create a feature layer from a featureCollectionObject
|
|
// the hasAttachments property does not get properly repopulated.
|
|
layer.hasAttachments = featureCollection.featureLayerCollection.layerDefinition.hasAttachments;
|
|
|
|
// If the featureCollectionsObject already exists
|
|
if(success){
|
|
var count = 0;
|
|
for(var i = 0; i < result.featureCollections.length; i++) {
|
|
|
|
// Update the current feature collection
|
|
if(result.featureCollections[i].featureLayerUrl === layer.url) {
|
|
count++;
|
|
result.featureCollections[i] = featureCollection;
|
|
}
|
|
}
|
|
|
|
// If we have a new feature layer then add it to the featureCollections array
|
|
if(count === 0) {
|
|
result.featureCollections.push(featureCollection);
|
|
}
|
|
}
|
|
// If it does not exist then we need to add a featureCollectionsObject
|
|
else if(!success && result === null) {
|
|
result = featureCollectionsObject;
|
|
}
|
|
else {
|
|
console.error("There was a problem retrieving the featureCollections from editStore.");
|
|
}
|
|
|
|
// Automatically update the featureCollectionsObject in the database with every ADD, UPDATE
|
|
// and DELETE. It can be retrieved via OfflineEditAdvanced.getFeatureCollections();
|
|
self._editStore._pushFeatureCollections(result, function(success, error) {
|
|
if(!success){
|
|
console.error("There was a problem creating the featureCollectionObject: " + error);
|
|
callback(false);
|
|
}
|
|
else {
|
|
callback(true);
|
|
}
|
|
}.bind(this));
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Pushes a DELETE request to the database after it's been validated
|
|
* @param layer
|
|
* @param deleteEdit
|
|
* @param operation
|
|
* @param resultsArray
|
|
* @param objectId
|
|
* @param deferred
|
|
* @private
|
|
*/
|
|
layer._pushValidatedDeleteFeatureToDB = function(layer,deleteEdit,operation,resultsArray,objectId,deferred){
|
|
self._editStore.pushEdit(operation, layer.url, deleteEdit, function (result, error) {
|
|
|
|
if(result){
|
|
resultsArray.deleteResults.push({success: true, error: null, objectId: objectId});
|
|
|
|
// Use the correct key as set by self.DB_UID
|
|
var tempIdObject = {};
|
|
tempIdObject[self.DB_UID] = objectId;
|
|
|
|
var phantomDelete = new Graphic(
|
|
deleteEdit.geometry,
|
|
self._getPhantomSymbol(deleteEdit.geometry, self._editStore.DELETE),
|
|
tempIdObject
|
|
);
|
|
|
|
layer._phantomLayer.add(phantomDelete);
|
|
|
|
// Add phantom graphic to the database
|
|
self._editStore.pushPhantomGraphic(phantomDelete, function (result) {
|
|
if (!result) {
|
|
console.log("There was a problem adding phantom graphic id: " + objectId);
|
|
}
|
|
else{
|
|
console.log("Phantom graphic " + objectId + " added to database as a deletion.");
|
|
domAttr.set(phantomDelete.getNode(), "stroke-dasharray", "4,4");
|
|
domStyle.set(phantomDelete.getNode(), "pointer-events", "none");
|
|
}
|
|
});
|
|
|
|
if (self.attachmentsStore) {
|
|
// delete local attachments of this feature, if any... we just launch the delete and don't wait for it to complete
|
|
self.attachmentsStore.deleteAttachmentsByFeatureId(layer.url, objectId, function (deletedCount) {
|
|
console.log("deleted", deletedCount, "attachments of feature", objectId);
|
|
});
|
|
}
|
|
}
|
|
else{
|
|
// If we can't push edit to database then we don't create a phantom graphic
|
|
resultsArray.deleteResults.push({success: false, error: error, objectId: objectId});
|
|
}
|
|
|
|
deferred.resolve(result);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Pushes an UPDATE request to the database after it's been validated
|
|
* @param layer
|
|
* @param updateEdit
|
|
* @param operation
|
|
* @param resultsArray
|
|
* @param objectId
|
|
* @param deferred
|
|
* @private
|
|
*/
|
|
layer._pushValidatedUpdateFeatureToDB = function(layer,updateEdit,operation,resultsArray,objectId,deferred){
|
|
self._editStore.pushEdit(operation, layer.url, updateEdit, function (result, error) {
|
|
|
|
if(result){
|
|
resultsArray.updateResults.push({success: true, error: null, objectId: objectId});
|
|
|
|
// Use the correct key as set by self.DB_UID
|
|
var tempIdObject = {};
|
|
tempIdObject[self.DB_UID] = objectId;
|
|
|
|
var phantomUpdate = new Graphic(
|
|
updateEdit.geometry,
|
|
self._getPhantomSymbol(updateEdit.geometry, self._editStore.UPDATE),
|
|
tempIdObject
|
|
);
|
|
|
|
layer._phantomLayer.add(phantomUpdate);
|
|
|
|
// Add phantom graphic to the database
|
|
self._editStore.pushPhantomGraphic(phantomUpdate, function (result) {
|
|
if (!result) {
|
|
console.log("There was a problem adding phantom graphic id: " + objectId);
|
|
}
|
|
else{
|
|
console.log("Phantom graphic " + objectId + " added to database as an update.");
|
|
domAttr.set(phantomUpdate.getNode(), "stroke-dasharray", "5,2");
|
|
domStyle.set(phantomUpdate.getNode(), "pointer-events", "none");
|
|
}
|
|
});
|
|
|
|
}
|
|
else{
|
|
// If we can't push edit to database then we don't create a phantom graphic
|
|
resultsArray.updateResults.push({success: false, error: error, objectId: objectId});
|
|
}
|
|
|
|
deferred.resolve(result);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Pushes an ADD request to the database after it's been validated
|
|
* @param layer
|
|
* @param addEdit
|
|
* @param operation
|
|
* @param resultsArray
|
|
* @param objectId
|
|
* @param deferred
|
|
* @private
|
|
*/
|
|
layer._pushValidatedAddFeatureToDB = function(layer,addEdit,operation,resultsArray,objectId,deferred){
|
|
self._editStore.pushEdit(operation, layer.url, addEdit, function (result, error) {
|
|
if(result){
|
|
resultsArray.addResults.push({success: true, error: null, objectId: objectId});
|
|
|
|
// Use the correct key as set by self.DB_UID
|
|
var tempIdObject = {};
|
|
tempIdObject[self.DB_UID] = objectId;
|
|
|
|
var phantomAdd = new Graphic(
|
|
addEdit.geometry,
|
|
self._getPhantomSymbol(addEdit.geometry, self._editStore.ADD),
|
|
tempIdObject
|
|
);
|
|
|
|
// Add phantom graphic to the layer
|
|
layer._phantomLayer.add(phantomAdd);
|
|
|
|
// Add phantom graphic to the database
|
|
self._editStore.pushPhantomGraphic(phantomAdd, function (result) {
|
|
if (!result) {
|
|
console.log("There was a problem adding phantom graphic id: " + objectId);
|
|
}
|
|
else{
|
|
console.log("Phantom graphic " + objectId + " added to database as an add.");
|
|
domAttr.set(phantomAdd.getNode(), "stroke-dasharray", "10,4");
|
|
domStyle.set(phantomAdd.getNode(), "pointer-events", "none");
|
|
}
|
|
|
|
});
|
|
}
|
|
else{
|
|
// If we can't push edit to database then we don't create a phantom graphic
|
|
resultsArray.addResults.push({success: false, error: error, objectId: objectId});
|
|
}
|
|
|
|
deferred.resolve(result);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Validates duplicate entries. Last edit on same feature can overwite any previous values.
|
|
* Note: if an edit was already added offline and you delete it then we return success == false
|
|
* @param graphic esri.Graphic.
|
|
* @param layerUrl the URL of the feature service
|
|
* @param operation add, update or delete action on an edit
|
|
* @returns deferred {success:boolean,graphic:graphic,operation:add|update|delete}
|
|
* @private
|
|
*/
|
|
layer._validateFeature = function (graphic,layerUrl,operation) {
|
|
|
|
var deferred = new Deferred();
|
|
|
|
var id = layerUrl + "/" + graphic.attributes[self.DB_UID];
|
|
|
|
self._editStore.getEdit(id,function(success,result){
|
|
if (success) {
|
|
switch( operation )
|
|
{
|
|
case self._editStore.ADD:
|
|
// Not good - however we'll allow the new ADD to replace/overwrite existing edit
|
|
// and pass it through unmodified. Last ADD wins.
|
|
deferred.resolve({"success":true,"graphic":graphic,"operation":operation});
|
|
break;
|
|
case self._editStore.UPDATE:
|
|
// If we are doing an update on a feature that has not been added to
|
|
// the server yet, then we need to maintain its operation as an ADD
|
|
// and not an UPDATE. This avoids the potential for an error if we submit
|
|
// an update operation on a feature that has not been added to the
|
|
// database yet.
|
|
if(result.operation == self._editStore.ADD){
|
|
graphic.operation = self._editStore.ADD;
|
|
operation = self._editStore.ADD;
|
|
}
|
|
deferred.resolve({"success":true,"graphic":graphic,"operation":operation});
|
|
break;
|
|
case self._editStore.DELETE:
|
|
|
|
var resolved = true;
|
|
|
|
if(result.operation == self._editStore.ADD){
|
|
// If we are deleting a new feature that has not been added to the
|
|
// server yet we need to delete it and its phantom graphic.
|
|
layer._deleteTemporaryFeature(graphic,function(success){
|
|
if(!success){
|
|
resolved = false;
|
|
}
|
|
});
|
|
}
|
|
deferred.resolve({"success":resolved,"graphic":graphic,"operation":operation});
|
|
break;
|
|
}
|
|
}
|
|
else if(result == "Id not found"){
|
|
// Let's simply pass the graphic back as good-to-go.
|
|
// No modifications needed because the graphic does not
|
|
// already exist in the database.
|
|
deferred.resolve({"success":true,"graphic":graphic,"operation":operation});
|
|
}
|
|
else{
|
|
deferred.reject(graphic);
|
|
}
|
|
});
|
|
|
|
return deferred;
|
|
};
|
|
|
|
/**
|
|
* Delete a graphic and its associated phantom graphic that has been added while offline.
|
|
* @param graphic
|
|
* @param callback
|
|
* @private
|
|
*/
|
|
layer._deleteTemporaryFeature = function(graphic,callback){
|
|
|
|
var phantomGraphicId = self._editStore.PHANTOM_GRAPHIC_PREFIX + self._editStore._PHANTOM_PREFIX_TOKEN + graphic.attributes[self.DB_UID];
|
|
|
|
function _deleteGraphic(){
|
|
var deferred = new Deferred();
|
|
self._editStore.delete(layer.url,graphic,function(success,error){
|
|
if(success){
|
|
deferred.resolve(true);
|
|
}
|
|
else{
|
|
deferred.resolve(false);
|
|
}
|
|
});
|
|
return deferred.promise;
|
|
}
|
|
|
|
function _deletePhantomGraphic(){
|
|
var deferred = new Deferred();
|
|
self._editStore.deletePhantomGraphic(phantomGraphicId,function(success){
|
|
if(success) {
|
|
deferred.resolve(true);
|
|
}
|
|
else {
|
|
deferred.resolve(false);
|
|
}
|
|
}, function(error) {
|
|
deferred.resolve(false);
|
|
});
|
|
return deferred.promise;
|
|
}
|
|
|
|
all([_deleteGraphic(),_deletePhantomGraphic()]).then(function (results) {
|
|
callback(results);
|
|
});
|
|
|
|
};
|
|
|
|
layer._getFilesFromForm = function (formNode) {
|
|
var files = [];
|
|
var inputNodes = array.filter(formNode.elements, function (node) {
|
|
return node.type === "file";
|
|
});
|
|
inputNodes.forEach(function (inputNode) {
|
|
files.push.apply(files, inputNode.files);
|
|
}, this);
|
|
return files;
|
|
};
|
|
|
|
layer._replaceFeatureIds = function (tempObjectIds, newObjectIds, callback) {
|
|
console.log("replacing ids of attachments", tempObjectIds, newObjectIds);
|
|
console.assert(tempObjectIds.length === newObjectIds.length, "arrays must be the same length");
|
|
|
|
if (!tempObjectIds.length) {
|
|
console.log("no ids to replace!");
|
|
callback(0);
|
|
}
|
|
|
|
var i, n = tempObjectIds.length;
|
|
var count = n;
|
|
var successCount = 0;
|
|
for (i = 0; i < n; i++) {
|
|
self.attachmentsStore.replaceFeatureId(this.url, tempObjectIds[i], newObjectIds[i], function (success) {
|
|
--count;
|
|
successCount += (success ? 1 : 0);
|
|
if (count === 0) {
|
|
callback(successCount);
|
|
}
|
|
}.bind(this));
|
|
}
|
|
};
|
|
|
|
// we need to identify ADDs before sending them to the server
|
|
// we assign temporary ids (using negative numbers to distinguish them from real ids)
|
|
// query the database first to find any existing offline adds, and find the next lowest integer to start with.
|
|
this._editStore.getNextLowestTempId(layer, function(value, status){
|
|
if(status === "success"){
|
|
console.log("_nextTempId:", value);
|
|
layer._nextTempId = value;
|
|
}
|
|
else{
|
|
console.log("_nextTempId, not success:", value);
|
|
layer._nextTempId = -1;
|
|
console.debug(layer._nextTempId);
|
|
}
|
|
});
|
|
|
|
layer._getNextTempId = function () {
|
|
return this._nextTempId--;
|
|
};
|
|
|
|
function _initPhantomLayer() {
|
|
try {
|
|
layer._phantomLayer = new GraphicsLayer({opacity: 0.8});
|
|
layer._map.addLayer(layer._phantomLayer);
|
|
}
|
|
catch (err) {
|
|
console.log("Unable to init PhantomLayer: " + err.message);
|
|
}
|
|
}
|
|
|
|
_initPhantomLayer();
|
|
|
|
// We are currently only passing in a single deferred.
|
|
all(extendPromises).then(function (r) {
|
|
|
|
// DB already initialized
|
|
if(r.length === 0 && url){
|
|
// Initialize the internal featureLayerCollectionObject
|
|
if(this.ENABLE_FEATURECOLLECTION) {
|
|
layer._pushFeatureCollections(function(success){
|
|
if(success){
|
|
callback(true, null);
|
|
}
|
|
else {
|
|
callback(false, null);
|
|
}
|
|
});
|
|
}
|
|
else {
|
|
callback(true, null);
|
|
}
|
|
}
|
|
else if(r[0].success && !url){
|
|
|
|
// This functionality is specifically for offline restarts
|
|
// and attempts to retrieve a feature layer url.
|
|
// It's a hack because layer.toJson() doesn't convert layer.url.
|
|
this._editStore.getFeatureLayerJSON(function(success,message){
|
|
if(success) {
|
|
this._featureLayers[message.__featureLayerURL] = layer;
|
|
layer.url = message.__featureLayerURL;
|
|
|
|
// Initialize the internal featureLayerCollectionObject
|
|
if(this.ENABLE_FEATURECOLLECTION) {
|
|
layer._pushFeatureCollections(function(success){
|
|
if(success){
|
|
callback(true, null);
|
|
}
|
|
else {
|
|
callback(false, null);
|
|
}
|
|
});
|
|
}
|
|
else {
|
|
callback(true, null);
|
|
}
|
|
}
|
|
else {
|
|
// NOTE: We have to have a valid feature layer URL in order to initialize the featureLayerCollectionObject
|
|
console.error("getFeatureLayerJSON() failed and unable to create featureLayerCollectionObject.");
|
|
callback(false, message);
|
|
}
|
|
}.bind(this));
|
|
}
|
|
else if(r[0].success){
|
|
|
|
if(this.ENABLE_FEATURECOLLECTION) {
|
|
layer._pushFeatureCollections(function(success){
|
|
if(success){
|
|
callback(true, null);
|
|
}
|
|
else {
|
|
callback(false, null);
|
|
}
|
|
});
|
|
}
|
|
else {
|
|
callback(true, null);
|
|
}
|
|
}
|
|
}.bind(this));
|
|
|
|
}, // extend
|
|
|
|
/**
|
|
* Forces library into an offline state. Any edits applied during this condition will be stored locally
|
|
*/
|
|
goOffline: function () {
|
|
console.log("offlineFeatureManager going offline");
|
|
this._onlineStatus = this.OFFLINE;
|
|
},
|
|
|
|
/**
|
|
* 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
|
|
* @param callback callback( boolean, errors )
|
|
*/
|
|
goOnline: function (callback) {
|
|
console.log("OfflineEditAdvanced going online");
|
|
this._onlineStatus = this.RECONNECTING;
|
|
this._replayStoredEdits(function (success, responses) {
|
|
var result = {success: success, responses: responses};
|
|
this._onlineStatus = this.ONLINE;
|
|
if (this.attachmentsStore != null) {
|
|
console.log("sending attachments");
|
|
this._sendStoredAttachments(function (success, uploadedResponses, dbResponses) {
|
|
//this._onlineStatus = this.ONLINE;
|
|
result.attachments = {success: success, responses: uploadedResponses, dbResponses: dbResponses};
|
|
callback && callback(result);
|
|
}.bind(this));
|
|
}
|
|
else {
|
|
//this._onlineStatus = this.ONLINE;
|
|
callback && callback(result);
|
|
}
|
|
}.bind(this));
|
|
},
|
|
|
|
/**
|
|
* Determines if offline or online condition exists
|
|
* @returns {string} ONLINE or OFFLINE
|
|
*/
|
|
getOnlineStatus: function () {
|
|
return this._onlineStatus;
|
|
},
|
|
|
|
/**
|
|
* Serialize the feature layer graphics
|
|
* @param features Array of features
|
|
* @param callback Returns a JSON string
|
|
*/
|
|
serializeFeatureGraphicsArray: function (features, callback) {
|
|
var length = features.length;
|
|
var jsonArray = [];
|
|
for (var i = 0; i < length; i++) {
|
|
var jsonGraphic = features[i].toJson();
|
|
jsonArray.push(jsonGraphic);
|
|
if (i == (length - 1)) {
|
|
var featureJSON = JSON.stringify(jsonArray);
|
|
callback(featureJSON);
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Retrieves the feature collection object. Specifically used in offline browser restarts.
|
|
* This is an object created automatically by the library and is updated with every ADD, UPDATE and DELETE.
|
|
* Attachments are handled separately and not part of the feature collection created here.
|
|
*
|
|
* It has the following signature: {id: "feature-collection-object-1001",
|
|
* featureLayerCollections: [{ featureCollection: [Object], featureLayerUrl: String }]}
|
|
*
|
|
* @param callback
|
|
*/
|
|
getFeatureCollections: function(callback){
|
|
if(!this._editStore._isDBInit){
|
|
|
|
this._initializeDB(null,null).then(function(result){
|
|
if(result.success){
|
|
this._editStore._getFeatureCollections(function(success,message){
|
|
callback(success,message);
|
|
});
|
|
}
|
|
}.bind(this), function(err){
|
|
callback(false, err);
|
|
});
|
|
}
|
|
else {
|
|
this._editStore._getFeatureCollections(function(success,message){
|
|
callback(success,message);
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Retrieves the optional feature layer storage object
|
|
* For use in full offline scenarios.
|
|
* @param callback callback(true, object) || callback(false, error)
|
|
*/
|
|
getFeatureLayerJSONDataStore: function(callback){
|
|
if(!this._editStore._isDBInit){
|
|
|
|
this._initializeDB(null,null).then(function(result){
|
|
if(result.success){
|
|
this._editStore.getFeatureLayerJSON(function(success,message){
|
|
callback(success,message);
|
|
});
|
|
}
|
|
}.bind(this), function(err){
|
|
callback(false, err);
|
|
});
|
|
}
|
|
else {
|
|
this._editStore.getFeatureLayerJSON(function(success,message){
|
|
callback(success,message);
|
|
});
|
|
}
|
|
},
|
|
|
|
/* internal methods */
|
|
|
|
/**
|
|
* Initialize the database and push featureLayer JSON to DB if required.
|
|
* NOTE: also stores feature layer url in hidden dataStore property dataStore.__featureLayerURL.
|
|
* @param dataStore Object
|
|
* @param url Feature Layer's url. This is used by this library for internal feature identification.
|
|
* @param callback
|
|
* @private
|
|
*/
|
|
//_initializeDB: function(dataStore,url,callback){
|
|
_initializeDB: function(dataStore,url){
|
|
var deferred = new Deferred();
|
|
|
|
var editStore = this._editStore;
|
|
|
|
// Configure the database
|
|
editStore.dbName = this.DB_NAME;
|
|
editStore.objectStoreName = this.DB_OBJECTSTORE_NAME;
|
|
editStore.objectId = this.DB_UID;
|
|
|
|
// Attempt to initialize the database
|
|
editStore.init(function (result, error) {
|
|
|
|
////////////////////////////////////////////////////
|
|
// OFFLINE RESTART CONFIGURATION
|
|
// Added @ v2.5
|
|
//
|
|
// Configure database for offline restart
|
|
// dataStore object allows you to store data that you'll
|
|
// use after an offline browser restart.
|
|
//
|
|
// If dataStore Object is not defined then do nothing.
|
|
//
|
|
////////////////////////////////////////////////////
|
|
|
|
if (typeof dataStore === "object" && result === true && (dataStore !== undefined) && (dataStore !== null)) {
|
|
|
|
// Add a hidden property to hold the feature layer's url
|
|
// When converting a feature layer to json (layer.toJson()) we lose this information.
|
|
// This library needs to know the feature layer url.
|
|
if(url) {
|
|
dataStore.__featureLayerURL = url;
|
|
}
|
|
|
|
editStore.pushFeatureLayerJSON(dataStore, function (success, err) {
|
|
if (success) {
|
|
deferred.resolve({success:true, error: null});
|
|
}
|
|
else {
|
|
deferred.reject({success:false, error: err});
|
|
}
|
|
});
|
|
}
|
|
else if(result){
|
|
deferred.resolve({success:true, error: null});
|
|
}
|
|
else{
|
|
deferred.reject({success:false, error: null});
|
|
}
|
|
});
|
|
|
|
return deferred;
|
|
},
|
|
|
|
/**
|
|
* internal method that checks if this browser supports everything that is needed to handle offline attachments
|
|
* it also extends XMLHttpRequest with sendAsBinary() method, needed in Chrome
|
|
*/
|
|
_checkFileAPIs: function () {
|
|
if (window.File && window.FileReader && window.FileList && window.Blob) {
|
|
console.log("All APIs supported!");
|
|
|
|
if (!XMLHttpRequest.prototype.sendAsBinary) {
|
|
// https://code.google.com/p/chromium/issues/detail?id=35705#c40
|
|
XMLHttpRequest.prototype.sendAsBinary = function (datastr) {
|
|
function byteValue(x) {
|
|
return x.charCodeAt(0) & 0xff; // jshint ignore:line
|
|
}
|
|
|
|
var ords = Array.prototype.map.call(datastr, byteValue);
|
|
var ui8a = new Uint8Array(ords);
|
|
this.send(ui8a.buffer);
|
|
};
|
|
console.log("extending XMLHttpRequest");
|
|
}
|
|
return true;
|
|
}
|
|
console.log("The File APIs are not fully supported in this browser.");
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* internal method that extends an object with sendAsBinary() method
|
|
* sometimes extending XMLHttpRequest.prototype is not enough, as ArcGIS JS lib seems to mess with this object too
|
|
* @param oAjaxReq object to extend
|
|
*/
|
|
_extendAjaxReq: function (oAjaxReq) {
|
|
oAjaxReq.sendAsBinary = XMLHttpRequest.prototype.sendAsBinary;
|
|
console.log("extending XMLHttpRequest");
|
|
},
|
|
|
|
//
|
|
// phantom symbols
|
|
//
|
|
|
|
_phantomSymbols: [],
|
|
|
|
_getPhantomSymbol: function (geometry, operation) {
|
|
if (this._phantomSymbols.length === 0) {
|
|
var color = [0, 255, 0, 255];
|
|
var width = 1.5;
|
|
|
|
this._phantomSymbols.point = [];
|
|
this._phantomSymbols.point[this._editStore.ADD] = new SimpleMarkerSymbol({
|
|
"type": "esriSMS", "style": "esriSMSCross",
|
|
"xoffset": 10, "yoffset": 10,
|
|
"color": [255, 255, 255, 0], "size": 15,
|
|
"outline": {"color": color, "width": width, "type": "esriSLS", "style": "esriSLSSolid"}
|
|
});
|
|
this._phantomSymbols.point[this._editStore.UPDATE] = new SimpleMarkerSymbol({
|
|
"type": "esriSMS", "style": "esriSMSCircle",
|
|
"xoffset": 0, "yoffset": 0,
|
|
"color": [255, 255, 255, 0], "size": 15,
|
|
"outline": {"color": color, "width": width, "type": "esriSLS", "style": "esriSLSSolid"}
|
|
});
|
|
this._phantomSymbols.point[this._editStore.DELETE] = new SimpleMarkerSymbol({
|
|
"type": "esriSMS", "style": "esriSMSX",
|
|
"xoffset": 0, "yoffset": 0,
|
|
"color": [255, 255, 255, 0], "size": 15,
|
|
"outline": {"color": color, "width": width, "type": "esriSLS", "style": "esriSLSSolid"}
|
|
});
|
|
this._phantomSymbols.multipoint = null;
|
|
|
|
this._phantomSymbols.polyline = [];
|
|
this._phantomSymbols.polyline[this._editStore.ADD] = new SimpleLineSymbol({
|
|
"type": "esriSLS", "style": "esriSLSSolid",
|
|
"color": color, "width": width
|
|
});
|
|
this._phantomSymbols.polyline[this._editStore.UPDATE] = new SimpleLineSymbol({
|
|
"type": "esriSLS", "style": "esriSLSSolid",
|
|
"color": color, "width": width
|
|
});
|
|
this._phantomSymbols.polyline[this._editStore.DELETE] = new SimpleLineSymbol({
|
|
"type": "esriSLS", "style": "esriSLSSolid",
|
|
"color": color, "width": width
|
|
});
|
|
|
|
this._phantomSymbols.polygon = [];
|
|
this._phantomSymbols.polygon[this._editStore.ADD] = new SimpleFillSymbol({
|
|
"type": "esriSFS",
|
|
"style": "esriSFSSolid",
|
|
"color": [255, 255, 255, 0],
|
|
"outline": {"type": "esriSLS", "style": "esriSLSSolid", "color": color, "width": width}
|
|
});
|
|
this._phantomSymbols.polygon[this._editStore.UPDATE] = new SimpleFillSymbol({
|
|
"type": "esriSFS",
|
|
"style": "esriSFSSolid",
|
|
"color": [255, 255, 255, 0],
|
|
"outline": {"type": "esriSLS", "style": "esriSLSDash", "color": color, "width": width}
|
|
});
|
|
this._phantomSymbols.polygon[this._editStore.DELETE] = new SimpleFillSymbol({
|
|
"type": "esriSFS",
|
|
"style": "esriSFSSolid",
|
|
"color": [255, 255, 255, 0],
|
|
"outline": {"type": "esriSLS", "style": "esriSLSDot", "color": color, "width": width}
|
|
});
|
|
}
|
|
|
|
return this._phantomSymbols[geometry.type][operation];
|
|
},
|
|
|
|
//
|
|
// methods to handle attachment uploads
|
|
//
|
|
|
|
_uploadAttachment: function (attachment) {
|
|
var dfd = new Deferred();
|
|
|
|
var layer = this._featureLayers[attachment.featureLayerUrl];
|
|
|
|
var formData = new FormData();
|
|
formData.append("attachment",attachment.file);
|
|
|
|
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);
|
|
});
|
|
|
|
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;
|
|
}
|
|
|
|
return dfd.promise;
|
|
},
|
|
|
|
_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({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 uploadAttachmentComplete = this._uploadAttachment(attachment);
|
|
promises.push(uploadAttachmentComplete);
|
|
}, this);
|
|
console.log("promises", promises.length);
|
|
var allPromises = all(promises);
|
|
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);
|
|
}
|
|
});
|
|
},
|
|
function (err) {
|
|
console.log("error!", err);
|
|
callback && callback(false, err);
|
|
});
|
|
}.bind(this));
|
|
},
|
|
|
|
//
|
|
// methods to send features back to the server
|
|
//
|
|
|
|
/**
|
|
* Attempts to send any edits in the database. Monitor events for success or failure.
|
|
* @param callback
|
|
* @event ALL_EDITS_SENT when all edits have been successfully sent. Contains {[addResults],[updateResults],[deleteResults]}
|
|
* @event EDITS_SENT_ERROR some edits were not sent successfully. Contains {msg: error}
|
|
* @private
|
|
*/
|
|
_replayStoredEdits: function (callback) {
|
|
var promises = {};
|
|
var that = this;
|
|
|
|
//
|
|
// send edits for each of the layers
|
|
//
|
|
var layer;
|
|
var adds = [], updates = [], deletes = [];
|
|
var tempObjectIds = [];
|
|
var tempArray = [];
|
|
var featureLayers = this._featureLayers;
|
|
var attachmentsStore = this.attachmentsStore;
|
|
var editStore = this._editStore;
|
|
|
|
this._editStore.getAllEditsArray(function (result, err) {
|
|
if (result.length > 0) {
|
|
tempArray = result;
|
|
|
|
var length = tempArray.length;
|
|
|
|
for (var n = 0; n < length; n++) {
|
|
layer = featureLayers[tempArray[n].layer];
|
|
|
|
// If the layer has attachments then check to see if the attachmentsStore has been initialized
|
|
if (attachmentsStore == null && layer.hasAttachments) {
|
|
console.log("NOTICE: you may need to run OfflineEditAdvanced.initAttachments(). Check the Attachments doc for more info. Layer id: " + layer.id + " accepts attachments");
|
|
}
|
|
|
|
// Assign the attachmentsStore to the layer as a private var so we can access it from
|
|
// the promises applyEdits() method.
|
|
layer._attachmentsStore = attachmentsStore;
|
|
|
|
layer.__onEditsComplete = layer.onEditsComplete;
|
|
layer.onEditsComplete = function () {
|
|
console.log("intercepting events onEditsComplete");
|
|
};
|
|
|
|
// Let's zero everything out
|
|
adds = [], updates = [], deletes = [], tempObjectIds = [];
|
|
|
|
// IMPORTANT: reconstitute the graphic JSON into an actual esri.Graphic object
|
|
// NOTE: we are only sending one Graphic per loop!
|
|
var graphic = new Graphic(tempArray[n].graphic);
|
|
|
|
switch (tempArray[n].operation) {
|
|
case editStore.ADD:
|
|
for (var i = 0; i < layer.graphics.length; i++) {
|
|
var g = layer.graphics[i];
|
|
if (g.attributes[layer.objectIdField] === graphic.attributes[layer.objectIdField]) {
|
|
layer.remove(g);
|
|
break;
|
|
}
|
|
}
|
|
tempObjectIds.push(graphic.attributes[layer.objectIdField]);
|
|
delete graphic.attributes[layer.objectIdField];
|
|
adds.push(graphic);
|
|
break;
|
|
case editStore.UPDATE:
|
|
updates.push(graphic);
|
|
break;
|
|
case editStore.DELETE:
|
|
deletes.push(graphic);
|
|
break;
|
|
}
|
|
|
|
//if(that._featureCollectionUsageFlag){
|
|
// Note: when the feature layer is created with a feature collection we have to handle applyEdits() differently
|
|
// TO-DO rename this method.
|
|
promises[n] = that._internalApplyEditsAll(layer, tempArray[n].id, tempObjectIds, adds, updates, deletes);
|
|
}
|
|
|
|
// wait for all requests to finish
|
|
// responses contain {id,layer,tempId,addResults,updateResults,deleteResults}
|
|
var allPromises = all(promises);
|
|
allPromises.then(
|
|
function (responses) {
|
|
console.log("OfflineEditAdvanced sync - all responses are back");
|
|
|
|
this._parseResponsesArray(responses).then(function(result) {
|
|
if(result) {
|
|
this.emit(this.events.ALL_EDITS_SENT,responses);
|
|
}
|
|
else {
|
|
this.emit(this.events.EDITS_SENT_ERROR, {msg: "Not all edits synced", respones: responses});
|
|
}
|
|
callback && callback(true, responses);
|
|
}.bind(this));
|
|
}.bind(that),
|
|
function (errors) {
|
|
console.log("OfflineEditAdvanced._replayStoredEdits - ERROR!!");
|
|
console.log(errors);
|
|
callback && callback(false, errors);
|
|
}.bind(that)
|
|
);
|
|
|
|
}
|
|
else{
|
|
// No edits were found
|
|
callback(true,[]);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Deletes edits from database.
|
|
* This does not handle phantom graphics!
|
|
* @param edit
|
|
* @returns {l.Deferred.promise|*|c.promise|q.promise|promise}
|
|
* @private
|
|
*/
|
|
_updateDatabase: function (edit) {
|
|
var dfd = new Deferred();
|
|
var fakeGraphic = {};
|
|
fakeGraphic.attributes = {};
|
|
|
|
// Use the correct attributes key!
|
|
fakeGraphic.attributes[this.DB_UID] = edit.id;
|
|
|
|
this._editStore.delete(edit.layer, fakeGraphic, function (success, error) {
|
|
if (success) {
|
|
dfd.resolve({success: true, error: null});
|
|
}
|
|
else {
|
|
dfd.reject({success: false, error: error});
|
|
}
|
|
}.bind(this));
|
|
|
|
return dfd.promise;
|
|
|
|
},
|
|
|
|
/**
|
|
* Retrieves f=json from the feature layer
|
|
* @param url FeatureLayer's URL
|
|
* @param callback
|
|
* @private
|
|
*/
|
|
getFeatureLayerJSON: function (url, callback) {
|
|
require(["esri/request"], function (esriRequest) {
|
|
var request = esriRequest({
|
|
url: url,
|
|
content: {f: "json"},
|
|
handleAs: "json",
|
|
callbackParamName: "callback"
|
|
});
|
|
|
|
request.then(function (response) {
|
|
console.log("Success: ", response);
|
|
callback(true, response);
|
|
}, function (error) {
|
|
console.log("Error: ", error.message);
|
|
callback(false, error.message);
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Applies edits. This works with both standard feature layers and when a feature layer is created
|
|
* using a feature collection.
|
|
*
|
|
* This works around specific behaviors in esri.layers.FeatureLayer when using the pattern
|
|
* new FeatureLayer(featureCollectionObject).
|
|
*
|
|
* Details on the specific behaviors can be found here:
|
|
* https://developers.arcgis.com/javascript/jsapi/featurelayer-amd.html#featurelayer2
|
|
*
|
|
* @param layer
|
|
* @param id
|
|
* @param tempObjectIds
|
|
* @param adds
|
|
* @param updates
|
|
* @param deletes
|
|
* @returns {*|r}
|
|
* @private
|
|
*/
|
|
_internalApplyEditsAll: function (layer, id, tempObjectIds, adds, updates, deletes) {
|
|
var that = this;
|
|
var dfd = new Deferred();
|
|
|
|
this._makeEditRequest(layer, adds, updates, deletes,
|
|
function (addResults, updateResults, deleteResults) {
|
|
layer._phantomLayer.clear();
|
|
|
|
// We use a different pattern if the attachmentsStore is valid and the layer has attachments
|
|
if (layer._attachmentsStore != null && layer.hasAttachments && tempObjectIds.length > 0) {
|
|
|
|
var newObjectIds = addResults.map(function (r) {
|
|
return r.objectId;
|
|
});
|
|
|
|
layer._replaceFeatureIds(tempObjectIds, newObjectIds, function (count) {
|
|
console.log("Done replacing feature ids. Total count = " + count);
|
|
});
|
|
}
|
|
|
|
if(addResults.length > 0) {
|
|
var graphic = new Graphic(adds[0].geometry,null,adds[0].attributes);
|
|
layer.add(graphic);
|
|
}
|
|
|
|
that._cleanDatabase(layer, tempObjectIds, addResults, updateResults, deleteResults).then(function(results){
|
|
dfd.resolve({
|
|
id: id,
|
|
layer: layer.url,
|
|
tempId: tempObjectIds, // let's us internally match an ADD to it's new ObjectId
|
|
addResults: addResults,
|
|
updateResults: updateResults,
|
|
deleteResults: deleteResults,
|
|
databaseResults: results,
|
|
databaseErrors: null,
|
|
syncError: null
|
|
});
|
|
}, function(error) {
|
|
dfd.resolve({
|
|
id: id,
|
|
layer: layer.url,
|
|
tempId: tempObjectIds, // let's us internally match an ADD to it's new ObjectId
|
|
addResults: addResults,
|
|
updateResults: updateResults,
|
|
deleteResults: deleteResults,
|
|
databaseResults: null,
|
|
databaseErrors: error,
|
|
syncError: error
|
|
});
|
|
});
|
|
|
|
},
|
|
function (error) {
|
|
layer.onEditsComplete = layer.__onEditsComplete;
|
|
delete layer.__onEditsComplete;
|
|
|
|
dfd.reject(error);
|
|
}
|
|
);
|
|
return dfd.promise;
|
|
},
|
|
|
|
_cleanDatabase: function(layer, tempId, addResults, updateResults, deleteResults) {
|
|
|
|
var dfd = new Deferred();
|
|
var id = null;
|
|
|
|
if (updateResults.length > 0) {
|
|
if (updateResults[0].success) {
|
|
id = updateResults[0].objectId;
|
|
}
|
|
}
|
|
if (deleteResults.length > 0) {
|
|
if (deleteResults[0].success) {
|
|
id = deleteResults[0].objectId;
|
|
}
|
|
}
|
|
if (addResults.length > 0) {
|
|
if (addResults[0].success) {
|
|
id = tempId;
|
|
}
|
|
}
|
|
|
|
var fakeGraphic = {};
|
|
fakeGraphic.attributes = {};
|
|
|
|
// Use the correct attributes key!
|
|
fakeGraphic.attributes[this.DB_UID] = id;
|
|
|
|
// Delete the edit from the database
|
|
this._editStore.delete(layer.url, fakeGraphic, function (success, error) {
|
|
if (success) {
|
|
|
|
var id = this._editStore.PHANTOM_GRAPHIC_PREFIX + this._editStore._PHANTOM_PREFIX_TOKEN + fakeGraphic.attributes[this.DB_UID];
|
|
|
|
// Delete the phantom graphic associated with the dit
|
|
this._editStore.deletePhantomGraphic(id, function(success,error){
|
|
if(!success) {
|
|
console.log("_cleanDatabase delete phantom graphic error: " + error);
|
|
dfd.reject({success: false, error: error, id: id});
|
|
}
|
|
else {
|
|
console.log("_cleanDatabase success: " + id);
|
|
dfd.resolve({success: true, error: null, id: id});
|
|
}
|
|
});
|
|
}
|
|
else {
|
|
dfd.reject({success: false, error: error, id: id});
|
|
}
|
|
}.bind(this));
|
|
|
|
return dfd.promise;
|
|
},
|
|
|
|
/**
|
|
* Used when a feature layer is created with a feature collection.
|
|
*
|
|
* In the current version of the ArcGIS JSAPI 3.12+ the applyEdit() method doesn't send requests
|
|
* to the server when a feature layer is created with a feature collection.
|
|
*
|
|
* The use case for using this is: clean start app > go offline and make edits > offline restart browser >
|
|
* go online.
|
|
*
|
|
* @param layer
|
|
* @param adds
|
|
* @param updates
|
|
* @param deletes
|
|
* @returns {*|r}
|
|
* @private
|
|
*/
|
|
_makeEditRequest: function(layer,adds, updates, deletes, callback, errback) {
|
|
|
|
var f = "f=json", a = "", u = "", d = "";
|
|
|
|
if(adds.length > 0) {
|
|
array.forEach(adds, function(add){
|
|
if(add.hasOwnProperty("infoTemplate")){ // if the add has an infoTemplate attached,
|
|
delete add.infoTemplate; // delete it to reduce payload size.
|
|
}
|
|
}, this);
|
|
a = "&adds=" + JSON.stringify((adds));
|
|
}
|
|
if(updates.length > 0) {
|
|
array.forEach(updates, function(update){
|
|
if(update.hasOwnProperty("infoTemplate")){ // if the update has an infoTemplate attached,
|
|
delete update.infoTemplate; // delete it to reduce payload size.
|
|
}
|
|
}, this);
|
|
u = "&updates=" + JSON.stringify(updates);
|
|
}
|
|
if(deletes.length > 0) {
|
|
var id = deletes[0].attributes[this.DB_UID];
|
|
d = "&deletes=" + id;
|
|
}
|
|
|
|
var params = f + a + u + d;
|
|
|
|
if(layer.hasOwnProperty("credential") && layer.credential){
|
|
if(layer.credential.hasOwnProperty("token") && layer.credential.token){
|
|
params = params + "&token=" + layer.credential.token;
|
|
}
|
|
}
|
|
|
|
var req = new XMLHttpRequest();
|
|
req.open("POST", layer.url + "/applyEdits", true);
|
|
req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
|
req.onload = function()
|
|
{
|
|
if( req.status === 200 && req.responseText !== "")
|
|
{
|
|
try {
|
|
var obj = JSON.parse(this.response);
|
|
callback(obj.addResults, obj.updateResults, obj.deleteResults);
|
|
}
|
|
catch(err) {
|
|
console.error("EDIT REQUEST REPONSE WAS NOT SUCCESSFUL:", req);
|
|
errback("Unable to parse xhr response", req);
|
|
}
|
|
}
|
|
|
|
};
|
|
req.onerror = function(e)
|
|
{
|
|
console.error("_makeEditRequest failed: " + e);
|
|
errback(e);
|
|
};
|
|
req.ontimeout = function() {
|
|
errback("xhr timeout error");
|
|
};
|
|
req.timeout = this._defaultXhrTimeout;
|
|
req.send(params);
|
|
},
|
|
|
|
/**
|
|
* Parses the respones related to going back online and cleaning up the database.
|
|
* @param responses
|
|
* @returns {promise} True means all was successful. False indicates there was a problem.
|
|
* @private
|
|
*/
|
|
_parseResponsesArray: function(responses) {
|
|
|
|
var dfd = new Deferred();
|
|
var err = 0;
|
|
|
|
for (var key in responses) {
|
|
if (responses.hasOwnProperty(key)) {
|
|
responses[key].addResults.map(function(result){
|
|
if(!result.success) {
|
|
err++;
|
|
}
|
|
});
|
|
|
|
responses[key].updateResults.map(function(result){
|
|
if(!result.success) {
|
|
err++;
|
|
}
|
|
});
|
|
|
|
responses[key].deleteResults.map(function(result){
|
|
if(!result.success) {
|
|
err++;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
if(err > 0){
|
|
dfd.resolve(false);
|
|
}
|
|
else {
|
|
dfd.resolve(true);
|
|
}
|
|
|
|
return dfd.promise;
|
|
}
|
|
}); // declare
|
|
}); // define
|
|
/**
|
|
* Creates a namespace for the non-AMD libraries in this directory
|
|
*/
|
|
/*jshint -W020 */
|
|
if(typeof O != "undefined"){
|
|
O.esri.Edit = {};
|
|
}
|
|
else{
|
|
O = {};
|
|
O.esri = {
|
|
Edit: {}
|
|
};
|
|
}
|
|
/*global indexedDB */
|
|
/*jshint -W030 */
|
|
O.esri.Edit.EditStore = function () {
|
|
|
|
"use strict";
|
|
|
|
this._db = null;
|
|
this._isDBInit = false;
|
|
|
|
// Public properties
|
|
|
|
this.dbName = "features_store";
|
|
this.objectStoreName = "features";
|
|
this.objectId = "objectid"; // set this depending on how your feature service is configured;
|
|
|
|
//var _dbIndex = "featureId"; // @private
|
|
|
|
// ENUMs
|
|
|
|
this.ADD = "add";
|
|
this.UPDATE = "update";
|
|
this.DELETE = "delete";
|
|
|
|
this.FEATURE_LAYER_JSON_ID = "feature-layer-object-1001";
|
|
this.FEATURE_COLLECTION_ID = "feature-collection-object-1001";
|
|
this.PHANTOM_GRAPHIC_PREFIX = "phantom-layer";
|
|
this._PHANTOM_PREFIX_TOKEN = "|@|";
|
|
|
|
this.isSupported = function () {
|
|
if (!window.indexedDB) {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Commit an edit to the database
|
|
* @param operation add, update or delete
|
|
* @param layerUrl the URL of the feature layer
|
|
* @param graphic esri/graphic. The method will serialize to JSON
|
|
* @param callback callback(true, edit) or callback(false, error)
|
|
*/
|
|
this.pushEdit = function (operation, layerUrl, graphic, callback) {
|
|
|
|
var edit = {
|
|
id: layerUrl + "/" + graphic.attributes[this.objectId],
|
|
operation: operation,
|
|
layer: layerUrl,
|
|
type: graphic.geometry.type,
|
|
graphic: graphic.toJson()
|
|
};
|
|
|
|
if(typeof graphic.attributes[this.objectId] === "undefined") {
|
|
console.error("editsStore.pushEdit() - failed to insert undefined objectId into database. Did you set offlineEdit.DB_UID? " + JSON.stringify(graphic.attributes));
|
|
callback(false,"editsStore.pushEdit() - failed to insert undefined objectId into database. Did you set offlineEdit.DB_UID? " + JSON.stringify(graphic.attributes));
|
|
}
|
|
else{
|
|
var transaction = this._db.transaction([this.objectStoreName], "readwrite");
|
|
|
|
transaction.oncomplete = function (event) {
|
|
callback(true);
|
|
};
|
|
|
|
transaction.onerror = function (event) {
|
|
callback(false, event.target.error.message);
|
|
};
|
|
|
|
var objectStore = transaction.objectStore(this.objectStoreName);
|
|
objectStore.put(edit);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Use this to store any static FeatureLayer or related JSON data related to your app that will assist in restoring
|
|
* a FeatureLayer.
|
|
*
|
|
* Handles both adds and updates. It copies any object properties, so it will not, by default, overwrite the entire object.
|
|
*
|
|
* Example 1: If you just submit {featureLayerRenderer: {someJSON}} it will only update the featureLayerRenderer property
|
|
* Example 2: This is a full example
|
|
* {
|
|
* featureLayerJSON: ...,
|
|
* graphics: ..., // Serialized Feature Layer graphics. Must be serialized!
|
|
* renderer: ...,
|
|
* opacity: ...,
|
|
* outfields: ...,
|
|
* mode: ...,
|
|
* extent: ...,
|
|
* zoom: 7,
|
|
* lastEdit: ...
|
|
* }
|
|
*
|
|
* NOTE: "dataObject.id" is a reserved property. If you use "id" in your object this method will break.
|
|
* @param dataStore Object
|
|
* @param callback callback(true, null) or callback(false, error)
|
|
*/
|
|
this.pushFeatureLayerJSON = function (dataStore /*Object*/, callback) {
|
|
|
|
console.assert(this._db !== null, "indexeddb not initialized");
|
|
if (typeof dataStore != "object") {
|
|
callback(false, "dataObject type is not an object.");
|
|
}
|
|
|
|
var db = this._db;
|
|
dataStore.id = this.FEATURE_LAYER_JSON_ID;
|
|
|
|
this.getFeatureLayerJSON(function (success, result) {
|
|
|
|
var objectStore;
|
|
|
|
if (success && typeof result !== "undefined") {
|
|
|
|
objectStore = db.transaction([this.objectStoreName], "readwrite").objectStore(this.objectStoreName);
|
|
|
|
// Make a copy of the object
|
|
for (var key in dataStore) {
|
|
if (dataStore.hasOwnProperty(key)) {
|
|
result[key] = dataStore[key];
|
|
}
|
|
}
|
|
|
|
// Insert the update into the database
|
|
var updateFeatureLayerDataRequest = objectStore.put(result);
|
|
|
|
updateFeatureLayerDataRequest.onsuccess = function () {
|
|
callback(true, null);
|
|
};
|
|
|
|
updateFeatureLayerDataRequest.onerror = function (err) {
|
|
callback(false, err);
|
|
};
|
|
}
|
|
else {
|
|
|
|
var transaction = db.transaction([this.objectStoreName], "readwrite");
|
|
|
|
transaction.oncomplete = function (event) {
|
|
callback(true, null);
|
|
};
|
|
|
|
transaction.onerror = function (event) {
|
|
callback(false, event.target.error.message);
|
|
};
|
|
|
|
objectStore = transaction.objectStore(this.objectStoreName);
|
|
|
|
// Protect against data cloning errors since we don't validate the input object
|
|
// Example: if you attempt to use an esri.Graphic in its native form you'll get a data clone error
|
|
try {
|
|
objectStore.put(dataStore);
|
|
}
|
|
catch (err) {
|
|
callback(false, JSON.stringify(err));
|
|
}
|
|
}
|
|
}.bind(this));
|
|
};
|
|
|
|
/**
|
|
* Retrieve the FeatureLayer data object
|
|
* @param callback callback(true, object) || callback(false, error)
|
|
*/
|
|
this.getFeatureLayerJSON = function (callback) {
|
|
|
|
console.assert(this._db !== null, "indexeddb not initialized");
|
|
|
|
var objectStore = this._db.transaction([this.objectStoreName], "readwrite").objectStore(this.objectStoreName);
|
|
|
|
//Get the entry associated with the graphic
|
|
var objectStoreGraphicRequest = objectStore.get(this.FEATURE_LAYER_JSON_ID);
|
|
|
|
objectStoreGraphicRequest.onsuccess = function () {
|
|
var object = objectStoreGraphicRequest.result;
|
|
if (typeof object != "undefined") {
|
|
callback(true, object);
|
|
}
|
|
else {
|
|
callback(false, "nothing found");
|
|
}
|
|
};
|
|
|
|
objectStoreGraphicRequest.onerror = function (msg) {
|
|
callback(false, msg);
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Safe delete. Checks if id exists, then reverifies.
|
|
* @param callback callback(boolean, {message: String})
|
|
*/
|
|
this.deleteFeatureLayerJSON = function (callback) {
|
|
// NOTE: the implementation of the IndexedDB spec has a design fault with respect to
|
|
// handling deletes. The result of a delete operation is always designated as undefined.
|
|
// What this means is that there is no way to tell if an operation was successful or not.
|
|
// And, it will always return 'true.'
|
|
//
|
|
// In order to get around this we have to verify if after the attempted deletion operation
|
|
// if the record is or is not in the database. Kinda dumb, but that's how IndexedDB works.
|
|
//http://stackoverflow.com/questions/17137879/is-there-a-way-to-get-information-on-deleted-record-when-calling-indexeddbs-obj
|
|
|
|
var db = this._db;
|
|
var deferred = null;
|
|
var self = this;
|
|
|
|
var id = this.FEATURE_LAYER_JSON_ID;
|
|
|
|
require(["dojo/Deferred"], function (Deferred) {
|
|
deferred = new Deferred();
|
|
|
|
// Step 4 - Then we check to see if the record actually exists or not.
|
|
deferred.then(function (result) {
|
|
// IF the delete was successful, then the record should return an error because it doesn't exist.
|
|
// We aren't 100% sure how all platforms will perform so we also trap the promise for return results.
|
|
self.editExists(id).then(function (results) {
|
|
// If edit does exist then we have not been successful in deleting the object.
|
|
callback(false, {message: "object was not deleted."});
|
|
},
|
|
function (err) {
|
|
// If the result is false then in theory the id no longer exists
|
|
// and we should return 'true' to indicate a successful delete operation.
|
|
callback(true, {message: "id does not exist"}); //because we want this test to throw an error. That means item deleted.
|
|
});
|
|
},
|
|
// There was a problem with the delete operation on the database
|
|
// This error message will come from editExists();
|
|
function (err) {
|
|
callback(false, {message: "id does not exist"});
|
|
});
|
|
|
|
// Step 1 - lets see if record exits. If it does not then return callback. Otherwise,
|
|
// continue on with the deferred.
|
|
self.editExists(id).then(function (result) {
|
|
|
|
var objectStore = db.transaction([self.objectStoreName], "readwrite").objectStore(self.objectStoreName);
|
|
|
|
// Step 2 - go ahead and delete graphic
|
|
var objectStoreDeleteRequest = objectStore.delete(id);
|
|
|
|
// Step 3 - We know that the onsuccess will always fire unless something serious goes wrong.
|
|
// So we go ahead and resolve the deferred here.
|
|
objectStoreDeleteRequest.onsuccess = function () {
|
|
deferred.resolve(true);
|
|
};
|
|
|
|
objectStoreDeleteRequest.onerror = function (msg) {
|
|
deferred.reject({success: false, error: msg});
|
|
};
|
|
|
|
},
|
|
// If there is an error in editExists()
|
|
function (err) {
|
|
deferred.reject({success: false, message: err});
|
|
}.bind(this));
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Add a phantom graphic to the store.
|
|
* IMPORTANT! Requires graphic to have an objectId
|
|
* @param graphic
|
|
* @param callback
|
|
*/
|
|
this.pushPhantomGraphic = function (graphic, callback) {
|
|
console.assert(this._db !== null, "indexeddb not initialized");
|
|
|
|
var db = this._db;
|
|
var id = this.PHANTOM_GRAPHIC_PREFIX + this._PHANTOM_PREFIX_TOKEN + graphic.attributes[this.objectId];
|
|
|
|
var object = {
|
|
id: id,
|
|
graphic: graphic.toJson()
|
|
};
|
|
|
|
var transaction = db.transaction([this.objectStoreName], "readwrite");
|
|
|
|
transaction.oncomplete = function (event) {
|
|
callback(true, null);
|
|
};
|
|
|
|
transaction.onerror = function (event) {
|
|
callback(false, event.target.error.message);
|
|
};
|
|
|
|
var objectStore = transaction.objectStore(this.objectStoreName);
|
|
objectStore.put(object);
|
|
|
|
};
|
|
|
|
/**
|
|
* Return an array of phantom graphics
|
|
* @param callback
|
|
*/
|
|
this.getPhantomGraphicsArray = function (callback) {
|
|
console.assert(this._db !== null, "indexeddb not initialized");
|
|
var editsArray = [];
|
|
|
|
if (this._db !== null) {
|
|
|
|
var phantomGraphicPrefix = this.PHANTOM_GRAPHIC_PREFIX;
|
|
|
|
var transaction = this._db.transaction([this.objectStoreName])
|
|
.objectStore(this.objectStoreName)
|
|
.openCursor();
|
|
|
|
transaction.onsuccess = function (event) {
|
|
var cursor = event.target.result;
|
|
if (cursor && cursor.value && cursor.value.id) {
|
|
|
|
// Make sure we are not return FeatureLayer JSON data or a Phantom Graphic
|
|
if (cursor.value.id.indexOf(phantomGraphicPrefix) != -1) {
|
|
editsArray.push(cursor.value);
|
|
|
|
}
|
|
cursor.continue();
|
|
}
|
|
else {
|
|
callback(editsArray, "end");
|
|
}
|
|
}.bind(this);
|
|
transaction.onerror = function (err) {
|
|
callback(null, err);
|
|
};
|
|
}
|
|
else {
|
|
callback(null, "no db");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Internal method that returns an array of id's only
|
|
* @param callback
|
|
* @private
|
|
*/
|
|
this._getPhantomGraphicsArraySimple = function (callback) {
|
|
console.assert(this._db !== null, "indexeddb not initialized");
|
|
var editsArray = [];
|
|
|
|
if (this._db !== null) {
|
|
|
|
var phantomGraphicPrefix = this.PHANTOM_GRAPHIC_PREFIX;
|
|
|
|
var transaction = this._db.transaction([this.objectStoreName])
|
|
.objectStore(this.objectStoreName)
|
|
.openCursor();
|
|
|
|
transaction.onsuccess = function (event) {
|
|
var cursor = event.target.result;
|
|
if (cursor && cursor.value && cursor.value.id) {
|
|
|
|
// Make sure we are not return FeatureLayer JSON data or a Phantom Graphic
|
|
if (cursor.value.id.indexOf(phantomGraphicPrefix) != -1) {
|
|
editsArray.push(cursor.value.id);
|
|
|
|
}
|
|
cursor.continue();
|
|
}
|
|
else {
|
|
callback(editsArray, "end");
|
|
}
|
|
}.bind(this);
|
|
transaction.onerror = function (err) {
|
|
callback(null, err);
|
|
};
|
|
}
|
|
else {
|
|
callback(null, "no db");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Deletes an individual graphic from the phantom layer
|
|
* @param id Internal ID
|
|
* @param callback callback(boolean, message)
|
|
*/
|
|
this.deletePhantomGraphic = function (id, callback) {
|
|
// NOTE: the implementation of the IndexedDB spec has a design fault with respect to
|
|
// handling deletes. The result of a delete operation is always designated as undefined.
|
|
// What this means is that there is no way to tell if an operation was successful or not.
|
|
// And, it will always return 'true.'
|
|
//
|
|
// In order to get around this we have to verify if after the attempted deletion operation
|
|
// if the record is or is not in the database. Kinda dumb, but that's how IndexedDB works.
|
|
//http://stackoverflow.com/questions/17137879/is-there-a-way-to-get-information-on-deleted-record-when-calling-indexeddbs-obj
|
|
|
|
var db = this._db;
|
|
var deferred = null;
|
|
var self = this;
|
|
|
|
require(["dojo/Deferred"], function (Deferred) {
|
|
deferred = new Deferred();
|
|
|
|
// Step 1 - lets see if record exits. If it does then return callback.
|
|
self.editExists(id).then(function (result) {
|
|
|
|
// Step 4 - Then we check to see if the record actually exists or not.
|
|
deferred.then(function (result) {
|
|
|
|
// IF the delete was successful, then the record should return 'false' because it doesn't exist.
|
|
self.editExists(id).then(function (results) {
|
|
callback(false, "item was not deleted"); // item is still in the database!!
|
|
},
|
|
function (err) {
|
|
callback(true, "item successfully deleted"); //because we want this test to throw an error. That means item deleted.
|
|
});
|
|
},
|
|
// There was a problem with the delete operation on the database
|
|
function (err) {
|
|
callback(false, err);
|
|
});
|
|
|
|
var objectStore = db.transaction([self.objectStoreName], "readwrite").objectStore(self.objectStoreName);
|
|
|
|
// Step 2 - go ahead and delete graphic
|
|
var objectStoreDeleteRequest = objectStore.delete(id);
|
|
|
|
// Step 3 - We know that the onsuccess will always fire unless something serious goes wrong.
|
|
// So we go ahead and resolve the deferred here.
|
|
objectStoreDeleteRequest.onsuccess = function () {
|
|
deferred.resolve(true);
|
|
};
|
|
|
|
objectStoreDeleteRequest.onerror = function (msg) {
|
|
deferred.reject({success: false, error: msg});
|
|
};
|
|
},
|
|
// If there is an error in editExists()
|
|
function (err) {
|
|
callback(false, "item doesn't exist in db");
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Removes all phantom graphics from database
|
|
* @param callback boolean
|
|
*/
|
|
this.resetPhantomGraphicsQueue = function (callback) {
|
|
|
|
var db = this._db;
|
|
|
|
// First we need to get the array of graphics that are stored in the database
|
|
// so that we can cycle thru them.
|
|
this._getPhantomGraphicsArraySimple(function (array) {
|
|
if (array != []) {
|
|
|
|
var errors = 0;
|
|
var tx = db.transaction([this.objectStoreName], "readwrite");
|
|
var objectStore = tx.objectStore(this.objectStoreName);
|
|
|
|
objectStore.onerror = function () {
|
|
errors++;
|
|
};
|
|
|
|
tx.oncomplete = function () {
|
|
errors === 0 ? callback(true) : callback(false);
|
|
};
|
|
|
|
var length = array.length;
|
|
for (var i = 0; i < length; i++) {
|
|
objectStore.delete(array[i]);
|
|
}
|
|
}
|
|
else {
|
|
callback(true);
|
|
}
|
|
}.bind(this));
|
|
};
|
|
|
|
/**
|
|
* Retrieve an edit by its internal ID
|
|
* @param id String identifier
|
|
* @param callback callback(true,graphic) or callback(false, error)
|
|
*/
|
|
this.getEdit = function(id,callback){
|
|
|
|
console.assert(this._db !== null, "indexeddb not initialized");
|
|
var objectStore = this._db.transaction([this.objectStoreName], "readwrite").objectStore(this.objectStoreName);
|
|
|
|
if(typeof id === "undefined"){
|
|
callback(false,"id is undefined.");
|
|
return;
|
|
}
|
|
|
|
//Get the entry associated with the graphic
|
|
var objectStoreGraphicRequest = objectStore.get(id);
|
|
|
|
objectStoreGraphicRequest.onsuccess = function () {
|
|
var graphic = objectStoreGraphicRequest.result;
|
|
if (graphic && (graphic.id == id)) {
|
|
callback(true,graphic);
|
|
}
|
|
else {
|
|
callback(false,"Id not found");
|
|
}
|
|
};
|
|
|
|
objectStoreGraphicRequest.onerror = function (msg) {
|
|
callback(false,msg);
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Returns all the edits recursively via the callback
|
|
* @param callback {value, message}
|
|
*/
|
|
this.getAllEdits = function (callback) {
|
|
|
|
console.assert(this._db !== null, "indexeddb not initialized");
|
|
|
|
if (this._db !== null) {
|
|
|
|
var fLayerJSONId = this.FEATURE_LAYER_JSON_ID;
|
|
var fCollectionId = this.FEATURE_COLLECTION_ID;
|
|
var phantomGraphicPrefix = this.PHANTOM_GRAPHIC_PREFIX;
|
|
|
|
var transaction = this._db.transaction([this.objectStoreName])
|
|
.objectStore(this.objectStoreName)
|
|
.openCursor();
|
|
|
|
transaction.onsuccess = function (event) {
|
|
var cursor = event.target.result;
|
|
if (cursor && cursor.hasOwnProperty("value") && cursor.value.hasOwnProperty("id")) {
|
|
|
|
// Make sure we are not return FeatureLayer JSON data or a Phantom Graphic
|
|
if (cursor.value.id !== fLayerJSONId && cursor.value.id !== fCollectionId && cursor.value.id.indexOf(phantomGraphicPrefix) == -1) {
|
|
callback(cursor.value, null);
|
|
}
|
|
cursor.continue();
|
|
}
|
|
else {
|
|
callback(null, "end");
|
|
}
|
|
}.bind(this);
|
|
transaction.onerror = function (err) {
|
|
callback(null, err);
|
|
};
|
|
}
|
|
else {
|
|
callback(null, "no db");
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Query the database, looking for any existing Add temporary OIDs, and return the nextTempId to be used.
|
|
* @param feature - extended layer from offline edit advanced
|
|
* @param callback {int, messageString} or {null, messageString}
|
|
*/
|
|
this.getNextLowestTempId = function (feature, callback) {
|
|
var addOIDsArray = [],
|
|
self = this;
|
|
|
|
if (this._db !== null) {
|
|
|
|
var fLayerJSONId = this.FEATURE_LAYER_JSON_ID;
|
|
var fCollectionId = this.FEATURE_COLLECTION_ID;
|
|
var phantomGraphicPrefix = this.PHANTOM_GRAPHIC_PREFIX;
|
|
|
|
var transaction = this._db.transaction([this.objectStoreName])
|
|
.objectStore(this.objectStoreName)
|
|
.openCursor();
|
|
|
|
transaction.onsuccess = function (event) {
|
|
var cursor = event.target.result;
|
|
if (cursor && cursor.value && cursor.value.id) {
|
|
// Make sure we are not return FeatureLayer JSON data or a Phantom Graphic
|
|
if (cursor.value.id !== fLayerJSONId && cursor.value.id !== fCollectionId && cursor.value.id.indexOf(phantomGraphicPrefix) == -1) {
|
|
if(cursor.value.layer === feature.url && cursor.value.operation === "add"){ // check to make sure the edit is for the feature we are looking for, and that the operation is an add.
|
|
addOIDsArray.push(cursor.value.graphic.attributes[self.objectId]); // add the temporary OID to the array
|
|
}
|
|
}
|
|
cursor.continue();
|
|
}
|
|
else {
|
|
if(addOIDsArray.length === 0){ // if we didn't find anything,
|
|
callback(-1, "success"); // we'll start with -1
|
|
}
|
|
else{
|
|
var filteredOIDsArray = addOIDsArray.filter(function(val){ // filter out any non numbers from the array...
|
|
return !isNaN(val); // .. should anything have snuck in or returned a NaN
|
|
});
|
|
var lowestTempId = Math.min.apply(Math, filteredOIDsArray); // then find the lowest number from the array
|
|
callback(lowestTempId-1, "success"); // and we'll start with one less than tat.
|
|
}
|
|
}
|
|
}.bind(this);
|
|
transaction.onerror = function (err) {
|
|
callback(null, err);
|
|
};
|
|
}
|
|
else {
|
|
callback(null, "no db");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns all the edits as a single Array via the callback
|
|
* @param callback {array, messageString} or {null, messageString}
|
|
*/
|
|
this.getAllEditsArray = function (callback) {
|
|
|
|
console.assert(this._db !== null, "indexeddb not initialized");
|
|
var editsArray = [];
|
|
|
|
if (this._db !== null) {
|
|
|
|
var fLayerJSONId = this.FEATURE_LAYER_JSON_ID;
|
|
var fCollectionId = this.FEATURE_COLLECTION_ID;
|
|
var phantomGraphicPrefix = this.PHANTOM_GRAPHIC_PREFIX;
|
|
|
|
var transaction = this._db.transaction([this.objectStoreName])
|
|
.objectStore(this.objectStoreName)
|
|
.openCursor();
|
|
|
|
transaction.onsuccess = function (event) {
|
|
var cursor = event.target.result;
|
|
if (cursor && cursor.value && cursor.value.id) {
|
|
|
|
// Make sure we are not return FeatureLayer JSON data or a Phantom Graphic
|
|
if (cursor.value.id !== fLayerJSONId && cursor.value.id !== fCollectionId && cursor.value.id.indexOf(phantomGraphicPrefix) == -1) {
|
|
editsArray.push(cursor.value);
|
|
|
|
}
|
|
cursor.continue();
|
|
}
|
|
else {
|
|
callback(editsArray, "end");
|
|
}
|
|
}.bind(this);
|
|
transaction.onerror = function (err) {
|
|
callback(null, err);
|
|
};
|
|
}
|
|
else {
|
|
callback(null, "no db");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Update an edit already exists in the database
|
|
* @param operation add, update or delete
|
|
* @param layer the URL of the feature layer
|
|
* @param graphic esri/graphic. The method will serialize to JSON
|
|
* @param callback {true, edit} or {false, error}
|
|
*/
|
|
this.updateExistingEdit = function (operation, layer, graphic, callback) {
|
|
|
|
console.assert(this._db !== null, "indexeddb not initialized");
|
|
|
|
var objectStore = this._db.transaction([this.objectStoreName], "readwrite").objectStore(this.objectStoreName);
|
|
|
|
//Let's get the entry associated with the graphic
|
|
var objectStoreGraphicRequest = objectStore.get(graphic.attributes[this.objectId]);
|
|
objectStoreGraphicRequest.onsuccess = function () {
|
|
|
|
//Grab the data object returned as a result
|
|
// TO-DO Do we keep this??
|
|
objectStoreGraphicRequest.result;
|
|
|
|
//Create a new update object
|
|
var update = {
|
|
id: layer + "/" + graphic.attributes[this.objectId],
|
|
operation: operation,
|
|
layer: layer,
|
|
graphic: graphic.toJson()
|
|
};
|
|
|
|
// Insert the update into the database
|
|
var updateGraphicRequest = objectStore.put(update);
|
|
|
|
updateGraphicRequest.onsuccess = function () {
|
|
callback(true);
|
|
};
|
|
|
|
updateGraphicRequest.onerror = function (err) {
|
|
callback(false, err);
|
|
};
|
|
}.bind(this);
|
|
};
|
|
|
|
/**
|
|
* Delete a pending edit's record from the database.
|
|
* IMPORTANT: Be aware of false negatives. See Step 4 in this function.
|
|
*
|
|
* @param layerUrl
|
|
* @param graphic Graphic
|
|
* @param callback {boolean, error}
|
|
*/
|
|
this.delete = function (layerUrl, graphic, callback) {
|
|
|
|
// NOTE: the implementation of the IndexedDB spec has a design fault with respect to
|
|
// handling deletes. The result of a delete operation is always designated as undefined.
|
|
// What this means is that there is no way to tell if an operation was successful or not.
|
|
// And, it will always return 'true.'
|
|
//
|
|
// In order to get around this we have to verify if after the attempted deletion operation
|
|
// if the record is or is not in the database. Kinda dumb, but that's how IndexedDB works.
|
|
//http://stackoverflow.com/questions/17137879/is-there-a-way-to-get-information-on-deleted-record-when-calling-indexeddbs-obj
|
|
|
|
var db = this._db;
|
|
var deferred = null;
|
|
var self = this;
|
|
|
|
var id = layerUrl + "/" + graphic.attributes[this.objectId];
|
|
|
|
require(["dojo/Deferred"], function (Deferred) {
|
|
deferred = new Deferred();
|
|
|
|
// Step 1 - lets see if record exits. If it does then return callback.
|
|
self.editExists(id).then(function (result) {
|
|
|
|
// Step 4 - Then we check to see if the record actually exists or not.
|
|
deferred.then(function (result) {
|
|
|
|
// IF the delete was successful, then the record should return 'false' because it doesn't exist.
|
|
self.editExists(id).then(function (results) {
|
|
callback(false);
|
|
},
|
|
function (err) {
|
|
callback(true); //because we want this test to throw an error. That means item deleted.
|
|
});
|
|
},
|
|
// There was a problem with the delete operation on the database
|
|
function (err) {
|
|
callback(false, err);
|
|
});
|
|
|
|
var objectStore = db.transaction([self.objectStoreName], "readwrite").objectStore(self.objectStoreName);
|
|
|
|
// Step 2 - go ahead and delete graphic
|
|
var objectStoreDeleteRequest = objectStore.delete(id);
|
|
|
|
// Step 3 - We know that the onsuccess will always fire unless something serious goes wrong.
|
|
// So we go ahead and resolve the deferred here.
|
|
objectStoreDeleteRequest.onsuccess = function () {
|
|
deferred.resolve(true);
|
|
};
|
|
|
|
objectStoreDeleteRequest.onerror = function (msg) {
|
|
deferred.reject({success: false, error: msg});
|
|
};
|
|
|
|
},
|
|
// If there is an error in editExists()
|
|
function (err) {
|
|
callback(false);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Full 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.
|
|
* @param callback boolean
|
|
*/
|
|
this.resetEditsQueue = 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);
|
|
};
|
|
};
|
|
|
|
this.pendingEditsCount = function (callback) {
|
|
console.assert(this._db !== null, "indexeddb not initialized");
|
|
|
|
var count = 0;
|
|
var id = this.FEATURE_LAYER_JSON_ID;
|
|
var fCollectionId = this.FEATURE_COLLECTION_ID;
|
|
var phantomGraphicPrefix = this.PHANTOM_GRAPHIC_PREFIX;
|
|
|
|
var transaction = this._db.transaction([this.objectStoreName], "readwrite");
|
|
var objectStore = transaction.objectStore(this.objectStoreName);
|
|
objectStore.openCursor().onsuccess = function (evt) {
|
|
var cursor = evt.target.result;
|
|
|
|
// IMPORTANT:
|
|
// Remember that we have feature layer JSON and Phantom Graphics in the same database
|
|
if (cursor && cursor.value && cursor.value.id && cursor.value.id.indexOf(phantomGraphicPrefix) == -1) {
|
|
if (cursor.value.id !== id && cursor.value.id !== fCollectionId) {
|
|
count++;
|
|
}
|
|
cursor.continue();
|
|
}
|
|
else {
|
|
callback(count);
|
|
}
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Verify is an edit already exists in the database. Checks the objectId.
|
|
* @param id
|
|
* @returns {deferred} {success: boolean, error: message}
|
|
* @private
|
|
*/
|
|
this.editExists = function (id) {
|
|
|
|
var db = this._db;
|
|
var deferred = null;
|
|
var self = this;
|
|
|
|
require(["dojo/Deferred"], function (Deferred) {
|
|
deferred = new Deferred();
|
|
|
|
var objectStore = db.transaction([self.objectStoreName], "readwrite").objectStore(self.objectStoreName);
|
|
|
|
//Get the entry associated with the graphic
|
|
var objectStoreGraphicRequest = objectStore.get(id);
|
|
|
|
objectStoreGraphicRequest.onsuccess = function () {
|
|
var graphic = objectStoreGraphicRequest.result;
|
|
if (graphic && (graphic.id == id)) {
|
|
deferred.resolve({success: true, error: null});
|
|
}
|
|
else {
|
|
deferred.reject({success: false, error: "objectId is not a match."});
|
|
}
|
|
};
|
|
|
|
objectStoreGraphicRequest.onerror = function (msg) {
|
|
deferred.reject({success: false, error: msg});
|
|
};
|
|
});
|
|
|
|
//We return a deferred object so that when calling this function you can chain it with a then() statement.
|
|
return deferred;
|
|
};
|
|
|
|
/**
|
|
* Returns the approximate size of the database in bytes
|
|
* IMPORTANT: Currently requires all data be serialized!
|
|
* @param callback callback({usage}, error) Whereas, the usage Object is {sizeBytes: number, editCount: number}
|
|
*/
|
|
this.getUsage = function (callback) {
|
|
console.assert(this._db !== null, "indexeddb not initialized");
|
|
|
|
var id = this.FEATURE_LAYER_JSON_ID;
|
|
var fCollectionId = this.FEATURE_COLLECTION_ID;
|
|
var phantomGraphicPrefix = this.PHANTOM_GRAPHIC_PREFIX;
|
|
|
|
var usage = {sizeBytes: 0, editCount: 0};
|
|
|
|
var transaction = this._db.transaction([this.objectStoreName])
|
|
.objectStore(this.objectStoreName)
|
|
.openCursor();
|
|
|
|
console.log("dumping keys");
|
|
|
|
transaction.onsuccess = function (event) {
|
|
var cursor = event.target.result;
|
|
if (cursor && cursor.value && cursor.value.id) {
|
|
var storedObject = cursor.value;
|
|
var json = JSON.stringify(storedObject);
|
|
usage.sizeBytes += json.length;
|
|
|
|
if (cursor.value.id.indexOf(phantomGraphicPrefix) == -1 && cursor.value.id !== id && cursor.value.id !== fCollectionId) {
|
|
usage.editCount += 1;
|
|
}
|
|
|
|
cursor.continue();
|
|
}
|
|
else {
|
|
callback(usage, null);
|
|
}
|
|
};
|
|
transaction.onerror = function (err) {
|
|
callback(null, err);
|
|
};
|
|
};
|
|
|
|
//
|
|
// internal methods
|
|
//
|
|
|
|
/**
|
|
* The library automatically keeps a copy of the featureLayerCollection and its
|
|
* associated layer.url.
|
|
*
|
|
* There should be only one featureLayerCollection Object per feature layer.
|
|
* @param featureCollectionObject
|
|
* @param callback
|
|
* @private
|
|
*/
|
|
this._pushFeatureCollections = function(featureCollectionObject, callback){
|
|
var transaction = this._db.transaction([this.objectStoreName], "readwrite");
|
|
|
|
transaction.oncomplete = function (event) {
|
|
callback(true);
|
|
};
|
|
|
|
transaction.onerror = function (event) {
|
|
callback(false, event.target.error.message);
|
|
};
|
|
|
|
var objectStore = transaction.objectStore(this.objectStoreName);
|
|
objectStore.put(featureCollectionObject);
|
|
};
|
|
|
|
this._getFeatureCollections = function(callback){
|
|
var objectStore = this._db.transaction([this.objectStoreName], "readonly").objectStore(this.objectStoreName);
|
|
|
|
//Get the entry associated with the graphic
|
|
var objectStoreGraphicRequest = objectStore.get(this.FEATURE_COLLECTION_ID);
|
|
|
|
objectStoreGraphicRequest.onsuccess = function () {
|
|
var object = objectStoreGraphicRequest.result;
|
|
if (typeof object != "undefined") {
|
|
callback(true, object);
|
|
}
|
|
else {
|
|
callback(false, null);
|
|
}
|
|
};
|
|
|
|
objectStoreGraphicRequest.onerror = function (msg) {
|
|
callback(false, msg);
|
|
};
|
|
};
|
|
|
|
this.init = function (callback) {
|
|
console.log("init editsStore.js");
|
|
|
|
var request = indexedDB.open(this.dbName, 11);
|
|
callback = callback || function (success) {
|
|
console.log("EditsStore::init() success:", success);
|
|
}.bind(this);
|
|
|
|
request.onerror = function (event) {
|
|
console.log("indexedDB error: " + event.target.errorCode);
|
|
callback(false, event.target.errorCode);
|
|
}.bind(this);
|
|
|
|
request.onupgradeneeded = function (event) {
|
|
var db = event.target.result;
|
|
|
|
if (db.objectStoreNames.contains(this.objectStoreName)) {
|
|
db.deleteObjectStore(this.objectStoreName);
|
|
}
|
|
|
|
db.createObjectStore(this.objectStoreName, {keyPath: "id"});
|
|
}.bind(this);
|
|
|
|
request.onsuccess = function (event) {
|
|
this._db = event.target.result;
|
|
this._isDBInit = true;
|
|
console.log("database opened successfully");
|
|
callback(true, null);
|
|
}.bind(this);
|
|
};
|
|
};
|
|
|
|
|
|
|
|
/*global IDBKeyRange,indexedDB */
|
|
|
|
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 () {
|
|
if (!window.indexedDB) {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* 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 {
|
|
// 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);
|
|
};
|
|
|
|
try {
|
|
transaction.objectStore(this.objectStoreName).put(newAttachment);
|
|
}
|
|
catch(err) {
|
|
callback(false, err);
|
|
}
|
|
}
|
|
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);
|
|
callback(false, err.stack);
|
|
}
|
|
};
|
|
|
|
this.retrieve = function (attachmentId, callback) {
|
|
console.assert(this._db !== null, "indexeddb not initialized");
|
|
|
|
var objectStore = this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName);
|
|
var request = objectStore.get(attachmentId);
|
|
request.onsuccess = function (event) {
|
|
var result = event.target.result;
|
|
if (!result) {
|
|
callback(false, "not found");
|
|
}
|
|
else {
|
|
callback(true, result);
|
|
}
|
|
};
|
|
request.onerror = function (err) {
|
|
console.log(err);
|
|
callback(false, err);
|
|
};
|
|
};
|
|
|
|
this.getAttachmentsByFeatureId = function (featureLayerUrl, objectId, callback) {
|
|
console.assert(this._db !== null, "indexeddb not initialized");
|
|
|
|
var featureId = featureLayerUrl + "/" + objectId;
|
|
var attachments = [];
|
|
|
|
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) {
|
|
var cursor = evt.target.result;
|
|
if (cursor) {
|
|
attachments.push(cursor.value);
|
|
cursor.continue();
|
|
}
|
|
else {
|
|
callback(attachments);
|
|
}
|
|
};
|
|
};
|
|
|
|
this.getAttachmentsByFeatureLayer = function (featureLayerUrl, callback) {
|
|
console.assert(this._db !== null, "indexeddb not initialized");
|
|
|
|
var attachments = [];
|
|
|
|
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) {
|
|
attachments.push(cursor.value);
|
|
cursor.continue();
|
|
}
|
|
else {
|
|
callback(attachments);
|
|
}
|
|
};
|
|
};
|
|
|
|
this.getAllAttachments = function (callback) {
|
|
console.assert(this._db !== null, "indexeddb not initialized");
|
|
|
|
var attachments = [];
|
|
|
|
var objectStore = this._db.transaction([this.objectStoreName]).objectStore(this.objectStoreName);
|
|
objectStore.openCursor().onsuccess = function (evt) {
|
|
var cursor = evt.target.result;
|
|
if (cursor) {
|
|
attachments.push(cursor.value);
|
|
cursor.continue();
|
|
}
|
|
else {
|
|
callback(attachments);
|
|
}
|
|
};
|
|
};
|
|
|
|
this.deleteAttachmentsByFeatureId = function (featureLayerUrl, objectId, callback) {
|
|
console.assert(this._db !== null, "indexeddb not initialized");
|
|
|
|
var featureId = featureLayerUrl + "/" + objectId;
|
|
|
|
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);
|
|
objectStore.delete(cursor.primaryKey);
|
|
deletedCount++;
|
|
cursor.continue();
|
|
}
|
|
else {
|
|
setTimeout(function () {
|
|
callback(deletedCount);
|
|
}, 0);
|
|
}
|
|
}.bind(this);
|
|
};
|
|
|
|
this.delete = function (attachmentId, callback) {
|
|
console.assert(this._db !== null, "indexeddb not initialized");
|
|
|
|
// before deleting an attachment we must revoke the blob URL that it contains
|
|
// in order to free memory in the browser
|
|
this.retrieve(attachmentId, function (success, attachment) {
|
|
if (!success) {
|
|
callback(false, "attachment " + attachmentId + " not found");
|
|
return;
|
|
}
|
|
|
|
//this._revokeLocalURL(attachment);
|
|
|
|
var request = this._db.transaction([this.objectStoreName], "readwrite")
|
|
.objectStore(this.objectStoreName)
|
|
.delete(attachmentId);
|
|
request.onsuccess = function (event) {
|
|
setTimeout(function () {
|
|
callback(true);
|
|
}, 0);
|
|
};
|
|
request.onerror = function (err) {
|
|
callback(false, err);
|
|
};
|
|
}.bind(this));
|
|
};
|
|
|
|
this.deleteAll = function (callback) {
|
|
console.assert(this._db !== null, "indexeddb not initialized");
|
|
|
|
this.getAllAttachments(function (attachments) {
|
|
//attachments.forEach(function (attachment) {
|
|
// this._revokeLocalURL(attachment);
|
|
//}, this);
|
|
|
|
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);
|
|
};
|
|
}.bind(this));
|
|
};
|
|
|
|
this.replaceFeatureId = function (featureLayerUrl, oldId, newId, callback) {
|
|
console.assert(this._db !== null, "indexeddb not initialized");
|
|
|
|
var featureId = featureLayerUrl + "/" + oldId;
|
|
|
|
var objectStore = this._db.transaction([this.objectStoreName], "readwrite").objectStore(this.objectStoreName);
|
|
var index = objectStore.index("featureId");
|
|
var keyRange = IDBKeyRange.only(featureId);
|
|
var replacedCount = 0;
|
|
index.openCursor(keyRange).onsuccess = function (evt) {
|
|
var cursor = evt.target.result;
|
|
if (cursor) {
|
|
var newFeatureId = featureLayerUrl + "/" + newId;
|
|
var updated = cursor.value;
|
|
updated.featureId = newFeatureId;
|
|
updated.objectId = newId;
|
|
objectStore.put(updated);
|
|
replacedCount++;
|
|
cursor.continue();
|
|
}
|
|
else {
|
|
// If no records match then evt.target.result = null
|
|
// allow time for all changes to persist...
|
|
setTimeout(function () {
|
|
callback(replacedCount);
|
|
}, 1);
|
|
}
|
|
};
|
|
};
|
|
|
|
this.getUsage = function (callback) {
|
|
console.assert(this._db !== null, "indexeddb not initialized");
|
|
|
|
var usage = {sizeBytes: 0, attachmentCount: 0};
|
|
|
|
var transaction = this._db.transaction([this.objectStoreName])
|
|
.objectStore(this.objectStoreName)
|
|
.openCursor();
|
|
|
|
console.log("dumping keys");
|
|
|
|
transaction.onsuccess = function (event) {
|
|
var cursor = event.target.result;
|
|
if (cursor) {
|
|
console.log(cursor.value.id, cursor.value.featureId, cursor.value.objectId);
|
|
var storedObject = cursor.value;
|
|
var json = JSON.stringify(storedObject);
|
|
usage.sizeBytes += json.length;
|
|
usage.attachmentCount += 1;
|
|
cursor.continue();
|
|
}
|
|
else {
|
|
callback(usage, null);
|
|
}
|
|
}.bind(this);
|
|
transaction.onerror = function (err) {
|
|
callback(null, err);
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 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(true,evt.target.result);
|
|
};
|
|
reader.onerror = function (evt) {
|
|
callback(false,evt.target.result);
|
|
};
|
|
reader.readAsBinaryString(attachmentFile);
|
|
};
|
|
|
|
// Deprecated @ v2.7
|
|
//this._createLocalURL = function (attachmentFile) {
|
|
// return window.URL.createObjectURL(attachmentFile);
|
|
//};
|
|
|
|
//this._revokeLocalURL = function (attachment) {
|
|
// window.URL.revokeObjectURL(attachment.url);
|
|
//};
|
|
|
|
this.init = function (callback) {
|
|
console.log("init AttachmentStore");
|
|
|
|
var request = indexedDB.open(this.dbName, 12);
|
|
callback = callback || function (success) {
|
|
console.log("AttachmentsStore::init() success:", success);
|
|
}.bind(this);
|
|
|
|
request.onerror = function (event) {
|
|
console.log("indexedDB error: " + event.target.errorCode);
|
|
callback(false, event.target.errorCode);
|
|
}.bind(this);
|
|
|
|
request.onupgradeneeded = function (event) {
|
|
var db = event.target.result;
|
|
|
|
if (db.objectStoreNames.contains(this.objectStoreName)) {
|
|
db.deleteObjectStore(this.objectStoreName);
|
|
}
|
|
|
|
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) {
|
|
this._db = event.target.result;
|
|
console.log("database opened successfully");
|
|
callback(true);
|
|
}.bind(this);
|
|
};
|
|
};
|
|
|