March 9, 2010

Retrieving legend images from an ArcGIS service

One of the things you're usually required to have in a map is a legend. The ESRI JavaScript ArcGIS REST API does not currently support getting a legend. You can however get a legend using the SOAP API. This example shows a simple hack to getting the legend images. (It makes use of SOAP, but it does not really use it the way it's supposed to be used.)

A SOAP endpoint might look like this http://server.arcgisonline.com/ArcGIS/services/Demographics/USA_Tapestry/MapServer

Here's the SOAP XML message that asks the server for the legend with GetLegendInfo:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soapenv:Body>
<GetLegendInfo xmlns="http://www.esri.com/schemas/ArcGIS/9.3">
<MapName>Layers</MapName>
<LayerIDs/>
<LegendPatch>
<Width>10.0</Width><Height>10.0</Height>
<ImageDPI>96.0</ImageDPI>
</LegendPatch>
<ImageType>
<ImageFormat>esriImagePNG</ImageFormat>
<ImageReturnType>esriImageReturnMimeData</ImageReturnType>
</ImageType>
</GetLegendInfo>
</soapenv:Body>
</soapenv:Envelope>
Adjust the width and height as needed. This assumes that the map name is 'Layers' which I've seen is true in all the services I tried. If you were using a proper SOAP call you might try GetDefaultMapName first to make sure you have the right map name.

The XML returned will contain all the layers in the service along with their legend. Here's the first part of the SOAP response from USA Tapestry:

<?xml version="1.0" encoding="utf-8" ?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://www.esri.com/schemas/ArcGIS/9.3">
<soap:Body>
<tns:GetLegendInfoResponse>
<Result xsi:type="tns:ArrayOfMapServerLegendInfo">
<MapServerLegendInfo xsi:type="tns:MapServerLegendInfo">
<LayerID>1</LayerID>
<Name>Block Groups</Name>
<LegendGroups xsi:type="tns:ArrayOfMapServerLegendGroup">
<MapServerLegendGroup xsi:type="tns:MapServerLegendGroup">
<Heading>2009 Dominant Tapestry Code and Segment Name</Heading>
<LegendClasses xsi:type="tns:ArrayOfMapServerLegendClass">
<MapServerLegendClass xsi:type="tns:MapServerLegendClass">
<Label>1, Top Rung</Label>
<Description></Description>
<SymbolImage xsi:type="tns:ImageResult">
<ImageData>iVBORw0KGgoAAAANSUhEUgAAABEAAAAOBAMAAAA7w+qHAAAACVBMVEXQt9r+/v7+///7vZCoAAAA

A3RSTlP//wDXyg1BAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAJUlEQVQImWNQggIFBiVBMBBSYFCE

sASBLAEGEKAeC2EynAV3AQBHrQhTtwitygAAAABJRU5ErkJggg==

</ImageData>


Each layer name is in the name tag and each symbol for that layer is under the label tag. There may be several label tags for each layer. The tag ImageData contains the image you need to save. If your requested ImageReturnType was esriImageReturnMimeData you get back the raw data for the legend images. The tricky part is converting the legend images, which are output in base64, to regular binary code and saving that into PNG files. Perl handles the decoding very easily with the MIME::Base64 module. If the requested ImageReturnType is esriImageReturnURL the legend images are output on the server and you get back a URL.

Here's the final script in Perl, which gets all the 153 legend images from the USA Tapestry service and parses the XML using some simple regex. The SOAP call is made with a POST using LWP::UserAgent :

use strict;
use LWP::UserAgent;
use MIME::Base64;

#GetLegendInfo
my $xmlRequestString; #set this to the above SOAP XML message
my $server = "server.arcgisonline.com";

my $service = "Demographics/USA_Tapestry";

my $ua = LWP::UserAgent->new;
$ua->timeout(10);
my $url ="http://$server/ArcGIS/services/$service/MapServer";
my $req = HTTP::Request->new('POST', $url, HTTP::Headers->new('Content-Type' => 'text/xml', 'SOAPAction' => ""), $xmlRequestString);
my $response = $ua->request($req);
my $content = $response->content;

my $i =0;
foreach my $layer ($content =~ m/<MapServerLegendInfo(.+?)<\/MapServerLegendInfo/sg){
my ($layerName) = $layer =~ m/<Name>(.+?)<\/Name>/;
print "Layer: $layerName \n";
while ($layer =~ m/<Label>(.*?)<\/Label.*?<ImageData>(.+?)<\/ImageData/sg){
print " $i $1 \n";
open PNG, ">$i.png" || die "Can't open file $!";
binmode PNG;
print PNG decode_base64($2);
close PNG;
$i++;
}
}
die "Error: ", $response->status_line unless $response->is_success;

If you're using the esriImageReturnURL and just want to print out the URLs then replace the while loop with this loop:


while ($layer =~ m/


PS: The ESRI Code Gallery now features code to get the legends. They're using JAVA and proper calls to the SOAP API. It's not as simple as my one script example, but check it out also: http://resources.esri.com/arcgisserver/apis/javascript/arcgis/index.cfm?fa=codeGalleryDetails&scriptID=16096

PPS: The new JavaScript API (version 2.1) features dojo dijit called esri.dijit.Legend that is able to get the legend swatches.