NYC DoITT, the New York City Department of Information Technology and Telecommunications, oversees the use of existing and emerging technologies in government operations and delivers various services to city residents. Since we have an office in NYC, we’re particularly proud to support them as OpenGeo Suite customers.
Not long ago, they asked for ways to create styles based on the number of hours that had elapsed since a feature was created, a calculation that would otherwise have been impossible using a regular SLD. In a previous support story, we described using WPS in SLD to alter a geometry prior to rendering. While some rendering transforms — such as heatmaps and contour maps — are relatively well-known, GeoServer facilitates the use of Python or other scripting languages to help style data. By writing the logic in Python, NYC DoITT would be able to use the standard datetime module to style features. Below we’ll review how to use GeoScript to perform more advanced styling programmatically.
Before we start, we will need to install the right extensions to OpenGeo Suite, particularly the GeoServer Python scripting extension. On Linux, you can simply install the
geoserver-script package. On Windows, scripting is an option in the OpenGeo Suite installer.
On OSX, you will need to manually install the plugin by downloading the community build, selecting “Open Webapps Directory” from the menu, and dropping the
Once GeoScript is installed, we can add our Python scripts to the
scripts/functions directory in the GeoServer data directory. A simple test is to create a script named
random_colour.py that returns a random colour:
import random def run(value, args): return random.choice(["#000000", "#ff0000", "#00ff00", "#0000ff"])
We can then create an SLD that calls this function. The following SLD is based on the default
polygon style in GeoServer, with one line changed:
<?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"> <NamedLayer> <Name>geoscript test</Name> <UserStyle> <FeatureTypeStyle> <Rule> <Name>random colour</Name> <PolygonSymbolizer> <Fill> <CssParameter name="fill"> <ogc:Function name='random_colour'/> </CssParameter> </Fill> <Stroke> <CssParameter name="stroke">#000000</CssParameter> <CssParameter name="stroke-width">1</CssParameter> </Stroke> </PolygonSymbolizer> </Rule> </FeatureTypeStyle> </UserStyle> </NamedLayer> </StyledLayerDescriptor>
Instead of referencing a fixed grey colour,
<ogc:Function name='random_colour'/> calls
random_colour.py and uses the evaluated value as the polygon fill colour. The Python code simply selects at random from a list of colors — in this case: #000000, #ff0000, #00ff00 and #0000ff — and then returns it.
We can test this new style with the default
opengeo:countries layer that ships with OpenGeo Suite. Every time a new request is made, the polygon colours should be randomly generated again.
To make our SLD functions more useful, we can style based on attribute values by passing arguments to the function. Let’s create a new Python script to manage the values of each colour by mapping the value of the
mapcolor9 attribute of the
opengeo:countries to a colour. We will call this script
colour_9.py and put it in the same scripts/functions directory:
def run(value, args): colour = args if colour == 1: return "#fbb4ae" if colour == 2: return "#b3cde3" if colour == 3: return "#ccebc5" if colour == 4: return "#decbe4" if colour == 5: return "#fed9a6" if colour == 6: return "#ffffcc" if colour == 7: return "#e5d8bd" if colour == 8: return "#fddaec" if colour == 9: return "#f2f2f2"
This will use the value of the first argument as the colour for the feature. Our SLD needs to call this function and pass the
mapcolor9 attribute as the argument:
<CssParameter name="fill"> <ogc:Function name='colour_9'> <ogc:PropertyName>mapcolor9</ogc:PropertyName> </ogc:Function> </CssParameter>
While this could be done in pure SLD, it can be easier to manage complex styling rules using scripts rather than XML.
Logging to geoserver.log
Any print statements in the Python script will appear in the GeoServer logs. Let’s modify our SLD to add a second argument:
<CssParameter name="fill"> <ogc:Function name='colour_9'> <ogc:PropertyName>mapcolor9</ogc:PropertyName> <ogc:PropertyName>name</ogc:PropertyName> </ogc:Function> </CssParameter>
Then let’s print out all our arguments from the script:
def run(value, args): print args colour = args …
We should now see lines like the following in
[2.0, Aruba] [6.0, Angola] [6.0, Anguilla] [1.0, Albania] [4.0, Aland] [1.0, Andorra] [3.0, United Arab Emirates] [2.0, Armenia] [5.0, Antigua and Barbuda] [3.0, Austria] [5.0, Azerbaijan] [5.0, Burundi] [1.0, Belgium] [2.0, Benin] [5.0, Burkina Faso] [1.0, Bulgaria] [1.0, Bahrain] …
Using feature geometries
Attentive readers will have noted that in addition to
args, our scripts have an additional
value parameter. This variable contains an instance of GeoScript’s
org.geotools.feature.simple.SimpleFeatureImpl. This means we have access to the feature’s geometry and, since these geometries are JTS objects, we can use methods such as
getArea() in our script:
def run(value, args): geom = value.getDefaultGeometry() print "%s: %f" % (args, geom.getArea()) …
The modified script above will simply record the area of each geometry in the GeoServer logs before styling it.
DoITT also identified scale-dependent styling as another use for GeoScript. Rather than writing filter functions in our SLD, we can instead pass our current scale as an argument to a new function and use this to format the label. Our script,
country_label.py, can use the current scale to decide which of the three possible labels to use:
def run(value, args): abbrev = args normal = args formal = args scale = args if scale > 10000000: return abbrev if scale < 5000000: return formal return normal
The three arguments are an abbreviated name, a regular name and a formal (long) name for each country. The corresponding SLD appears as:
<TextSymbolizer> <Label> <ogc:Function name='country_label'> <ogc:PropertyName>abbrev</ogc:PropertyName> <ogc:PropertyName>name</ogc:PropertyName> <ogc:PropertyName>formal_en</ogc:PropertyName> <ogc:Function name="env"> <ogc:Literal>wms_scale_denominator</ogc:Literal> </ogc:Function> </ogc:Function> </Label> <Font> <CssParameter name="font-family">DejaVuSans</CssParameter> <CssParameter name="font-size">10.0</CssParameter> <CssParameter name="font-style">normal</CssParameter> <CssParameter name="font-weight">normal</CssParameter> </Font> <LabelPlacement> <PointPlacement> <AnchorPoint> <AnchorPointX>0.5</AnchorPointX> <AnchorPointY>0.5</AnchorPointY> </AnchorPoint> </PointPlacement> </LabelPlacement> <Halo> <Radius>2.0</Radius> <Fill> <CssParameter name="fill">#FFFFFF</CssParameter> </Fill> </Halo> <Fill> <CssParameter name="fill">#000000</CssParameter> </Fill> </TextSymbolizer>
Once again, we could have written these rules directly into our SLD, but using GeoScript allows us to maintain complex styling more efficiently.
We might want to combine our scale-dependent rule with the area calculation so that we do not have instances of large countries with the abbreviated labels (as in the case of France being labelled Fr. in the image above):
def run(value, args): abbrev = args normal = args formal = args scale = args area = value.getDefaultGeometry().getArea() if scale > 10000000 and area < 5000: return abbrev if scale < 5000000: return formal return normal
Now, countries with a large area will use the country name rather than the abbreviations that we saw in the earlier example. Smaller countries, such as most of those in the Balkans, still have their names abbreviated.
Another use of GeoScript in SLDs is creating filter functions. To continue the example of using country names, we might want to find all countries whose formal designations are comprised of five or more separate words. In this case, Federal Republic of Germany would not be shown on our map, but Independent State of Papua New Guinea would.
Using Python, we can create a file called
long_names.py with this relatively simple calculation:
def run(value, args): formal = args if len(formal.split()) >= 5: return True return False
We can then use this filter in our SLD by adding a filter after our rule name:
<Rule> <Name>long country names</Name> <ogc:Filter> <ogc:PropertyIsEqualTo> <ogc:Function name="long_names"> <ogc:PropertyName>formal_en</ogc:PropertyName> </ogc:Function> <ogc:Literal>true</ogc:Literal> </ogc:PropertyIsEqualTo> </ogc:Filter> ... </Rule>
When the script returns
True, the feature will be displayed.
Benjamin Trigona-Harany leads our global support team from our offices in Victoria, BC. Interested in support or training for your enterprise? Contact us to learn more.