Show:

File: ia\maps\layers\FeatureServiceLayer.js

/** 
 * The base class for AGS Feature layers.
 *
 * @author J Clare
 * @class ia.FeatureServiceLayer
 * @extends ia.FeatureLayer
 * @constructor
 * @param {String} inSource The spatial source.
 */
ia.FeatureServiceLayer = function(inSource)
{
	ia.FeatureServiceLayer.baseConstructor.call(this, inSource);

	this._firstRender = true; 			// Indicates its the first time the layer is rendered.
	this._rendering = false;   			// indicates that the layer is this._rendering.
	this._renderQueue = false;  		// indicates thatthere have been multiple render requests.
	this._maxRecordCount = Infinity; 	// The maximum number of features a FeatureServer will return on one request.
	this._objectIdField = ""; 			// The field that hols the object ids for features.

	this.idField = "";
	this.nameField = "";
	this.srs = 102100;
};
ia.extend(ia.FeatureLayer, ia.FeatureServiceLayer);

/**
 * The id field.
 *
 * @property idField
 * @type String
 * @default ""
 */
ia.FeatureServiceLayer.prototype.idField;

/**
 * The name field.
 *
 * @property nameField
 * @type String
 * @default ""
 */
ia.FeatureServiceLayer.prototype.nameField;

/**
 * The spatial reference system.
 *
 * @property srs
 * @type Number
 * @default 102100
 */
ia.FeatureServiceLayer.prototype.srs;

/**
 * The FeatureServer geometry type, used to differentiate between multi-point and single-point features.
 *
 * @property esriGeometryType
 * @type String
 */
ia.FeatureServiceLayer.prototype.esriGeometryType;

/**
 * A list of objectids to use.
 *
 * @property objectIds
 * @type String[]
 */
ia.FeatureServiceLayer.prototype.objectIds;

/**
 * The extents of the layer usin.
 *
 * @property featureBBox
 * @type ia.BoundingBox
 */
ia.FeatureServiceLayer.prototype.featureBBox;

/** 
 * Overrides loadSource in FeatureLayer because we dont actually
 * load anything. The layer data is loaded and rendered every time the map is redrawn.
 *
 * @method loadSource
 */	
