leaflet.wms
Version:
A Leaflet plugin for working with Web Map services, providing: single-tile/untiled/nontiled layers, shared WMS sources, and GetFeatureInfo-powered identify
495 lines (437 loc) • 14.1 kB
JavaScript
/*!
* leaflet.wms.js
* A collection of Leaflet utilities for working with Web Mapping services.
* (c) 2014-2016, Houston Engineering, Inc.
* MIT License
*/
(function (factory) {
// Module systems magic dance, Leaflet edition
if (typeof define === 'function' && define.amd) {
// AMD
define(['leaflet'], factory);
} else if (typeof module !== 'undefined') {
// Node/CommonJS
module.exports = factory(require('leaflet'));
} else {
// Browser globals
if (typeof this.L === 'undefined')
throw 'Leaflet must be loaded first!';
// Namespace
this.L.WMS = this.L.wms = factory(this.L);
}
}(function (L) {
// Module object
var wms = {};
// Quick shim for Object.keys()
if (!('keys' in Object)) {
Object.keys = function(obj) {
var result = [];
for (var i in obj) {
if (obj.hasOwnProperty(i)) {
result.push(i);
}
}
return result;
};
}
/*
* wms.Source
* The Source object manages a single WMS connection. Multiple "layers" can be
* created with the getLayer function, but a single request will be sent for
* each image update. Can be used in non-tiled "overlay" mode (default), or
* tiled mode, via an internal wms.Overlay or wms.TileLayer, respectively.
*/
wms.Source = L.Layer.extend({
'options': {
'untiled': true,
'identify': true
},
'initialize': function(url, options) {
L.setOptions(this, options);
if (this.options.tiled) {
this.options.untiled = false;
}
this._url = url;
this._subLayers = {};
this._overlay = this.createOverlay(this.options.untiled);
},
'createOverlay': function(untiled) {
// Create overlay with all options other than untiled & identify
var overlayOptions = {};
for (var opt in this.options) {
if (opt != 'untiled' && opt != 'identify') {
overlayOptions[opt] = this.options[opt];
}
}
if (untiled) {
return wms.overlay(this._url, overlayOptions);
} else {
return wms.tileLayer(this._url, overlayOptions);
}
},
'onAdd': function() {
this.refreshOverlay();
},
'getEvents': function() {
if (this.options.identify) {
return {'click': this.identify};
} else {
return {};
}
},
'setOpacity': function(opacity) {
this.options.opacity = opacity;
if (this._overlay) {
this._overlay.setOpacity(opacity);
}
},
'bringToBack': function() {
this.options.isBack = true;
if (this._overlay) {
this._overlay.bringToBack();
}
},
'bringToFront': function() {
this.options.isBack = false;
if (this._overlay) {
this._overlay.bringToFront();
}
},
'getLayer': function(name) {
return wms.layer(this, name);
},
'addSubLayer': function(name) {
this._subLayers[name] = true;
this.refreshOverlay();
},
'removeSubLayer': function(name) {
delete this._subLayers[name];
this.refreshOverlay();
},
'refreshOverlay': function() {
var subLayers = Object.keys(this._subLayers).join(",");
if (!this._map) {
return;
}
if (!subLayers) {
this._overlay.remove();
} else {
this._overlay.setParams({'layers': subLayers});
this._overlay.addTo(this._map);
}
},
'identify': function(evt) {
// Identify map features in response to map clicks. To customize this
// behavior, create a class extending wms.Source and override one or
// more of the following hook functions.
var layers = this.getIdentifyLayers();
if (!layers.length) {
return;
}
this.getFeatureInfo(
evt.containerPoint, evt.latlng, layers,
this.showFeatureInfo
);
},
'getFeatureInfo': function(point, latlng, layers, callback) {
// Request WMS GetFeatureInfo and call callback with results
// (split from identify() to faciliate use outside of map events)
var params = this.getFeatureInfoParams(point, layers),
url = this._url + L.Util.getParamString(params, this._url);
this.showWaiting();
this.ajax(url, done);
function done(result) {
this.hideWaiting();
var text = this.parseFeatureInfo(result, url);
callback.call(this, latlng, text);
}
},
'ajax': function(url, callback) {
ajax.call(this, url, callback);
},
'getIdentifyLayers': function() {
// Hook to determine which layers to identify
if (this.options.identifyLayers)
return this.options.identifyLayers;
return Object.keys(this._subLayers);
},
'getFeatureInfoParams': function(point, layers) {
// Hook to generate parameters for WMS service GetFeatureInfo request
var wmsParams, overlay;
if (this.options.untiled) {
// Use existing overlay
wmsParams = this._overlay.wmsParams;
} else {
// Create overlay instance to leverage updateWmsParams
overlay = this.createOverlay(true);
overlay.updateWmsParams(this._map);
wmsParams = overlay.wmsParams;
wmsParams.layers = layers.join(',');
}
var infoParams = {
'request': 'GetFeatureInfo',
'query_layers': layers.join(','),
'X': Math.round(point.x),
'Y': Math.round(point.y)
};
return L.extend({}, wmsParams, infoParams);
},
'parseFeatureInfo': function(result, url) {
// Hook to handle parsing AJAX response
if (result == "error") {
// AJAX failed, possibly due to CORS issues.
// Try loading content in <iframe>.
result = "<iframe src='" + url + "' style='border:none'>";
}
return result;
},
'showFeatureInfo': function(latlng, info) {
// Hook to handle displaying parsed AJAX response to the user
if (!this._map) {
return;
}
this._map.openPopup(info, latlng);
},
'showWaiting': function() {
// Hook to customize AJAX wait animation
if (!this._map)
return;
this._map._container.style.cursor = "progress";
},
'hideWaiting': function() {
// Hook to remove AJAX wait animation
if (!this._map)
return;
this._map._container.style.cursor = "default";
}
});
wms.source = function(url, options) {
return new wms.Source(url, options);
};
/*
* Layer
* Leaflet "layer" with all actual rendering handled via an underlying Source
* object. Can be called directly with a URL to automatically create or reuse
* an existing Source. Note that the auto-source feature doesn't work well in
* multi-map environments; so for best results, create a Source first and use
* getLayer() to retrieve wms.Layer instances.
*/
wms.Layer = L.Layer.extend({
'initialize': function(source, layerName, options) {
L.setOptions(this, options);
if (!source.addSubLayer) {
// Assume source is a URL
source = wms.getSourceForUrl(source, options);
}
this._source = source;
this._name = layerName;
},
'onAdd': function() {
if (!this._source._map)
this._source.addTo(this._map);
this._source.addSubLayer(this._name);
},
'onRemove': function() {
this._source.removeSubLayer(this._name);
},
'setOpacity': function(opacity) {
this._source.setOpacity(opacity);
},
'bringToBack': function() {
this._source.bringToBack();
},
'bringToFront': function() {
this._source.bringToFront();
}
});
wms.layer = function(source, options) {
return new wms.Layer(source, options);
};
// Cache of sources for use with wms.Layer auto-source option
var sources = {};
wms.getSourceForUrl = function(url, options) {
if (!sources[url]) {
sources[url] = wms.source(url, options);
}
return sources[url];
};
// Copy tiled WMS layer from leaflet core, in case we need to subclass it later
wms.TileLayer = L.TileLayer.WMS;
wms.tileLayer = L.tileLayer.wms;
/*
* wms.Overlay:
* "Single Tile" WMS image overlay that updates with map changes.
* Portions of wms.Overlay are directly extracted from L.TileLayer.WMS.
* See Leaflet license.
*/
wms.Overlay = L.Layer.extend({
'defaultWmsParams': {
'service': 'WMS',
'request': 'GetMap',
'version': '1.1.1',
'layers': '',
'styles': '',
'format': 'image/jpeg',
'transparent': false
},
'options': {
'crs': null,
'uppercase': false,
'attribution': '',
'opacity': 1,
'isBack': false,
'minZoom': 0,
'maxZoom': 18
},
'initialize': function(url, options) {
this._url = url;
// Move WMS parameters to params object
var params = {};
for (var opt in options) {
if (!(opt in this.options)) {
params[opt] = options[opt];
delete options[opt];
}
}
L.setOptions(this, options);
this.wmsParams = L.extend({}, this.defaultWmsParams, params);
},
'setParams': function(params) {
L.extend(this.wmsParams, params);
this.update();
},
'getAttribution': function() {
return this.options.attribution;
},
'onAdd': function() {
this.update();
},
'onRemove': function(map) {
if (this._currentOverlay) {
map.removeLayer(this._currentOverlay);
delete this._currentOverlay;
}
if (this._currentUrl) {
delete this._currentUrl;
}
},
'getEvents': function() {
return {
'moveend': this.update
};
},
'update': function() {
if (!this._map) {
return;
}
// Determine image URL and whether it has changed since last update
this.updateWmsParams();
var url = this.getImageUrl();
if (this._currentUrl == url) {
return;
}
this._currentUrl = url;
// Keep current image overlay in place until new one loads
// (inspired by esri.leaflet)
var bounds = this._map.getBounds();
var overlay = L.imageOverlay(url, bounds, {'opacity': 0});
overlay.addTo(this._map);
overlay.once('load', _swap, this);
function _swap() {
if (!this._map) {
return;
}
if (overlay._url != this._currentUrl) {
this._map.removeLayer(overlay);
return;
} else if (this._currentOverlay) {
this._map.removeLayer(this._currentOverlay);
}
this._currentOverlay = overlay;
overlay.setOpacity(
this.options.opacity ? this.options.opacity : 1
);
if (this.options.isBack === true) {
overlay.bringToBack();
}
if (this.options.isBack === false) {
overlay.bringToFront();
}
}
if ((this._map.getZoom() < this.options.minZoom) ||
(this._map.getZoom() > this.options.maxZoom)){
this._map.removeLayer(overlay);
}
},
'setOpacity': function(opacity) {
this.options.opacity = opacity;
if (this._currentOverlay) {
this._currentOverlay.setOpacity(opacity);
}
},
'bringToBack': function() {
this.options.isBack = true;
if (this._currentOverlay) {
this._currentOverlay.bringToBack();
}
},
'bringToFront': function() {
this.options.isBack = false;
if (this._currentOverlay) {
this._currentOverlay.bringToFront();
}
},
// See L.TileLayer.WMS: onAdd() & getTileUrl()
'updateWmsParams': function(map) {
if (!map) {
map = this._map;
}
// Compute WMS options
var bounds = map.getBounds();
var size = map.getSize();
var wmsVersion = parseFloat(this.wmsParams.version);
var crs = this.options.crs || map.options.crs;
var projectionKey = wmsVersion >= 1.3 ? 'crs' : 'srs';
var nw = crs.project(bounds.getNorthWest());
var se = crs.project(bounds.getSouthEast());
// Assemble WMS parameter string
var params = {
'width': size.x,
'height': size.y
};
params[projectionKey] = crs.code;
params.bbox = (
wmsVersion >= 1.3 && crs === L.CRS.EPSG4326 ?
[se.y, nw.x, nw.y, se.x] :
[nw.x, se.y, se.x, nw.y]
).join(',');
L.extend(this.wmsParams, params);
},
'getImageUrl': function() {
var uppercase = this.options.uppercase || false;
var pstr = L.Util.getParamString(this.wmsParams, this._url, uppercase);
return this._url + pstr;
}
});
wms.overlay = function(url, options) {
return new wms.Overlay(url, options);
};
// Simple AJAX helper (since we can't assume jQuery etc. are present)
function ajax(url, callback) {
var context = this,
request = new XMLHttpRequest();
request.onreadystatechange = change;
request.open('GET', url);
request.send();
function change() {
if (request.readyState === 4) {
if (request.status === 200) {
callback.call(context, request.responseText);
} else {
callback.call(context, "error");
}
}
}
}
return wms;
}));