nc_map

Okay, so why would you prefer to do it the Geoanalytics way over using SLD?  Because you can do this using just the stylesheet:

That is to say you can do the five-coloring and the county-width based font sizing in the styler alone.  Now, truth be told you should compute the five-coloring ahead because it requires a rather large number of comparisons.  What I’m going to detail in this tutorial is not the best nor most efficient way to do things, but rather a demo of the kinds of things you can do, whether or not you really should.  First off, let’s look at the WMS query that brings this back.  I have the entire county listing for the United States in the database here, and I’m only showing the counties of North Carolina.  I do that with a filter parameter.  There are others that will work, but this is based off the TigerLINE 2010 data and I knew that it would work:

http://localhost:8000/iei_commons/wms/?service=WMS&request=GetMap&version=1.0.0
    &bbox=-85,30,-75,38
    &format=png&layers=the_geom
    &width=1536&height=1280
    &srs=EPSG:4326
    &styles=fancy
    &filter={"geoid10__startswith":"37"}

I’ve broken things up across multiple lines so they’re easier to see, but take note of the filter, because it’s not CQL.   Filtering in Geoanalytics follows the same rules as filtering in Django or Tastypie.  (Nearly) any legal filter in Django will work on the URL line in Geoanalytics.  Filters are expressed as JSON documents and can contain any number of clauses.  37 is the FIPS state code for NC, so I simply wrote a filter of “geoid10__startswith” : 37.  Filters are applied after the geographic filter in Geoanalytics, so they tend not to be too expensive.  If you’re using an indexed field, they’re even cheaper.

So in rendering this, I did not modify the table beyond the default shapefile import of the US county census data as downloaded from census.gov.  The five coloring is done on the fly.  The font sizing is done on the fly.  How?  Here is my styling code.  This is put in views.py:

import csp
import json
import os

palette = [(a/255.0,b/255.0,c/255.0,1.0) for a,b,c in [
    (141,211,199),
    (255,255,179),
    (190,186,218),
    (251,128,114),
    (128,177,211)]
]
colors = {}

def calculate_colors():
    global colors, palette
    if os.path.exists("colors.json"):
        colors = json.loads(file("colors.json").read())
    else:
        adj = dict([(county.geoid10, [s.geoid10 for s in CensusCounty.objects.filter(the_geom__touches=county.the_geom)]) for county in CensusCounty.objects.all().only('the_geom')])
        problem = csp.MapColoringCSP([0,1,2,3,4], adj)
        solution = csp.min_conflicts(problem)
        for s, c in solution.items():
            colors[s] = palette[c]
        with open('colors.json', 'w') as f:
            f.write(json.dumps(colors))

def five_color(data, _):
    global colors
    if not colors:
        calculate_colors()
    return tuple(colors[data['geoid10']])

def county_label(data, _):
    return data['name10']

def stroke_width(_, pxlsz):
    k = 1./pxlsz
    if k > 1:
        return 1
    elif k < 0.3:
        return 0.3
    else:
        return k

def font_size(data, pxlsz):
    minx, miny, maxx, maxy = data['the_geom'].extent
    width = (maxx-minx) / pxlsz
    length = len(data['name10']) * 10.
    return max(10, min(14, (width/length) * 12))

fancy_county_styler = styler.Stylesheet(
    label = county_label,
    label_color = (0./255.,0./255,0./255,1.),
    font_face = 'Tw Cen MT',
    font_weight = 'bold',
    font_size = font_size,
    label_halo_size = 1.5,
    label_halo_color = (1.,1.,0.7,0.4),
    label_align = 'center',
    label_offsets = (0, 5),
    stroke_color = (72./255, 72./255, 72./255, 1.0),
    stroke_width = stroke_width,
    fill_color = five_color
)

class CensusCountyWMSView(WMS):
    title = '2010 TigerLINE Census Counties'
    adapter = GeoDjangoWMSAdapter(CensusCounty, simplify=True, styles = {
        'default' : default_county_styler,
        'fancy' : fancy_county_styler
    })