ia.FeatureServiceLayer.prototype._useToken = true;
ia.FeatureServiceLayer.prototype.loadSource = function() 
{
	var me = this;

	me.eval = false; // Removes eval message.
	
	// Get the FeatureServer layer description.
	var queryUrl = me.source + "?f=json";
	if (ia.accessToken !== "" && me._useToken) queryUrl += "&token=" + ia.accessToken;
	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;
				me.loadSource();
				return;
			}
			else
			{
				// Get the geometry type
				me.esriGeometryType = fsLayerDescription.geometryType;

				// Check if theres a maxRecordCount.
				if (fsLayerDescription.maxRecordCount !== undefined) me._maxRecordCount = ia.parseInt(fsLayerDescription.maxRecordCount);

				// Get the feature ids.
				var queryUrl = me.source + "/query";

				var stringifiedJSON = "where=1+%3D+1&f=json"
						+ "&returnGeometry=false"
						+ "&returnIdsOnly=true"
						+ "&returnCountOnly=false";

				// Override if feature subset has been selected.
				if (me.objectIds !== undefined) 
				{
					stringifiedJSON = "where=1+%3D+1&f=json"
					+ "&returnGeometry=false"
					+ "&returnIdsOnly=true"
					+ "&returnCountOnly=false"
					+ "&objectIds="+me.objectIds;	
				}

				if (ia.accessToken !== "" && me._useToken) stringifiedJSON += "&token=" + ia.accessToken;

				ia.File.load(
				{
					url: queryUrl,
					type: "POST",
					dataType: "json",
					data: stringifiedJSON, 
					onSuccess:function(fsLayer)
					{
						if (fsLayer.error && fsLayer.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;
						}

						me._objectIdField = fsLayer.objectIdFieldName;

						// 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 when the map is refreshed.
						me._maxRecordCount = Math.min(fsLayer.objectIds.length, me._maxRecordCount);

						// This bit of code effectively breaks up the object ids into multiple
						// arrays of length below or equal to the maxRecordCount that we can use 
						// to send multiple requests to the FeatureServer.
						var objectIdsToRequest = new Array();
						while (fsLayer.objectIds.length > 0) objectIdsToRequest[objectIdsToRequest.length] = fsLayer.objectIds.splice(0, me._maxRecordCount);

						// Initialise the layer items.
						me.items = {};
						me.itemArray = [];	
						if (me.geometry === "line") me.symbolSize = me.style.lineWidth;

						// Make multiple requests to the FeatureServer until we have initialised all items.
						var noRequests = objectIdsToRequest.length;
						var requestCount = 0;
						function onFeaturesReturned(features)
						{
							var n = features.length;
							for (var i = 0; i < n; i++) 		
							{
								var feature = features[i];
								var item = {};
								item.objectId = String(feature.attributes[me._objectIdField]);
								item.id = String(feature.attributes[me.idField]);
								item.name = String(feature.attributes[me.nameField]);
								item.parent = me;
								item.state = ia.ItemLayer.UNSELECTED;
								item.layer = me;
								item.symbolSize = me.symbolSize;
								item.shapes = new Array();

								// Only add if it isnt a comparison area.
								if (item.id.indexOf("#") !== 0)
								{
									me.items[item.id] = item;
									me.itemArray.push(item);
								}
							}
							requestCount++;

							if (requestCount < noRequests) me.getFeatureDetails(objectIdsToRequest[requestCount], onFeaturesReturned); 
							else
							{
								// Dispatch layer ready event.
								me.isLoaded = true;
								me.render();
								var e = new ia.Event(ia.Event.LAYER_READY, me);
								me.dispatchEvent(e);
							}
						};
						me.getFeatureDetails(objectIdsToRequest[requestCount], onFeaturesReturned);
					}
				});
			}
		}
	});
};

/**
 * Gets the feature details for initialising layer items.
 *
 * @method getFeatureDetails
 * @return {String[]} objectIds An array of ids.
 * @param {Function} callbackFunction The call back function. 
 */
ia.FeatureServiceLayer.prototype.getFeatureDetails = function(objectIds, callbackFunction) 
{
	var me = this;
	var url = this.source + "/query";

	var stringifiedJSON = "where=1+%3D+1&f=json"
			+ "&returnGeometry=false"
			+ "&objectIds="+objectIds
			+ "&outFields="+this._objectIdField+","+this.idField+","+this.nameField;
	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.
		}
	});
};

/** 
 * Renders the layer.
 *
 * @method render
 */
ia.FeatureServiceLayer.prototype.render = function() 
{
	if (this.map && this.getVisible() && this.isLoaded)
	{
		// This code allows for multiple calls to render().
		// It allows the current render to end gracefully
		// then call a new render if one was requested.

		// Check if the layer is already being rendered.
		if (this._rendering === false)
		{
			this._rendering = true;
			this._updateItems(function()
			{
				this._rendering = false;

				// If render has called multiple times this._renderQueue will be true.
				// So the layer will be re-rendered.
				if (this._renderQueue)
				{
					this._renderQueue = false;
					this.render();
				}
			}.bind(this));
		}
		else 
		{
			// If its the first time the layer is being rendered we need to
			// redraw it otherwise features will be drawn in the wrong position.
			if (this._firstRender) 
			{
				this.clear();
				this.draw();
			}

			// If render is called multiple times indicate theres a render queue.
			// The render in progress will be notified of this and will execute
			// its callbackfunction.
			this._renderQueue = true;
		}
	}
};

/** 
 * Updates the layer items.
 *
 * @method _updateItems
 * @private
 */
