Saturday 28 April 2012

Exposing geographic information with Geoserver and OpenLayers

Exposing geographic information


Visualising geographic data especially on a map has been scientific subject for ages :-) But one of the basics is actually still colouring and labeling features as means of symbolisation to recognise and distinguish geographic features and to compare specific attribute values.

The OpenGIS® Styled Layer Descriptor (SLD) Profile of the OpenGIS® Web Map Service (WMS) Encoding Standard [http://www.opengeospatial.org/standards/wms] defines an encoding that extends the WMS standard to allow user-defined symbolization and coloring of geographic feature[http://www.opengeospatial.org/ogc/glossary/f] and coverage[http://www.opengeospatial.org/ogc/glossary/c] data. SLD addresses the need for users and software to be able to control the visual portrayal of the geospatial data. The ability to define styling rules requires a styling language that the client and server can both understand. The OpenGIS® Symbology Encoding Standard (SE) [http://www.opengeospatial.org/standards/symbol] provides this language, while the SLD profile of WMS enables application of SE to WMS layers using extensions of WMS operations. Additionally, SLD defines an operation for standardized access to legend symbols.  (http://www.opengeospatial.org/standards/sld)

I would like to show three nice examples I used in the SMART web mapping application. The first is a a point style - a neat triangle with a label, the 2nd is colouring of contour lines and labeling and the use of Geoserver's FeatureInfo template system.
The first example is a point feature, a State of the Environment monitoring well in New Zealand. Besides other data fields It has an ID, which is actually an officially assigned number.

<?xml version="1.0" encoding="UTF-8"?>
<StyledLayerDescriptor version="1.0.0"
  xsi:schemaLocation="http://www.opengis.net/sld http://schemas.opengis.net/sld/1.0.0/StyledLayerDescriptor.xsd" xmlns="http://www.opengis.net/sld"
  xmlns:ogc="http://www.opengis.net/ogc" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <NamedLayer>
    <Name>soe_gwl_monitoring_wells</Name>
    <UserStyle>
      <Name>soe_gwl_monitoring_wells:withLabel</Name>
      <Title>withLabel</Title>
      <FeatureTypeStyle>
        <Name>triangleWithLabel</Name>
        <Rule>
          <Name>triangleWithLabel</Name>
          <MinScaleDenominator>0</MinScaleDenominator>
          <MaxScaleDenominator>9999999</MaxScaleDenominator>
          <TextSymbolizer>
            <Label>
              <ogc:PropertyName>ID</ogc:PropertyName>
            </Label>
            <Font>
              <CssParameter name="font-family">Sans-Serif</CssParameter>
              <CssParameter name="font-style">italic</CssParameter>
              <CssParameter name="font-size">10</CssParameter>
              <CssParameter name="font-color">#000000</CssParameter>
            </Font>
            <LabelPlacement>
              <PointPlacement>
                <AnchorPoint>
                  <AnchorPointX>
                    <ogc:Literal>0.0</ogc:Literal>
                  </AnchorPointX>
                  <AnchorPointY>
                    <ogc:Literal>0.0</ogc:Literal>
                  </AnchorPointY>
                </AnchorPoint>
                <Displacement>
                  <DisplacementX>
                    <ogc:Literal>2.0</ogc:Literal>
                  </DisplacementX>
                  <DisplacementY>
                    <ogc:Literal>2.0</ogc:Literal>
                  </DisplacementY>
                </Displacement>
                <Rotation>
                  <ogc:Literal>0.0</ogc:Literal>
                </Rotation>
              </PointPlacement>
            </LabelPlacement>
            <Halo>
              <Fill>
                <CssParameter name="fill">#FF4000</CssParameter>
                <CssParameter name="fill-opacity">0.3</CssParameter>
              </Fill>
            </Halo>
            <Fill>
              <CssParameter name="fill">#000000</CssParameter>
            </Fill>
          </TextSymbolizer>
          <PointSymbolizer>
            <Graphic>
              <Mark>
                <WellKnownName>triangle</WellKnownName>
                <Fill>
                  <CssParameter name="fill">
                    <ogc:Literal>#FF4000</ogc:Literal>
                  </CssParameter>
                </Fill>
              </Mark>
              <Opacity>
                <ogc:Literal>1.0</ogc:Literal>
              </Opacity>
              <Size>
                <ogc:Literal>10</ogc:Literal>
              </Size>
            </Graphic>
          </PointSymbolizer>
        </Rule>
      </FeatureTypeStyle>
    </UserStyle>
  </NamedLayer>
</StyledLayerDescriptor>
 The main things I do here, are essentially defining the text representation and then the point symbolisation by giving it the shape of small orange triangle with no surrounding line. For the text symbolisation I define, what data field shall the label represent, the  font, the label placement in relation to the actual point and I use the halo-tag to give the font I nice colourful background "glow" :-)