class CensusCountyDeferredWMSView(CensusCountyWMSView):
    task = census_county_renderer

Now this could have been done better, to be sure.  For one thing, the first time someone uses this the computation of the five-coloring takes forever.  There are better queries that could have been used to determine the adjacency matrix, and it could be calculated once at the time that the data is loaded and stored in a file or in a separate table.  But this is an illustration of the power of fully programmable styling as opposed to the kind of styling you can specify in an XML document.

So let’s break this down a bit.  The palette variable is just a dictionary we use to populate the color table for each of the counties.  It contains four-tuples of colors (taken from colorbrewer2.org).  We define a calculate_colors function that sets up a consrtaint-satisfaction problem for five-coloring a map.  This is called once for our map, and should properly save the file to some sane, safe location and not “colors.json”, but no worries for now.

Then we have four functions that style particular properties of the data.  They are:

  • five_color, which looks up the color in our cached map of colors and calculates colors if no map exists.
  • county_label, which simply returns the name field from the data.
  • stroke_width, which scales the stroke width slightly so that extremely zoomed out views don’t have the outlines overcrowd the view and extremely zoomed in views are too boldfaced.
  • font_size, which checks the extent of the geometry that the label will be rendered onto and then scales the font so that it (mostly) fits inside each county.

Finally, we create a stylesheet, fancy_county_styler that uses all our functions to dynamically style the data.

Now, you may argue, “But Jeff, I can simply append color and font size to the data and use an SLD stylesheet to achieve the same thing,” and you’d be right, but this and other issues like it are at the root of how so many copies of data end up existing when one copy of that data would do (locally cached, but unmodified, of course).  It’s a simplification, and honestly styling data should not be part of the underlying data model.  It blurs the MVC boundary and that ends up leading to siloed applications that can’t talk to each other because everyone has different requirements for their views.

Folks, I have checked into GitHub a lot of stuff that needed updating for quite some time.  The ga_ows application now “officially” supports WMS – as in there’s documentation on how to use it now.  WMS has been modified significantly to be in line with the way WFS works.  There are numerous speed and efficiency improvements in GeoDjango’s WMS implementation.  Other big news is that there is now Celery support for WMS’s rendering step, meaning that you can do a number of cool things, including:

  • Render WMS tiles on a cluster
  • Pre-cache WMS requests that are common
  • Use the WMS renderer to populate a Pyramid instance (requires manual coding) instead of using gdal_retile.
The (reasonably) complete list of changes:
ga_ows:
  • Simplify the creation of WMS instances and put them more in line with other Django Generic Views (tutorials on this will come in due time)
  • Added documentation: ga_ows.rendering.styler, ga_ows.rendering.palettes, ga_ows.rendering.geodjango_cairo_renderer, ga_ows.views.wms. ga_ows.utils
  • Removed ga_ows.views.wmsconfig
  • Added support for caching via a WMSCache class
  • Added support for caching by default into the GeoDjangoWMSAdapter class
  • Added support for a new keyword parameter to WMS, “fresh”, which is a boolean parameter when set to true will force the rendering of a new image and refresh the cache at the same time.
  • Added “name” and “required_fields” keyword argument to the styler.Stylesheet constructor. “required_fields” gives the query engine hints as to how to make the query more efficient for rendering.
  • Added “simplify” keyword argument to GeoDjangoWMSAdapter.  Simplify is false by default. Given “true” it uses GEOS simplify to reduce geometry complexity for rendering LineString and Polygon objects
  • Added tastyhacks.py to allow GeoDjango models to expose RESTful APIs that return GeoJSON instead of plain JSON.
ga_pyramid
  • Updated to support GetCapabilities
  • Removed “application” and “cls” parameters from the Pyramid’s WMSAdapter class.
New documentation can be found at:
  • http://www2.renci.org/~jeff/docs/ga_dynamic_models
  • http://www2.renci.org/~jeff/docs/ga_ows
  • http://www2.renci.org/~jeff/docs/ga_irods
I will attempt to get some tutorials up for using WMS in the next two weeks.

Dynamic models in Django