ia.FeatureServiceLayer.prototype._updateItems = function(callbackFunction) 
{
	// Dont bother drawing if the bBox isnt set or the layer isnt visible.
	if (this.map && this.getVisible())
	{
		// Draw cached items whilst waiting for new items to to be loaded and parsed.
		this.clear();
		this.draw();

		// Get the data width of one pixel and use that as the maxAllowableOffset.
		var maxAllowableOffset = this.map.getDataWidth(1);

		// Hold multiple arrays containing the object ids of the features that need to be requested from the FeatureServer.
		var objectIdsToRequest = new Array();

		// Hold multiple arrays containing the items that need to be rendered.
		var itemsToRender = new Array();
		var allItemsToRender = new Array();

		var mapBBox = this.map.getBBox();
		var index = 0;

		var objectIdSubset = new Array(); 	// Holds the object ids of the features that need to be requested.
		var itemSubset = new Array();  		// Holds the items that need to be rendered.

		// Loop through the items.
		var n = this.itemArray.length;
		for (var i = 0; i < n; i++) 		
		{
			var item = this.itemArray[i];
			if (item.bBox === undefined) // Item hasnt had geometry returned yet.
			{
				itemSubset[itemSubset.length] = item;
				allItemsToRender[allItemsToRender.length] = item;
				objectIdSubset[objectIdSubset.length] = item.objectId;
				index++;
			}
			else if (item.bBox.intersects(mapBBox)) // If an item intersects the map bbox it needs to be rendered.
			{
				itemSubset[itemSubset.length] = item;
				allItemsToRender[allItemsToRender.length] = item;

				// If an item isnt of sufficient detail we need to request a more detailed version of it.
				if (item.maxAllowableOffset > maxAllowableOffset)
				{
					objectIdSubset[objectIdSubset.length] = item.objectId;
					index++;
				}
			}

			if (index === this._maxRecordCount || i === n-1 && objectIdSubset.length > 0)
			{
				objectIdsToRequest[objectIdsToRequest.length] = objectIdSubset;
				itemsToRender[itemsToRender.length] = itemSubset;

				index = 0;
				objectIdSubset = new Array();
				itemSubset = new Array();
			}
		}

		if (objectIdsToRequest.length > 0)
		{
			var noRequests = objectIdsToRequest.length;
			var requestCount = 0;

			// Makes multiple requests to the FeatureServer until we have all the features.
			var me = this;
			function onFeaturesReturned(features)
			{
				if (me._firstRender && requestCount === 0) me.clear();
				me.parseData(features, maxAllowableOffset);

				// On the first render draw after each subset of features is parsed
				// so that the end user has something to look at.
				if (me._firstRender) me._renderItems(itemsToRender[requestCount]);
				requestCount++;

				if (me._renderQueue === true)
				{
					callbackFunction.call(null);
					return;
				}

				if (requestCount === noRequests) 
				{
					// After the first complete render just render everything
					// in one go.
					if (!me._firstRender) 
					{
						me.clear();
						me._renderItems(allItemsToRender);
					}
					me.renderSelection();

					me._firstRender = false;
					callbackFunction.call(null);
				}
				else me.getFeatures(objectIdsToRequest[requestCount], maxAllowableOffset, onFeaturesReturned); 
			};
			this.getFeatures(objectIdsToRequest[requestCount], maxAllowableOffset, onFeaturesReturned);

		}
		else
		{
			this.renderSelection();
			callbackFunction.call(null);
		}
	}
};

/** 
 * Gets the attribute data for the given objectIds from a FeatureServer.
 *
 * @method getFeatures
 * @return {String[]} objectIds An array of ids.
 * @param {Number} maxAllowableOffset How detailed the returned geometry should be. 
 * @param {Function} callbackFunction The call back function. 
 */
ia.FeatureServiceLayer.prototype.getFeatures = function(objectIds, maxAllowableOffset, callbackFunction) 
{
	var me = this;

	var url = this.source + "/query";

	var stringifiedJSON = "where=1+%3D+1&f=json"
			+ "&returnGeometry=true"
			+ "&objectIds="+objectIds
			+ "&maxAllowableOffset="+maxAllowableOffset
			+ "&outFields="+this._objectIdField+","+this.idField+","+this.nameField
			+ "&outSR="+this.srs;	
	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.
		}
	});
};

