An important element of many geospatial applications is the ability to search for features that meet certain criteria. The nature of those criteria might be spatial (e.g., land parcels adjacent to a particular street) or non-spatial (e.g., land parcels owned by a particular person).
In Lesson 7, we’ll see how Query objects can be used in an Esri JS API app to answer both spatial and non-spatial questions. Before that, however, we’ll look at the API’s Search widget, which can be used to find values in a layer’s attribute table or to find places when configured to work with a locator service.
At the successful completion of this lesson, you should be able to:
If you have any questions now or at any point during this week, please feel free to post them to the Lesson 7 Discussion Forum. (That forum can be accessed at any time by clicking on the Discussions tab.)
Lesson 7 is one week in length. (See the Calendar in Canvas for specific due dates.) To finish this lesson, you must complete the activities listed below. You may find it useful to print this page out first so that you can follow along with the directions.
Step | Activity | Access/Directions |
---|---|---|
1 | Work through Lesson 7. | Lesson 7 |
2 | You will choose a scenario from the list in the poll linked from the assignment page requiring the development of a map based on Esri's JavaScript API.
|
Follow the directions throughout the lesson and in the scenario chosen from the poll. |
3 | Take Quiz 7 after you read the online content. |
Click on "Lesson 7 Quiz" to begin the quiz. |
A common feature of many mapping apps is the ability to search for a place by entering text in a search box. This feature can be added to an Esri JS app through the Search widget. This widget can be configured to find locations using a Locator (geocoding) service or features in a FeatureLayer. Let’s take a look at various implementations of this widget.
A basic implementation of this widget is really quite simple, as demonstrated by Esri’s Search Widget (3D) [1] sample. After adding the Search module to the require() list, all that is necessary to implement the widget is to create a new object of the Search class, set its view property to the app’s view, then add the widget to the view’s UI in the desired position. The widget will match the user’s text against Esri’s World Geocode service by default.
Some properties of the widget that you might consider customizing include:
Esri’s World Geocoding service is suitable for many apps, particularly those built at a global or continental scale. However, for apps built for relatively small areas of interest, it may be possible to utilize a locator built using data from an authoritative source, such as a city or county. The advantage to using a locator from such a source is that it is much more likely to be kept up to date (e.g., to contain new subdivisions).
The example above was built using a locator developed specifically for the City of St. Paul, Minnesota. The first step in creating an app like this is discovering the desired locator. Many locators can be found in ArcGIS Online by searching for locator in the Tools category. (1277 Madison St is an address in St. Paul if you're looking to try the locator.) If this sample does not work for you, it may be that the City has taken its geocoder / locator service offline probably due to costs as geocoding is one of the more expensive services to maintain due to the number of credits used.
Once you've identified the ArcGIS Server REST endpoint of your desired locator, incorporating it into your app involves modifying the Search widget’s sources property. In my example, I included only the St. Paul locator [5], but as the property name implies, you can wire the widget up to multiple locators (e.g., you might have locators available for adjacent jurisdictions). The widget is designed such that Esri's World Geocoding service is included as a source by default. I've disabled that behavior by setting the includeDefaultSources property to false. We’ll see a multi-source example later in the lesson.
As explained in the SDK, the Search widget can have its sources property set to a LocatorSearchSource, LayerSearchSource, or some combination of the two. Looking at the LocatorSearchSource [6] documentation, you’ll notice that a LocatorSearchSource object has some of the same properties mentioned on the previous page (e.g., popupEnabled). While perhaps a bit confusing, this duplication allows for having different settings for these properties on a source-by-source basis. For example, you might have a reason to disable popups for one of your widget’s sources, but enable them for another.
Getting back to the St. Paul example, the sources property is set using a single LocatorSearchSource object. Unsurprisingly, the most important property to set for this object is its url, which should be set using the REST endpoint of the locator service.
From there, because the widget provides just a single text box, you’ll want to look for matches against the locator field that is labeled as the Single Line Address Field in the REST Services Directory. In the case of the St. Paul service, that field is called SingleLine.
Finally, the placeholder property is used to provide a prompt or hint to the user on what should be entered in the search box.
In addition to its locator, there are several other LocatorSearchSource properties that you might want to set. Here are a few of them:
As mentioned earlier, the Search widget can also be configured to find features in a FeatureLayer. The app below allows for finding cities and counties in Jen & Barry's world.
The key difference as compared to the previous example is that a LayerSearchSource (actually two) is used instead of a LocatorSearchSource. The SDK shows that a LayerSearchSource [8] has many properties in common with a LocatorSearchSource, though it obviously has others that are unique to it.
Here’s a quick rundown of what’s happening in this app:
One of the most common operations performed in GIS is the query. The Esri JS API 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.
Perhaps the simplest form of query you can perform using Esri’s API 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.
As mentioned, Esri’s API 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 [10]. 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.
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.
The folks who run the Mozilla Developer Network define a promise object [12] 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 API Reference, 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 API Reference example, it’s called numFeatures. The important thing is that you know the data type being returned -- the API Reference 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.
The Guide section of the SDK provides further reading on Working with promises. [14]
Now that you know how to handle methods that return a promise, you should be aware that there are certain classes in the API (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 API. 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 [15].
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 API employed by an app matters.
Simply 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.
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:
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:
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.
If you’re an ArcGIS Desktop 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:
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).
Again, here is a list of the main points to take away from this script:
For this week's assignment, please select from one of the scenarios below. Regardless of the scenario you choose, I'd like you to follow these guidelines:
Before reading over all of the scenarios, go to the sign-up page in Canvas to see which of them are still available. Here are the scenarios:
Note: ArcGIS Server-hosted feature services are configured by default to return a maximum of 1000 features. This limitation comes into play for some of the scenarios above and is clearly not ideal. The publisher of the service has the ability to override this setting, but app developers like yourselves do not. One solution for developers is to query the service recursively, such that the full set of features is retrieved in 1000-feature chunks. If you found that you were able to complete the assignment fairly easily, you might consider researching and attempting such an approach. But we will not be expecting you to work out a solution to this problem if it affects your app.
This project is one week in length. Please refer to the Canvas course Calendar for the due date.
In Lesson 7, you learned how to add place-finding capability to an app through the use of locator services and the Search widget. You also saw how that widget can be used to search for values in layer attribute tables. Finally, you looked at the use of the Query class in defining queries (spatial, non-spatial, or both at once) and how Query objects can be used as inputs to methods that return feature counts, feature IDs, or the features themselves. Importantly, you learned how promises play a prominent role in handling the results of asynchronous operations both in geospatial and more generic JavaScript applications.
One last note on the topic of queries: in looking at the SDK or the source code of other apps, you may come across a class called QueryTask. This class was used in earlier versions of Esri's API to execute queries defined in Query objects. While queries can still be executed using a QueryTask, you should note that the QueryTask class supports only layers derived from ArcGIS Server services (i.e., it does not support ArcGIS Online layers).
One aspect of this lesson that you may have found dissatisfying was that the queries were all hard coded into the apps. There was no way for users to supply their own query parameters as we're all used to seeing in more sophisticated apps. The good news is that Lesson 8 is all about UI development. With the knowledge you'll gain from that lesson combined with what you learned here in Lesson 7, you should be well on your way to developing your own killer apps.
Links
[1] https://developers.arcgis.com/javascript/latest/sample-code/widgets-search-3d/index.html
[2] https://codepen.io/jimdetwiler/pen/YYozbm
[3] https://codepen.io/jimdetwiler
[4] https://codepen.io
[5] https://stpaul.maps.arcgis.com/home/item.html?id=e787bdc04cfd4760ba6264bf214e1926
[6] https://developers.arcgis.com/javascript/latest/api-reference/esri-widgets-Search-LocatorSearchSource.html
[7] https://codepen.io/jimdetwiler/pen/wpLvVz
[8] https://developers.arcgis.com/javascript/latest/api-reference/esri-widgets-Search-LayerSearchSource.html
[9] https://codepen.io/jimdetwiler/pen/ExogOYx
[10] https://developers.arcgis.com/javascript/latest/api-reference/esri-tasks-support-Query.html
[11] https://codepen.io/jimdetwiler/pen/eYydQNq
[12] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
[13] https://codepen.io/jimdetwiler/pen/WNdGYGY
[14] https://developers.arcgis.com/javascript/latest/programming-patterns/#async-data
[15] https://www.esri.com/arcgis-blog/products/js-api-arcgis/mapping/making-better-promises/
[16] https://codepen.io/jimdetwiler/pen/GRyjwOd
[17] https://codepen.io/jimdetwiler/pen/abEmPZV
[18] https://www.w3schools.com/jsref/jsref_map.asp
[19] https://codepen.io/jimdetwiler/pen/OJzRrWK
[20] http://www.w3schools.com/jsref/jsref_forEach.asp
[21] https://www.w3schools.com/jsref/met_win_prompt.asp
[22] http://colorbrewer.org
[23] https://pennstate.maps.arcgis.com/home/item.html?id=a630b7db333c464caa003f8806e329a3
[24] https://www.arcgis.com/home/item.html?id=6996f03a1b364dbab4008d99380370ed
[25] https://www.arcgis.com/home/item.html?id=57c1ade4fa7c4e2384e6a23f2b3bd254
[26] https://services1.arcgis.com/fBc8EJBxQRMcHlei/ArcGIS/rest/services/NPSParkBoundaries/FeatureServer
[27] https://www.arcgis.com/home/item.html?id=866f3ab5307240d49f62ba95c2f452ca
[28] https://services.arcgis.com/P3ePLMYs2RVChkJx/ArcGIS/rest/services/Generations/FeatureServer/2
[29] https://www.arcgis.com/home/item.html?id=4a8305b53ed7416795fc6a129ec989ee
[30] https://www.e-education.psu.edu/geog863/sites/www.e-education.psu.edu.geog863/files/file/AlternativeFuelIcons.zip
[31] http://www.arcgis.com/home/item.html?id=9e8b9dbdfd604ad2912c9821af08727a
[32] https://nifc.maps.arcgis.com/home/item.html?id=ef25d7e8c9f3499ba9e3d8e09606e488
[33] https://www.arcgis.com/home/item.html?id=d053e72aabfd4c5ab4139c3829c1e11c#