General FOSSGIS

Some geek eyecandy

We and our clients regularly work with very large spatial datasets. One of the servers I set up for a client gives this when I run du:

Filesystem            Size  Used Avail Use% Mounted on
/dev/sdd1              14T   11T  2.1T  84% /mnt/storage
/dev/sdc1              14T  143M   13T   1% /mnt/storage2

With apologies to non-geeks who don’t see the point of the above :-P For those interested in setting up large filesystems, you may be interested to know that we are using ext4 on both storage arrays. We are using one massive partition on each, and so far the filesystems have been extremely reliable. In order to create such a large partition just be sure to use parted (or gparted for you point & clickers) and use GPT for the partition table.

A workflow for creating beautiful relief shaded dems using GDAL

Sometimes I create hillshades using the QGIS hillshade plugin and then overlay the original DEM over it. I set the DEM to have a false colour pallette and set it to be semi-transparent to produce something like this:

Typical usage of a hillshade with false colour overlay

Typical usage of a hillshade with false colour overlay

That is all well and good but a bit impractical. It would be much nice to have the colour pallette permanetly assigned to the hillshade. Also I want to be able to clip and mask the resulting raster to the boundary specified in a shapefile. Fortunately, GDAL provides all the tools you need to make this happen. There are already some nice articles (like this one) that describe parts of this workflow but I am writing this because I wanted to note the additional steps that I took to make this work well for me.

Before you begin

Before you begin you should have:

  1. a raster containing digital elevation data (DEM) – in my case its called ‘alt.tif’
  2. a vector layer (typically a shapefile) containing the area of interest for your final product – in my case its called ‘tanzania.shp’

Create the hillshade image

The first thing we need to do is generate a hillshade. There is a nice python plugin for doing this in QGIS, you can do it in GRASS using the QGIS-GRASS plugin. But in this article I’m going for an all-GDAL approach so we will be using the handy gdaldem command line application.  I won’t be explaining the parameters I have used here as they are well explained in the gdaldem manual pages.

So to create our hillshade we do something like this:

gdaldem hillshade alt.tif shade.tif -z 5 -s 111120 -az 90

Which will produce something like this:

Our initial hillshade image

Our initial hillshade image

Create the terrain map

Here we want to create a pleasing colour palette for the terrain. There is a lot of content on colour theory available out there on the internet – googling for ‘colorbrewer‘ would be a good place to start if you want to learn more.  Another favourite site of mine is colourlovers.com and for this tutorial I decided to use a pallette from there to see how it would turn out.

Once you have selected a suitable colour pallette (around 5 or 6 colour classes should do it), the next thing you need to do is get some information about the range of values contained in your DEM. Once again you can easily point and click your way to this in QGIS, but here is the way to get it in gdal from the command line:

gdalinfo -mm alt.tif

The resulting output includes the computed min/max for Band 1 – which is what we are after:

Band 1 Block=1334x3 Type=UInt16, ColorInterp=Gray
 Computed Min/Max=1.000,5768.000
 NoData Value=65535

Ok so our minimum height is 1m and maximum is 5768m – Tanzania is the home of Kilimanjaro after all! So lets split that range up into 5 classes to match the ‘Landcarpet Europe‘ colourlover pallette I selected. I set nearly white as an additional colour for the highest altitude range.

65535 255 255 255
5800 254 254 254
3000 121 117 10
1500 151 106 47
800 127 166 122
500 213 213 149
1 218 179 122

The value in the first column is the base of the scale range. The subsequent values are RGB triplets for that range. I saved this as a text file called ‘ramp.txt’ in the same directory as my ‘alt.tiff’ dataset. You will notice that I made the value ranges closer together at lower altitudes and wider appart at higher altitudes. You may want to experiment a little to get pleasing results – especially if you have a relatively small number of high lying terrain pixels and the rest confined to lower lying areas.

