Proxy Servers
A quick note before we get started. You probably need to use a proxy program, to get around the browsers' same-origin restriction.Browsers refuse to accept data from a server (e.g. your geoprocessor) unless the URL is the same as your website. The ArcGIS JS API has workarounds in place so you don't need a proxy for very small requests (e.g. a Query with only 1 where clause and a very simple geometry) but a proxy is required for anything larger (e.g. when you create a long clause and/or more complex geometries).
You really should set up a proxy service:
- Step 1: Install a proxy program. You can write your own (I have a previous blog post on that topic) or download one of ESRI's on their page about proxies.
- Step 2: Define the proxyUrl like this: esri.config.defaults.io.proxyUrl = "proxy.php";
Geoprocessing Services
In the last posting, I showed how to use a Query to request info from an ArcGIS server. There are other services available via the API such as geocoders and directions services. I'll suffice with a simple example, as these services are well documented and you should be able to adapt the previous examples to ESRI's documentation and be off to a good start.// a hypothetical GraphicsLayer, which may have polygons drawn onto itEvery GeometryService method has its own parameters and returns, and you'll want to spend some time with the documentation for whatever services you'll need in your application. Don't memorize the docs, as much as learn what services exist and learn the basic "input and callback" design pattern.
// if you read the previous post, you know all about GraphicsLayer and Graphics ;)
function findPolygonAcres() {
polygons = DRAWN_AREAS.graphics;
if (! polygons.length) return alert("No polygons showing.");
unionAndArea(polygons);
}
// step 1: find the union of all the submitted polygons, so we don't double-count acres if two polygons overlap
// the service accepts a list of Geometry objects, returns a single Geometry which is the union of the ones you gave it
// we're passing in Graphics, and need to extract their Geometries
function unionAndArea(graphics) {
var geoms = [];
for (var i=0, l=graphics.length; i<l; i++) geoms.push( graphics[i].geometry );
var geomsvc = new esri.tasks.GeometryService("http://sampleserver3.arcgisonline.com/ArcGIS/rest/services/Geometry/GeometryServer");
geomsvc.union(geoms, function(singlegeom) {
getPolygonArea(singlegeom);
});
}
// step 2: given a single Geometry, ask the GeometryService to calculate the area
// note that the service will accept a list of polygons, but we did want to find the union
// so we don't double count AND I wanted to be flashy about cascading a geoprocessing result to another geoprocessor
function getPolygonArea(polygon) {
var params = new esri.tasks.AreasAndLengthsParameters();
params.lengthUnit = esri.tasks.GeometryService.UNIT_FOOT;
params.areaUnit = esri.tasks.GeometryService.UNIT_ACRES;
params.calculationType = 'geodesic';
params.polygons = [ polygon ];
// since we did a union, we know there's only 1 result, and can simply take item 0 from the returned list
var geomsvc = new esri.tasks.GeometryService("http://sampleserver3.arcgisonline.com/ArcGIS/rest/services/Geometry/GeometryServer");
geomsvc.areasAndLengths(params,function (areas_and_lengths) {
var acres = areas_and_lengths.areas[0];
alert('acres + ' acres');
},function (error) {
alert(error.details);
});
}
References:
https://developers.arcgis.com/en/javascript/jsapi/geometryservice-amd.html
https://developers.arcgis.com/en/javascript/jsapi/geometryservice-amd.html#union
https://developers.arcgis.com/en/javascript/jsapi/geometryservice-amd.html#areasandlengths
The Geoprocessor Task
The Geoprocessor task is to access some arbitrary geoprocessing endpoint. Typically these will be some custom geoprocessing service, to perform some calculation that ESRI's own services don't provide. Every custom geoprocessor will be different, by nature. For example, a geoprocessor could accept as parameters a point location and a dollar value, and return a list of Graphics which are houses within 10 miles within the price range. Or a geoprocessor could accept a list of polygon geometries, take the union internally and compare against the Census/ACS data, and return a structure of demographic attributes within that polygon (not necessarily Graphics at all).Geoprocessors do have a .execute() method which hypothetically will submit the parameters and get back results in one call. But in reality I've always had to use the .submitJob() design pattern. It's slightly more complex, but for the services I've used it's more reliable than .execute().
// the generic design pattern for Geoprocessor, using submitJobReferences:
// this hypothetical geoprocessing service accepts a single Point geometry and a dollar amount
// and returns a list of all houses fitting the price range (a list of Graphics which could go onto the map)
var params = {};
params.dollars = document.getElementById('pricerange').value;
params.location = MAP.extent.getCenter();
var housefinder = new esri.tasks.Geoprocessor(SERVICE_URL);
housefinder.submitJob(params, function (results) {
// param 2: success callback, with the parameter being the results structure
// the structure depends on the service, and every service is different
// this one says how many results came up, the largest square footage found, and a list of Graphics
alert('Found ' + results.numresults + ' houses.' + "\n" 'Largest was ' + results.maxsqft + ' sq ft');
for (var i=0, l=results.houses.length; i<l; i++) {
GRAPHICS.add( results.houses[i] );
document.getElementById('listing').innerHTML += results.houses[i].attributes.title;
}
}, function (status) {
// param 3: status callback; every few seconds the API will ping the service again to ask the status of the job
// your service may be super spiffy and have useful messages such as "23 out of 55 processed" or "62% done"
// and maybe your application would like to display that status in a popup
console.log(status.message);
}, function (error) {
// param 4: error callback
alert(error.message);
});
https://developers.arcgis.com/en/javascript/jsapi/geoprocessor-amd.html
http://sampleserver6.arcgisonline.com/arcgis/rest/services/Elevation/WorldElevations/MapServer/exts/ElevationsSOE_NET/ElevationLayers/0
The Elevation Geoprocessor
In our case, we're interested in ESRI's elevation geoprocessing service. It took a little fussing to figure out, but the inputs and outputs are:
- Only one input param: geometries a list of esri.Geometry.Polyline objects
- Output: an object with a .geometries attribute, one for each of your input Polylines; each geometry has a .paths attribute, a list of which corresponds to the paths in each Polyline; each path is a list of 3-tuples, each one being [ x, y, elevation ] X and Y are in whatever coordinates you gave (usually the map's spatialReference) and Elevation is in meters.
Question: When we query the trails, why not just have the trails server hand back the Z as part of the Query? We're already using .returnGeometry=true right? Answer: The Query made to the server will omit the &returnZ=true parameter if you include it. The ArcGIS JS API doesn't handle 3D data, all the way down to the esri.Geometry.Point, so even if you could modify the request over the wire, you'd get back data that the rest of the API can't handle. No, elevation data is a truly separate thing.In our case we'll pass in exactly 1 line (the trail that's highlighted) and get back 1 geometry with 1 path, and there we go. And here it is. This builds on my previous code bites, where we created the MAP and the HIGHLIGHT_TRAIL GraphicsLayer.
// on a map click, make a query for the trail and then for its elevation profile...We're almost there! Clicking the map triggers a Query to find a trail under the click. The Query callback draws the returned Graphic onto the map (highlighting the trail) and then submits a geoprocessing request to the Elevation service. On a successful return, we have a single "path" which is a list of [x,y,z] tuples... and that's our elevation profile.
dojo.connect(MAP, "onClick", function (event) {
handleMapClick(event);
});
// handle a map click, by firing a Query
function handleMapClick(event) {
// if the trails layer isn't in range, skip this
if (! OVERLAY_TRAILS.visibleAtMapScale ) return;
// compose the query: just the name field, and in this 50 meter "radius" from our click
var query = new esri.tasks.Query();
query.returnGeometry = true;
query.outFields = [ "NAME" ];
query.geometry = new esri.geometry.Extent({
"xmin": event.mapPoint.x - 50,
"ymin": event.mapPoint.y - 50,
"xmax": event.mapPoint.x + 50,
"ymax": event.mapPoint.y + 50,
"spatialReference": event.mapPoint.spatialReference
});
var task = new esri.tasks.QueryTask(ARCGIS_URL + '/' + LAYERID_TRAILS );
task.execute(query, function (featureSet) {
handleMapClickResults(featureSet);
});
}
// handle the Query result
function handleMapClickResults(features) {
// start by clearing previous results
HIGHLIGHT_TRAIL.clear();
// grab the first hit; nothing found? bail
if (! features.features.length) return;
var feature = features.features[0];
// highlight using the given vector geometry...
var symbol = new esri.symbol.SimpleLineSymbol(esri.symbol.SimpleLineSymbol.STYLE_SOLID, new dojo.Color(HIGHLIGHT_COLOR), 5);
HIGHLIGHT_TRAIL.add(feature);
feature.setSymbol(symbol);
// now make the geoprocessing call to fetch the elevation info
// there's only 1 param: a list of geometries; in our case the list is 1 item, that being the feature we got as a result
var elevsvc = new esri.tasks.Geoprocessor("http://sampleserver6.arcgisonline.com/arcgis/rest/services/Elevation/WorldElevations/MapServer/exts/ElevationsSOE_NET/ElevationLayers/0/GetElevations");
var params = { geometries:[ feature.geometry ] };
elevsvc.submitJob(params, function (reply) {
// success: grab the 1 path we were given back, convert it into chart-friendly points, then chart them
var path;
try {
path = reply.geometries[0].paths[0];
} catch(e) { alert("Elevation service didn't return an elevation profile."); }
// we now have a valid path, and want to massage it into chart-friendly format
// more on that next time!
console.log(path);
}, function (status) {
}, function (error) {
alert(error);
});
}
In my next posting, we'll massage the returned tuples into a nice chart-friendly structure, including miles traveled and elevation at each point, then chart is using my favorite chart system, Highcharts.
References:
https://developers.arcgis.com/en/javascript/jsapi/geoprocessor-amd.html
http://sampleserver6.arcgisonline.com/arcgis/rest/services/Elevation/WorldElevations/MapServer/exts/ElevationsSOE_NET/ElevationLayers/0
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.