March 29th, 2012 | Posted by Jeff in Uncategorized - (0 Comments)

This module is a hack.  There’s no doubt about that.  But it’s an interesting hack, so I’m posting it and sharing the code in the hopes that it will be useful to people.  If you have a mongodb database installed as well as your regular Relational DB setup, you can use this reusable Django app to create models in the ORM dynamically.  That is, you can add them programmatically through the methods found in ga_dynamic_models.utils, and they will show up in your admin tool.  You can also use this same mechanism to dynamically add TastyPie RESTful apis, so you can create / update / delete model instances dynamically.  Furthermore, it interacts with ga_ows to allow you to create APIs that return data as GeoJSON instead of just plain old JSON if you have GeoDjango models.

So… How does it work?  This app just contains the barebones mechanisms for declaring these models.  It makes no supposition about how you’re actually going to expose that functionality to a user.  One example view is given in ga_dynamic_models.views.csv_upload, but the idea is that you will want to craft your own ways for creating models.  This framework is also generic enough to encompass anything that follows the Django ORM pattern roughly – that is, if it declares fields as part of the class declaration and optionally includes an embedded “meta” class, then this framework can be used to create those dynamically.  Oh, and by the way, actual Django Models are automagically added to the admin tool.

The one caveat is that although the tool is capable of running syncdb, it does not yet use South and therefore no migrations are available.  One could potentially do this, of course, but I haven’t gotten that far yet.  Also you must restart the server somehow after models are added or they won’t show up in the tools.  This may change when Django supports Python3 and the module cache can be invalidated, but right now, i’m not sure how to accomplish the hack of making sure that the models propagate to all worker processes.

Now that I’m done admonishing my work for it’s hackitude, however, let’s look at how one might actually declare a model:

from ga_dynamic_models.utils import *
declare_model(simple_geomodel('MyGeoModel',
 geom = simple_geofield('PointField'),
 some_name = simple_geofield('CharField', max_length=255, default='', null=True, db_index=True),
 some_integer = simple_geofield("IntegerField", default=10)
))
declare_model(simple_model("MyRegularModel",
 some_name = simple_field('CharField', max_length=255, default='', null=True, db_index=True),
 some_integer = simple_field("IntegerField", default=10)
))

And this is how you would declare a TastyPie resource:

declare_resource(simple_model_resource(
 'ga_dynamic_models.models',
 'MyRegularModel',
 "my_regular_model"
))
declare_resource(simple_geo_resource(
 'ga_dynamic_models.models',
 'MyGeoModel',
 'my_geo_model'
))

The call to declare_resource inserts the resource into the MongoDB collection.  The associated simple_* calls are simplified ways of creating the resources.  For more information and more general ways of creating resources, models, and other ORM style classes, see the project documentation and download the code on GitHub.

Geoanalytics’ IRODS connector

March 29th, 2012 | Posted by Jeff in Uncategorized - (0 Comments)

The code is here.  The documentation is here.

The IRODS connector makes Geoanalytics, and really Django in general able to access IRODS through the icommands interface.  All the icommands are setup as Celery tasks so that they don’t block your server worker processes, and so you can distribute iRODS tasks across multiple machines.  What is IRODS, you ask?  Well, somewhat informative is www.irods.org, but if that doesn’t make sense to you, suffice to say it’s a relatively easy to use system for

  • Exposing a filesystem securely over the internet
  • Archiving data
  • Attaching metadata to files
  • Attaching rules to files, directories, users, and groups that will manipulate data.   This can be used for things like:
    • Change the format for a particular user when s/he  accesses a file
    • Move or delete a file when it ages out of usefulness
    • Start a process when a particular collection of files is present.

 

This is the first revision of this tutorial.  There are likely to be mistakes, omissions, errors, and gaffes. If you get to the end and nothing works, comment on the post and I will endeavour to fix the problems.

Getting geoanalytics up and running on CentOS-6 is still a bit involved, but has been much simplified with the addition of an installer.  The installer can be downloaded from GitHub here

1. Setup a fresh CentOS-6 or RHEL VM.

