Skip to content

Instantly share code, notes, and snippets.

@rclark
Last active January 28, 2024 01:18
Show Gist options
  • Star 91 You must be signed in to star a gist
  • Fork 35 You must be signed in to fork a gist
  • 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);
};
@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