1/*! 2 * leaflet.wms.js 3 * A collection of Leaflet utilities for working with Web Mapping services. 4 * (c) 2014, Houston Engineering, Inc. 5 * MIT License 6 */ 7 8(function (factory) { 9 // Module systems magic dance, Leaflet edition 10 if (typeof define === 'function' && define.amd) { 11 // AMD 12 define(['leaflet'], factory); 13 } else if (typeof module !== 'undefined') { 14 // Node/CommonJS 15 module.exports = factory(require('leaflet')); 16 } else { 17 // Browser globals 18 if (typeof this.L === 'undefined') 19 throw 'Leaflet must be loaded first!'; 20 // Namespace 21 this.L.WMS = this.L.wms = factory(this.L); 22 } 23}(function (L) { 24 25// Module object 26var wms = {}; 27 28// Hacky backport of L.Layer for 0.7.3 29if (!L.Layer) { 30 L.Layer = L.LayerGroup.extend({ 31 'onAdd': function(map) { 32 this._map = map; 33 if (map && this.getEvents) { 34 var events = this.getEvents(); 35 for (var evt in events) { 36 map.on(evt, events[evt], this); 37 } 38 } 39 }, 40 'onRemove': function() { 41 delete this._map; 42 } 43 }); 44} 45 46/* 47 * wms.Source 48 * The Source object manages a single WMS connection. Multiple "layers" can be 49 * created with the getLayer function, but a single request will be sent for 50 * each image update. Can be used in non-tiled "overlay" mode (default), or 51 * tiled mode, via an internal wms.Overlay or wms.TileLayer, respectively. 52 */ 53wms.Source = L.Layer.extend({ 54 'options': { 55 'tiled': false, 56 'identify': true 57 }, 58 59 'initialize': function(url, options) { 60 this._url = url; 61 this._subLayers = {}; 62 options = L.setOptions(this, options); 63 this._overlay = this.createOverlay(url, options); 64 }, 65 66 'createOverlay': function(url, options) { 67 if (options.tiled) { 68 return wms.tileLayer(url, options); 69 } else { 70 return wms.overlay(url, options); 71 } 72 }, 73 74 'onAdd': function(map) { 75 L.Layer.prototype.onAdd.call(this, map); 76 this.refreshOverlay(); 77 }, 78 79 'getEvents': function() { 80 if (this.options.identify) { 81 return {'click': this.identify}; 82 } else { 83 return {}; 84 } 85 }, 86 87 'getLayer': function(name) { 88 return wms.layer(this, name); 89 }, 90 91 'addSubLayer': function(name) { 92 this._subLayers[name] = true; 93 this.refreshOverlay(); 94 }, 95 96 'removeSubLayer': function(name) { 97 delete this._subLayers[name]; 98 this.refreshOverlay(); 99 }, 100 101 'refreshOverlay': function() { 102 var subLayers = Object.keys(this._subLayers).join(","); 103 if (!this._map) { 104 return; 105 } 106 if (!subLayers) { 107 this._map.removeLayer(this._overlay); 108 } else { 109 this._overlay.setParams({'layers': subLayers}); 110 this._overlay.addTo(this._map); 111 } 112 }, 113 114 'identify': function(evt) { 115 // Identify map features in response to map clicks. To customize this 116 // behavior, create a class extending wms.Source and override one or 117 // more of the following hook functions. 118 119 var layers = this.getIdentifyLayers(); 120 if (!layers.length) { 121 return; 122 } 123 this.getFeatureInfo( 124 evt.containerPoint, evt.latlng, layers, 125 this.showFeatureInfo 126 ); 127 }, 128 129 'getFeatureInfo': function(point, latlng, layers, callback) { 130 // Request WMS GetFeatureInfo and call callback with results 131 // (split from identify() to faciliate use outside of map events) 132 var params = this.getFeatureInfoParams(point, layers), 133 url = this._url + L.Util.getParamString(params, this._url); 134 135 this.showWaiting(); 136 ajax.call(this, url, done); 137 138 function done(result) { 139 this.hideWaiting(); 140 var text = this.parseFeatureInfo(result, url); 141 callback.call(this, latlng, text); 142 } 143 }, 144 145 'getIdentifyLayers': function() { 146 // Hook to determine which layers to identify 147 if (this.options.identifyLayers) 148 return this.options.identifyLayers; 149 return Object.keys(this._subLayers); 150 }, 151 152 'getFeatureInfoParams': function(point, layers) { 153 // Hook to generate parameters for WMS service GetFeatureInfo request 154 var wmsParams, overlay; 155 if (this.options.tiled) { 156 // Create overlay instance to leverage updateWmsParams
157 overlay = wms.overlay(this._url, this.options); 158 overlay.updateWmsParams(this._map); 159 wmsParams = overlay.wmsParams; 160 wmsParams.layers = layers.join(','); 161 } else { 162 // Use existing overlay 163 wmsParams = this._overlay.wmsParams; 164 } 165 var infoParams = { 166 'request': 'GetFeatureInfo', 167 'query_layers': layers.join(','), 168 'X': point.x, 169 'Y': point.y 170 }; 171 return L.extend({}, wmsParams, infoParams); 172 }, 173 174 'parseFeatureInfo': function(result, url) { 175 // Hook to handle parsing AJAX response 176 if (result == "error") { 177 // AJAX failed, possibly due to CORS issues. 178 // Try loading content in <iframe>. 179 result = "<iframe src='" + url + "' style='border:none'>"; 180 } 181 return result; 182 }, 183 184 'showFeatureInfo': function(latlng, info) { 185 // Hook to handle displaying parsed AJAX response to the user 186 if (!this._map) { 187 return; 188 } 189 this._map.openPopup(info, latlng); 190 }, 191 192 'showWaiting': function() { 193 // Hook to customize AJAX wait animation 194 if (!this._map) 195 return; 196 this._map._container.style.cursor = "progress"; 197 }, 198 199 'hideWaiting': function() { 200 // Hook to remove AJAX wait animation 201 if (!this._map) 202 return; 203 this._map._container.style.cursor = "default"; 204 } 205}); 206 207wms.source = function(url, options) { 208 return new wms.Source(url, options); 209}; 210 211/* 212 * Layer 213 * Leaflet "layer" with all actual rendering handled via an underlying Source 214 * object. Can be called directly with a URL to automatically create or reuse 215 * an existing Source. Note that the auto-source feature doesn't work well in 216 * multi-map environments; so for best results, create a Source first and use 217 * getLayer() to retrieve wms.Layer instances. 218 */ 219 220wms.Layer = L.Layer.extend({ 221 'initialize': function(source, layerName, options) { 222 if (!source.addSubLayer) { 223 // Assume source is a URL 224 source = wms.getSourceForUrl(source, options); 225 } 226 this._source = source; 227 this._name = layerName; 228 }, 229 'onAdd': function(map) { 230 L.Layer.prototype.onAdd.call(this, map); 231 if (!this._source._map) 232 this._source.addTo(this._map); 233 this._source.addSubLayer(this._name); 234 }, 235 'onRemove': function() { 236 this._source.removeSubLayer(this._name); 237 } 238}); 239 240wms.layer = function(source, options) { 241 return new wms.Layer(source, options); 242}; 243 244// Cache of sources for use with wms.Layer auto-source option 245var sources = {}; 246wms.getSourceForUrl = function(url, options) { 247 if (!sources[url]) { 248 sources[url] = wms.source(url, options); 249 } 250 return sources[url]; 251}; 252 253 254// Copy tiled WMS layer from leaflet core, in case we need to subclass it later 255wms.TileLayer = L.TileLayer.WMS; 256wms.tileLayer = L.tileLayer.wms; 257 258/* 259 * wms.Overlay: 260 * "Single Tile" WMS image overlay that updates with map changes. 261 * Portions of wms.Overlay are directly extracted from L.TileLayer.WMS. 262 * See Leaflet license. 263 */ 264wms.Overlay = L.Layer.extend({ 265 'defaultWmsParams': { 266 'service': 'WMS', 267 'request': 'GetMap', 268 'version': '1.1.1', 269 'layers': '', 270 'styles': '', 271 'format': 'image/jpeg', 272 'transparent': false 273 }, 274 275 'initialize': function(url, options) { 276 this._url = url; 277 278 // Move WMS parameters to params object 279 var optNames = { 280 'crs': true, 281 'uppercase': true, 282 'tiled': true, 283 'identify': true, 284 'opacity': true, 285 'attribution': true 286 }; 287 var params = {}; 288 for (var opt in options) { 289 if (options.hasOwnProperty(opt) && !optNames[opt]) { 290 params[opt] = options[opt]; 291 delete options[opt]; 292 } 293 } 294 L.setOptions(this, options); 295 this.wmsParams = L.extend({}, this.defaultWmsParams, params); 296 }, 297 298 'setParams': function(params) { 299 L.extend(this.wmsParams, params); 300 this.update(); 301 }, 302 303 'getAttribution': function() { 304 return this.options.attribution; 305 }, 306 307 'onAdd': function(map) { 308 L.Layer.prototype.onAdd.call(this, map); 309 this.update(); 310 }, 311 312 'onRemove': function(map) { 313 if (this._currentOverlay) { 314 map.removeLayer(this._currentOverlay); 315 delete this._currentOverlay; 316 } 317 if (this._currentUrl) { 318 delete this._currentUrl; 319 } 320 L.Layer.prototype.onRemove.call(this, map); 321 }, 322 323 'getEvents': function() { 324 return { 325 'moveend': this.update 326 }; 327 }, 328 329 'update': function() { 330 if (!this._map) { 331 return; 332 } 333 // Determine image URL and whether it has changed since last update 334 this.updateWmsParams(); 335 var url = this.getImageUrl(); 336 if (this._currentUrl == url) { 337 return; 338 } 339 this._currentUrl = url; 340 341 // Keep current image overlay in place until new one loads 342 // (inspired by esri.leaflet) 343 var bounds = this._map.getBounds();
344 var overlay = L.imageOverlay(url, bounds, {'opacity': 0}); 345 overlay.addTo(this._map); 346 overlay.once('load', _swap, this); 347 function _swap() { 348 if (!this._map) { 349 return; 350 } 351 if (overlay._url != this._currentUrl) { 352 this._map.removeLayer(overlay); 353 return; 354 } else if (this._currentOverlay) { 355 this._map.removeLayer(this._currentOverlay); 356 } 357 this._currentOverlay = overlay; 358 overlay.setOpacity( 359 this.options.opacity ? this.options.opacity : 1 360 ); 361 } 362 }, 363 364 // See L.TileLayer.WMS: onAdd() & getTileUrl() 365 'updateWmsParams': function(map) { 366 if (!map) { 367 map = this._map; 368 } 369 // Compute WMS options 370 var bounds = map.getBounds(); 371 var size = map.getSize(); 372 var wmsVersion = parseFloat(this.wmsParams.version); 373 var crs = this.options.crs || map.options.crs; 374 var projectionKey = wmsVersion >= 1.3 ? 'crs' : 'srs'; 375 var nw = crs.project(bounds.getNorthWest()); 376 var se = crs.project(bounds.getSouthEast()); 377 378 // Assemble WMS parameter string 379 var params = { 380 'width': size.x, 381 'height': size.y 382 }; 383 params[projectionKey] = crs.code; 384 params.bbox = ( 385 wmsVersion >= 1.3 && crs === L.CRS.EPSG4326 ? 386 [se.y, nw.x, nw.y, se.x] : 387 [nw.x, se.y, se.x, nw.y] 388 ).join(','); 389 390 L.extend(this.wmsParams, params); 391 }, 392 393 'getImageUrl': function() { 394 var uppercase = this.options.uppercase || false; 395 var pstr = L.Util.getParamString(this.wmsParams, this._url, uppercase); 396 return this._url + pstr; 397 } 398}); 399 400wms.overlay = function(url, options) { 401 return new wms.Overlay(url, options); 402}; 403 404// Simple AJAX helper (since we can't assume jQuery etc. are present) 405function ajax(url, callback) { 406 var context = this, 407 request = new XMLHttpRequest(); 408 request.onreadystatechange = change; 409 request.open('GET', url); 410 request.send(); 411 412 function change() { 413 if (request.readyState === 4) { 414 if (request.status === 200) { 415 callback.call(context, request.responseText); 416 } else { 417 callback.call(context, "error"); 418 } 419 } 420 } 421} 422 423return wms; 424 425}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.