This doesn’t have to be anything fancy.  In fact, I would recommend setting up the most basic, minimal CentOS, as the installer will take care of making all the proper packages show up.  You don’t want an existing webserver or database server on there, as this will only make for confusing errors down the road.

2. Download ga_prep and start the installer

$ wget https://github.com/JeffHeard/ga_prep/zipball/master
$ unzip JeffHeard.zip
$ mv JeffHeard* ga_prep
$ cd ga_prep
$ sudo ./install_ga-centos6.sh

Make sure to run as root or everything will blow up!!

3. Installing geoanalytics

Now things will begin installing.  The installer first takes care of adding a couple of repositories for you, including the ELGIS repository, which contains the RPMs for most of the OSGeo toolchain, and the 10gen repository, which contains the latest stable version of MongoDB.  Then it prompts you to install RPMs.  If you have a custom setup system with GDAL, PostGIS, GEOS, HDF5, NetCDF, and so forth, you may not want to let the installer perform this part.

From there, the installer installs an updated version of GDAL that includes support for building python extensions.  The ELGIS gdal is incompatible with the latest GDAL python bindings, and will need to be replaced.

Then the installer will update the /etc/profile with some extra paths that are necessary, including those for GRASS and for locally installed libraries.

Next the installer begins installing Django in a Python virtual environment. The installer creates a django user (and prompts you for a password) whose home is /opt/django.  The virtual environment for geoanalytics will be /opt/django/apps/ga/current.  To operate within this environment later, type:

$ sudo su django

Once the virtual environment is setup, the installer install the geoanalytics basic codebase.  Then it will ask you if you want to install PostGIS.  If you are running everything on the local machine, because you’re just experimenting with Geoanalytics, or you have a small installation, then you will want to answer “y” to this.  If you choose to answer “n” either because you want to setup PostGIS yourself or you have an existing PostGIS installation already running, you will merely want to make sure the following preconditions are true for your installation of PostGIS:

  • The DATABASES parameter in /opt/django/apps/ga/current/ga/settings.py is setup to point to your PostGIS installation
  • A PostGIS template database is loaded into the database (this is important for test running)
  • Your PostGIS’s pg_hba.conf is configured to allow connections coming from the geoanalytics machine (this may be obvious, but I’ve forgotten it enough times that it bears repeating).

Then the installer will ask you if you want to install MongoDB.  MongoDB can be quite complex to setup if you are creating a sharded, clustered instance of MongoDB. This installer assumes that you are creating the most basic installation of MongoDB possible.  If you are interested in a more robust solution, answer ‘n’ to this question and go to the MongoDB website for more information on a clustered configuration.  The basic configuration will work, however if you will be serving significant web application loads, you will want to move MongoDB off the same machine that PostGIS is on.

When you have your MongoDB server figured out, add the following lines to your settings.py file:

import mongoengine
mongoengine.connect('geoanalytics', host={server}, port={port})

Finally the installer will ask you if you want to setup the task broker on this machine.  If everything is running on the same machine, then the answer to this is “y”.  The task broker is relatively lightweight.  It handles apportioning Celery tasks among nodes of the Geoanalytics cluster.  There need be only one task broker, so if you are running this installer on multiple machines, you only need to answer ‘y’ to this once.  Just be sure to add the following line in your settings.py file once you have your broker figured out.  Note that it is possible to use the task queue “unbrokered” in which it uses the Django ORM, but this is much slower and has reduced functionality:

BROKER_URL = "amqp://geoanalytics:geoanalytics@{hostname}:5672//"

And that’s it!

3. Post install instructions

There are a number of things that should be done after the installer has finished.

  • Setup Celery the way you want it.
If you are using Celery, you should add autostart=true and autorestart=true to the  [celerycam] and [celerybeatd] applications on exactly one machine of your geoanalytics cluster in the file /opt/django/configs/supervisord/myapp.conf .  Usually this will be the “headnode”
  • Setup nginx the way you want it.
