Skip to content

Commit

Permalink
Added RasterField
Browse files Browse the repository at this point in the history
PostGIS support only at the moment
  • Loading branch information
yellowcap committed Mar 17, 2015
1 parent 88d798d commit 4d02221
Show file tree
Hide file tree
Showing 12 changed files with 326 additions and 7 deletions.
3 changes: 3 additions & 0 deletions django/contrib/gis/db/backends/base/features.py
Expand Up @@ -33,6 +33,9 @@ class BaseSpatialFeatures(object):
supports_distances_lookups = True
supports_left_right_lookups = False

# Does the database have raster suport?
supports_raster = False

@property
def supports_bbcontains_lookup(self):
return 'bbcontains' in self.connection.ops.gis_operators
Expand Down
1 change: 1 addition & 0 deletions django/contrib/gis/db/backends/postgis/features.py
Expand Up @@ -6,3 +6,4 @@
class DatabaseFeatures(BaseSpatialFeatures, Psycopg2DatabaseFeatures):
supports_3d_functions = True
supports_left_right_lookups = True
supports_raster = True
13 changes: 13 additions & 0 deletions django/contrib/gis/db/backends/postgis/operations.py
Expand Up @@ -7,6 +7,8 @@
from django.contrib.gis.db.backends.utils import SpatialOperator
from django.contrib.gis.geometry.backend import Geometry
from django.contrib.gis.measure import Distance
from django.contrib.gis.db.backends.postgis.pgraster import from_pgraster, to_pgraster
from django.contrib.gis.gdal.raster.source import GDALRaster
from django.core.exceptions import ImproperlyConfigured
from django.db.backends.postgresql_psycopg2.operations import \
DatabaseOperations
Expand Down Expand Up @@ -222,6 +224,9 @@ def geo_db_type(self, f):
the `AddGeometryColumn` stored procedure, unless the field
has been specified to be of geography type instead.
"""
if f.geom_type == 'RASTER':
return 'raster'

if f.geography:
if f.srid != 4326:
raise NotImplementedError('PostGIS only supports geography columns with an SRID of 4326.')
Expand Down Expand Up @@ -373,3 +378,11 @@ def geometry_columns(self):

def spatial_ref_sys(self):
return PostGISSpatialRefSys

def parse_raster(self, value):
if value and not isinstance(value, GDALRaster):
value = GDALRaster(from_pgraster(value))
return value

def deconstruct_raster(self, value):
return to_pgraster(value)
137 changes: 137 additions & 0 deletions django/contrib/gis/db/backends/postgis/pgraster.py
@@ -0,0 +1,137 @@
import binascii
import struct

from django.contrib.gis.gdal.raster.const import GDAL_TO_STRUCT, STRUCT_SIZE
from django.forms import ValidationError

GDAL_TO_POSTGIS = [None, 4, 6, 5, 8, 7, 10, 11, None, None, None, None]

POSTGIS_TO_GDAL = [1, 1, 1, 3, 1, 3, 2, 5, 4, None, 6, 7, None, None]

HEADER_STRUCTURE = 'B H H d d d d d d i H H'


def pack(structure, data):
"""
Packs data into hex string with little endian format.
"""
return binascii.hexlify(struct.pack('<' + structure, *data)).upper()


def unpack(structure, data):
"""
Unpacks little endian hexlified binary string ino python list.
"""
return struct.unpack('<' + structure, binascii.unhexlify(data))


def chunk(data, index):
"""
Splits a string into two parts at the input index.
"""
return data[:index], data[index:]


def band_to_hex(band):
"""
Returns a GDALBand's pixel values as PGRaster Band hex string.
"""
return binascii.hexlify(band.data(as_memoryview=True)).upper()


def from_pgraster(data):
"""
Converts a PostGIS HEX String into a python dictionary.
"""
# Split raster header from data
header, data = chunk(data, 122)
header = unpack(HEADER_STRUCTURE, header)

# Parse band data
bands = []
pixeltypes = []
while data:
# Get pixel type for this band
pixeltype, data = chunk(data, 2)
pixeltype = unpack('B', pixeltype)[0]

# Substract nodata byte from band nodata value if exists
has_nodata = pixeltype >= 64
if has_nodata:
pixeltype -= 64

# Convert datatype from PostGIS to GDAL & get pack type and size
pixeltype = POSTGIS_TO_GDAL[pixeltype]
pack_type = GDAL_TO_STRUCT[pixeltype]
pack_size = 2 * STRUCT_SIZE[pack_type]

# Parse band nodata value, even if it is ignored by the nodata flag
nodata, data = chunk(data, pack_size)
nodata = unpack(pack_type, nodata)[0]

# Chunk and unpack band data (pack size times nr of pixels)
band, data = chunk(data, pack_size * header[10] * header[11])
bnd_result = {'data': binascii.unhexlify(band)}
if has_nodata:
bnd_result['nodata_value'] = nodata

bands.append(bnd_result)
pixeltypes.append(pixeltype)

# Check that all bands have the same pixeltype, this is required by GDAL
if len(set(pixeltypes)) != 1:
raise ValidationError("Band pixeltypes are not all equal.")

# Process raster header
return {
'srid': int(header[8]),
'width': header[10], 'height': header[11],
'datatype': pixeltypes[0],
'origin': (header[5], header[6]),
'scale': (header[3], header[4]),
'skew': (header[7], header[8]),
'bands': bands
}


def to_pgraster(rast):
"""
Converts a GDALRaster into PostGIS Raster format.
"""
# Prepare the raster header data as tuple. The first two numbers are
# the endianness and the PostGIS Raster Version, both are fixed by
# postgis at the moment.
rasterheader = (1, 0, len(rast.bands), rast.scale.x, rast.scale.y,
rast.origin.x, rast.origin.y, rast.skew.x, rast.skew.y,
rast.srs.srid, rast.width, rast.height)

# Hexlify raster header
result = pack(HEADER_STRUCTURE, rasterheader)

for band in rast.bands:
# The PostGIS WKB band header has two elements, a 8BUI byte and the
# nodata value. The 8BUI stores both the PostGIS pixel data type
# and a nodata flag.
#
# The integer is composed as the datatype integer plus 64 as a flag
# for existing nodata values, i.e.
# 8BUI_VALUE = PG_PIXEL_TYPE (0-11) + FLAG (0 or 64)
#
# For example, if the byte value is 71, then the datatype is
# 71-64 = 7 (32BSI) and the nodata value is True.
structure = 'B' + GDAL_TO_STRUCT[band.datatype()]

# Get band pixel type in postgis notation
pixeltype = GDAL_TO_POSTGIS[band.datatype()]

# Set the nodata flag
if band.nodata_value is not None:
pixeltype += 64

# Pack band header
bandheader = pack(structure, (pixeltype, band.nodata_value or 0))

# Add packed header and band data to result string
result += bandheader + band_to_hex(band)

return result
18 changes: 12 additions & 6 deletions django/contrib/gis/db/backends/postgis/schema.py
Expand Up @@ -5,6 +5,7 @@ class PostGISSchemaEditor(DatabaseSchemaEditor):
geom_index_type = 'GIST'
geom_index_ops = 'GIST_GEOMETRY_OPS'
geom_index_ops_nd = 'GIST_GEOMETRY_OPS_ND'
rast_index_wrap = 'ST_ConvexHull(%s)'

sql_add_geometry_column = "SELECT AddGeometryColumn(%(table)s, %(column)s, %(srid)s, %(geom_type)s, %(dim)s)"
sql_drop_geometry_column = "SELECT DropGeometryColumn(%(table)s, %(column)s)"
Expand All @@ -20,12 +21,12 @@ def geo_quote_name(self, name):
return self.connection.ops.geo_quote_name(name)

def column_sql(self, model, field, include_default=False):
from django.contrib.gis.db.models.fields import GeometryField
if not isinstance(field, GeometryField):
from django.contrib.gis.db.models.fields import GeometryField, RasterField
if not isinstance(field, (GeometryField, RasterField)):
return super(PostGISSchemaEditor, self).column_sql(model, field, include_default)

if field.geography or self.connection.ops.geometry:
# Geography and Geometry (PostGIS 2.0+) columns are
if isinstance(field, RasterField) or field.geography or self.connection.ops.geometry:
# Geography, Geometry and Raster (PostGIS 2.0+) columns are
# created normally.
column_sql = super(PostGISSchemaEditor, self).column_sql(model, field, include_default)
else:
Expand Down Expand Up @@ -57,7 +58,11 @@ def column_sql(self, model, field, include_default=False):
# we use GIST_GEOMETRY_OPS, on 2.0 we use either "nd" ops
# which are fast on multidimensional cases, or just plain
# gist index for the 2d case.
if field.geography:
column = self.quote_name(field.column)
if isinstance(field, RasterField):
column = self.rast_index_wrap % field.column
index_ops = ''
elif field.geography:
index_ops = ''
elif self.connection.ops.geometry:
if field.dim > 2:
Expand All @@ -66,11 +71,12 @@ def column_sql(self, model, field, include_default=False):
index_ops = ''
else:
index_ops = self.geom_index_ops

self.geometry_sql.append(
self.sql_add_spatial_index % {
"index": self.quote_name('%s_%s_id' % (model._meta.db_table, field.column)),
"table": self.quote_name(model._meta.db_table),
"column": self.quote_name(field.column),
"column": column,
"index_type": self.geom_index_type,
"ops": index_ops,
}
Expand Down
38 changes: 38 additions & 0 deletions django/contrib/gis/db/models/fields.py
Expand Up @@ -4,6 +4,7 @@
from django.contrib.gis.geometry.backend import Geometry, GeometryException
from django.db.models.expressions import Expression
from django.db.models.fields import Field
from django.contrib.gis.gdal.raster.source import GDALRaster
from django.utils import six
from django.utils.translation import ugettext_lazy as _

Expand Down Expand Up @@ -368,3 +369,40 @@ class ExtentField(GeoSelectFormatMixin, Field):

def get_internal_type(self):
return "ExtentField"


class RasterField(Field):
"""
Raster field for GeoDjango
"""

description = "Raster Field"
geom_type = 'RASTER'

def __init__(self, spatial_index=True, **kwargs):
# Setting the index flag with the value of the `spatial_index` keyword.
self.spatial_index = spatial_index
super(RasterField, self).__init__(**kwargs)

def deconstruct(self):
name, path, args, kwargs = super(RasterField, self).deconstruct()
if self.spatial_index is not True:
kwargs['spatial_index'] = self.spatial_index
return name, path, args, kwargs

def db_type(self, connection):
return connection.ops.geo_db_type(self)

def from_db_value(self, value, expression, connection, context):
return connection.ops.parse_raster(value)

def get_db_prep_value(self, value, connection, prepared=False):
if not prepared:
value = connection.ops.deconstruct_raster(value)
return super(RasterField, self).get_db_prep_value(value, connection, prepared)

def contribute_to_class(self, cls, name, **kwargs):
super(RasterField, self).contribute_to_class(cls, name, **kwargs)

# Setup for lazy-instantiated Raster object.
setattr(cls, self.attname, GeometryProxy(GDALRaster, self))
4 changes: 3 additions & 1 deletion django/contrib/gis/db/models/proxy.py
Expand Up @@ -52,7 +52,9 @@ def __set__(self, obj, value):

# The geometry type must match that of the field -- unless the
# general GeometryField is used.
if isinstance(value, self._klass) and (str(value.geom_type).upper() == gtype or gtype == 'GEOMETRY'):
if gtype == 'RASTER':
pass
elif isinstance(value, self._klass) and (str(value.geom_type).upper() == gtype or gtype == 'GEOMETRY'):
# Assigning the SRID to the geometry.
if value.srid is None:
value.srid = self._field.srid
Expand Down
19 changes: 19 additions & 0 deletions django/contrib/gis/gdal/raster/const.py
Expand Up @@ -29,3 +29,22 @@
None, c_byte, c_uint16, c_int16, c_uint32, c_int32,
c_float, c_double, None, None, None, None
]

GDAL_TO_STRUCT = [
None, 'B', 'H', 'h', 'L', 'l', 'f', 'd',
None, None, None, None
]

STRUCT_SIZE = {
'b': 1, # Signed char
'B': 1, # Unsigned char
'?': 1, # _Bool
'h': 2, # Short
'H': 2, # Unisgned short
'i': 4, # Interger
'I': 4, # Unsigned Integer
'l': 4, # Long
'L': 4, # Unsigned Long
'f': 4, # Float
'd': 8 # Double
}
16 changes: 16 additions & 0 deletions postgis.py
@@ -0,0 +1,16 @@
DATABASES = {
'default': {
'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': 'geodjango',
'USER': 'docker',
'PASSWORD': 'docker', 'PORT': '49153', 'HOST': 'localhost',
},
'other': {
'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': 'other',
'PASSWORD': 'docker', 'PORT': '49153',
'USER': 'docker', 'HOST': 'localhost',
}
}

SECRET_KEY = 'django_tests_secret_key'

0 comments on commit 4d02221

Please sign in to comment.