/* global define, console, RAMP */
/**
*
*
* @module RAMP
* @submodule AttributeLoader
*/
//TODO should this be under DataLoader submodule??
/**
* Attribute Loader class.
*
* Handles the extraction of attribute data from map services, and transforms it to standard format
*
* ####Imports RAMP Modules:
* {{#crossLink "EventManager"}}{{/crossLink}}
* {{#crossLink "GlobalStorage"}}{{/crossLink}}
*
* @class AttributeLoader
* @static
* @uses dojo/topic
* @uses dojo/Deferred
* @uses dojo/request/script
*/
define([
/* Dojo */
'dojo/topic', 'dojo/request/script', 'dojo/Deferred',
/* Esri */
'esri/request',
/* RAMP */
'ramp/eventManager', 'ramp/globalStorage', 'ramp/map'],
function (
/* Dojo */
topic, script, Deferred,
/* Esri */
esriRequest,
/* RAMP */
EventManager, GlobalStorage, RampMap) {
'use strict';
/**
* Will generate object id indexes and parent pointers on a layer data object.
* Assumes data object already has features and object id field defined
*
* @method addLayerData
* @private
* @param {Object} layerData layer data object
* @param {Array} featureData feature objects to enhance and add to layer data
*/
function addLayerData(layerData, featureData) {
var offset = layerData.features.length;
//add new data to layer data's array
layerData.features = layerData.features.concat(featureData);
//make parent pointers and a fun index on object id
featureData.forEach(function (elem, idx) {
//map object id to index of object in feature array
//use toString, as objectid is integer and will act funny using array notation.
layerData.index[elem.attributes[layerData.idField].toString()] = idx + offset;
//pointer back to parent
elem.parent = layerData;
});
}
/**
* Will generate an empty layer data object
*
* @method newLayerData
* @private
* @return {Object} empty layer data object
*/
function newLayerData() {
return {
layerId: '',
idField: '',
features: [],
index: {}
//maxRecord: 0,
//loadRangeSet: []
};
}
/**
* Recursive function to load a full set of attributes, regardless of the maximum output size of the service
* Passes result back on the provided Deferred object
*
* @method loadDataBatch
* @private
* @param {Integer} maxId largest object id that has already been downloaded
* @param {Integer} maxBatch maximum number of results the service will return. if -1, means currently unknown
* @param {String} layerUrl URL to feature layer endpoint
* @param {String} idField name of attribute containing the object id for the layer
* @param {String} layerId id of the layer
* @param {dojo/Deferred} callerDef deferred object that resolves when current data has been downloaded
*/
function loadDataBatch(maxId, maxBatch, layerUrl, idField, layerId, callerDef) {
//fetch attributes from feature layer. where specifies records with id's higher than stuff already downloaded. outFields * (all attributes). no geometry.
var defData = script.get(layerUrl + '/query', {
query: 'where=' + idField + '>' + maxId + '&outFields=' + RAMP.layerRegistry[layerId].ramp.config.layerAttributes + '&returnGeometry=false&f=json',
jsonp: 'callback'
});
defData.then(function (dataResult) {
if (dataResult.features) {
var len = dataResult.features.length;
if (len > 0) {
if (maxBatch === -1) {
//this is our first batch and our server is 10.0. set the max batch size to this batch size
maxBatch = len;
}
if (len < maxBatch) {
//this batch is less than the max. this is last batch. no need to query again.
callerDef.resolve(dataResult.features);
} else {
//stash the result and call the service again for the next batch of data.
//max id becomes last object id in the current batch
var thisDef = new Deferred();
loadDataBatch(dataResult.features[len - 1].attributes[idField], maxBatch, layerUrl, idField, layerId, thisDef);
thisDef.then(function (dataArray) {
callerDef.resolve(dataResult.features.concat(dataArray));
}, function (error) {
callerDef.reject(error);
});
}
} else {
//no more data. we are done
callerDef.resolve([]);
}
} else {
//it is possible to have an error, but it comes back on the "success" channel.
callerDef.reject(dataResult.error);
}
},
function (error) {
callerDef.reject(error);
});
}
/**
* Will download the attribute data for a layer.
*
* @method loadAttributeData
* @private
* @param {String} layerId id of the layer
* @param {String} layerUrl the URL of the layer
* @param {String} layerType type of the layer. should be a value from GlobalStorage.layerType
*/
function loadAttributeData(layerId, layerUrl, layerType) {
switch (layerType) {
case GlobalStorage.layerType.feature:
console.log('BEGIN ATTRIB LOAD: ' + layerId);
//extract info for this service
var defService = esriRequest({
url: layerUrl,
content: { f: 'json' },
callbackParamName: 'callback',
handleAs: 'json'
});
defService.then(function (serviceResult) {
if (serviceResult && (typeof serviceResult.error === 'undefined')) {
RampMap.updateDatagridUpdatingState(RAMP.layerRegistry[layerId], true);
//set up layer data object based on layer data
var maxBatchSize = serviceResult.maxRecordCount || -1, //10.0 server will not supply a max record value
defFinished = new Deferred(),
layerData = newLayerData();
layerData.layerId = layerId;
//find object id field
serviceResult.fields.every(function (elem) {
if (elem.type === 'esriFieldTypeOID') {
layerData.idField = elem.name;
return false; //break the loop
}
return true; //keep looping
});
//begin the loading process
loadDataBatch(-1, maxBatchSize, layerUrl, layerData.idField, layerId, defFinished);
//after all data has been loaded
defFinished.promise.then(function (features) {
addLayerData(layerData, features);
//store attribData
// (Set layer data in global object only once all data has been downloaded
// Used as both a flag and a data store)
RAMP.data[layerId] = layerData;
//new data. tell grid to reload
topic.publish(EventManager.Datagrid.APPLY_EXTENT_FILTER);
RampMap.updateDatagridUpdatingState(RAMP.layerRegistry[layerId], false);
console.log('END ATTRIB LOAD: ' + layerId);
},
function (error) {
console.log('error getting attribute data for id ' + layerId);
//set layer to error state
RampMap.updateDatagridUpdatingState(RAMP.layerRegistry[layerId], false);
topic.publish(EventManager.LayerLoader.LAYER_ERROR, { layer: RAMP.layerRegistry[layerId], error: error });
});
} else {
console.log('Service metadata load error');
if (serviceResult && serviceResult.error) {
console.log(serviceResult.error);
}
}
},
function (error) {
console.log('Service metadata load error : ' + error);
});
break;
default:
console.log('Layer type not supported by attribute loader: ' + layerType);
}
//TODO do we need to return any sort of promise to indicate when the loading has finished?
}
/**
* Will extract the attribute data from a file based layer.
*
* @method extractAttributeData
* @private
* @param {Object} layer the layer object
*/
function extractAttributeData(layer) {
switch (layer.ramp.type) {
case GlobalStorage.layerType.feature:
//change to standard format and store.
var layerData = newLayerData();
layerData.layerId = layer.id;
layerData.idField = layer.objectIdField;
addLayerData(layerData, layer.graphics.map(function (elem) {
return { attributes: elem.attributes };
}));
//we dont care about setting range values, as all the data is already loaded
//store attribData
RAMP.data[layer.id] = layerData;
//new data. tell grid to reload
topic.publish(EventManager.Datagrid.APPLY_EXTENT_FILTER);
console.log('END ATTRIB LOAD: ' + layer.id);
break;
default:
console.log('Layer type not supported by attribute extractor: ' + layer.ramp.type);
}
//TODO do we need to return any sort of promise to indicate when the loading has finished?
}
return {
loadAttributeData: loadAttributeData,
extractAttributeData: extractAttributeData
};
});