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);
};
@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