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);
}
}
};