If you are using multiple machines for Geoanalytics and you want to load balance among the servers, there are a number of ways to go about it.  You may want to setup nginx to round-robin between servers.  This can be handled via the standard nginx configuration file, which is described on nginx.org
  • Setup MongoDB the way you want it.
If you want a clustered instance of MongoDB, because you need more space than you have on your main machine, or you want better resilience or throughput, go to mongodb.org and follow their instructions.
  • Sudo to the django user and do the following:
$ cd $VIRTUAL_ENV/ga
$ python manage.py collectstatic
$ python manage.py syncdb
$ supervisorctl restart ga

Finally, RedHat and CentOS generally firewall everything but ssh by default.  You will want to add rules to iptables to open your machine to port 80 and 443.  Also, if you’re running a geoanalytics cluster, you’ll want to make sure that all appropriate database ports are visible between the machines of your cluster.  To setup just port 80, you need a rule like this:

$ iptables --new Bills-Chain
$ iptables --insert INPUT 1 --jump Bills-Chain 
$ iptables -A Bills-Chain -p tcp --dport 80 -j ACCEPT

Once you do all this, you should be able to surf to your host and get a 404 page that gives you a list of valid URLs, like “^/admin” If you get that, you’re done with this tutorial!  From here you can write your own GeoDjango apps using the Geoanalytics core libraries.  As I publish new source code on GitHub, I will go into detail about how to use the libraries.


WMS and WFS have had extensions for Time for sometime, but I found these to be insufficient for my purposes.  For one thing, in these extensions, there is no way to query the service for what timesteps are valid.  Some services define values for continuous time.  Some services have exact values for certain times and interpolate for others.  Some services define values over over certain specific times.

To compensate for this, I first added a new call into the Geoanalytics implementations of WFS, WCS, and WMS called GetValiidTimes.  Since the time domain can be quite large, the call can be filtered in the same way as a GetFeature, GetCoverage, or GetMap call, including a layers parameter.  The return is a JSON(-P) document that contains a list of all valid timesteps and or pairs of times over which a continuous interval is defined.  If exact timesteps exist within a continuous interval, these are taken to be exact as opposed to interpolated values.

For example, the following query:

http://localhost/ndfd/wms?service=WMS&request=GetValidTimes&layers=apt&time__gte=2012-01-01&time__lt=2012-01-02

Might return a list like this:

[ { begin: "2012-01-01 00:00:00+0000",
    end: "2012-01-02 23:59:59+0000" },
  "2012-01-01 00:00:00+0000",
  "2012-01-01 01:00:00+0000",
  "2012-01-01 02:00:00+0000",
...
  "2012-01-01 23:00:00+0000" ]

The filtering on the URL includes time__gte and time__lt components.  Geoanalytics implements filtering in an alternative way to CQL, using a filtering style found in REST frameworks such as Rails or Django.  These bounds bracket the times that will be returned by the webservice, since some webservices could have years of hourly data, causing a download of tens of thousands of dates per year.

While a result-set of that magnitude is fine for some archiving applications, interactive applications using the service would suffer severe lag, so we allow filtering to contrain the set of times returned.  Additionally, some layers may have data points defined for only certain times, and this allows a user to interpret time for a given layer context correctly.

From here, it is straightforward to define three more calls, GetValidVersions and GetValidElevations.  GetValidElevations works in exactly the same way as GetValidTimes, except it returns numbers instead of strings that can be parsed as dates.  Units are not returned, since these are generally specified in the Capabilitles document associated with a web-service.

GetValidVersions returns freeform strings, but never defines continuous fields since these make very little sense.  Additionally, because multiple versions may be defined for a single time, and because versioning can be so granular, GetValidVersions returns version strings contrained to specific times like so:

[ "2012-01-01:00:00+0000" : [ "v1", "v2", "v3" ],
  ... ]

These times, elevations, and versions can then be used in accessing the service with the time, elevation, and version parameters in the GET or POST call.

The Geoanalytics Image Pyramid, ga_pyramid has had an initial release on GitHub today.  With this release, you can store tiled image pyramids, like the kind used to store the satellite imagery of Google Earth.  Features of ga_pyramid that you may not find in other packages:

  • Supports 16 and 32 bit images, including floating-point images.
  • Supports adding time and elevation dimensions to the pyramid.
  • Supports very-large pyramids via sharded MongoDB collections.

