Skip to content

Instantly share code, notes, and snippets.

@rclark
Last active July 9, 2024 20:16
Show Gist options
  • Save rclark/6908938 to your computer and use it in GitHub Desktop.
Save rclark/6908938 to your computer and use it in GitHub Desktop.
Leaflet WMS + GetFeatureInfo
<!doctype html>
<html>
<head>
<title>WMS GetFeatureInfo</title>
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.6.4/leaflet.css" />
<!--[if lte IE 8]>
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.6.4/leaflet.ie.css" />
<![endif]-->
<script src="http://cdn.leafletjs.com/leaflet-0.6.4/leaflet.js"></script>
<style type="text/css">
html, body, #map {
margin: 0px;
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
<script src="L.TileLayer.BetterWMS.js"></script>
<script>
var map = L.map('map', {
center: [34,-111],
zoom: 7
});
var url = 'http://data.azgs.az.gov/arizona/wms';
L.tileLayer('http://{s}.tiles.mapbox.com/v3/rclark.map-wm3i8w1a/{z}/{x}/{y}.png').addTo(map);
L.tileLayer.betterWms(url, {
layers: 'azgs:mapunitpolys',
transparent: true,
format: 'image/png'
}).addTo(map);
</script>
</body>
</html>

There are a bunch of reasons why this is convoluted, mostly in building the URL to make the request:

  1. You have to rely on an AJAX request, this example uses jQuery
  2. To make a GetFeatureInfo request, you must provide a BBOX for a image, and the pixel coordinates for the part of the image that you want info from. A couple of squirrely lines of Leaflet code can give you that.
  3. Output formats. The info_format parameter in the request. We don't know a priori which will be supported by a WMS that we might make a request to. See Geoserver's docs for what formats are available from Geoserver. That won't be the same from WMS to WMS, however.
  4. WMS services return XML docs when there's a mistake in the request or in processing. This sends an HTTP 200, which jQuery doesn't think is an error.
L.TileLayer.BetterWMS = L.TileLayer.WMS.extend({
onAdd: function (map) {
// Triggered when the layer is added to a map.
// Register a click listener, then do all the upstream WMS things
L.TileLayer.WMS.prototype.onAdd.call(this, map);
map.on('click', this.getFeatureInfo, this);
},
onRemove: function (map) {
// Triggered when the layer is removed from a map.
// Unregister a click listener, then do all the upstream WMS things
L.TileLayer.WMS.prototype.onRemove.call(this, map);
map.off('click', this.getFeatureInfo, this);
},
getFeatureInfo: function (evt) {
// Make an AJAX request to the server and hope for the best
var url = this.getFeatureInfoUrl(evt.latlng),
showResults = L.Util.bind(this.showGetFeatureInfo, this);
$.ajax({
url: url,
success: function (data, status, xhr) {
var err = typeof data === 'string' ? null : data;
showResults(err, evt.latlng, data);
},
error: function (xhr, status, error) {
showResults(error);
}
});
},
getFeatureInfoUrl: function (latlng) {
// Construct a GetFeatureInfo request URL given a point
var point = this._map.latLngToContainerPoint(latlng, this._map.getZoom()),
size = this._map.getSize(),
params = {
request: 'GetFeatureInfo',
service: 'WMS',
srs: 'EPSG:4326',
styles: this.wmsParams.styles,
transparent: this.wmsParams.transparent,
version: this.wmsParams.version,
format: this.wmsParams.format,
bbox: this._map.getBounds().toBBoxString(),
height: size.y,
width: size.x,
layers: this.wmsParams.layers,
query_layers: this.wmsParams.layers,
info_format: 'text/html'
};
params[params.version === '1.3.0' ? 'i' : 'x'] = point.x;
params[params.version === '1.3.0' ? 'j' : 'y'] = point.y;
return this._url + L.Util.getParamString(params, this._url, true);
},
showGetFeatureInfo: function (err, latlng, content) {
if (err) { console.log(err); return; } // do nothing if there's an error
// Otherwise show the content in a popup, or something.
L.popup({ maxWidth: 800})
.setLatLng(latlng)
.setContent(content)
.openOn(this._map);
}
});
L.tileLayer.betterWms = function (url, options) {
return new L.TileLayer.BetterWMS(url, options);
};
@markgis
Copy link

