Using HTML for WMS GetFeatureInfo somehow alleviate the situation, because without parsing clients can just throw HTML to browser. Some other implementations support GML to be more interoperable, but again those are just workarounds and the real issue should be solved in spec itself.
Anyway while waiting for the next version of spec (1.4.0?, 2.0? or else) recently, I spent some time with GeoServer 2.0 and MapServer 5.4.2 to customize the WMS GetFeatureInfo response in HTML, which is a feature available in both popular open source WMS server implementations.
What is the goal?
I have a simple vector dataset for San Francisco area in shape file format, which I published as WMS in both GeoServer and MapServer. Below is the map I will get when I send a WMS GetMap request:
http://<wms_service_url>?REQUEST=GetMap&SERVICE=WMS&VERSION=1.1.1&LAYERS=blockgroups,highways,pizzastores&STYLES=&FORMAT=image/png&BGCOLOR=0xEEEEEE&TRANSPARENT=false&SRS=EPSG:4326&BBOX=-122.545074509804,37.6736653056517,-122.35457254902,37.8428758708189&WIDTH=1020&HEIGHT=906
Now if I send a GetFeatureInfo request to query features on the map:
http://<wms_service_url>?REQUEST=GetFeatureInfo&SERVICE=WMS&VERSION=1.1.1&LAYERS=pizzastores,highways,blockgroups&STYLES=&FORMAT=image/png&BGCOLOR=0xFFFFFF&TRANSPARENT=TRUE&SRS=EPSG:4326&BBOX=-122.545074509804,37.6736653056517,-122.35457254902,37.8428758708189&WIDTH=1020&HEIGHT=906&QUERY_LAYERS=pizzastores,highways,blockgroups&X=652&Y=368&INFO_FORMAT=text/html
Zero or more features records will be returned (Note: this particular request actually returns records from all three layers: ‘pizzastores’, ‘highways’, and ‘blockgroups’). So what I am trying to achieve here is to have both GeoServer and MapServer output GetFeatureInfo results in html with styles like below:
It’s a very simple customization but it proves the ability in GeoServer and MapServer, and of course more rich HTML elements like charts & pies, audios and videos can be easily added.
MapServer 5.4.2
For MapServer (the ms4w.exe that contains MapServer 5.4.2) I finally made what I planned, but I have to say that the whole user experience is far less smooth than I expected. A non-guru user like me mostly replies on the doc and samples as a start point, but the information regarding to this topic is scattered all over piece by piece without links pointing to each other. So compared to this I liked the new GeoServer 2.0 user manual much better.
To make shape file layer queryable in MapServer WMS, I started from a sample map file that I modified from online sample, and here is a section for one of my layers
But unfortunately, all WMS layers are defined unqueryable (you will see <Layer …queryable=”0”…> in WMS capabilities files) by default, and online documentation doesn’t say anything clear on how to enable query on WMS layers. I figured out in the end by searching through the forum, in which some others people are asking the similar questions. Basically you have to add following line in each layer definition in map file:1: # ===============================================================================
2: # highways layer3: # ===============================================================================
4: LAYER
5: NAME "highways"6: METADATA
7: "ows_title" "highways"8: "ows_srs" "EPSG:4326"9: END
10: TYPE LINE11: STATUS ON12: DATA "./sanfrancisco/shp/highways"13: PROJECTION
14: "init=epsg:4326"15: END
16: CLASS
17: NAME "highways"18: STYLE
19: WIDTH 120: COLOR 0 0 25521: END
22: END
23: END
“TEMPLATE” is suppose to point to a html file used as a template for WMS query result, and it makes WMS layer queryable even though the html file you’re pointing to doesn’t exists (I just feel a little awkward about this)1: LAYER
2: ...3: TEMPLATE "blank.html"4: ...5: END
Now I can actually get GetFeatureInfo response from WMS, but I encountered three more problems right way:
1. GetFeatureInfo response doesn’t support GML as it claims; by reading the MapServer WMS doc “Reference Section” it can be solved by adding “DUMP TRUE” into layer definition:
2. Either GML response or plain text response of GetFeatureInfo doesn’t include any attribute of the result feature; by reading the doc, you can solve that for GML by adding “gml_include_items all” in metadata of layer definition. But I didn’t complete get rid of the problem until I searched through the forum again and found another undocumented “wms_include_items”. So what you need is:1: LAYER
2: ...3: DUMP TRUE4: ...5: END
3. “text/html” is not supported in GetFeatureInfo response; this is also documented but in a very obvious place. “wms_feature_info_mime_type text/html” in the web section of the map files fix the problem:1: LAYER
2: ...3: METADATA
4: ...5: "gml_include_items" "all"6: "wms_include_items" "all"7: ...8: END
9: ...10: END
Until now I can finally start creating the html template for GetFeatureInfo response. Although not specific to WMS GetFeatureInfo response, there is a detailed documentation page on MapServer template. As expected, you can define a header template, a footer template and another template for the content.1: Map
2: ...3: WEB
4: ...5: "wms_feature_info_mime_type" "text/html"6: ...7: END
8: ...9: END
All three html templates are specified at layer level which allows you to customize GetFeatureInfo response differently for individual layer. Two pros of MapServer template are (1) a lot of server related information are exposed in template which you can reference by operation “[]”, e.g. server host, port, map and layer metadata etc instead of just limited to query results of GetFeatureInfo; (2) since there is no other template language involved, I always feel easier to embed javascript code in template which is a big plus. But there are also many cons in this work flow too. If you have more than one included in WMS “query_layers” and the query result has multiple features then the the html content in header and footer template will be repeated for every feature in query result. It’s probably not too difficult to tweak the template to achieve what you want, but I don’t see a clean solution if I just want one header and footer template for all layers. Another thing I didn’t figure out is how to loop through each feature in query result in a more generic way instead of hard code the attribute name in each layer. I think it’s a very simple and typical work flow but I just don’t know how to do it in MapServer. Currently what I did for my “pizzastores” point layer (other two layers have similar but separate templates too) is like below:1: LAYER
2: ...3: HEADER "../template/getfeatureinfo_header.html" # header html template4: TEMPLATE "../template/getfeatureinfo_content.html" # content html template5: FOOTER "../template/getfeatureinfo_footer.html" # footer html template6: ...7: END
header template for “pizzastores” layer:
content template for “pizzastores” layer1: <!-- MapServer Template -->
2: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/transitional.dtd">3: <html>4: <head>5: <!-- enforce the client to display result html as UTF-8 encoding -->
6: <meta http-equiv="content-type" content="text/html; charset=UTF-8"></meta>7: <style type="text/css">8: table, th, td {9: border:1px solid #e5e5e5;10: border-collapse:collapse;11: font-family: arial;12: font-size: 80%;13: color: #33333314: }15: th, td {16: valign: top;17: text-align: center;18: }19: th {20: background-color: #aed7ff21: }22: caption {23: border:1px solid #e5e5e5;24: border-collapse:collapse;25: font-family: arial;26: font-weight: bold;27: font-size: 80%;28: text-align: left;29: color: #333333;30: }31: </style>32: <title>GetFeatureInfo Response</title>33:34: </head>35: <body>36: <table>37: <caption>layer names: pizzastores</caption>38: <tbody>39: <th>Layer Name</th>40: <th>NAME</th>41: <th>ADDRESS</th>42: <th>TYPE</th>
footer layer for “pizzastores” layer1: <!-- MapServer Template -->
2: <tr>3: <td>Pizzastores</td>4: <td>[item name=NAME format=$value escape=none]</td>5: <td>[item name=ADDRESS format=$value escape=none]</td>6: <td>[item name=TYPE format=$value escape=none]</td>7: </tr>
You notice that in content template for the layer I can only access the current single query result which makes me think the templates will be repeatedly called for each feature in query results. I got the result below in browser but I have multiple <html> tags in the source:1: <!-- MapServer Template -->
2: </tbody>3: </table>4: <br/>5: </body>6: </html>
The native GML format for WMS GetFeatureInfo response doesn’t have such issue though.
GeoServer 2.0
For GeoServer (latest released version 2.0.0), I found the work flow of customizing WMS GetFeatureInfo is very straight forward and smooth. All related information and samples are described in “GetFeatureInfo Templates” and “Freemaker Templates” sections of the online user manual, which is neat. Similar to MapServer, GeoServer also uses the concept of header, footer and content html template (templates are with suffix .ftl which is just html with freemaker engine tags). There are two things I really like about GeoServer: (1) templates can be set at different levels like global, workspace (not tested though), datastore, layer so common header and footer template can be shared; (2) the content template is repeatedly applied for each feature collection (meaning all the query results from one layer) instead of each feature such that I can loop through each feature in a generic way which in the end reduces the number of templates I need.
The only place I got trapped is that the online documentation is up to date enough the reflect the data folder structure change introduced in GeoServer 2.0.0. The old “featuretypes” folder is gone (“workspaces” folder is replacing it) but the online manual has a lot of places pointing to it.
Here is what I did for geoserver:
I created header.ftl and footer.ftl templates at global level and copy them to GEOSERVER_DATA_DIR\templates\ (create templates folder if it doesn’t exist):
1: <html>2: <head>3: <title>Geoserver GetFeatureInfo output</title>4: </head>5: <style type="text/css">6: table, th, td {7: border:1px solid #e5e5e5;8: border-collapse:collapse;9: font-family: arial;10: font-size: 80%;11: color: #33333312: }13: th, td {14: valign: top;15: text-align: center;16: }17: th {18: background-color: #aed7ff19: }20: caption {21: border:1px solid #e5e5e5;22: border-collapse:collapse;23: font-family: arial;24: font-weight: bold;25: font-size: 80%;26: text-align: left;27: color: #333333;28:29: }30: </style>31: <body>
I created content.ftl template at datastore level which is copied to GEOSERVER_DATA_DIR\workspaces\<my_workspace>\<my_datastore>\1: </body>2: </html>3:
New templates seem to require a restart of GeoServer and after that I get GetFeatureInfo response displayed in browser like below:1: <table>2: <caption>layer names: ${type.name}</caption>3: <tr>4: <th>fid</th>5: <#list type.attributes as attribute>6: <#if !attribute.isGeometry>7: <th >${attribute.name}</th>8: </#if>9: </#list>10: </tr>11:12: <#assign odd=false>13: <#list features as feature>14: <#if odd>15: <tr class="odd">16: <#else>17: <tr>18: </#if>19: <#assign odd=!odd>20: <td>${feature.fid}</td>21: <#list feature.attributes as attribute>22: <#if !attribute.isGeometry>23: <td>${attribute.value?string}</td>24: </#if>25: </#list>26: </tr>27: </#list>28: </table>29: <br/>
Great guide!
ReplyDeleteJust a question:
But if I don't know the attribute lists, e.g working on the GRIB format+mapserver.
Is it possible to get all attribute.names associated to a layer using mapserver?
The old “featuretypes” folder is gone (“workspaces” folder is replacing it).."
ReplyDeleteStill reading your post, but this helped me figure out where to put the template files in 2.0. Thank you!
Hi!
ReplyDeleteThank you for this great guide!! Now my GetFeatureInfo with MapServer is running.
Cheers
Bennos
Thanks a lot
ReplyDeleteHari
This is great stuff! Thanks a bunch.
ReplyDeleteOnly 'issue' I have left is that the top and buttom of my table show a lot of space in the standard OL 'balloon'. Any resolutions for that?
Good post...
ReplyDeleteThank you.
thank you very much ... tried so hard ... now it works!!!
ReplyDeleteVery useful post, only one question, for mapserver: Where to place those templates? the path is relative to what? the mapfile?
ReplyDeleteHi,
ReplyDeleteThe path should be relative to the mapfile.
can i use php code which returns the html table in content.ftl
ReplyDeleteNguyen Van Ty An from Vietnam
ReplyDeleteThanks you alot! That's very useful for me!
does it work for raster data as well?
ReplyDeleteYes, it works for raster data as well. I tried it and finally got it running. You can follow this excellent guide.
DeleteThe main problem for me was what to set in
"item name=NAME format=$value"
It works for me with
"item name=value_0 format=$value"
(value_0 seems to refer to Band1 of my raster data. I think you could use value_n for your n-th Band of raster data).
Respect and I have a tremendous offer you: Whole House Renovation Checklist Pdf house renovation outside
ReplyDelete