Future releases will concentrate on flexibility and performance, but this initial cut seems useful enough to announce as a release.  Find it on GitHub

OGC WFS for Django released on GitHub

January 31st, 2012 | Posted by Jeff in Uncategorized - (0 Comments)

The project can be found at  https://github.com/JeffHeard/ga_ows

Core to Geoanalytics are the Open Geographic Consortium’s Open Web Services. ga_ows is a reusable GeoDjango webapp that provides you the ability to expose GeoDjango models and Python objects as geographic webservices.

A geographic webservice allows you to access data in your GeoDjango models by bounding box and other filters; these data can then be imported into other geographic databases, or perhaps more importantly as layers on a map. For layering WFS services, see the OpenLayers project .

How does it work? OWS is based on Django’s class-based generic views. The following in your urls.py will create a WFS service for a the models in your app:

from ga_ows.views.wfs import WFS
from myapp import models as m

urlpatterns = patterns('',

# ...

    url(r'^wfs/?', WFS.as_view(
        models=[m.MyModel1, m.MyModel2], # everything but this is optional.
        title='My app\'s WFS',
        keywords=['some','keywords'],
        fees='one dollar',
        provider_name='RENCI',
        addr_street='100 Europa Dr. ste 529',
        addr_city='Chapel Hill',
        addr_admin_area='NC',
        addr_postcode='27515',
        addr_country='USA',
        addr_email='jeff@renci.org'
    )),

# ...

)

This will create a WFS endpoint at $django_server/$myapp_root/wfs that serves up features in GML and any of the formats that support the creation of single-file dataset in OGR_ (note that for now this means shapefiles are not supported for output since they require multiple files, although they will be in the near future).

Adding style to your OpenLayers

January 25th, 2012 | Posted by Jeff in Uncategorized - (0 Comments)

OpenLayers supports a subset of the SLD standard for stylesheets. Anything in an OpenLayers StyleMap can also be put into an SLD and parsed by the OpenLayers SLD parser. SLDs are limited in that they cannot perform smooth gradients, cannot combine multiple properties into a single output (you can get around this by imputing properties), as well as other limitations, but for many rendering tasks they can be quite useful.

The SLD is generally stored as a separate XML file on the server, then loaded asynchronously, applying the styles to layers as they are added to the map:

OpenLayers.loadURL("tasmania/sld-tasmania.xml", null, null, complete);

