8.2 Executing a Synchronous Task
The example below is built around the drive-time service we’ve been discussing. The app will display 5-, 10-, and 15-minute drive times emanating from a point clicked by the user. While the map is zoomed in to State College, PA, it should work anywhere in the U.S.
Preparing the input parameters
Lines 31-34 define a couple of variables to hold information critical to consuming the service: the REST endpoint of the CreateDriveTimePolygons task and the drive time of interest.
Line 55 creates a new empty GraphicsLayer that will hold the drive-time polygons returned by the service.
A SimpleMarkerSymbol (line 58) is created to show the location of the user’s click.
An array of SimpleFillSymbols is created to depict the drive-time polygons. Note that each symbol is added to the array with an index that corresponds to the drive time the symbols is intended to depict (i.e., one symbol is given an index of 5, one an index of 10, and the last an index of 15).
The first step in executing a geoprocessing service task itself is to create a new Geoprocessor object. Here a Geoprocessor object is instantiated through setting just its url property (lines 87-89).
Line 91 sets up a handler for clicks on the view, causing execution of the findDriveTimePolys() function that begins on line 95.
Earlier in the course, we saw that a promise passes along an object or value (its "payload") to the callback function that the developer attaches to it. Similarly, when you create a handler for the MapView’s click event, as seen here, its callback function has access to an event object. In this case, the object is stored in a variable called evt. The Event Details section of the MapView class page in the SDK documents the properties of this event object. The one used most typically is mapPoint, which returns a Point object representing the location of the user’s click. The latitude and longitude properties of this object are used to create a new Point object. That object is in turn used to set the geometry property of a new Graphic. The Graphic is then added to the GraphicsLayer to remind the user where he/she clicked on the map.
Recall that when looking at the documentation of the CreateDriveTimePolygons task in the REST Service Directory, we saw that its Input_Location parameter should be set using a FeatureSet object. Thus, line 110 creates a new empty FeatureSet; line 111 sets its features property. That property must be set using an array of Graphic objects. Here the Graphic that was created from the user’s click on the map is used to set the property by putting it in brackets (making it a one-item array).
With the FeatureSet created and the driveTimes variable defined earlier, we have what we need to execute the task. The two parameters are stored in an object variable called params. Note that the keys of this object match the names of the input parameters we saw in the task's documentation: Input_Location and Drive_Times.
Running the task
At this point, we’re ready to run the task, passing along the two parameters. We saw in the task's documentation that its execution type was synchronous, which leads us to call on the Geoprocessor object’s execute() method. (If the task was asynchronous, we’d use submitJob instead.)
The execute() method returns a promise, so we can use the then() method to specify a function to execute when the promise has resolved. Here that function is called drawPolys().
As shown in the execute() method’s documentation, when its promise has been resolved, it returns an object (stored here in a variable called gpResponse). This object has two properties: messages and results. The messages can be useful in trying to debug a task that isn't producing the expected outcome. But if you’ve supplied the right inputs, you’ll want to read the results property to get at the task's outputs. This property returns an array of ParameterValue objects.
Processing the task’s response
Because the CreateDriveTimePolys task has just one output parameter, we can retrieve that parameter using the expression results. We then need to read the parameter’s value property to get at the actual output data. What we get back could be a string, double, or many other things, depending on the parameter's data type. As was discussed earlier, this task’s Output_Drive_Time_Polygons parameter returns a GPFeatureRecordSetLayer, which translates to a FeatureSet in the JS API. As we saw when working with query results, we read the features property to get at the array of Graphics in a FeatureSet. Given that 3 drive times were supplied to the task, we can expect the drivePolys variable on line 121 to store an array of 3 polygon Graphics.
Lines 123-126 use the now familiar map() method to create a new array of polygon Graphics, in which each one has a symbol assigned to it. Let’s talk about how that symbol is assigned.
Another thing we learned from the Services Directory was that the polygons returned had fields FromBreak and ToBreak. Using console.log(), I determined that the 5-minute polygon had a FromBreak value of 1 and a ToBreak value of 5. Likewise, the other polygons had values of 6 and 10, and 11 and 15, respectively. Thus, the expression poly.attributes.ToBreak will return a value of 5, 10 or 15. That value is plugged inside the brackets to specify which SimpleFillSymbol in the fillSymbols array to use for the Graphic being processed by the map() method.
With the new symbolized Graphics array created, it is added to the GraphicsLayer (line 128). It’s then also used as the target of the MapView’s goTo() method to zoom to the extent of the polygons.
Implementing the Standby dijit
While synchronous tasks typically don’t take too long, they can still cause enough of a delay that the user might wonder if there’s a problem, click again on the map, etc. A good practice to follow in such cases is to provide some sort of indicator that the app is "thinking." The Standby dijit is one option for providing this sort of indicator.
In this example, a Standby dijit is created on lines 47-50, with its target set to the MapView that was just created.
The dijit is then added to the DOM and its Startup() method called.
The dijit is then made visible at the very beginning of the findDriveTimePolys() function using its show() method.
Recall that the findDriveTimePolys() function finishes with a call to the Geoprocessor's execute() method. Chained to the call to execute() is a call to the then() method, which specifies that the drawPolys() function should be run after the geoprocessing task has completed. It is at the end of drawPolys(), after all the polygons have been added, that the dijit's hide() method is used to make it invisible.
This example was built using Esri JS API version 4.8. If you attempt to reproduce it using version 4.9 or higher, you'll see an error in the console to the effect of "Failed to load.... No 'Access-Control-Allow-Origin' header is present on the requested resource" and no drivetime polygons will be displayed. The reason behind this is a bit complicated...
Browsers have a built-in "same-origin policy" that prevents scripts from accessing resources from a different origin (i.e., web server). This is a security measure to prevent the downloading of malicious content.
CORS (Cross Origin Resource Sharing) developed as a means to allow certain resources to be transferred safely from server to client browsers. An Esri blog post discusses changes made in CORS support in the ArcGIS JS API at version 4.9:
The server that hosts the drivetime service does not support CORS (because its version of ArcGIS Server predates the development of CORS). The JS API v4.8 app is able to process the service's response, however, because pre-4.9 versions of the API were written to request the data in JSONP format, a bit of a hack that makes it possible to get around the same-origin policy. (A popular stackoverflow post provides more info on JSONP.)
ArcGIS Server version 10.1 and higher offers support for CORS, though it's up to server administrators to enable it. Beginning at v4.9 of the JS API, Esri decided to ditch JSONP in favor of the more secure CORS approach. If CORS is enabled on the server hosting the service you're consuming in your app, then you can consume that service using an approach modeled after the example above. However, if CORS is not enabled, you will need to employ a proxy script.
Esri's JS API SDK describes how to configure a proxy script to enable you to use this or any other non-CORS-enabled service. Here are two pages to consult:
Following the info found in the above pages, I:
1. downloaded Esri's PHP proxy script since the personal.psu.edu server is configured to run PHP scripts. Other server-side languages could be used as well,
2. modified the proxy.config file to remove the logfile setting and to refer to the sampleserver1.arcgisonline.com server that hosts the drivetime polygons service,
3. uploaded the proxy files to a folder in my PSU web space, and
4. modified my JS code so that the proxy script is used to connect to the service.
Here's this proxy-based version of the app:
Note the URL used above... php.scripts.psu.edu is actually the server that supports PHP. It has access to the files you upload to personal.psu.edu.
The takeaway message here is that to consume services published on non-CORS-supporting servers, you need to either a) use a pre-4.9 version of the API, or b) configure a proxy.
Now let's have a look at how an asynchronous task is run.