I’ve been using GeoDjango for some time now, and found myself in a situation where I wanted to retrieve raster metadata using the ST_MetaData function on some datasets stored in PostGIS.
To do so, you can of course use the raw function from the db Manager, but for this I went the extra mile (although, it’s more like the extra half-mile, since it’s very easy)
On Func and GeoFunc
GeoDjango provides django.contrib.gis.db.models.functions.GeoFunc
and Django provides django.db.models.Func
to create extra functions not available on the framework, or simply to write special functions for your project.
A first instinct tells us to use GeoFunc
, but on a closer inspection it seems that it is only suitable for Geometric fields, which is not the case for a Raster. In this case we can use a simple Function and parse the results, because the result of the function we want to implement (ST_MetaData
) contains no raster it will be easy to parse.
Calling ST_Metadata
This is fairly clear and direct.
from django.db.models import Func class ST_RasterMetadata(Func): """ Returns Raster metadata of a raster_field """ arity=1 function='ST_MetaData'
Creating this class, we now have a function that can be called in a queryset, expecting one parameter (the raster).
MyRasters.objects.annotate( metadata=ST_RasterMetadata('raster_field') )
As a simple note, the arity of the function is 1, since it only receives one parameter ('raster_field'
).
But that’s not enough…
Parsing ST_Metadata
The result from the previously created function leaves much to be desired, so what we’ll do is parse it in a dictionary.
The ST_Metadata
function returns a set of values, as stated in the docs:
rid | upperleftx | upperlefty | width | height | scalex | scaley | skewx | skewy | srid | numbands ----+------------+------------+-------+--------+--------+-----------+-------+-------+------+------- 1 | 0.5 | 0.5 | 10 | 20 | 2 | 3 | 0 | 0 | 0 | 0 2 | 3427927.75 | 5793244 | 5 | 5 | 0.05 | -0.05 | 0 | 0 | 0 | 3
My end goal is to store the returned values in a dictionary with the proper keys. To do so, we need to create our own Field
that knows how to handle the return, and link it to our PG_RasterMetadata
function.
Since the default return is a treated as a string we can subclass our field from CharField
and work from there.
On the version that I’m using, I end up with a response like: (92.3,24.5,300,1233,1.0,1.7,0.4,0.5,4326,1)
(note that I totally made up those values), corresponding to (upperleftx| upperlefty| width| height| scalex| scaley| skewx| skew| srid| numbands)
.
To get the values instead of strings, it’s a matter of removing the parentheses, and casting. In a simple line:
tuple(map(float, value[1:-1].split(',')))
This is exactly what we have write on the from_db_value
function of our MetadataField:
from django.db.models import Func class ST_MetaDataField(models.CharField): def from_db_value(self, value, expression, connection): if not value: return None return tuple(map(float, value[1:-1].split(',')))
This will end up with the tuple and the values. Since we already know what each value is, we can map them to an appropriate dictionary, create pairs using zip
and generate a dictionary from it.
from django.db.models import Func class ST_MetaDataField(models.CharField): def from_db_value(self, value, expression, connection): if not value: return None parsed_dbdata = tuple(map(float, value[1:-1].split(','))) output = { d[0] : d[1] for d in zip(('upperleftx', 'upperlefty', 'width', 'height', 'scalex', 'scaley', 'skewx', 'skew', 'srid','numbands'), parsed_dbdata) } return output
And finally, the newly created class has to be assigned to the ST_RasterMetadata
class as it’s output field.
class ST_RasterMetadata(Func): arity=1 function='ST_MetaData' output_field=ST_MetaDataField()
As stated at the beginning, this is quite easy to achieve and helps us keep our code clean from calls to raw
, very neat!.