function getDefaultStyle(sld, layerName) {
	var styles = sld.namedLayers[layerName].userStyles;
	var style;
	for(var i=0; i

OpenLayers.loadURL is a proxy for the asynchronous XmlHTTPRequest method, just like in WFS, and this means that they must reside on the same server as the one serving the page, or they must be accessible via proxy. It also means that you cannot directly apply an SLD, but must instead wait to load layers until the SLD is completely loaded. Generally this is achieved with a callback method to the loadURL call – in this case, complete.

The getDefaultStyle function is a convenience function that traverses through a style document and finds a named style which can be associated with a layer.

Data driven feature styling

When the SLD standard fails to encompass your styling needs, and it often will, you’re going to have to style features by hand. In OpenLayers, a style is a bare Javascript object with no required methods and a number of optional properties attached to either a layer as a whole or to a single feature (unstyled features inherit the style of the layer). A complete list of those style properties is given at the end of this chapter, but the meat of this section will be exploring best practices.

Styling your own features means creating a number of style objects and attaching those to the feature data or a whole layer. OpenLayers provides a way to both use constants and to use feature properties to style a layer. If the features don’t contain all the properties in the form you need to do your styling, pass the features through a pre-processing function that adds attributes to the feature containing the styling values:

function styleByCityAttributes(feature) {
	var loColor = parseInt(“0x0000FF”)
	var hiColor = parseInt(“0xFF0000”)
	var v = feature.attributes.population;
	var minV = 1000.0;
	var maxV = 1000000.0;
	if (v > maxV) { v=maxV; }
	if (v < minV) { v=minV; }
	var x = (v-minV);
	features.attributes.fillColor =
		“#” + Math.round((v-minV) / (maxV-minV)).toString(16);
	features.attributes.opacity = (v-minV) / (maxV-minV);
	if( v  < lt; 100000 ) {
		features.attributes.graphicName = “circle”;
	} else if(v  < lt; 500000) {
		features.attributes.graphicName = “square”;
	}
	if( feature.attributes.isCapital ) {
		features.attributes.graphicName = “star”;
	}
}

for(var f in features) {
	if(features.hasOwnAttribute(f) {
		styleByCityAttributes(features[f]);
	}
}

Style objects can either take literal values or use feature attributes as their inputs using the notation "{property}":

var style = {
	label : ‘${cityName}’,
	fillColor : ‘${fillColor}’,
	fillOpacity : ‘${opacity},
	strokeColor : ‘#fffccd’,
	strokeOpacity : 0.7,
	graphicName : ‘${graphicName}’,
	graphic : true
}

Doing all your attribute based styling in a single function is sensible for smaller datasets and less complex applications, but a more reusable way to do it is to define your styles piecemeal and assemble them. In this way, styles can be “built up” gradually. The following function creates a data driven styler object:

function DataDrivenStyler() { return {
 	style : { }
	add : function(styleAttr, func) {
		if(typeOf(func) === ‘function’) {
			funcs.push({attr : styleAttr, func : func});
			style[styleAttr] = “${“ + styleAttr “}”;
		}
		else {
			style[styleAttr] = func;
		}
 	},
 	applyStyle : function(layer) {
		var i, p, param, f, feature;
		for(f=0; f

The applyStyle method takes an OpenLayers.Layer.Vector and styles each feature individually. The add method takes a style attribute and a function for use by the styler to style that attribute. For every feature passed to the styler, the function is passed the feature’s associated data and should return a value. That value is assigned to the attribute named in the add function.

We use this object like this:

var featureLayer = //OpenLayers.Layer.Vector definition
var property = function(p) { return function(f) { return f[p]; } };
var pointSizeByDollars = //scale point size by feature property ‘dollars’
var fillColorByCompanies = //function blending fill color by feature property “num companies”

var fstyler = DataDrivenStyler();
fstyler.add(‘fillColor’, fillColorByNumCompanies);
fstyler.add(‘fillOpacity’, 0.7);
fstyler.add(‘label’, ‘${caption}’));
fstyler.add(‘pointRadius’, pointSizeByDollars);

fstyler.style(featureLayer.features)
map.addLayers([featureLayer]);

Here we have a layer, featureLayer, that we pass through our custom data-driven styler, fstyler. We create a number of functions including the trivial but useful constant and property functions that return either a constant value or an unmodified data value from the feature as the value for a style attribute.

Here, we modify from the default the fill color, fill opacity, text label, and point size based on three data attributes that come in all our features.

The cost of doing things this way might look on the face of things to be quite high, but Javascript optimizers are powerful and furthermore the cost of actually rendering a feature on the screen is much higher than calling a series of lightweight-functions to style each feature. The rewards of “functional programming” applied in this way are consistency and readability, and they outweigh the negatives for all but the more extreme cases. This technique is used to great effectiveness in the general visualization toolkit, Protovis

Semantic feature styling

When large groups of features all need to have the exact same style, the data-driven styler seems a bit overkill. Instead, you may want to define a number of custom style objects and assign them based on semantically meaningful categories of your data.

var fireTruckStyle = { externalGraphic: ‘/images/fireTruck.png’ };
var policeCarStyle = { externalGraphic: ‘/images/policeCar.png’ };
var blockedRoadSegment = { strokeColor: “#ffff00” };

var roadsLayer = // OpenLayers.Layer.Vector
var carsLayer = // OpenLayers.Layer.Vector

var x;
for(x=0; x

The following reference material is provided here in hopes that it will be useful, but the definitive reference can be found at OpenLayers.org

OpenLayers StyleMap attribute reference

Fill attributes

  • fill Boolean true for filled features, false for unfilled.
  • fillColor String HTML fill color.  Default is “#ee9900”.
  • fillOpacity Number Fill opacity (0-1).  Default is 0.4

Outline attributes

  • stroke Boolean True for stroked features, false for nonstroked.
  • strokeColor String HTML stroke color.  Default is “#ee9900”.
  • strokeOpacity Number Stroke opacity (0-1).  Default is 1.
  • strokeWidth Number Stroke width in pixels.  Default is 1.
  • strokeLinecap String Line cap type.  Default is “round”.  Options are butt | round | square
  • strokeDashstyle String Line dash style.  Default is “solid”.  Options are dot | dash | dashdot | longdash | longdashdot | solid

Rendering external graphics at points

  • externalGraphic String URL for an graphic for a point.
  • graphicWidth Number Width in pixels for re-sizing an externalGraphic. If not provided, the image width will be used.
  • graphicHeight Number Height in pxiels for re-sizing an externalGraphic. If not provided, the image height will be used.
  • graphicOpacity Number Opacity (0-1) for an externalGraphic.
  • graphicXOffset Number Pixel offset west-to-east from the feature location for placing an externalGraphic.
  • graphicYOffset Number Pixel offset north-to-south from the feature for placing an externalGraphic.
  • graphicTitle String Tooltip for an external graphic. Some browsers do not support this.
  • backgroundGraphic String URL to a graphic to be used as the background under an externalGraphic. This can be used for drop shadows.
  • backgroundGraphicZIndex Number The integer z-index value to use in rendering the background graphic. Higher z-indexes are “closer” to the user.
  • backgroundXOffset Number The x offset west-to-east from the feature location (not from the externalGraphic) for the background graphic.
  • backgroundYOffset Number The y offset north-to-south from the feature location (not the externalGraphic) for the background graphic.
  • backgroundHeight Number The height in pixels of the background graphic.  If not provided, the graphicHeight will be used.
  • backgroundWidth Number The width in pixels of the background width.  If not provided, the graphicWidth will be used.

Rendering points

  • pointRadius Number Point radius in pixels.  Default is 6.
  • rotation Number For point symbolizers, this is the rotation of a graphic in the clockwise direction about its center point or any point off center as specified by graphicXOffset and graphicYOffset.
  • graphicZIndex Number The integer z-index value to use when rendering the point. Use higher indices for bringing it closer to the user. The default is the feature layer’s z-index.
  • graphicName String Named graphic to use when rendering points.  Options are “circle” (default) | “square” | “star” | “x” | “cross” | “triangle”

Rendering feature labels

  • label String The text for an optional label.  For browsers that use the canvas renderer, this requires either fillText or mozDrawText to be available. This works in Firefox, Chrome, and Internet Explorer 8 and above.
  • labelAlign String Whee the insertion point is relative to the text.  It’s a two character string where the first character is the horizontal alignment, and the second gives the vertical alignment.
    • Horizontal alignment options: “l” | “c” | “r”.
    • Vertical alignment options: “t”=top, “m”=middle, “b”=bottom.  Not supported by the canvas renderer.
  • labelXOffset Number Pixel offset east-to-west from the feaature origin.
  • labelYOffset Number Pixel offset north-to-south from the feature origin.
  • labelSelect Boolean Whether or not labels will be selectable using SelectFeature or similar controls.  Default is false.
  • fontColor String The font color for the label.
  • fontOpacity Number Opacity (0-1) for the label.
  • fontFamily String The CSS font family for the label.
  • fontSize String The CSS font size for the label.
  • fontWeight String The CSS font weight for the label.
  • <

Miscellaneous attributes

  • graphic Boolean Whether or not to draw the feature graphic. This is useful if you only want to draw text, but don’t need an anchor point for it.
  • cursor String Default is “”.
  • pointerEvents String Default is “visiblePainted”.
  • display String If this is set to “none”, the feature will not be displayed at all. Useful for hiding or for marking features as deleted while maintaining undo-able state.