Also note that I assigned the highest terrain ‘nearly white’ so that I could reserve white (RGB: 255 255 255) for the nodata value (65535) in this dataset. We will use the fact that white is only used for nodata to our advantage when we do a clip further on in these instructions.

Ok now we can use gdaldem to generate the terrain map:

gdaldem color-relief alt.tif ramp.txt relief.tif

This is what my relief map looked like:

The terrain colour map I produced

The terrain colour map I produced

Don’t worry about the fact that it does not resemble the colour pallette you have chosen – it will do in the next step!

Merging the two rasters

The next step is to combine the two products. I used Frank Warmerdam’s handy hsv_merge.py script for this purpose.

./hsv_merge.py relief.tif shade.tif colour_shade.tif

Which produced this:

The result of merging my hillshade and my terrain colour map

The result of merging my hillshade and my terrain colour map

You may have noticed that it is only at this point that the colours of our product resemble the original pallette we used.

One little gotcha with the hsv_merge.py script is that it throws away our nodata values, so what was sea before (and nodata in my original alt.tif dataset) is now white (RGB: 255 255 255).

Clipping and masking

You may have everything you need from the above steps. Alternatively you can also clip and mask the dataset using a shapefile.

gdalwarp -co compress=deflate -dstnodata 255 -cutline Tanzania.shp \
          colour_shade.tif colour_shade_clipped.tif

My final image is now a compressed tif with nodata outside of the country of Tanzania and looks like this:

Final result: A false coloured elevation map for Tanzania

Final result: A false coloured elevation map for Tanzania

A final note

One of the things I love about the command line is the repeatable and reusable workflows it allows for. I can distill everything in this blog post into a sequence of commands and replay them any time. If you are still stuck doing things in a GUI only way, give BASH a try – you can really do powerful geospatial data processing with it!

QGIS & PostGIS Training at AIMS

This week we (Gavin Fleming, Sam Lee Pan and myself) are doing more training – a week of QGIS and PostGIS. Its a small group this time and they have GIS knowledge already so we get to go much deeper into the nuts & bolts of QGIS than I normally would do on a course (which is a real pleasure for me). Our attendees are a mix of people from industry, government and students – and we have one attendee from Botswana!

Also very interesting is the venue we are using to run the course in. AIMS (African Institute for Mathematical Sciences) is situated a stone’s throw from the popular Muizenberg beach,  Cape Town.  Now this (AIMS) is one awesome place.  When you walk in the door there is a statue of Steven Hawking who paid a visit to the center. And the place is brimming with maths students from all over Africa. They are all on fully paid up scholarships to attend a residential diploma course in mathematics which will prepare them to go on to do masters and Phd courses in maths. So they get a room, full board and access to top notch tutors and leave with excellent skills and a network of friends around Africa with whom they can collaborate in future years. The best part is that the whole center runs on Linux. They have labs full of ubuntu machines where they beat out complicated formulas using SAGE and make python jump through mathematical hoops.

Now the funny thing is that I have been dreaming for the last few years of having just such a center for FOSSGIS – where people from all over Africa can come and do a residential course in using and programming FOSSGIS. So it was with fascination, pleasure and yes, a little bit of envy that I was taken around the AIMS facilities. One day hopefully someone with an unbelievable fortune and no idea what to do with it will see this blog post and contact me so that we can start AIFS (African Institute for FOSSGIS Studies) :-) .  We are really grateful to AIMS for hosting us this week in their excellent facility, and showing us what a little foresight and ingenuity can achieve.

I’ll leave you with a couple of pics of our course attendees doing their thing…

Batch clipping with GDAL and bash

I know the little bash scripts I write and post here are popular so here is another one.  The script sequentially unzips worldclim future climate scenario datasets, and then clips them to the bounding box of Tanzania using gdal. After clipping, it removes the extracted files again so you are left with just your original downloaded zip files, and the clipped files in compressed geotif format.

