In December 2018 the 3.27 ArcGIS API for JavaScript released support for accessing data from multiple data sources inside a single feature’s popup. This is accomplished using the Feature Set functions made available in ArcGIS Arcade version 1.5. Feature Sets allow you to query features from any layer in a map or feature service within the context of a feature’s popup and use those features’ attributes and geometries in calculations. The results of those calculations, otherwise not present in the feature’s attributes, can be displayed in the popup.
For example, you can use feature sets to compare the value of a feature’s attribute to the same attribute in neighboring features; summarize the number of points in another layer within a polygon feature; or compare a feature’s value to the mean of all features in the same layer.
In this post we’ll use feature sets in an Arcade expression to calculate the floor area ratio of buildings in land parcels. If you’re unfamiliar with Arcade, I encourage you to check out the following resources:
- Using Arcade expressions in web apps
- Arcade Guide: Getting Started
- JS API Guide: Arcade – expression language
- Introducing Arcade
Floor Area Ratio
Floor area ratio (FAR) is defined as the ratio of the gross floor area of all buildings on a lot to the area of the lot. For example, if a lot has one building with one level that completely covers the lot, then the FAR of the lot is 1.0. If the lot has an FAR of 1.0, but the building has two levels, that indicates the lot is 50% covered.
The image below, which appears in an historic information report from the American Planning Association, depicts how FAR values of 1.0, 4.0, and 9.0 appear in various scenarios.
FAR is used in zoning to control the volume of buildings in particular zones. For example, a commercial zone may limit building up a parcel to an FAR of 5.0, thus allowing for taller buildings, while residential zones may limit building in parcels to an FAR of 0.3.
This measure has also been used in real estate applications as one of many factors for analyzing the price per square foot of a structure.
Check out the following app, which displays two layers in a map: one for parcels and one for buildings. The parcels don’t have FAR in the attributes, but we can calculate that using Arcade and display it to the user as they click a parcel. Go ahead and try it! Explore the map, clicking on some parcels to view the FAR value.
How it works
The following snippet shows the bulk of the JavaScript used in this application, which you’ll note isn’t too verbose.
const farArcade = `
var buildingFootprints = Intersects($feature, FeatureSetByName($map, "Building Footprints"));
var grossFloorArea = 0;
for (var building in buildingFootprints){
var floors = IIF(building.FLOORCOUNT == 0, 1, building.FLOORCOUNT);
grossFloorArea += ( AreaGeodetic( Intersection(building, $feature), 'square-feet') * floors );
}
Round( ( grossFloorArea / AreaGeodetic($feature, 'square-feet') ), 1);
`;
// Loads the necessary resources for the expression including the
// geometry engine and permits async execution of the script
popupProfile.initialize( [ farArcade ] ).then(function(){
// loads layers from a webmap with the given id
arcgisUtils.createMap("da634028a734418f8a5416c675559c3a", "map")
.then(function (response) {
const map = response.map;
const layerIds = map.graphicsLayerIds;
const layer = map.getLayer(layerIds[1]);
// Reference the expression in the popup template and set it on the parcels layer
layer.setInfoTemplate( new PopupTemplate({
description: "Floor Area Ratio (FAR): {expression/far}",
expressionInfos: [{
name: "far",
title: "far",
expression: farArcade
}]
}) );
});
});
The Arcade expression is stored as a string and initialized using the esri/arcadeProfiles/popupProfile module. This initialization is critical because it inspects the script and loads all required dependencies for it to properly execute. Since geometry functions and feature set functions are used, the popup needs to load the geometryEngine and allow the expression to execute in an asynchronous environment. The async behavior is required because network requests will likely be made during the execution of the script.
Once the script is initialized, we can set it in the PopupTemplate of the parcels layer.
// Reference the expression in the popup template and set it on the parcels layer
layer.setInfoTemplate( new PopupTemplate({
description: "Floor Area Ratio (FAR): {expression/far}",
expressionInfos: [{
name: "far",
title: "far",
expression: farArcade
}]
}) );
Let’s take a close look at the expression itself. Remember, the goal is to calculate FAR for the parcel. Keep in mind the parcel layer doesn’t have this data, nor does it have data related to buildings or building area in the feature attributes.
// buildingFootprints represents the buildings that intersect the clicked parcel
var buildingFootprints = Intersects($feature, FeatureSetByName($map, "Building Footprints"));
var totalArea = 0;
// since a building may have multiple floors, we must multiply the floor area by the number
// of floors. Also note the building data shows some buildings in dense areas as crossing
// multiple polygons. To avoid miscalculation, we calculate the intersection of the building.
for (var building in buildingFootprints){
var floors = IIF(building.FLOORCOUNT == 0, 1, building.FLOORCOUNT);
totalArea += ( AreaGeodetic( Intersection(building, $feature), 'square-feet') * floors );
}
// Compute the ratio of the gross building area to the parcel area
Round( ( totalArea / AreaGeodetic($feature, 'square-feet') ), 1);
The first line of the expression executes a query that returns all buildings that intersect the clicked parcel.
// queries for all buildings that intersect the clicked feature
var buildingFootprints = Intersects($feature, FeatureSetByName($map, "Building Footprints"));
Two functions are used in this line: Intersects() and FeatureSetByName(). FeatureSetByName
returns a FeatureSet, which is a representation of all the features of another layer in the map. In this case, the feature set refers to all features in the layer with the name “Building Footprints”.
Note my use of the word representation in the previous paragraph. At this point, the expression does not execute a query for ALL features in that layer. No query is made until the FeatureSet is used in another function parameter or statement. In the case of this expression, we use it immediately in the Intersects
function. This is an example of chaining functions for improving performance.
Intersects
builds and executes a query for all features in the input FeatureSet (i.e. the buildings layer) that intersect the input geometry (the clicked parcel), and returns those features as a FeatureSet. So in layman’s terms, we’re executing a query for all buildings that intersect the clicked parcel. You can observe this behavior when you inspect the network traffic upon clicking the feature.
Once the building footprint features have been fetched, they are now available on the client, allowing the rest of this expression to execute client-side.
var grossFloorArea = 0;
for (var building in buildingFootprints){
var floors = IIF(building.FLOORCOUNT == 0, 1, building.FLOORCOUNT);
// multiply the square footage of the footprint by the number of floors
grossFloorArea += ( AreaGeodetic( Intersection(building, $feature), 'square-feet') * floors );
}
Note the buildingFootprints
variable is a FeatureSet of client-side features, as opposed to a reference to a service.
I then iterate through each feature in the FeatureSet and sum the gross floor area of all buildings (e.g. floor area * number of floors) intersecting the parcel.
Notice that I also calculate the intersection of the building and the parcel. This particular buildings dataset contains generalized buildings that span multiple parcels in more dense areas. Without calculating the intersection of the building to the parcel, the expression would return FAR values much higher than they are in reality. The parcels in this image, for example, intersect a single one-story building.
Although the building in the image above is only one story, the intersecting parcels would report a much larger FAR, often eclipsing 10.0 to 14.0. The Intersection
function allows me to only account for the intersecting portion of the one-story building in the parcel, and thus get more realistic values for this area, usually between 0.6 and 1.0.
The final line of the expression completes the final FAR calculation, dividing the gross floor area by the area of the parcel.
Round( ( grossFloorArea / AreaGeodetic($feature, 'square-feet') ), 1);
Since FAR is typically expressed as a number rounded to one decimal place, we can use the Round function to help us with that as well.
Summary
Keep in mind this Arcade expression is just an approximation of FAR. It does not account for the shape of the buildings, what counts as a floor, or whether all floors have the same square footage. It makes a lot of general assumptions. However, this example does provide a glimpse into the power and complexity FeatureSets can bring to your apps.
Note also that the unwise use of feature sets in Arcade expressions can cause popup performance to suffer. Here are a few suggestions of good practices when working with feature sets in Arcade expressions:
- Avoid iterating through all features in a layer. This action requires the expression to fetch all features from the service. The query itself can be slow, and depending on how many features are in your layer, the iteration can slow things down as well.
- Chain functions as much as possible. If you can reduce two or three requests down to one via chaining, then do it! Limiting the number of trips to the server will keep the app more useable.
- Keep the expression as simple as possible. Using seven geometry functions in a complex 100-line expression may make you feel like an Arcade ninja, but it could cause your users to spend extra time waiting for popup content to show up while unnecessary processing takes place behind the scenes. Geometry operations can be especially slow if you are working with complex features with many vertices.
- If you don’t need it, don’t request it! If you’re creating a Feature Set, only request the fields you need. If you’re not using geometry functions, then be sure to exclude geometries from the feature set. Small details like this can go a long way in improving performance.
If you’re interested in learning more about feature sets, I encourage you to read the following blogs by Paul Barker, who describes FeatureSets in detail with additional context.
Commenting is not enabled for this article.