The next is a contour line clouring description. The contour lines represent mean annual rainfall and regarding the data sets I used a Jenks classification with 5 classes, which I hard-coded in the SLD document.

<?xml version="1.0" encoding="ISO-8859-1"?>
<StyledLayerDescriptor version="1.0.0" xsi:schemaLocation="http://www.opengis.net/sld StyledLayerDescriptor.xsd" xmlns="http://www.opengis.net/sld" xmlns:ogc="http://www.opengis.net/ogc" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <!-- a Named Layer is the basic building block of an SLD document -->
  <NamedLayer>
    <Name>horowhenua_mean_rainfall_contours</Name>
    <UserStyle>
      <Name>horowhenua_mean_rainfall_contours:withContourLabel</Name>
      <Title>withCountourLabels</Title>
      <Abstract>withCountourLabels</Abstract>
      <FeatureTypeStyle>
        <Name>withCountourLabels</Name>
        <Title>withCountourLabels</Title>
<!-- 5 breaks natural jenks, 900-1200, 1201-1700, 1701-2200, 2201-2700, 2701-3200 -->
        <Rule>
          <Name>900-1200</Name>
          <Title>900-1200</Title>
          <Abstract>900-1200</Abstract>
          <ogc:Filter>
            <ogc:PropertyIsLessThanOrEqualTo>
              <ogc:PropertyName>CONTOUR</ogc:PropertyName>
              <ogc:Literal>1200</ogc:Literal>
            </ogc:PropertyIsLessThanOrEqualTo>
          </ogc:Filter>
          <MinScaleDenominator>0</MinScaleDenominator>
          <MaxScaleDenominator>9999999</MaxScaleDenominator>
          <TextSymbolizer>
            <Label>
              <ogc:PropertyName>CONTOUR</ogc:PropertyName>
            </Label>
            <LabelPlacement>
              <LinePlacement />
            </LabelPlacement>
            <Halo>
              <Fill>
                <CssParameter name="fill">#CEF6F5</CssParameter>
                <CssParameter name="fill-opacity">0.6</CssParameter>
              </Fill>
            </Halo>
            <Fill>
              <CssParameter name="fill">#0B0B3B</CssParameter>
            </Fill>
            <VendorOption name="followLine">true</VendorOption>
          </TextSymbolizer>
          <LineSymbolizer>
            <Stroke>
              <CssParameter name="stroke">#2ECCFA</CssParameter>
              <CssParameter name="stroke-opacity">0.7</CssParameter>
              <CssParameter name="stroke-width">
                <ogc:Literal>3</ogc:Literal>
              </CssParameter>
            </Stroke>
          </LineSymbolizer>
        </Rule>
<!-- 5 breaks natural jenks, 900-1200, 1201-1700, 1701-2200, 2201-2700, 2701-3200 -->
        <Rule>
          <Name>1201-1700</Name>
          <Title>1201-1700</Title>
          <Abstract>1201-1700</Abstract>
          <ogc:Filter>
            <ogc:And>
              <ogc:PropertyIsGreaterThan>
                <ogc:PropertyName>CONTOUR</ogc:PropertyName>
                <ogc:Literal>1201</ogc:Literal>
              </ogc:PropertyIsGreaterThan>
              <ogc:PropertyIsLessThanOrEqualTo>
                <ogc:PropertyName>CONTOUR</ogc:PropertyName>
                <ogc:Literal>1700</ogc:Literal>
              </ogc:PropertyIsLessThanOrEqualTo>
            </ogc:And>
          </ogc:Filter>
          <MinScaleDenominator>0</MinScaleDenominator>
          <MaxScaleDenominator>9999999</MaxScaleDenominator>
          <TextSymbolizer>
            <Label>
              <ogc:PropertyName>CONTOUR</ogc:PropertyName>
            </Label>
            <LabelPlacement>
              <LinePlacement />
            </LabelPlacement>
            <Halo>
              <Fill>
                <CssParameter name="fill">#CEF6F5</CssParameter>
                <CssParameter name="fill-opacity">0.6</CssParameter>
              </Fill>
            </Halo>
            <Fill>
              <CssParameter name="fill">#0B0B3B</CssParameter>
            </Fill>
            <VendorOption name="followLine">true</VendorOption>
          </TextSymbolizer>
          <LineSymbolizer>
            <Stroke>
              <CssParameter name="stroke">#2E9AFE</CssParameter>
              <CssParameter name="stroke-opacity">0.7</CssParameter>
              <CssParameter name="stroke-width">
                <ogc:Literal>3</ogc:Literal>
              </CssParameter>
            </Stroke>
          </LineSymbolizer>
        </Rule>
