WMS service based on GeoMedia WebMap (version 2018) supports only the following WMS Feature Info output formats:
Modern client applications tend to favor JSON data which is easier to manipulate wherever JavaScript language takes a place. Fortunately enough, it is possible to extend WebMap's WMS using a custom pipe, adding support for any output format required. In this article, we will cover steps necessary to add support of application/json output for the GetFeatureInfo request.
The implementation of the added functionality is straightforward. The basic workflow is as following:
Because we want our transformations to be very flexible and easy to adjust as per WMS client needs, a simple approach using XSL transformation was adopted. This approach is very close to the currently implemented XML to HTML transformations.
For an easy start it is recommended to use the ExamplePipe project from the Developers KB article referenced above. In that project, there is a couple of things to adjust.
Our custom feature info pipe will read a XSL file which is recommended to put into XMLTemplates subfolder of a WMS instance. Therefore, we need to create a property that will read the path value defined by the administrator. Example usage:
<CustomFIPipe name="customFeatureInfoPipe"
nextPipeName="gwmOrigPipe"
firstPipe="false"
featureInfoXslPath="XMLTemplates/XmlToJsonFeatureInfo.xsl"/>
Class implementation:
using Intergraph.GeoMedia.Web.SDI.Common.BasePipe.PipesConfiguration; using System.Configuration; namespace Intergraph.GeoMedia.Web.SDI.Pipes.ExamplePipe { /// <summary> /// Configuration class for <see cref="ExamplePipe"/>. /// </summary> public class CustomFIPipeConfiguration : PipeConfiguration { /// <summary> /// Example configuration property. Tag is used to distinguish between different ExamplePipes. /// </summary> [ConfigurationProperty("featureInfoXslPath", IsRequired = true)] public string XSLTPath { get { return (string)this["featureInfoXslPath"]; } set { this["featureInfoXslPath"] = value; } } } }
As stated above, our custom pipe will hook up onto GetFeatureInfo request type and process it by its own. It must also allow to pass through all other requests such as GetMap and GetCapabilities. Therefore, following methods will be required.
Overridden Initialize method will only set up a path for the XSL file defined in web.config pipe configuration
public override void Initialize(PipeConfiguration config, IPipe nextPipe) { _pipeConfiguration = (CustomFIPipeConfiguration)config; xsltPath = Path.Combine(config.RootFilePath, _pipeConfiguration.XSLTPath); base.NextPipe = nextPipe; }
This is the most interesting, yet a simple part. Custom GetFeatureInfo handler implementation will read the request parameters, manipulate them to be accepted by the lower-level pipes and once response is ready, it will translate it from XML to JSON. The sample implementation was done only for WMS version 1.3.0 but should be similar (if not the same) for version 1.1.1.
[SDIMethodHandler("GetFeatureInfo", "1.3.0", "wms")] [SDIMethodHandler("GetFeatureInfo", "1.1.1", "wms")] public IResponseBase GetFeatureInfoHandler(SDIMethodParameters parameters, AdditionalParametersCollection additionalParameters, IUserContext userContext) { IResponseBase response = null; // TODO: Split into two distinct GetFeatureInfoHandler if (parameters is GetFeatureInfoParameters111 fi111) { if (fi111.InfoFormat == "application/json") throw new NotImplementedException("Custom FeatureInfo handler for version 1.1.1 not implemented."); } if (parameters is GetFeatureInfoParameters130 fi130) { if (fi130.InfoFormat == "application/json") { fi130.InfoFormat = "text/xml"; response = InvokeOnNextPipe(parameters, additionalParameters, userContext); response = ProcessResponse(response); return response; } } response = InvokeOnNextPipe(parameters, additionalParameters, userContext); return response; }
IResponseBase ProcessResponse(IResponseBase response) { var xdoc = XDocument.Load(response.ResponseStream); StringBuilder output = new StringBuilder(); XmlWriterSettings writerSettings = new XmlWriterSettings { OmitXmlDeclaration = true, ConformanceLevel = ConformanceLevel.Fragment }; using (XmlWriter transformedData = XmlWriter.Create(output, writerSettings)) { XslCompiledTransform transform = new XslCompiledTransform(); transform.Load(xsltPath); transform.Transform(xdoc.CreateReader(), transformedData); } var resultResponse = new StringResponse(output.ToString(), "application/json"); resultResponse.Headers.Add(response.Headers); resultResponse.HttpCode = response.HttpCode; return resultResponse; }
This is only a pass-through handler as we're not going to manipulate other request / response types.
[SDIMethodHandler("GetMap", "1.1.1", "wms")] [SDIMethodHandler("GetMap", "1.3.0", "wms")] [SDIMethodHandler("GetCapabilities", "1.1.1", "wms")] [SDIMethodHandler("GetCapabilities", "1.3.0", "wms")] public IResponseBase OtherMethodsHandler(SDIMethodParameters parameters, AdditionalParametersCollection additionalParameters, IUserContext userContext) { return InvokeOnNextPipe(parameters, additionalParameters, userContext); }
This template is used to convert from a WebMap's specific WMS Feature Info XML response to its JSON representation. It is recommended to put this template into XMLTemplates subfolder.
<xsl:stylesheet xmlns:def="http://www.intergraph.com/geomedia/gml" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:gml="http://www.opengis.net/gml" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0"> <xsl:template match="/"> { "type": "FeatureCollection", "features": [<xsl:for-each select="def:FeatureCollection/gml:featureMember"> { "type": "Feature", "geometry": null, "layerName":"<xsl:value-of select="def:Layer/@Name"/>", "properties": {<xsl:for-each select="def:Layer/def:Attribute"> "<xsl:value-of select="@Name"/>":<xsl:choose> <xsl:when test="number(current()) = current()"> <xsl:value-of select="number(current())"/> </xsl:when> <xsl:otherwise> "<xsl:value-of select="current()"/>" </xsl:otherwise> </xsl:choose><xsl:choose> <xsl:when test="position() != last()">,</xsl:when> </xsl:choose> </xsl:for-each> } }<xsl:choose> <xsl:when test="position() != last()">,</xsl:when> </xsl:choose> </xsl:for-each>] } </xsl:template> </xsl:stylesheet>
For the client application to recognize that our WMS now supports a new format, related change shall be done in ...\XMLTemplates\WMS_Capabilities_1_3_0.xml file:
... <GetFeatureInfo> <Format>text/xml</Format> <Format>text/html</Format> <Format>application/gml+xml; version=3.1</Format> <Format>application/json</Format> ...
Now just build the project, place the generated DLL into WMS instance \bin folder and reference it in web.config. It is suggested to add this pipe past MetadataConfigurationPipe (if used) in the pipe chain. More details on this can be found in the Developing Custom Pipes article.
<sectionGroup name="pipes"> ... <section name="CustomFIPipe" type="Intergraph.GeoMedia.Web.SDI.Pipes.ExamplePipe.CustomFIPipeConfiguration, Intergraph.GeoMedia.Web.SDI.Pipes.ExamplePipe"/> </sectionGroup> ... <CustomFIPipe name="customFeatureInfoPipe" nextPipeName="gwmOrigPipe" firstPipe="false" featureInfoXslPath="XMLTemplates/XmlToJsonFeatureInfo.xsl"/> ...
Sample request:
.../WMSTest/service.svc/get
?bbox=-13927381.678361,2352208.31620937,-6892729.09121969,6671817.65866125
&request=GetFeatureInfo
&service=WMS
&CRS=EPSG:3857
&feature_count=10
&format=image/png
&height=883
&width=1438
&i=371
&j=268
&info_format=text/xml
&layers=States11,Counties
&query_layers=States11,Counties
&styles=,
&version=1.3.0
XML response:
<?xml version="1.0" encoding="utf-8"?> <FeatureCollection xmlns="http://www.intergraph.com/geomedia/gml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gml="http://www.opengis.net/gml" xmlns:xlink="http://www.w3.org/1999/xlink"> <gml:featureMember> <Layer Name="Counties"> <Attribute Name="ID">3094</Attribute> <Attribute Name="FIPS">56013</Attribute> <Attribute Name="COUNTY_NAME">FREMONT</Attribute> <Attribute Name="STATE_FIPS">56</Attribute> <Attribute Name="STATE_NAME">Wyoming</Attribute> <Attribute Name="POP">33662</Attribute> <Attribute Name="WHPOP">26852</Attribute> <Attribute Name="BLKPOP">17</Attribute> <Attribute Name="HISPPOP">1173</Attribute> <Attribute Name="ASIANPOP">136</Attribute> <Attribute Name="MEDAGEMAL">31.6</Attribute> <Attribute Name="MEDAGEFEM">33.5</Attribute> <Attribute Name="POP_EDU">20645</Attribute> <Attribute Name="ED_LT_HS">4646</Attribute> <Attribute Name="EDHS">6973</Attribute> <Attribute Name="EDSOMCOL">4369</Attribute> <Attribute Name="EDCOL">3586</Attribute> <Attribute Name="EDGRAD">1071</Attribute> <Attribute Name="SQMILES">9181.1</Attribute> <Attribute Name="TOTCRIME">97</Attribute> <Attribute Name="AVETEMP">44.6</Attribute> <Attribute Name="ANNULRAIN">13</Attribute> <Attribute Name="ANNULSNOW">102</Attribute> </Layer> </gml:featureMember> <gml:featureMember> <Layer Name="States11"> <Attribute Name="ID">49</Attribute> <Attribute Name="FIPS">56</Attribute> <Attribute Name="STATE_NAME">Wyoming</Attribute> <Attribute Name="STAABBRV">WY</Attribute> <Attribute Name="ST_PLACE">5600000</Attribute> <Attribute Name="ST_COUNTY">56000</Attribute> <Attribute Name="POP">453588</Attribute> <Attribute Name="WHPOP">427220</Attribute> <Attribute Name="BLKPOP">3290</Attribute> <Attribute Name="HISPPOP">24976</Attribute> <Attribute Name="ASIANPOP">2742</Attribute> <Attribute Name="MEDAGEMAL">31.2</Attribute> <Attribute Name="MEDAGEFEM">32.3</Attribute> <Attribute Name="POP_EDU">277769</Attribute> <Attribute Name="ED_LT_HS">47113</Attribute> <Attribute Name="EDHS">92081</Attribute> <Attribute Name="EDSOMCOL">67231</Attribute> <Attribute Name="EDCOL">55503</Attribute> <Attribute Name="EDGRAD">15841</Attribute> <Attribute Name="SQMILES">96989.1</Attribute> <Attribute Name="TOTCRIME">50</Attribute> <Attribute Name="AVETEMP">44.9</Attribute> <Attribute Name="ANNULRAIN">13</Attribute> <Attribute Name="ANNULSNOW">86</Attribute> </Layer> </gml:featureMember> </FeatureCollection>
Sample JSON response after changing &info_format=application/json
{ "type": "FeatureCollection", "features": [{ "type": "Feature", "geometry": null, "layerName": "Counties", "properties": { "ID": 3094, "FIPS": 56013, "COUNTY_NAME": "FREMONT", "STATE_FIPS": 56, "STATE_NAME": "Wyoming", "POP": 33662, "WHPOP": 26852, "BLKPOP": 17, "HISPPOP": 1173, "ASIANPOP": 136, "MEDAGEMAL": 31.6, "MEDAGEFEM": 33.5, "POP_EDU": 20645, "ED_LT_HS": 4646, "EDHS": 6973, "EDSOMCOL": 4369, "EDCOL": 3586, "EDGRAD": 1071, "SQMILES": 9181.1, "TOTCRIME": 97, "AVETEMP": 44.6, "ANNULRAIN": 13, "ANNULSNOW": 102 } }, { "type": "Feature", "geometry": null, "layerName": "States11", "properties": { "ID": 49, "FIPS": 56, "STATE_NAME": "Wyoming", "STAABBRV": "WY", "ST_PLACE": 5600000, "ST_COUNTY": 56000, "POP": 453588, "WHPOP": 427220, "BLKPOP": 3290, "HISPPOP": 24976, "ASIANPOP": 2742, "MEDAGEMAL": 31.2, "MEDAGEFEM": 32.3, "POP_EDU": 277769, "ED_LT_HS": 47113, "EDHS": 92081, "EDSOMCOL": 67231, "EDCOL": 55503, "EDGRAD": 15841, "SQMILES": 96989.1, "TOTCRIME": 50, "AVETEMP": 44.9, "ANNULRAIN": 13, "ANNULSNOW": 86 } } ] }