7.2 Querying
7.2 Querying jed124One of the most common operations performed in GIS is the query. The Maps SDK for JS makes it possible for developers to query layers in a number of different ways. For one, you can set a layer’s definition expression so that it displays just a subset of its data. You can also query a layer to get a count of features meeting the criteria, to get the IDs of the features, or to get the features (attributes & geometries) themselves.
7.2.1 Definition Expressions
7.2.1 Definition Expressions ksc17Perhaps the simplest form of query you can perform using Esri’s SDK is defining a layer’s definitionExpression. This is a property supported by a few different layer sub-classes, including FeatureLayer and ImageryLayer. If you’ve worked with desktop GIS, you’ve probably encountered this sort of feature. The idea is that you can restrict a layer’s underlying data to a subset meeting certain criteria. Using a definition expression is common when working with datasets that cover multiple political subdivisions (e.g., states in the U.S.).
The definitionExpression property can be set to any valid SQL where clause. Here is an example that filters out roughly half of the Jen & Barry's cities by selecting only the features whose UNIVERSITY value is 1. Note that the names of the cities meeting the criterion are outputted to the console.
See the Pen Definition Expression Demo by Jim Detwiler (@jimdetwiler) on CodePen.
7.2.2 Getting Feature Counts
7.2.2 Getting Feature Counts ksc17As mentioned, Esri’s SDK provides methods for getting a count of features meeting some selection criteria, getting the IDs of those features, or getting the features themselves. Regardless of which kind of response you require, the first step in the process is creating an object of the Query class. Perhaps the most-used property on the Query class is where, which takes the same sort of SQL where clause that we saw earlier when discussing the definitionExpression property.
There are many other Query properties, some of which we’ll discuss momentarily. For now, let’s look at this example that reports the number of counties in Jen & Barry’s world that meet the criterion of NO_FARMS87 > 150.
See the Pen queryFeatureCount() Demo by Jim Detwiler (@jimdetwiler) on CodePen.
Note that after creating a FeatureLayer of the counties, a Query object is created on lines 29-31. The object’s where property is set to a variable that was defined near the top of the script on line 8. The Query object is then used on line 34 as the argument to the queryFeatureCount() method (a method of the FeatureLayer class). Line 35 contains the alert() statement that produces the message you saw when you first opened this page. What we skipped over at the end of line 34 is some code that handles what’s returned by the queryFeatureCount() method: a promise. You’ve probably seen references to promises while poking around the SDK. Well, now we’re finally going to discuss what a promise is.
7.2.3 Promises
7.2.3 Promises ksc17The folks who run the Mozilla Developer Network define a promise object as follows:
The Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.
The basic idea behind promises in JavaScript (and this goes well beyond geospatial apps) is that a lot of the operations that scripts perform take some time to complete. Rather than grinding the user experience to a halt while waiting for one of these operations, the browser gets to work on it using part of the device’s resources, but continues on executing the code that comes next. The notion of a promise came about as a way to simplify the coding of applications that contain asynchronous operations.
A promise can be in one of three states: resolved, rejected, or pending. As a developer working with a promise, you can write code to be executed when the promise resolves successfully and when it is rejected (fails to finish successfully).
Returning to the example from the previous page, running a query against a layer on some server somewhere takes some time. So Esri wrote the queryFeatureCount() method to return a promise. As is typical, working with this promise typically involves calling on its then() method. The then() method requires that you specify a callback function to be executed when the promise has been resolved. In the example, I inserted an anonymous function, though it is also acceptable to plug in the name of a function that’s been defined elsewhere. This can be a good idea when the function is relatively long.
Referring back to the definition of a promise object, when a promise is resolved successfully, it returns a value. In the case of queryFeatureCount(), as explained on its page in the SDK documentation, the returned value is the number of features meeting the query criteria. Something that is a bit tricky getting used to in working with promises is that the return value is passed along to the callback function specified in the then() method call. When defining the function, you need to create a variable to hold that passed value. In my example, I called this variable count; in the documentation example, it’s called numFeatures. The important thing is that you know the data type being returned -- the documentation conveys this to you in this case with the return type being listed as Promise<Number> -- and write your code to work with it properly.
As mentioned, you can also write code to be run in the event that the promise is rejected (fails). This error handling function should come immediately after the success handling function. The example below again uses queryFeatureCount(), this time with a misspelling of the field name in the query. Note the second anonymous function embedded within the then() method, which logs an error to the browser console when queryFeatureCount() fails.
See the Pen Promise Rejected Demo by Jim Detwiler (@jimdetwiler) on CodePen.
Now that you know how to handle methods that return a promise, you should be aware that there are certain classes in the SDK (MapView, SceneView, and all of the Layer sub-classes) that also return a promise when you create an instance of the class. So when you create a MapView, for example, you can write code that defines what you want to happen when that view is ready. It might help to conceptualize this second type of promise as class-based, as opposed to the method-based promises discussed above.
An important change in working with class-based promises occurred with the release of version 4.7 of the SDK. Prior to that release, developers would use then() to specify what to do with the object once it is ready to be used. In other words, then() was used with both types of promises. Beginning with version 4.7, class-based promises are instead handled using when(). The reasoning behind this change, having to do with compatibility with native JavaScript promises, is detailed in this Esri blog post.
Returning to the queryFeatureCount() example, handling an object as a promise was actually an important part of the coding. The FeatureLayer referenced by the counties variable takes a moment to load, so the counties.when on line 34 essentially tells the browser to wait to execute the queryFeatureCount() method until that layer has finished loading.
To help illustrate the change in class-based promise handling, below is the same app written for version 4.6, in which then() is used instead of when(). A lesson to learn here is that the version of the SDK employed by an app matters.
See the Pen Untitled by Jim Detwiler (@jimdetwiler) on CodePen.
7.2.4 Selecting Features
7.2.4 Selecting Features ksc17Simply getting a count of the features meeting certain criteria is sometimes sufficient, but it’s often necessary to work with the features themselves. To do that, you use the queryFeatures() method. As with queryFeatureCount(), queryFeatures() returns a promise. The difference is that the queryFeatures() promise resolves to a FeatureSet object rather than a Number.
See the Pen queryFeatures() Demo by Jim Detwiler (@jimdetwiler) on CodePen.
The example above uses the same Query where clause as the previous ones and displays the counties meeting the criterion as graphics. Note the following important points:
- A new empty GraphicsLayer object is created to hold the counties meeting the query criterion.
- The Query has its returnGeometry property set to true. This is necessary to be able to map the results.
- The queryFeatures() method is executed after the counties layer has finished loading (line 43).
- The FeatureSet that gets returned by the queryFeatures() method is passed along to a function called displayResults().
- The displayResults() function is defined such that the FeatureSet returned by queryFeatures() is stored in a variable called results.
- Getting at the counties in the FeatureSet is done by reading its features property, which returns an array of Graphic objects.
- The array of Graphic objects is immediately passed to a JavaScript array method called map. JavaScript developers use the map() method to do something to each item in an array and return the result as a new array. In this case, the anonymous function plugged into the map() method changes the symbol of each graphic to a yellow SimpleFillSymbol.
- The new array of Graphic objects is added to the GraphicsLayer (in the resultsLayer variable).
So far, where and returnGeometry are the only Query properties we’ve looked at. However, there are several others that are important in certain contexts. Here is a brief description of just a few of these properties:
- num – the maximum number of features to return; often used together with the start property to implement paging of results
- orderByFields – an array of fields to use for sorting the results
- outFields – an array of fields to include in the results; limiting this array to only what you need can improve performance
There are actually a few more Query properties that I think are worth discussing, but I left them out of this list because they’re considered in greater depth in the next section on spatial queries.
7.2.5 Spatial Queries
7.2.5 Spatial Queries ksc17If you’re an ArcGIS Pro user, the sort of query we’ve dealt with so far has been analogous to the kind you’d build using the Select By Attributes dialog. Now let’s look at how you’d go about performing a query like one you’d build using the Select By Location dialog.
To implement a spatial query, the main properties of concern are geometry and spatialRelationship. The geometry should be set to some point, line or polygon that you’d like to check against the features in the layer you’re applying the query to. The spatialRelationship should be set to a string defining the sort of check to run, such as "intersects", "overlaps", "touches", etc.
For example, let’s say you were writing an app in which the user was given the ability to draw a shape on the map and you want to identify the land parcels that intersect that shape. The basic steps for carrying out this sort of operation would be to:
- Get a reference to the land parcel layer.
- Create a Query, setting its geometry to the user-defined shape and its spatialRelationship to "intersects".
- Invoke the queryFeatures() method on the parcel layer.
- Write code that handles the FeatureSet returned by queryFeatures().
Have a look at the example below, which essentially carries out the Jen & Barry's site selection queries (minus the ones having to do with the recreation areas and highways).
See the Pen Spatial query demo by Jim Detwiler (@jimdetwiler) on CodePen.
Again, here is a list of the main points to take away from this script:
- A series of variables holding the selection criteria are defined at the top of the script, making it easier to modify those values if desired.
- Those values are incorporated into a couple of where clause variables using string concatenation.
- After the cities and counties layers have loaded, the county-level query is executed and the resulting FeatureSet is passed along to a function called findGoodCities().
- The findGoodCities() function is defined to store that FeatureSet in a variable called goodCounties.
- In findGoodCities(), a Query is created to be used against the cities layer.
- The expression goodCounties.features returns the array of Graphics that met the county-level criteria.
- The JavaScript forEach method is used to loop through the array of county Graphics. On each pass through the loop, the geometry property of the cities Query is set to the geometry of the county in that iteration of the loop.
- The cities Query is then applied to the cities layer, with the FeatureSet from that query being passed to the callback function called displayResults().
- The displayResults() function is similar to how it looked in the previous example, creating a yellow graphic for the cities identified by the query. One difference is that embedded within the map() function is a console.log statement that prints the name of the city currently being processed. The expression graphic.attributes.NAME returns the value from the NAME field for the current city. Other field values can be retrieved in this way also, provided they are included in the outFields list when defining the Query. (Be sure to open your browser’s developer console to see the results listed.)
- Note that cityQuery was used here to evaluate both attribute and spatial criteria at the same time.
- We’ll see how to write content like the cities list to a more user-friendly place in the next lesson when we look at user interface development.