[GIS] How to query features of raster layers hosted on GeoServer that are added to Mapbox

mapboxmapbox-gl-jsraster

Currently working with Mapbox gl js. I know for OpenLayers there's a function called GetFeatureInfo to do this, but is there a similar one for Mapbox? What I want to do is whenever the user clicks on a point in the map, which is a Mapbox style, it will show the data from the currently added layers at that point. I used this example to add the layer from GeoServer to my Mapbox map: https://www.mapbox.com/mapbox-gl-js/example/wms/

Best Answer

Normally, with WMS, you start by requesting a single raster-image map that represents an area bounded by some real-world coordinates. The coordinates will be in some specific system as identified by the CRS attribute of the GetMap request. The request also includes the required width and height of the returned raster image.

To use the GetFeatureInfo request, you would get the X,Y coordinates of some mouse click, relative to the top-left corner of the previously requested map and the WMS server will take care of converting that to real-world coordinates in order to find the requested feature info. The GetFeatureInfo request includes the same bounding box and width/height attributes as GetMap, and in addition, also has an x,y (or i,j) position to identify the point of interest.

With Mapbox, things are slightly different. A Mapbox setup similar to the one linked in the question splits the area of interest into multiple 256x256 tiles, and issues separate requests to the WMS server for each tile. Here's a typical URL as might be used to configure the Mapbox source, and that will be used to get the image for each tile: (attributes shown on separate lines for readability - this should all be one continuous line with no spaces)

http://your/geoserver/url?
    BBOX={bbox-epsg-3857}&
    SERVICE=WMS&
    REQUEST=GetMap&
    CRS=EPSG:3857&
    WIDTH=256&
    HEIGHT=256&
    LAYERS=your_layer_name&
    FORMAT=image/png

Unfortunately, when the user clicks on a point somewhere on one of the displayed tiles, we have no easy way of knowing which tile was clicked, and the offsets within that tile in order to construct an appropriate GetFeatureInfo query. We can get the X,Y relative to the top left of the map, but cannot easily convert that into an offset of any of the GetMap requests that Mapbox has issued. Fortunately, we don't have to...

The GetFeatureInfo will work with any imaginary map that may or may not have been returned by GetMap (or at least it does for GeoServer - I can't speak for other WMS servers). One way would be to work out the bounding box for the entire map display and then use the X,Y offset of the click in a GetFeatureInfo request. While this would work in principle, I think it would get far too tricky when the on-screen map is rotated or tilted.

My alternative solution was to imagine a very small map centred around the point clicked. I arbitrarily chose a distance of 50m in each NSEW direction, giving a 100mx100m area and then I construct a GetFeatureInfo request for the exact centre of this area. This is the GetFeatureInfo URL pattern we're aiming to construct:

http://your/geoserver/url?
    SERVICE=WMS&
    VERSION=1.3.0&
    REQUEST=GetFeatureInfo&
    BBOX={bbox-epsg-3857}&
    CRS=EPSG:3857&
    WIDTH=256&
    HEIGHT=256&
    LAYERS=your_layer_name&
    STYLES=&
    FORMAT=image/png&
    QUERY_LAYERS=your_layer_name&
    INFO_FORMAT=application/json&
    I=128&
    J=128

If you start with a similar pattern (but with actual values for the WMS server and the layer name), then the only thing still to replace in this pattern is the {bbox-epsg-3857} part to represent the bounding box of our small imaginary map surrounding the point of interest. Notice that we will abitrarily ask for the bounding box to represent a 256x256 pixel square, and our point of interest is at 128,128, exactly in the centre.

The mouse event's X,Y click position, e.point is not useful for our purposes, so we can use the e.lngLat position instead. The first step would be to convert this to 3857 projection so we can construct the bounding box for our imaginary 100mx100m map:

// e is the Mapbox mouse click event
var r = 6378137 * Math.PI * 2;
var x = (e.lngLat.lng / 360) * r;
var sin = Math.sin(e.lngLat.lat * Math.PI / 180);
var y = 0.25 * Math.log((1 + sin) / (1 - sin)) / Math.PI * r;
// x,y = the Web Mercator 3857 coordinates of the click position

Construct the bounding box:

var bbox = (x-50) + ',' + (y-50) + ',' + (x+50) + ',' + (y+50);

Now construct the final URL, starting from the pattern we specified above:

var url = 'http://your/geoserver/url?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetFeatureInfo&BBOX={bbox-epsg-3857}&CRS=EPSG:3857&WIDTH=256&HEIGHT=256&LAYERS=your_layer_name&STYLES=&FORMAT=image/png&QUERY_LAYERS=your_layer_name&INFO_FORMAT=application/json&I=128&J=128';
url = url.replace('{bbox-epsg-3857}', bbox);

// url is now ready to return a JSON representation of
// features at the clicked point

Caveats: 1) This solution may need more work if the clicked point was somewhere near the international dateline (in my own use case, that will never happen); 2) I've no idea whether GeoServer (or any other WMS server) actually constructs the 'imaginary' map during a GetFeatureInfo request but if it does, there may be a more efficient solution. This solution has worked well enough for me!

Related Question