/**
* Contains information about a report.
*
* @author J Clare
* @class ia.AGOLData
* @constructor
*/
ia.AGOLData = function()
{
this.iaData = {};
this._fsIndicators = new Array(); // FeatureServer Indicators.
this._csvIndicators = new Array(); // CSV file Indicators.
};
/**
* The raw json data describing the IA attribute data.
*
* @property iaData
* @type JSON
*/
ia.AGOLData.prototype.iaData;
/**
* Loads and parses the source file then calls the given function.
*
* @method loadSource
* @param {String} url The url to the data.
* @param {Function} callbackFunction The call back function.
*/
ia.AGOLData.prototype.loadSource = function(url, callbackFunction)
{
var me = this;
this.url = url;
this.path = ia.File.getFileDirectory(this.url);
ia.File.load(
{
url: url,
dataType: "json",
onSuccess:function(json)
{
// Parse AGOL data
me.parseData(json, callbackFunction);
}
});
};
/**
* Parses AGOL "data" JSON into IA "data" JSON.
*
* @method parseData
* @param {JSON} agolData The AGOL data file.
* @param {Function} callbackFunction The call back function.
*/
ia.AGOLData.prototype.parseData = function(agolData, callbackFunction)
{
var me = this;
me.agolData = agolData;
// Initialise IA Data.
this.iaData = {};
this.iaData.version = agolData.version;
this.iaData.geographies = new Array();
var agolGeogs = agolData.geographies;
if (agolGeogs !== undefined && agolGeogs.length > 0)
{
// Wait till all geographies have loaded before calling the callbackFunction.
var geogCount = 0;
function onGeogReady()
{
geogCount++;
if (geogCount === agolGeogs.length) callbackFunction.call(null, me.iaData); // return.
else me.buildGeography(agolGeogs[geogCount], onGeogReady); // Build subsequent geographies.
};
// Build the first iaGeography
this.buildGeography(agolGeogs[0], onGeogReady);
}
else callbackFunction.call(null, me.iaData); // return.
};
/**
* Builds a geography.
*
* @method buildGeography
* @param {JSON} agolGeography The AGOL Geography.
* @param {Function} callbackFunction The call back function.
*/
ia.AGOLData.prototype.buildGeography = function(agolGeography, callbackFunction)
{
var me = this;
// Define a new iaGeography.
var iaGeography = {};
iaGeography.id = agolGeography.id;
iaGeography.name = agolGeography.name;
iaGeography.type = agolGeography.type;
iaGeography.url = agolGeography.url;
iaGeography.idField = agolGeography.idField;
iaGeography.nameField = agolGeography.nameField;
if (agolGeography.objectIds !== undefined) iaGeography.objectIds = agolGeography.objectIds.split(",");
iaGeography.themes = new Array();
iaGeography.features = new Array();
iaGeography.comparisonFeatures = new Array();
this.iaData.geographies[this.iaData.geographies.length] = iaGeography;
// Store field names, so we can get all the data in one call to the feature server.
// Add id and name field to field names.
iaGeography.fieldNames = iaGeography.idField;
iaGeography.fieldNames = iaGeography.fieldNames + "," + iaGeography.nameField;
// Arrays to hold indicators with different sources.
this._fsIndicators = new Array(); // FeatureServer Indicators.
this._csvIndicators = new Array(); // CSV file Indicators.
// Themes.
this.buildThemes(iaGeography, iaGeography, agolGeography.model.themes);
// Get the objectIds and maxRecordCount for this geography.
this.getObjectIds(iaGeography, function(objectIds, maxRecordCount)
{
// Check if the maxRecordCount is greater than the actual number of features
// If it is we only need to make one request to the FeatureServer.
maxRecordCount = Math.min(objectIds.length, maxRecordCount);
// Populate idArray with comma separated lists of ids.
var idArray = new Array();
while (objectIds.length > 0) idArray[idArray.length] = objectIds.splice(0, maxRecordCount);
// An array to hold all the returned AGOL features as
// we may have to make multiple requests to the FeatureServer
// if the number of features is greater than the maxRecordCount.
var requestedFeatures = new Array();
var noRequests = idArray.length;
var requestCount = 0;
// Makes multiple requests to the FeatureServer until we have all the features.
function onFeaturesReturned(features)
{
// Add the returned features to our master feature array.
requestedFeatures = requestedFeatures.concat(features);
requestCount++;
if (requestCount === noRequests)
{
// Iterate through the layer features to populate the feature and indicator values.
var featureLength = requestedFeatures.length;
for (var i = 0; i < featureLength; i++)
{
var fsFeature = requestedFeatures[i];
// Define a new iaFeature.
var iaFeature = {};
iaFeature.id = String(fsFeature.attributes[iaGeography.idField]);
iaFeature.name = String(fsFeature.attributes[iaGeography.nameField]);
if (iaFeature.id.indexOf("#") === 0)
iaGeography.comparisonFeatures[iaGeography.comparisonFeatures.length] = iaFeature;
else
iaGeography.features[iaGeography.features.length] = iaFeature;
// Add the indicator / associate values for this feature.
for (var j = 0; j < me._fsIndicators.length; j++) // FeatureServer indicators.
{
var iaIndicator = me._fsIndicators[j]
if (iaFeature.id.indexOf("#") === 0)
iaIndicator.comparisonValues[iaIndicator.comparisonValues.length] = fsFeature.attributes[iaIndicator.fieldName];
else
iaIndicator.values[iaIndicator.values.length] = fsFeature.attributes[iaIndicator.fieldName];
}
}
if (me._csvIndicators.length > 0) // CSV File indicators.
{
var indCount = 0;
function onCsvFileRead()
{
indCount++;
if (indCount === me._csvIndicators.length) callbackFunction.call(null, me); // return.
else me.readCsvFile(iaGeography.idField, requestedFeatures, me._csvIndicators[indCount], onCsvFileRead); // Build subsequent indicators.
};
me.readCsvFile(iaGeography.idField, requestedFeatures, me._csvIndicators[0], onCsvFileRead);
}
else
{
callbackFunction.call(null, me); // return.
}
}
else me.getAttributeData(iaGeography.url, idArray[requestCount], iaGeography.fieldNames, onFeaturesReturned);
};
me.getAttributeData(iaGeography.url, idArray[requestCount], iaGeography.fieldNames, onFeaturesReturned);
});
};
/**
* Reads in a csv file.
*
* @method readCsvFile
* @param {String} idField The feature id field.
* @param {JSON} features The list of features.
* @param {JSON} iaIndicator An IA Indicator.
* @param {Function} callbackFunction The call back function.
*/
ia.AGOLData.prototype.readCsvFile = function(idField, features, iaIndicator, callbackFunction)
{
var me = this;
// Strip beginning and ending single or double quotes.
function trim(str)
{
if (str.charAt(0) === '"' && str.charAt(str.length - 1) === '"') str = str.substring(1, str.length-1)
else if (str.charAt(0) === "'" && str.charAt(str.length-1) === "'") str = str.substring(1, str.length-2)
return str;
}
// Testing
//iaIndicator.url = "./config-agol/census3.csv";
//iaIndicator.idField = "polyId"
var url = iaIndicator.url;
if (ia.accessToken !== "" && me._useToken)
{
if (url.indexOf("?") === -1) url += "?";
else url += "&";
url += "token=" + ia.accessToken;
}
ia.File.load(
{
url: url,
contentType: "text/csv; charset=utf-8",
dataType: "text",
onSuccess:function(csvData)
{
var fields = csvData.split(/\n/);
fields.pop(fields.length-1);
var headers = fields[0].split(',');
var idColumnIndex = 0;
var dataColumnIndex = 0;
for(var i = 0; i < headers.length; i++)
{
var header = $j.trim(headers[i]);
header = trim(header);
if ($j.trim(iaIndicator.idField) === header) idColumnIndex = i;
if ($j.trim(iaIndicator.fieldName) === header) dataColumnIndex = i;
}
var featureValues = {};
var comparisonValues = {};
var data = fields.slice(1, fields.length);
for (var i = 0; i < data.length; i++)
{
var featureId = $j.trim(data[i].split(',')[idColumnIndex]);
featureId = trim(featureId);
var dataValue = $j.trim(data[i].split(',')[dataColumnIndex]);
dataValue = trim(dataValue);
if (iaIndicator.type === "numeric" && ia.isNumber(dataValue)) dataValue = parseFloat(dataValue);
if (featureId.indexOf("#") === 0)
comparisonValues[featureId] = dataValue;
else
featureValues[featureId] = dataValue;
}
// Match the feature ids up.
var n = features.length;
for (var i = 0; i < n; i++)
{
var fsFeature = features[i];
var featureId = String(fsFeature.attributes[idField]);
if (featureId.indexOf("#") === 0)
{
var value = comparisonValues[featureId];
if (value) iaIndicator.comparisonValues[iaIndicator.comparisonValues.length] = comparisonValues[featureId];
else iaIndicator.comparisonValues[iaIndicator.comparisonValues.length] = me.formatter.noDataValue;
}
else
{
var value = featureValues[featureId];
if (value) iaIndicator.values[iaIndicator.values.length] = featureValues[featureId];
else iaIndicator.values[iaIndicator.values.length] = me.formatter.noDataValue;
}
}
callbackFunction.call(null, me);
}
});
};
/**
* Builds a geography.
*
* @method buildThemes
* @param {JSON} iaGeography An IA Geography.
* @param {JSON} iaParent The parent of these themes. An IA Geography or IA Theme.
* @param {JSON} agolThemes A list of AGOL Themes.
*/
ia.AGOLData.prototype.buildThemes = function(iaGeography, iaParent, agolThemes)
{
for (var i = 0; i < agolThemes.length; i++)
{
var agolTheme = agolThemes[i];
var iaTheme = {};
iaTheme.id = agolTheme.id;
iaTheme.name = agolTheme.name;
iaTheme.indicators = new Array();
iaParent.themes[iaParent.themes.length] = iaTheme;
// Nested Themes.
if (agolTheme.themes && agolTheme.themes.length > 0)
{
iaTheme.themes = new Array();
this.buildThemes(iaGeography, iaTheme, agolTheme.themes);
}
// Indicators.
var agolIndicators = agolTheme.indicators;
for (var j = 0; j < agolIndicators.length; j++)
{
var agolIndicator = agolIndicators[j];
var iaIndicator = this.buildIndicator(iaGeography, agolIndicator);
iaTheme.indicators[iaTheme.indicators.length] = iaIndicator;
// Associates.
if (agolIndicator.associates !== undefined && agolIndicator.associates.length > 0)
{
iaIndicator.associates = new Array();
for (var k = 0; k < agolIndicator.associates.length; k++)
{
var agolAssociate = agolIndicator.associates[k];
var iaAssociate = this.buildIndicator(iaGeography, agolAssociate);
iaIndicator.associates[iaIndicator.associates.length] = iaAssociate;
}
}
}
}
};
/**
* Builds an indicator or associate.
*
* @method buildIndicator
* @param {JSON} iaGeography An IA Geography.
* @param {JSON} agolIndicator The AGOL indicator or associate.
* @return {JSON} The IA indicator or associate.
*/
ia.AGOLData.prototype.buildIndicator = function(iaGeography, agolIndicator)
{
var iaIndicator = {};
iaIndicator.id = agolIndicator.id;
iaIndicator.name = agolIndicator.name;
iaIndicator.type = agolIndicator.type.toLowerCase();
iaIndicator.date = agolIndicator.date;
iaIndicator.precision = agolIndicator.precision;
iaIndicator.url = agolIndicator.src.url;
iaIndicator.fieldName = agolIndicator.src.fieldName;
iaIndicator.properties = agolIndicator.properties;
iaIndicator.values = new Array();
iaIndicator.comparisonValues = new Array();
// If the indicator/associate url matches the geography url its
// using the same FeatureServer for the source data.
if (iaIndicator.url === iaGeography.url) // FeatureServer
{
iaGeography.fieldNames = iaGeography.fieldNames + "," + iaIndicator.fieldName;
this._fsIndicators[this._fsIndicators.length] = iaIndicator;
}
else // Otherwise its using a CSV file
{
// For csv indicators fieldname is a comma-separated list containing
// the data field followed by the id field.
iaIndicator.fieldName = agolIndicator.src.fieldName.split(",")[0];
iaIndicator.idField = agolIndicator.src.fieldName.split(",")[1];
this._csvIndicators[this._csvIndicators.length] = iaIndicator;
}
return iaIndicator;
};
/**
* Gets the object ids and the maxRecordCount of the FeatureServer.
* The maxRecordCount is the maximum number of records that a FeatureServer will return.
*
* @method getObjectIds
* @param {JSON} iaGeography An IA Geography.
* @param {Function} callbackFunction The call back function.
* @return {Object[]} A list of features and their object ids.
* @return {Number} The maxRecordCount.
*/
ia.AGOLData.prototype._useToken = true;
ia.AGOLData.prototype.getObjectIds = function(iaGeography, callbackFunction)
{
// Load the FeatureServer layer description.
var queryUrl = iaGeography.url + "?f=json";
if (ia.accessToken !== "" && me._useToken) queryUrl += "&token=" + ia.accessToken;
var me = this;
ia.File.load(
{
url: queryUrl,
dataType: "json",
onSuccess:function(fsLayerDescription)
{
if (fsLayerDescription.error) ia.log(fsLayerDescription.error);
if (fsLayerDescription.error && fsLayerDescription.error.code === 498)
{
// Case when map is protected by a token but the feature services arent. So we need to drop the token for the feature services.
me._useToken = false;
return me.getObjectIds(iaGeography, callbackFunction)
}
else if (fsLayerDescription.error && fsLayerDescription.error.code === 499)
{
// This will open the AGOL login page. Once logged in the page is redirected to the report with an appended token
// that can be used to access the data in the report.
var authUrl = 'https://www.arcgis.com/sharing/oauth2/authorize?client_id=83wV2txRMBrDpKjq&response_type=token&redirect_uri=' + encodeURI(window.location.href);
window.location.href = authUrl;
}
else
{
var maxRecordCount = Infinity;
// Check if theres a max record requestCount.
if (fsLayerDescription.maxRecordCount !== undefined) maxRecordCount = ia.parseInt(fsLayerDescription.maxRecordCount);
// Get the object ids.
var queryUrl = iaGeography.url + "/query?where=1+%3D+1&f=json&returnIdsOnly=true";
if (ia.accessToken !== "" && me._useToken) queryUrl += "&token=" + ia.accessToken;
// Override if feature subset has been selected.
if (iaGeography.objectIds !== undefined)
{
callbackFunction.call(null, iaGeography.objectIds, maxRecordCount);
}
else
{
ia.File.load(
{
url: queryUrl,
dataType: "json",
onSuccess:function(fsLayer)
{
callbackFunction.call(null, fsLayer.objectIds, maxRecordCount); // return.
}
});
}
}
}
});
};
/**
* Gets the attribute data for the given objectIds from a FeatureServer.
*
* @method getAttributeData
* @param {String} url The url of the FeatureServer.
* @return {String[]} objectIds An array of ids.
* @param {String} fieldNames The field names to get attributes data for.
* @param {Function} callbackFunction The call back function.
* @return {Object[]} A list of features and their attributes.
*/
ia.AGOLData.prototype.getAttributeData = function(url, objectIds, fieldNames, callbackFunction)
{
var me = this;
// Get the attribute data.
// Append a query to the url - "where=1+%3D+1" is just a hack to get back all the features.
var url = url + "/query";
var stringifiedJSON = "where=1+%3D+1&f=json"
+ "&returnGeometry=false"
+ "&returnIdsOnly=false"
+ "&returnCountOnly=false"
+ "&objectIds="+objectIds
+ "&outFields="+fieldNames;
if (ia.accessToken !== "" && me._useToken) stringifiedJSON += "&token=" + ia.accessToken;
ia.File.load(
{
url: url,
type: "POST",
dataType: "json",
data: stringifiedJSON,
onSuccess:function(fsLayer)
{
callbackFunction.call(null, fsLayer.features); // return.
}
});
};