<!-- 5 breaks natural jenks, 900-1200, 1201-1700, 1701-2200, 2201-2700, 2701-3200 -->
        <Rule>
          <Name>1701-2200</Name>
          <Title>1701-2200</Title>
          <Abstract>1701-2200</Abstract>
          ...
<!-- 5 breaks natural jenks, 900-1200, 1201-1700, 1701-2200, 2201-2700, 2701-3200 -->
        <Rule>
          <Name>2201-2700</Name>
          <Title>2201-2700</Title>
          <Abstract>2201-2700</Abstract>
          ...
<!-- 5 breaks natural jenks, 900-1200, 1201-1700, 1701-2200, 2201-2700, 2701-3200 -->
        <Rule>
          <Name>2701-3200</Name>
          <Title>2701-3200</Title>
          <Abstract>2701-3200</Abstract>
          ...
        </Rule>
      </FeatureTypeStyle>
    </UserStyle>
  </NamedLayer>
</StyledLayerDescriptor>

I cut the last three declarations, as anybody might derive that from the first two ones, I think :-) Here I used the OGC filter encoding specification (http://www.opengeospatial.org/standards/filter) to define that only these lines, which have values of a specific range in the named attribute. Additionally the vendor-option <VendorOption name="followLine">true</VendorOptionfollowLine aligns the contour's elevation labels along the contour lines :-). A nice fact is that if you use not only the name-tag within the rule, but also title and/or abstract (I am actually not 100% sure :-p ) that naming will be represented in the WMS layer legend.

Finally I would like to shortly introduce my first experiences with Geoserver's freemarker template engine (http://docs.geoserver.org/latest/en/user/tutorials/GetFeatureInfo/index.html and http://geoserver.org/display/GEOS/Templates) to customise featureInfo output with OpenLayers. The first code example is a pretty basic template that fetches attribute names and values from the Geoserver provided featureCollection and then fetches a hydrograph picture based on the ID of the current feature, to demonstrate the flexibilty. In future developments  a bit more dynamic functionality like rendering such a graph online by requesting the time-series from a SOS server would way cooler :-) 

<#--
Body section of the GetFeatureInfo template, it's provided with one feature collection, and
will be called multiple times if there are various feature collections
-->
<table class="featureInfo">
  <caption class="featureInfo">SOE Wells</caption>
  <tr>
<#list type.attributes as attribute>
  <#if !attribute.isGeometry>
    <th >${attribute.name}</th>
  </#if>
</#list>
  </tr>

<#assign odd = false>
<#list features as feature>
  <#if odd>
    <tr class="odd">
  <#else>
    <tr>
  </#if>
  <#assign odd = !odd>

  <#list feature.attributes as attribute>
    <#if !attribute.isGeometry>
      <td>${attribute.value}</td>
    </#if>
  </#list>
  </tr>
  <tr>
        <td colspan="4">
                <img src="demorequests/${feature.attributes["ID"].value}.png" width="400" height="216" alt="SOE Well ID ${feature.attributes["ID"].value} Hydrograph">
        </td>
  </tr>
</#list>
</table>
<br/>

In the JavaScript implementation of your OpenLayers map-object you fetch this template "auomagically" filled with data from Geoserver. You need to patch together following things:

  • featureInfo = OpenLayers.Control.WMSGetFeatureInfo()
    • map.addControl(featureInfo)
    • featureInfo.activate()
    • document.getElementById('map').style.cursor='pointer';
And finally you could display that generated html via an Ext.Window

The JavaScript stuff is a big hassle in my opinion. It gives you a lot of flexibility and nice cool things directly in the client browser, but it costs a lot of nerves. But maybe I am just not the real JavaScript developer at all :-p