Show:

File: ia\custom\AGOLData.js

/** 
 * 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.
		}
	});
};