markgis commented Mar 20, 2018

This was really handy, thanks.

For 1.3.0 getfeatureinfo the lat and longs have been reversed so the bbox: this._map.getBounds().toBBoxString(), line doenst work. A quick function to create a string with the lat and longs in the right order solved it.

@dior1
Copy link

dior1 commented May 17, 2018

Is there a way to enlarge the klick area from who the info get? And if I get the info from 2 or more layers how do I show this in a popup with tabs?

@anakaine
Copy link

anakaine commented Oct 14, 2018

Hey Ryan, is seems like you 1st August blocks example of this is broken. Ive been trying to implement your solution but not been having much luck unfortunately. Not seeing a popup at all on leaflet 1.3.4

Also, @santiblanko wms may have been around for some time, but that neither makes it old or the implied 'not as useful. There are plenty of situations where it is still the best and fastest option, particularly with raster data.

http://bl.ocks.org/rclark/6908938

@ISURU9900
Copy link

ISURU9900 commented Oct 25, 2018

I was able successfully run this BetterWMSLayer. But now I want to show the attributes of a raster through a marker popup. Any idea?

@ATejed
Copy link

ATejed commented Dec 17, 2018

Is there a way to enlarge the klick area from who the info get?

I have the same problem. How did you solve it? @dior1

@Tim0917
Copy link

Tim0917 commented Jan 11, 2019

Super awesome!

Question:
How do i select a single value of the wms?
So, i want just one popup, for one value.
Somewhere there is a selection needed, but how??

Please help :)

@ivominic
Copy link

Is there a way to enlarge the klick area from who the info get?

I have the same problem. How did you solve it? @dior1

If you want to get info for multiple features, you should add
feature_count: '5',
to parameter list in getFeatureInfoUrl, and set number of maximum features you want to show info.

capture

@aichaous
Copy link

it worked but i have a blank popup

@nmtoken
Copy link

nmtoken commented Aug 12, 2019

     params = {
      request: 'GetFeatureInfo',
      service: 'WMS',
      srs: 'EPSG:4326',
      styles: this.wmsParams.styles,
      transparent: this.wmsParams.transparent,
      version: this.wmsParams.version,      
      format: this.wmsParams.format,
      bbox: this._map.getBounds().toBBoxString(),
      height: size.y,
      width: size.x,
      layers: this.wmsParams.layers,
      query_layers: this.wmsParams.layers,
      info_format: 'text/html'
    };

params[params.version === '1.3.0' ? 'i' : 'x'] = point.x;
params[params.version === '1.3.0' ? 'j' : 'y'] = point.y;

You have switch of x/y for i/j dependent on version, but version also switches other things, for example the parameter for specifying the Coordinate Reference system is CRS not SRS in WMS 1.3.0.

Also in WMS 1.3.0 the axes order as specified by EPSG is honoured, so EPSG:4326 is specified with a lat/long bounding box and not as per WMS 1.1.1 in long/lat order.

@madmierul
Copy link

hai admin.this code working well on desktop.but when open it mobile the information of the layer clicked not popup.
is there any method to fix this issue.Hope reply this comment very soon.

@Virgil-z
Copy link

Virgil-z commented Nov 5, 2019

hello, thanks for the amazing extension.
i have one question though.. im relatively new to WMS and GIS in general.
im using leaflet at the moment along with your extension and it works great ! however i dont seem to be able to like "select" an state ( as in highlight borders of it ) when its clicked. i have searched for days and i have yet to come up with a solution...

am i doing something wrong?can i not use the coordinates that GetFeatureInfo returns? or is there some sort of automated highlighting border function etc that im not aware of?