/** 
 * Parses the layerData every time the map is redrawn.
 *
 * @method parseData
 * @param {JSON} features The FeatureServer features. 
 * @param {Number} maxAllowableOffset How detailed the returned geometry should be. 
 */
ia.FeatureServiceLayer.prototype.parseData = function(features, maxAllowableOffset)
{
	// Polys and lines use same code so hold the geometry type in a variable.
	var geoType = "rings";
	if (this.geometry === "line") geoType = "paths";

	// Iterate through the features.
	var fLength = features.length;

	for (var i = 0; i < fLength; i++) 		
	{
		var feature = features[i];
	
		// Check if the item already exists.
		var id = String(feature.attributes[this.idField]);
		var item = this.items[id];
		if (item)
		{
			item.maxAllowableOffset = maxAllowableOffset;

			if (this.geometry === "polygon" || this.geometry === "line") 
			{
				var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;

				// Because sometimes the features dont have geometry.
				if (feature.geometry)
				{
					// Iterate through all rings/paths in this feature.
					var noRings = feature.geometry[geoType].length;
					var shapes = new Array();		

					for (var j = 0; j < noRings; j++) 	
					{
						var ring = feature.geometry[geoType][j];
						var noPoints = ring.length;	

						var shape = new Array();			
						var cx = 0;
						var cy = 0;

						// Iterate through all points in this ring.
						for (var k = 0; k < noPoints; k++) 
						{
							var point = ring[k];
							var p = new Object()
							p.x = point[0];
							p.y = point[1];
							shape.push(p);

							// Calculation to find bBox of item.
							minX = (p.x < minX) ? p.x : minX;
							minY = (p.y < minY) ? p.y : minY;
							maxX = (p.x > maxX) ? p.x : maxX;                       
							maxY = (p.y > maxY) ? p.y : maxY;
						}
						shapes.push(shape);
					}
					item.shapes = shapes;
				}
			}
			else 
			{
				if (this.esriGeometryType ===  "esriGeometryPoint") // Point.
				{
					// Because sometimes the features dont have geometry.
					if (feature.geometry)
					{
						var p = new Object();
						p.x = feature.geometry.x;
						p.y = feature.geometry.y;
					
						// Calculation to find bBox of item.
						minX = p.x;
						minY = p.y;
						maxX = p.x;                       
						maxY = p.y;
						
						item.shapes = [p];
					}
				}

				else // Multi-point
				{
					// Iterate through all points in this feature.
					var noPoints = feature.geometry.points.length;
					var shapes = new Array();		

					// Because sometimes the features dont have geometry.
					if (feature.geometry)
					{
						for (var j = 0; j < noPoints; j++) 	
						{
							var point =  feature.geometry.points[j];
							var p = new Object();
							p.x = point[0];
							p.y = point[1];
						
							// Calculation to find bBox of item.
							minX = p.x;
							minY = p.y;
							maxX = p.x;                       
							maxY = p.y;

							shapes.push(shape);
						}
						item.shapes = shapes;
					}
				}
			}

			// Set the calculated item bBox.
			item.bBox = new ia.BoundingBox(minX, minY, maxX, maxY);
			item.size = Math.max(item.bBox.getWidth(), item.bBox.getHeight());
		}
	}
	
	// Check if layer uses an icon.
	if (this.iconPath !== "")
	{
		this.icon = new Image();
		this.icon.onload = function() {};
		this.icon.src = this.iconPath;
	}
};

/** 
 * Renders the passed items.
 *
 * @method _renderItems
 * @param {Object[]} items An array of layer items to render. 
 * @private
 */
ia.FeatureServiceLayer.prototype._renderItems = function(items)
{
	if (items === undefined) items = this.itemArray.concat()

	var n = items.length;
	for (var i = 0; i < n; i++) 
	{
		var item = items[i];
		if ((this.geometry !== "point") || (this.geometry === "point" && item.symbolSize > 0))
		{
			this.renderItem(item);
			if (this.showLabels) this.renderItemLabel(item);
		}
	}
};