ArcGIS Image Services and Leaflet

I’ve become a big fan of Leaflet for putting maps on the web. It gives me most of what I need without much of what I don’t and is fairly easily extended, as shown by the impressive work of Jason Sanford.

A while back, Dave Bouwman blogged about work he and the team at DTS Agile had done extending Leaflet to support ArcGIS Server layers. Given that there are a lot of ArcGIS Servers out there, this is a good thing to have. Thanks to section 4(f) of the Esri Web Services Terms of Use, it’s less useful for use with ArcGIS Online, but that’s probably the topic of another post.

I recently had the need to use an ArcGIS Server image service with a Leaflet app. Specifically, I was using the USGS NAIP image service. This service is available as a WMS, which works perfectly well with Leaflet, but I needed to take advantage of some of the capabilities of the Esri service, such being able to set the interpolation method.

The API signature for interacting with an image service is somewhat different from that of a dynamic map service with ArcGIS Server so I took the DTS AgsDynamicLayer class and modified to an AgsImageLayer class. Because the image service does some raster operations on the fly, it can be a little slower than a standard tiled or dynamic service. As a result, I’d recommend only going this route if you need to allow your users to fiddle with some options. For most production web-mapping applications, you’ll probably want to stick with tiles.

It seemed kind of silly to fork the DTS code for one class so I sent it to Dave, who was gracious enough to accept it. Thanks to DTS for their work, which made my life much easier. Since they have day jobs, too, I’m not sure when it will appear so I thought I’d post it here as well in case someone may find it useful. Most of the code came over from the DTS class, with modifications I needed to work with image services. With apologies for the length, here it is:

[sourcecode language=”javascript”]
//Class for interacting with ArcGIS Server image services
//Bill Dollins – Zekiah Technologies
//Modified from AgsDynamicLayer.js by DTSAgile