@madmierul
Copy link

try using cql_filter

@Virgil-z
Copy link

try using cql_filter

that seems to be very helpful! thanks!
however, if i use this approach, i would have to then add a new image layer for every highlight that i want to make and my styling is limited to styles which is defined ( to my understanding ) as server side.

what i want to do is like highlight the area around a state for example using polygon or some other object...
its weird because i see coordinates that GetFeatureInfo returns but i cant seem to add a polygon using those coordinates..
i tried adding polygons using leaflet and the coordinates in GetFeatureInfo but nothing shows up on the map.
did i get something wrong?is that not something i can do? like click a state on the map and highlight it using a polygon ?

@madmierul
Copy link

madmierul commented Nov 12, 2019 via email

@Abhishek950650
Copy link

sir thanks for this code i solve my problem exactly

@jlsevillano
Copy link

jlsevillano commented Dec 13, 2019

Hi,
I'm using this library. Everything looks works right:
1.- I load a WMS from geoserver, ok
2.- From html form I do a filter
3.- I setParameters (filter paramaters)
4.- The WMS apply the cql_filter
5.- Display the items filtered
6.- I can do getFeatureInfo and recive the data
¡¡But if I click on the map over a hidden item or should not be there (an item) I GET THE featureInfo!! And I should not get anything.
Am i doing something wrong?

myLayer.setParams({cql_filter :final_geo_query},false); myLayer.redraw();

@jamieodonnell
Copy link

For anyone that gets this error:

X and Y incorrectly specified

Try rounding the X and Y values (lines 54 & 55):

params[params.version === '1.3.0' ? 'i' : 'x'] = Math.round(point.x);
params[params.version === '1.3.0' ? 'j' : 'y'] = Math.round(point.y);

@orientino
Copy link

I had this same problem and in order to deal with CORS issue while using a local geoserver, you have to enable CORS in the geoserver configuration. It can be done by following the documentation: https://docs.geoserver.org/latest/en/user/production/container.html#enable-cors

Thanks very much for posting this example - it looks really useful! I had some problems getting it to work with my local geoserver, but firebug finally showed me that a CORS restriction was preventing geoserver returning the getFeatureInfo to my webserver. Enabling "Access data sources across domains" in Internet Explorer finally got around the problem - now I have to work out how to do that properly with CORS headers...

@arminus
Copy link

arminus commented Dec 22, 2021

Is there any way to avoid (expensive) ajax calls being fired when clicking on the base map (in which case GetFeatureInfo will not return anything anyway) ? So far I've found no way to determine the layer the event actually happend on - or is this always the map itself and there's no way to detected wether "something from the WMS" layer is actually displayed at that point? (see https://github.com/mapbox/leaflet-pip - which only works for a GeoJSON layer)

@UsamaAndroid
Copy link

anybody here who is working in flutter with geoserver getFeatureInfo api. please help me i have got BBOX but unable to get height and width, x, y parameters for api call.
here is my code
child: FlutterMap(
mapController: mapController,
options: MapOptions(
// Set the default CRS
// crs: epsg3413CRS,
controller: mapController,
// crs: epsg4326CRS,
center: LatLng(33.680088056393814, 72.7987044426807),
// center: currentLatLng,
zoom: 14,
maxZoom: 18,
onTap: (p, k) {
// _handleTap(k, epsg4326.transform(epsg4326, point).x.toStringAsFixed(2), epsg4326.transform(epsg4326, point).y.toStringAsFixed(2));
final bounds = mapController.bounds;
var a = mapController;
var sw = epsg3413CRS.projection.project(bounds.southWest!);
var ne = epsg3413CRS.projection.project(bounds.northEast!);
print('Map bounds:sw.x: ${sw.x},${sw.y},${ne.x},${ne.y}');
print('Map bounds:ne.x: ${epsg3413CRS.code}');

                    },
                  ),
                  layers: layerz,
                ),

@nmtoken
Copy link

nmtoken commented Jan 20, 2022

@arminus there is no way to know what features exist (if any) in the underlying data that is used to create the WMS image, without querying the map layer or layers though a GetFeatureInfo request; a WMS map image has no attributes. You can though determine if a layer is intended to be queried by parsing the GetCapabilities response for the service and checking whether the layer is marked as queryable <Layer queryable="1". There's no reason particularly why a base map can't be queryable.

@emmexx
Copy link

emmexx commented Mar 8, 2022

I created a layer that uses cql_filter and env and the styling server side displays features based on said cql_filter and on values of env.
I had to add those 2 parameters in getFeatureInfoUrl to get BetterWMS getFeatureInfo to work correctly.

if(this.wmsParams.env)	
{
     params['env'] = this.wmsParams.env
}
if(this.wmsParams.cql_filter)	
{
     params['cql_filter'] = this.wmsParams.cql_filter
}

@ultramenid
Copy link

Super awesome!

Question: How do i select a single value of the wms? So, i want just one popup, for one value. Somewhere there is a selection needed, but how??

Please help :)