#!/bin/bash
mkdir clip
for FILE in *.zip
do
  unzip $FILE
  cd 30s/
  for BIL in *.bil
  do
    CLIP=../clip/$(basename $BIL .bil).tif
    gdal_translate -of GTiff -co COMPRESS=LZW -co TILED=YES -projwin 29 0 40 -11 $BIL $CLIP
    rm $BIL
  done
  cd ..
  rm -rf 30s
done

Raster Masks in QGIS/FOSSGIS

A common activity in GIS is to mask off parts of an image such that it has transparent pixels in the masked area. We can do this in QGIS too with a bit of help from gdal. Lets look at this niche model I ran for Anopheles gambiae:

Ecological Niche model for Anopheles gambiae

Ecological Niche model for Anopheles gambiae

What I really want is all the parts of the model that fall outside of the borders of Tanzania to appear transparent. We will use a vector layer to delineate the masked area. I am going to make a mask for Tanzania. To make the mask from the vector layer we can use the gdal rasterize tool. But before we can do that we need to deal with the fact that rasterize is a bit awkward (in current version of GDAL) in that the image you are rasterizing must pre-exist. The simple work around is to just make a copy of my raster that I am trying to make a mask for using gdalwarp. I am using warp with the cutline option, in so doing assigning all cells outside of my country of interest to have a value of nodata. Unfortunately no gui tool exists for this yet so you need to run the command from the command line:

gdalwarp -of GTiff -dstnodata 0 \
-cutline Boundaries/Tanzania.shp Anopheles_gambiae__projection.tif maskbase.tif
The first step to making our mask - all the non TZ areas are assigned a null value.

The first step to making our mask - all the non TZ areas are assigned a null value.

Next I will load my vector layer containing the Tanzanian political boundary into QGIS:

Our base mask and the Tanzania boundaries loaded into QGIS.

Our base mask and the Tanzania boundaries loaded into QGIS.

Now I will use the Raster -> Rasterize tool to create the next phase mask – to fill the inside of the mask with the same value. Note that it will overwrite the contents of maskbase.tif – thats ok since we created maskbase specifically for that purpose. You can choose any attribute field for the mask, it doesn’t really matter since we will be replacing all no data values in a few steps time:

Using the rasterize tool to assign areas inside the mask with the same value.

Using the rasterize tool to assign areas inside the mask with the same value.

gdal_rasterize -a POP_CNTRY \
-l Tanzania \
/home/timlinux/gisdata/Africa/Tanzania/MasterDataSet/Boundaries/Tanzania.shp \
/home/timlinux/gisdata/Africa/Tanzania/MasterDataSet/maskbase.tif

After it has run we should have something like the image below. Note how the areas outside the country borders are transparent, while the areas inside all have the same value (in my case 222).

After rasterization, all non-masked areas are assigned a value from the vector layer.

After rasterization, all non-masked areas are assigned a value from the vector layer.

Thats great, but I want a value of 1 inside all of the country so I use the shiny new raster calculator tool that will be in QGIS 1.6:

Layer -> Raster Calculator

And then create a simple boolean expression to assign all cells of value greate than 1 a value of 1.

Maskbase@1 > 1

I’ve called the resulting file mask.tif. I’ll be throwing maskbase.tif away when I am done since is is a temporary working file. Note that the boolean (< and >) operators in the raster calculator are undocumented, and don’t appear as buttons on the user interface – you will need to type the expression manually. Buttons for these will be added in QGIS 1.7. In the screen shot below if you look at the Value Tool dock window (ValueTool is a great plugin!), you will see that the final mask has a value of 1 where the cursor is placed while the maskbase layer has a value of 222.

The masked areas are now all assigned a value of 1.

The masked areas are now all assigned a value of 1.

Ok so now that we have a mask, we can apply it to arbitrary rasters to make areas of the raster transparent e.g. (Using Layer -> Raster Calculator again):

Mask@1 * Anopheles_gambiae__projection@1

Which produces this:

Our final masked model (shown in greyscale) over the original, non-masked model (shown in colour).

Our final masked model (shown in greyscale) over the original, non-masked model (shown in colour).

Some afterthoughts:

There are always many ways to do things with computers in general and with FOSSGIS in particular – and after writing this I thought of some ways to reduce the number of steps … I’ll leave it to my readers to post in the comments if they have other more efficient ways of going about this activity.

Also just a note that I am using the QGIS raster calculator from SVN with a patch applied from Marco for transparency support. The patch will make its way into SVN in the next day or two and the raster calculator in general will arrive in 1.6 release of QGIS (or grab a nightly test build).

Update:  The patch mentioned above is now in SVN trunk.

Addition to the Linfiniti team

Just an update to let you know that I have recently joined Linfiniti. I was lucky enough to be an intern earlier in the year, and am happy to be part of the team again, and helping in bringing Open Source GIS to Africa and other parts of the world :)

A quick introduction – my name’s Samantha Lee Pan (sam for short).  I have an undergrad in environmental & geographical science (with a second major in oceanography!)  I’ve done my honours in GIS.  I am also looking at doing a masters degree in the environmental field.

There is still a lot that I am learning here at Linfiniti and been very busy with a host of different projects. But I’m sure you will be hearing more from me, as I find useful bits of info or updates on the FOSS scene – and make time to post them here!

keep well

-sam

A week in Tanzania

I spent most of last week in Dar es Salaam, Tanzania. A lovely tropical country in the heart of Africa. I was there as part of a project I am working to create tools for Biodiversity Informatics practitioners. Of course the tools are based on Free Software: Quantum GIS and openModeller.

The attendees at the workshop were entertained by my talk about what FOSS is and why it is important, an introduction to QGIS slideshow (superbly presented by Marco Hugentobler), and ending with a tour of openModellerDesktop. We also did some live demonstrations of QGIS and openModeller, before going on to discuss details about how these tools can be used to support their Biodiversity Informatics workflows.

The meeting was funded by the Global Biodiversity Information Facility (GBIF) with Juan Bello as their representitive, and hosted by the Tanzanian Commission for Science and Technology (COSTECH).

In case you are unfamiliar with the aims of GBIF, they are facilitating the digitisation (or digitization for our american readers) of the worlds biodiversity records – herbarium records, museum collections and so on. COSTECH provides the local infrastructure and staff for the ‘TanBif’ node in Tanzania.

The meeting also included ‘in-country’ experts in the fields of GIS, Meteorology, Ecology, IT and so on. I think for all of the attendees, the concept of FOSS was a real eye-opener. African economies can’t compare with those in Europe and the USA and the capital outlay for proprietary software that presents an irritation in the Western world is a major burden in the third world. So just knowing that they could dive in and use QGIS was a great revelation.

We finished our workshop a little early on the Friday so Marco and I offered to go along to the COSTECH offices and geo-enable their PostgreSQL species occurrence database and install QGIS on their desktop PC’s running Windows XP. In the space of a couple of hours we were done – the major part of which was spent showing the TanBif staff members how to bring up the PostGIS layer in QGIS, perform simple queries and make maps. Having spent days in the past trying to get proprietary software like Oracle and Arc*** configured, optimised, licensed and generally usable, I was struck by just how easy and quick it is to get someone up and running with a robust enterprise ready PostGIS geospatial datastore and a user friendly Free Software desktop GIS like QGIS.

Thanks to the friendly Tanzanian folks for their hospitality – I look forward to my next visit! Here are some piccies from the trip…

Juan Bello telling us about the cool things you can do with a good Biodiversity Information repository.

The workshop attendees (Marco and Juan out of shot)


Marco showing Godfrey how to use QGIS to bring up their PostGIS Biodiversity dataset.

Godfrey proudly showing off his first map (made with QGIS)!

Marco killing a mosquito – he became something of an expert!

Overpainting with Mapnik

The problem

I’ve been having a little poke around with Mapnik today (awesome software!). One of the things on my todo list has been to sort out rendering issues with roads we have been having. Our last iteration described roads something like this:

A style…

    <Style name="Freeway30th_style">
        <Rule>
            <LineSymbolizer>
                <CssParameter name="stroke">rgb(169,170,153)</CssParameter>
                <CssParameter name="stroke-width">12.26</CssParameter>
                <CssParameter name="stroke-linejoin">bevel</CssParameter>
                <CssParameter name="stroke-linecap">round</CssParameter>
                <CssParameter name="stroke-opacity">1</CssParameter>
            </LineSymbolizer>
            <LineSymbolizer>
                <CssParameter name="stroke">rgb(255,172,88)</CssParameter>
                <CssParameter name="stroke-width">12.16</CssParameter>
                <CssParameter name="stroke-linejoin">miter</CssParameter>
                <CssParameter name="stroke-linecap">round</CssParameter>
            </LineSymbolizer>
        </Rule>
    </Style>

…and this layer definition…

        <Layer name="Freeway30th" srs="+init=epsg:&srid;" maxzoom="39105.90277777778">
        <StyleName>Freeway30th_style</StyleName>
        <Datasource>
            <Parameter name="dbname">&dbname;</Parameter>
            <Parameter name="estimate_extent">0</Parameter>
            <Parameter name="extent">&extent;</Parameter>
            <Parameter name="geometry_field">&geometry_field;</Parameter>
            <Parameter name="host">&host;</Parameter>
            <Parameter name="password">&password;</Parameter>
            <Parameter name="port">&port;</Parameter>
            <Parameter name="srid">&srid;</Parameter>
            <Parameter name="table">(SELECT * FROM "l_roads" WHERE "type" = \
            'Freeway' ORDER BY LENGTH(&geometry_field;) DESC) as "l_roads"</Parameter>
            <Parameter name="type">&datasourcetype;</Parameter>
            <Parameter name="user">&password;</Parameter>
        </Datasource>
    </Layer>

With the idea being to render freeways with a gray outline and orange center. Unfortunately, it doesnt produce good results:

The problem being those little line ends you see making gray splodges at the end of each segment.

The solution

Michael Migurski’s blog discusses this issue a little in this article but doesnt directly explain how to achieve the desired effect. So here is what you do:

First the styles are split into two…

     <Style name="Freeway30th_style-bottom">
        <Rule>
            <LineSymbolizer>
                <CssParameter name="stroke">rgb(169,170,153)</CssParameter>
                <CssParameter name="stroke-width">12.26</CssParameter>
                <CssParameter name="stroke-linejoin">bevel</CssParameter>
                <CssParameter name="stroke-linecap">round</CssParameter>
                <CssParameter name="stroke-opacity">1</CssParameter>
            </LineSymbolizer>
        </Rule>
    </Style>
    <Style name="Freeway30th_style-top">
        <Rule>
            <LineSymbolizer>
                <CssParameter name="stroke">rgb(255,172,88)</CssParameter>
                <CssParameter name="stroke-width">12.16</CssParameter>
                <CssParameter name="stroke-linejoin">miter</CssParameter>
                <CssParameter name="stroke-linecap">round</CssParameter>
            </LineSymbolizer>
        </Rule>
    </Style>

and then the layer is now rendered as two layers, the bottom layer first, then the top:

    <Layer name="Freeway30th-bottom" srs="+init=epsg:&srid;" maxzoom="39105.90277777778">
        <StyleName>Freeway30th_style-bottom</StyleName>
        <Datasource>
            <Parameter name="dbname">&dbname;</Parameter>
            <Parameter name="estimate_extent">0</Parameter>
            <Parameter name="extent">&extent;</Parameter>
            <Parameter name="geometry_field">&geometry_field;</Parameter>
            <Parameter name="host">&host;</Parameter>
            <Parameter name="password">&password;</Parameter>
            <Parameter name="port">&port;</Parameter>
            <Parameter name="srid">&srid;</Parameter>
            <Parameter name="table">(SELECT * FROM "l_roads" WHERE "type" = \
            'Freeway' ORDER BY LENGTH(&geometry_field;) DESC) as "l_roads"</Parameter>
            <Parameter name="type">&datasourcetype;</Parameter>
            <Parameter name="user">&password;</Parameter>
        </Datasource>
    </Layer>
    <Layer name="Freeway30th-top" srs="+init=epsg:&srid;" maxzoom="39105.90277777778">
        <StyleName>Freeway30th_style-top</StyleName>
        <Datasource>
            <Parameter name="dbname">&dbname;</Parameter>
            <Parameter name="estimate_extent">0</Parameter>
            <Parameter name="extent">&extent;</Parameter>
            <Parameter name="geometry_field">&geometry_field;</Parameter>
            <Parameter name="host">&host;</Parameter>
            <Parameter name="password">&password;</Parameter>
            <Parameter name="port">&port;</Parameter>
            <Parameter name="srid">&srid;</Parameter>
            <Parameter name="table">(SELECT * FROM "l_roads" WHERE "type" = \
            'Freeway' ORDER BY LENGTH(&geometry_field;) DESC) as "l_roads"</Parameter>
            <Parameter name="type">&datasourcetype;</Parameter>
            <Parameter name="user">&password;</Parameter>
         </Datasource>
    </Layer>

A much cleaner rendering!

Note

This approach consumes more cpu time and hits your database harder than the ‘messier’ approach shown first.

Also you can see in the example above, I have adopted Michaels approach of rendering long lines first.

Have fun with your mapnik maps!

Django’s ForeignKey and inheritance limitations – solved

Last week I had to cope with one of the (few!) limitations of Django ORM model about inheritance. For the GeoDjango website we are developing, we created a model that stores user-layer pairs, so that a logged user can restore the state of map and legend as [s]he left it at previous login, and anonymous users receive a default list of layers. Here is the code for the model:

class UserWmsLayer( models.Model ):
  """ Stores custom user preferences for a layer. """
  wmslayer = models.ForeignKey( WmsLayer )
  user = models.ForeignKey( User )
  is_visible = models.NullBooleanField( null=True, blank=True )
  is_deleted =  models.NullBooleanField( null=True, blank=True, default=False )

The Layers in Legend can be of WmsLayer class or any of its subclasses, at the moment the only subclass is DateQueryLayer, a WMS layer with a filter on the date attribute. We also have a Layer class, that is an ABC (Abstract Base Class), but it’s not possible to create a ForeignKey to an ABC as it has no table in the database. So we decided to create the ForeignKey to WmsLayer as a working, half-hack solution.

That worked nicely until I relied on inheritance and method overriding. I expected Django to store the ForeignKey to the actual class of the object, but the ORM stores it as a reference to the superclass, in our case of WmsLayer. This is very well explained in enlightening answer #4 of this stackoverflow QA.

In few words, I can actually have a User-DateQueryLayer pair, but in the model it will be stored as WmsLayer, and all methods I call will be WmsLayer‘s, not DateQueryLayer‘s as I would expect from OO programming. DateQueryLayer redefines asOpenLayer method, but due to this ORM limitation I could never call it.

The solutions were multiple, more or less Pythonic, Django-ish, brittle and scalable. I took two days to google and get through them, and the only two applicable solutions seemed to be:

  1. Run Time Type identification via a ifelse cascade: that would be the less scalable and most brittle hack.
  2. Django’s Generic Relations: better, but still overcomplicated.

I was about to implement Generic Relations when I realised that asOpenLayers() simply returns a JavaScript string. A simple solution came to me in a flash. Instead of creating the JavaScript code on the fly every time the layer is used somewhere, I decided to store that string into an attribute owed by Layer, the abstract superclass, so that all subclasses will simply inherit it and populate it at instantiation time using their own asOpenLayers method. The User-*Layer pair will therefore rely on an attribute in WmsLayer table without any inheritance issue.

Here is what the relevant code looks like:

class Layer( models.Model ):
  """This is an ABC (Abstract Base Class) for all models that are layers.
  It provides common api so that all layers can be treated in a similar way."""
  name = models.CharField( max_length=256 )
  owner = models.ForeignKey( User, related_name = 'owner' )
  # store the javascript as *attribute*, instead of generating it on-the-fly
  as_open_layer = models.TextField( )

class WmsLayer( Layer ):
  url = models.URLField( max_length=1024, verify_exists=True )
  layers = models.CharField( max_length=256 )
  # link to user-layer model to keep status of legend
  users = models.ManyToManyField(User, through='UserWmsLayer')

  def asOpenLayer( self ):
    """Return a string representation of this model as an open layers
    layer definition. The created layer def will be added
    to the openlayers map of name theMap (which defaults to "map". """
    return "the JavaScript string" #omitted for brevity

  def save( self, *args, **kwargs ):
    """ Overrides standard save, generating the asOpenLayers javascript
    and storing it into as_open_layer attribute. """
    self.as_open_layer = self.asOpenLayer()
    super(WmsLayer, self).save( *args, **kwargs)

class DateQueryLayer( WmsLayer ):
  """A layer model for storing user date range queries persistently"""
  date_query_type = models.ForeignKey( DateQueryType )
  sensor = models.ForeignKey( Sensor )
  start_date = models.DateTimeField( null=True, blank=True )
  end_date = models.DateTimeField( null=True, blank=True )

  def asOpenLayer( self ):
  """ Overrides WmsLayer's method. """
  return "the JavaScript string" #omitted for brevity

  def save( self, *args, **kwargs ):
    self.as_open_layer = self.asOpenLayer()
    super(DateQueryLayer, self).save( *args, **kwargs)

That’s it.

Once you filter the UserWmsLayer table by user, you won’t call anymore the asOpenLayer method…

myObjects = UserWmsLayer.objects.filter(user__username = "anonymous")
for myObject in myObjects:
  myObjects.asOpenLayer()

but fetch the as_open_layer attribute instead!

myObjects = UserWmsLayer.objects.filter(user__username = "anonymous")
for myObject in myObjects:
  myObjects.as_open_layer

This allows you to pick up the correct string, without worrying about which type myObject is. This is good. Clean. Scalable. Pythonic. Yes, the asOpenLayer method simply returns a string according to some Layer properties, this solution could not work with more complex methods. But if you face a similar inheritance issue, and your method returns something that can be stored in an attribute of the superclass, that’s one of the cleanest solutions I’m aware of.

Hope this helps, and better solutions are most welcome :)
Happy coding,
–anne

Fancy map toolbar for OpenLayers

Are you looking for giving your OpenLayers map controls a cool appearance, smoothly integrated with the site’s theme, without writing a papyrus and scatter code among lot of files?

Then have a look at jQuery UI CSS framework, a system of classes developed for jQuery UI widgets.

This is the map toolbar of the webGIS site we are busy developing, rendered with UI-Darkness theme:
Fancy OpenLayers control toolbar

The controls (pan, measure and zoom) are OpenLayers’ controls. They are all created in the map’s init() (see first js snippet below). The binding with the buttons is made by name – therefore be sure that the names of the OpenLayers controls match exactly the name properties of the respective buttons. The activation of the selected control is done by the toggleControl() function, further below in the js snippet. That way you can add as many control-button pairs as you need.

Let’s see what the code looks like. It is not so much indeed.. My tribute to the proverbial programmer’s laziness and to the koan of Master Foo’s and the Ten Thousand Lines.

First be sure to include all necessary scripts in the html head:

<link type="text/css" href="/static/css/jquery.fancybox-1.2.6.css" rel="stylesheet" media="screen" />
<script type="text/javascript" src="/static/js/jquery-1.3.2.min.js"></script>
<script type="text/javascript" src="/static/js/jquery-ui-1.7.2.custom.min.js"></script>
<script type="text/javascript" src="/static/js/jquery.fancybox-1.2.6.pack.js"></script>
<script type="text/javascript" src="/static/js/jquery.easing.1.3.js"></script>

then add the buttons – I took inspiration from Filament Group excellent post:

<div id="mapcontrols" class="fg-buttonset fg-buttonset-single ui-helper-clearfix">
<button name='navigate'class="fg-button ui-state-default ui-state-active ui-priority-primary ui-corner-left" >Navigate</button>
<button name='line' class="fg-button ui-state-default ui-priority-primary">Measure line</button>
<button name='polygon' class="fg-button ui-state-default ui-priority-primary">Measure area</button>
<a href="#" name='zoomin' class="fg-button ui-state-default fg-button-icon-solo" title="Zoom in"><span class="ui-icon ui-icon-circle-zoomin"></span> Zoom in</a>
<a href="#" name='zoomout' class="fg-button ui-state-default fg-button-icon-solo ui-corner-right" title="Zoom out"><span class="ui-icon ui-icon-circle-zoomout"></span> Zoom out</a>
</div>

And in the end the JavaScript – add the following snippet in the map’s init() function:

    mapControls = {
	line: new OpenLayers.Control.Measure(
	    OpenLayers.Handler.Path, {
		persist: true
	    }
	),
	polygon: new OpenLayers.Control.Measure(
	    OpenLayers.Handler.Polygon, {
		persist: true
	    }
	),
	zoomin: new OpenLayers.Control.ZoomBox(
	    {title:"Zoom in box", out: false}
	),
	zoomout: new OpenLayers.Control.ZoomBox(
	    {title:"Zoom out box", out: true}
	)
    };

    var control;
    for(var key in mapControls) {
	control = mapControls[key];
	control.events.on({
	    "measure": handleMeasurements,
	    "measurepartial": handleMeasurements
	});
	map.addControl(control);
    }

and these functions at the bottom of your js file:

function handleMeasurements(event) {
    var geometry = event.geometry;
    var units = event.units;
    var order = event.order;
    var measure = event.measure;
    var element = document.getElementById('output'); //TODO redirect to other area?
    var out = "";
    if(order == 1) {
	out += "Measure: " + measure.toFixed(3) + " " + units;
    } else {
	out += "Measure: " + measure.toFixed(3) + " " + units + "2";
    }
    element.innerHTML = out;
}

function toggleControl(element) {
    for(key in mapControls) {
	var control = mapControls[key];
	//alert ($(element).is('.ui-state-active'));
	if(element.name == key && $(element).is('.ui-state-active')) {
	    control.activate();
	} else {
	    control.deactivate();
	}
    }
}

$(function(){
    //all hover and click logic for buttons
    $(".fg-button:not(.ui-state-disabled)")
    .hover(
	function(){
	    $(this).addClass("ui-state-hover");
	},
	function(){
	    $(this).removeClass("ui-state-hover");
	}
    )
    .mousedown(function(){
	$(this).parents('.fg-buttonset-single:first').find\
            (".fg-button.ui-state-active").removeClass("ui-state-active");
	if( $(this).is('.ui-state-active.fg-button-toggleable, \
            .fg-buttonset-multi .ui-state-active') )
	    { $(this).removeClass("ui-state-active"); }
	else { $(this).addClass("ui-state-active"); }
    })
    .mouseup(function(){
	if(! $(this).is('.fg-button-toggleable, .fg-buttonset-single .fg-button,  \
            .fg-buttonset-multi .fg-button') ){
	    $(this).removeClass("ui-state-active");
	}
	//TODO use this else only for measure/pan toggle.
	else {toggleControl(this);}
    });
});

Ok, should be all you have to know to set up the toolbar! Feel free to reuse the code and improve it :)
Oh, and don’t forget to tweak the CSS to get the perfect look and feel ;)

–anne