L.AgsImageLayer = L.Class.extend({
includes: L.Mixin.Events,

options: {
minZoom: 0,
maxZoom: 18,
attribution: ”,
opacity: 1,
format: ‘PNG8′,
bandids: ”,
compressionquality: 0,
interpolation: ‘RSP_NearestNeighbor’,
pixelType: ‘U8′,

unloadInvisibleTiles: L.Browser.mobileWebkit
},

initialize: function (/*String*/url, /*Object*/options) {
L.Util.setOptions(this, options);
this._url = url;
},

//public properties that modify the map

setInterpolation: function (interpolation) {
this.options.interpolation = interpolation;
},

getInterpolation: function () {
return this.options.interpolation;
},

setOpacity: function (opacity) {
//set it immediately
if (this._image) {
this._image.style.opacity = opacity;
// stupid webkit hack to force redrawing of tiles
this._image.style.webkitTransform += ‘ translate(0,0)';
}
this.options.opacity = opacity;
},

getOpacity: function () {
return this.options.opacity;
},

reset: function () {
this._reset();
},

update: function () {
// var topLeft = this._map.latLngToLayerPoint(this._map.getBounds().getNorthWest()),
// bottomRight = this._map.latLngToLayerPoint(this._map.getBounds().getSouthEast()),
// size = bottomRight.subtract(topLeft);

// L.DomUtil.setPosition(this._image, topLeft);
// this._image.style.width = size.x + ‘px';
// this._image.style.height = size.y + ‘px';

this._image.updating = false;
this._updateLayer();
},

show: function () {
this._image.style.display = ‘block';
this._image.style.visibility = ‘visible';
},

hide: function () {
this._image.style.display = ‘none';
},

isVisible: function () {
return this._image.style.display === ‘block';
},

onAdd: function (map) {
this._map = map;

this._reset();

map.on(‘viewreset’, this._reset, this);
map.on(‘moveend’, this._moveEnd, this);
map.on(‘zoomend’, this._zoomEnd, this);
},

onRemove: function (map) {
map.getPanes().mapPane.removeChild(this._image);
map.off(‘viewreset’, this._reset, this);
map.off(‘moveend’, this._moveEnd, this);
map.off(‘zoomend’, this._zoomEnd, this);
},

_initImage: function () {
this._image = L.DomUtil.create(‘img’, ‘leaflet-image-layer’);

this._image.style.visibility = ‘hidden';
this._image.style.opacity = this.options.opacity;
this._image.style.display = ‘block';
//TODO createImage util method to remove duplication
L.Util.extend(this._image, {
onselectstart: L.Util.falseFn,
onmousemove: L.Util.falseFn,
onload: this._onImageLoad,
src: this._getImageUrl(),
updating: false,
agsLayer: this,
map: this._map
});
this._map.getPanes().mapPane.appendChild(this._image);
},

_getImageUrl: function () {
//construct the export image url
var bnds = this._map.getBounds();
var sz = this._map.getSize();
//bboxsr & imagesr params need to be specified like so to avoid alignment problems on some map services – not sure why
var bbox = ‘bbox=’ + bnds.getSouthEast().lng + ‘%2C’ + bnds.getSouthEast().lat + ‘%2C’ + bnds.getNorthWest().lng + ‘%2C’ + bnds.getNorthWest().lat + ‘&bboxsr=4326&imageSR=3857′;
var size = ‘&size=’ + sz.x + ‘%2C’ + sz.y;
var format = ‘&format=’ + this.options.format;
var pixeltype = ‘&pixelType=’ + this.options.pixelType;
var interpolation = ‘&interpolation=’ + this.options.interpolation;
//Some of the following parameters are supported by ArcGIS Server Image Services but not implemented here.
//They have been included as placeholders.
var nodata = ‘&noData=';
var compressionquality = ‘&compressionQuality=’ + this.options.compressionquality;
var bandids = ‘&bandIds=’ + this.options.bandids;
var mosaicprops = ‘&mosaicProperties=';
var viewpointprops = ‘&viewpointProperties=';
var url = this._url + ‘/exportImage?’ + bbox + size + format + pixeltype + nodata + interpolation + compressionquality + bandids + mosaicprops + viewpointprops + ‘&f=image';
return url; // this._url + ‘/export?’ + bbox + size + layers + format + transparent + ‘&f=image';
},

_updateLayer: function () {
if (!this._image.updating) {
//console.log(‘Updating layer NW: ‘ + map.getBounds().getNorthWest());
this._image.updating = true;

//update the src based on the new location
this._image.src = this._getImageUrl();
//reset the image location on the map
// //hang the info on the image, we’ll actually update it onload to make sure we don’t reposition it before the new image comes down
//this doesn’t seem to work on mobile
// this._image.topLeft = this._map.latLngToLayerPoint(this._map.getBounds().getNorthWest());
// var bottomRight = this._map.latLngToLayerPoint(this._map.getBounds().getSouthEast());
// this._image.size = bottomRight.subtract(this._image.topLeft);

var topLeft = this._map.latLngToLayerPoint(this._map.getBounds().getNorthWest()),
bottomRight = this._map.latLngToLayerPoint(this._map.getBounds().getSouthEast()),
size = bottomRight.subtract(topLeft);
L.DomUtil.setPosition(this._image, topLeft);
this._image.style.width = size.x + ‘px';
this._image.style.height = size.y + ‘px';
}
},

_moveEnd: function () {
//console.log(‘in _moveEnd : NW: ‘ + map.getBounds().getNorthWest());
//don’t set display:none for moves – makes for smoother panning – no flicker
//oops, that didn’t work on mobile

this._image.style.display = ‘none';
this._updateLayer();
},

_zoomEnd: function () {
//console.log(‘in _moveEnd’);

// //zoom the image…(animate it?)
// //L.DomUtil.setPosition(this, this.topLeft);
// //debugger;
// //it’s gonna be something like this but it’s not quite right – also will need to get/ calculate the correct factor (using 1.5 below) and change it for zoom out
// //and we need to properly calculate the new left and top – just hard coded approximate values below
// this._image.style.left = ‘-420px';
// this._image.style.top = ‘-228px';
// this._image.style.width = this._image.width * 1.5 + ‘px';
// this._image.style.height = this._image.height * 1.5 + ‘px';

//for now, we’ll just do this
this._image.style.display = ‘none';
this._updateLayer();
},

_reset: function () {
if (this._image) {
this._map.getPanes().mapPane.removeChild(this._image);
}
this._initImage();
this._updateLayer();
},

_onImageLoad: function () {
// //reset the image location on the map – doing it this way does not seem to work on mobile
// L.DomUtil.setPosition(this, this.topLeft);
// this.style.width = this.size.x + ‘px';
// this.style.height = this.size.y + ‘px';

//this is the image

//make sure it’s visible and reset the updating flag
this.style.visibility = ‘visible';
this.style.display = ‘block';

this.updating = false;
}
});
[/sourcecode]