did you solve this issue?

@inzamambaig
Copy link

inzamambaig commented Jan 24, 2023

Super awesome!
Question: How do i select a single value of the wms? So, i want just one popup, for one value. Somewhere there is a selection needed, but how??
Please help :)

did you solve this issue?

@ultramenid Did you find any solution?

image

@inzamambaig
Copy link

@ultramenid I Found the solution. In getFeatureInfoUrl function just specify the attributes you want to see in propertyName parameter. e.g propertyName: "attr1,attr2,attr3,attr4,attr5"

@ragnarheidar
Copy link

Amazing! Works out of the box with jquery 3.6.3 and Leaflt 1.9.2. Thanks 👍

@ydrea
Copy link

ydrea commented Nov 4, 2023

I needed pictures in (html) popups, so I made the following changes:

  // with thumbs
	showGetFeatureInfo: function (
		err,
		latlng,
		content
		// signaturaFromUrl
	) {
		if (err) {
			console.log(err);
			return;
		}

		var tempDiv = document.createElement('div');
		tempDiv.innerHTML = content;
		var rows = tempDiv.querySelectorAll('tr');

		// var shouldFlyTo = false; // Initialize the flag

		for (var i = 0; i < rows.length; i++) {
			var row = rows[i];
			var cells = row.querySelectorAll('th, td');

			// console.log(header);

			if (cells.length >= 2) {
				var header = cells[0].textContent.trim();
				var value = cells[1].textContent.trim();

				if (header === 'signatura') {
					console.log('Header:', header);
					console.log('Value:', value);
				}

				if (header === 'foto_url') {
					var imgUrl = value.replace(/["']/g, '');
					var thumbUrl;
					var publicIndex = imgUrl.indexOf('/public/');
					if (publicIndex !== -1) {
						thumbUrl =
							imgUrl.slice(0, publicIndex + '/public/'.length) +
							'thumbs/' +
							imgUrl.slice(publicIndex + '/public/'.length);
					}
					console.log(imgUrl, thumbUrl);

					var newRow = document.createElement('tr');
					newRow.style.width = '100%';
					var newCell = document.createElement('td');
					newCell.colSpan = 2;
					var image = document.createElement('img');
					image.src = thumbUrl;
					image.alt = 'thumb';
					image.style.minWidth = '300px';
					image.style.width = '100%';
					image.style.marginTop = '8px';

					newCell.appendChild(image);
					newRow.appendChild(newCell);
					//
					row.parentNode.replaceChild(newRow, row);
				} else {
					if (value === 'NULL' || value === '') {
						row.parentNode.removeChild(row);
					}
				}
			}
		}

		var filteredContent = tempDiv.innerHTML;
		// console.log(filteredContent);
		L.popup({ minWidth: '500px', width: '100%' })
			.setLatLng(latlng)
			.setContent(filteredContent)
			.openOn(this._map);

	},


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment