1
0
mirror of https://github.com/django/django.git synced 2025-07-04 09:49:12 +00:00

gis: Added preliminary spatial backend for Oracle; added GEOS routine fromfile.

git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@6524 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Justin Bronn 2007-10-16 06:41:16 +00:00
parent 189335acaa
commit ba9fa9844c
24 changed files with 596 additions and 76 deletions

View File

@ -35,8 +35,16 @@ if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
PostGISField as GeoBackendField, POSTGIS_TERMS as GIS_TERMS, \
create_spatial_db, get_geo_where_clause, gqn, \
ASGML, ASKML, GEOM_SELECT, TRANSFORM, UNION
SPATIAL_BACKEND = 'postgis'
elif settings.DATABASE_ENGINE == 'oracle':
from django.contrib.gis.db.backend.oracle import \
OracleSpatialField as GeoBackendField, \
ORACLE_SPATIAL_TERMS as GIS_TERMS, \
create_spatial_db, get_geo_where_clause, gqn, \
ASGML, GEOM_SELECT, TRANSFORM, UNION
SPATIAL_BACKEND = 'oracle'
else:
raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_NAME)
raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE)
def geo_quotename(value):
"""

View File

@ -0,0 +1,12 @@
"""
The Oracle spatial database backend module.
Please note that WKT support is broken on the XE version, and this will
not work.
"""
from django.contrib.gis.db.backend.oracle.creation import create_spatial_db
from django.contrib.gis.db.backend.oracle.field import OracleSpatialField, gqn
from django.contrib.gis.db.backend.oracle.query import \
get_geo_where_clause, ORACLE_SPATIAL_TERMS, \
ASGML, GEOM_SELECT, TRANSFORM, UNION

View File

@ -0,0 +1,21 @@
"""
This object provides the database adaptor for Oracle geometries.
"""
from cx_Oracle import CLOB
class OracleSpatialAdaptor(object):
def __init__(self, geom):
"Initializes only on the geometry object."
self.wkt = geom.wkt
def __str__(self):
"WKT is used for the substitution value of the geometry."
return self.wkt
def oracle_type(self):
"""
The parameter type is a CLOB because no string (VARCHAR2) greater
than 4000 characters will be accepted through the Oracle database
API and/or SQL*Plus.
"""
return CLOB

View File

@ -0,0 +1,8 @@
from django.db.backends.oracle.creation import create_test_db
def create_spatial_db(test=True, verbosity=1, autoclobber=False):
"A wrapper over the Oracle `create_test_db` routine."
if not test: raise NotImplementedError('This uses `create_test_db` from db/backends/oracle/creation.py')
from django.conf import settings
from django.db import connection
create_test_db(settings, connection, verbosity, autoclobber)

View File

@ -0,0 +1,159 @@
import re
from types import StringType, UnicodeType
from django.db import connection
from django.db.backends.util import truncate_name
from django.db.models.fields import Field # Django base Field class
from django.contrib.gis.geos import GEOSGeometry
from django.contrib.gis.db.backend.util import GeoFieldSQL
from django.contrib.gis.db.backend.oracle.adaptor import OracleSpatialAdaptor
from django.contrib.gis.db.backend.oracle.query import ORACLE_SPATIAL_TERMS, TRANSFORM
# Quotename & geographic quotename, respectively.
qn = connection.ops.quote_name
def gqn(value):
if isinstance(value, UnicodeType): value = value.encode('ascii')
return "'%s'" % value
class OracleSpatialField(Field):
"""
The backend-specific geographic field for Oracle Spatial.
"""
empty_strings_allowed = False
def __init__(self, extent=(-180.0, -90.0, 180.0, 90.0), tolerance=0.00005, **kwargs):
"""
Oracle Spatial backend needs to have the extent -- for projected coordinate
systems _you must define the extent manually_, since the coordinates are
for geodetic systems. The `tolerance` keyword specifies the tolerance
for error (in meters).
"""
# Oracle Spatial specific keyword arguments.
self._extent = extent
self._tolerance = tolerance
# Calling the Django field initialization.
super(OracleSpatialField, self).__init__(**kwargs)
def _add_geom(self, style, db_table):
"""
Adds this geometry column into the Oracle USER_SDO_GEOM_METADATA
table.
"""
# Checking the dimensions.
# TODO: Add support for 3D geometries.
if self._dim != 2:
raise Exception('3D geometries not yet supported on Oracle Spatial backend.')
# Constructing the SQL that will be used to insert information about
# the geometry column into the USER_GSDO_GEOM_METADATA table.
meta_sql = style.SQL_KEYWORD('INSERT INTO ') + \
style.SQL_TABLE('USER_SDO_GEOM_METADATA') + \
' (%s, %s, %s, %s)\n ' % tuple(map(qn, ['TABLE_NAME', 'COLUMN_NAME', 'DIMINFO', 'SRID'])) + \
style.SQL_KEYWORD(' VALUES ') + '(\n ' + \
style.SQL_TABLE(gqn(db_table)) + ',\n ' + \
style.SQL_FIELD(gqn(self.column)) + ',\n ' + \
style.SQL_KEYWORD("MDSYS.SDO_DIM_ARRAY") + '(\n ' + \
style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") + \
("('LONG', %s, %s, %s),\n " % (self._extent[0], self._extent[2], self._tolerance)) + \
style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") + \
("('LAT', %s, %s, %s)\n ),\n" % (self._extent[1], self._extent[3], self._tolerance)) + \
' %s\n );' % self._srid
return meta_sql
def _geom_index(self, style, db_table):
"Creates an Oracle Geometry index (R-tree) for this geometry field."
# Getting the index name, Oracle doesn't allow object
# names > 30 characters.
idx_name = truncate_name('%s_%s_id' % (db_table, self.column), 30)
sql = style.SQL_KEYWORD('CREATE INDEX ') + \
style.SQL_TABLE(qn(idx_name)) + \
style.SQL_KEYWORD(' ON ') + \
style.SQL_TABLE(qn(db_table)) + '(' + \
style.SQL_FIELD(qn(self.column)) + ') ' + \
style.SQL_KEYWORD('INDEXTYPE IS ') + \
style.SQL_TABLE('MDSYS.SPATIAL_INDEX') + ';'
return sql
def _post_create_sql(self, style, db_table):
"""
Returns SQL that will be executed after the model has been
created.
"""
# Getting the meta geometry information.
post_sql = self._add_geom(style, db_table)
# Getting the geometric index for this Geometry column.
if self._index:
return (post_sql, self._geom_index(style, db_table))
else:
return (post_sql,)
def db_type(self):
"The Oracle geometric data type is MDSYS.SDO_GEOMETRY."
return 'MDSYS.SDO_GEOMETRY'
def get_db_prep_lookup(self, lookup_type, value):
"""
Returns field's value prepared for database lookup, accepts WKT and
GEOS Geometries for the value.
"""
if lookup_type in ORACLE_SPATIAL_TERMS:
# special case for isnull lookup
if lookup_type == 'isnull': return GeoFieldSQL([], [])
# When the input is not a GEOS geometry, attempt to construct one
# from the given string input.
if isinstance(value, GEOSGeometry):
pass
elif isinstance(value, (StringType, UnicodeType)):
try:
value = GEOSGeometry(value)
except GEOSException:
raise TypeError("Could not create geometry from lookup value: %s" % str(value))
else:
raise TypeError('Cannot use parameter of %s type as lookup parameter.' % type(value))
# Getting the SRID of the geometry, or defaulting to that of the field if
# it is None.
if value.srid is None: srid = self._srid
else: srid = value.srid
# The adaptor will be used by psycopg2 for quoting the WKT.
adapt = OracleSpatialAdaptor(value)
if srid != self._srid:
# Adding the necessary string substitutions and parameters
# to perform a geometry transformation.
return GeoFieldSQL(['%s(SDO_GEOMETRY(%%s, %s), %%s)' % (TRANSFORM, srid)],
[adapt, self._srid])
else:
return GeoFieldSQL(['SDO_GEOMETRY(%%s, %s)' % srid], [adapt])
else:
raise TypeError("Field has invalid lookup: %s" % lookup_type)
def get_db_prep_save(self, value):
"Prepares the value for saving in the database."
if not bool(value):
# Return an empty string for NULL -- but this doesn't work yet.
return ''
if isinstance(value, GEOSGeometry):
return OracleSpatialAdaptor(value)
else:
raise TypeError('Geometry Proxy should only return GEOSGeometry objects.')
def get_placeholder(self, value):
"""
Provides a proper substitution value for Geometries that are not in the
SRID of the field. Specifically, this routine will substitute in the
SDO_CS.TRANSFORM() function call.
"""
if isinstance(value, GEOSGeometry) and value.srid != self._srid:
# Adding Transform() to the SQL placeholder.
return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (TRANSFORM, value.srid, self._srid)
elif value is None:
return '%s'
else:
return 'SDO_GEOMETRY(%%s, %s)' % self._srid

View File

@ -0,0 +1,45 @@
"""
The GeometryColumns and SpatialRefSys models for the Oracle spatial
backend.
It should be noted that Oracle Spatial does not have database tables
named according to the OGC standard, so the closest analogs are used.
For example, the `USER_SDO_GEOM_METADATA` is used for the GeometryColumns
model and the `SDO_COORD_REF_SYS` is used for the SpatialRefSys model.
"""
from django.db import models
from django.contrib.gis.models import SpatialRefSysMixin
class GeometryColumns(models.Model):
"Maps to the Oracle USER_SDO_GEOM_METADATA table."
table_name = models.CharField(maxlength=32)
column_name = models.CharField(maxlength=1024)
srid = models.IntegerField(primary_key=True)
# TODO: Add support for `diminfo` column (type MDSYS.SDO_DIM_ARRAY).
class Meta:
db_table = 'USER_SDO_GEOM_METADATA'
@classmethod
def table_name_col(self):
return 'table_name'
def __unicode__(self):
return '%s - %s (SRID: %s)' % (self.table_name, self.column_name, self.srid)
class SpatialRefSys(models.Model, SpatialRefSysMixin):
"Maps to the Oracle MDSYS.CS_SRS table."
cs_name = models.CharField(maxlength=68)
srid = models.IntegerField(primary_key=True)
auth_srid = models.IntegerField()
auth_name = models.CharField(maxlength=256)
wktext = models.CharField(maxlength=2046)
#cs_bounds = models.GeometryField()
class Meta:
# TODO: Figure out way to have this be MDSYS.CS_SRS without
# having django's quoting mess up the SQL.
db_table = 'CS_SRS'
@property
def wkt(self):
return self.wktext

View File

@ -0,0 +1,78 @@
"""
This module contains the spatial lookup types, and the get_geo_where_clause()
routine for Oracle Spatial.
"""
from django.db import connection
qn = connection.ops.quote_name
ORACLE_GEOMETRY_FUNCTIONS = {
'contains' : 'SDO_CONTAINS',
'coveredby' : 'SDO_COVEREDBY',
'covers' : 'SDO_COVERS',
'disjoint' : 'SDO_DISJOINT',
'dwithin' : ('SDO_WITHIN_DISTANCE', float),
'intersects' : 'SDO_OVERLAPBDYINTERSECT', # TODO: Is this really the same as ST_Intersects()?
'equals' : 'SDO_EQUAL',
'exact' : 'SDO_EQUAL',
'overlaps' : 'SDO_OVERLAPS',
'same_as' : 'SDO_EQUAL',
#'relate' : ('SDO_RELATE', str), # Oracle uses a different syntax, e.g., 'mask=inside+touch'
'touches' : 'SDO_TOUCH',
'within' : 'SDO_INSIDE',
}
# This lookup type does not require a mapping.
MISC_TERMS = ['isnull']
# Assacceptable lookup types for Oracle spatial.
ORACLE_SPATIAL_TERMS = ORACLE_GEOMETRY_FUNCTIONS.keys()
ORACLE_SPATIAL_TERMS += MISC_TERMS
ORACLE_SPATIAL_TERMS = tuple(ORACLE_SPATIAL_TERMS) # Making immutable
def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
"Returns the SQL WHERE clause for use in Oracle spatial SQL construction."
if table_prefix.endswith('.'):
table_prefix = qn(table_prefix[:-1])+'.'
field_name = qn(field_name)
# See if a PostGIS Geometry function matches the lookup type next
lookup_info = ORACLE_GEOMETRY_FUNCTIONS.get(lookup_type, False)
if lookup_info:
# Lookup types that are tuples take tuple arguments, e.g., 'relate' and
# 'dwithin' lookup types.
if isinstance(lookup_info, tuple):
# First element of tuple is lookup type, second element is the type
# of the expected argument (e.g., str, float)
func, arg_type = lookup_info
# Ensuring that a tuple _value_ was passed in from the user
if not isinstance(value, tuple) or len(value) != 2:
raise TypeError('2-element tuple required for %s lookup type.' % lookup_type)
# Ensuring the argument type matches what we expect.
if not isinstance(value[1], arg_type):
raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
if func == 'dwithin':
# TODO: test and consider adding different distance options.
return "%s(%s, %%s, 'distance=%s')" % (func, table_prefix + field_name, value[1])
else:
return "%s(%s, %%s, %%s) = 'TRUE'" % (func, table_prefix + field_name)
else:
# Returning the SQL necessary for the geometry function call. For example:
# SDO_CONTAINS("geoapp_country"."poly", SDO_GEOMTRY('POINT(5 23)', 4326)) = 'TRUE'
return "%s(%s, %%s) = 'TRUE'" % (lookup_info, table_prefix + field_name)
# Handling 'isnull' lookup type
if lookup_type == 'isnull':
return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or ''))
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
ASGML = 'SDO_UTIL.TO_GMLGEOMETRY'
UNION = 'SDO_AGGR_UNION'
TRANSFORM = 'SDO_CS.TRANSFORM'
# Want to get SDO Geometries as WKT (much easier to instantiate GEOS proxies
# from WKT than SDO_GEOMETRY(...) strings ;)
GEOM_SELECT = 'SDO_UTIL.TO_WKTGEOMETRY(%s)'

View File

@ -26,8 +26,8 @@ class GeometryColumns(models.Model):
db_table = 'geometry_columns'
@classmethod
def table_name(self):
"Class method for returning the table name field for this model."
def table_name_col(self):
"Class method for returning the table name column for this model."
return 'f_table_name'
def __unicode__(self):

View File

@ -105,8 +105,8 @@ MISC_TERMS = ['isnull']
# These are the PostGIS-customized QUERY_TERMS -- a list of the lookup types
# allowed for geographic queries.
POSTGIS_TERMS = list(POSTGIS_OPERATORS.keys()) # Getting the operators first
POSTGIS_TERMS += list(POSTGIS_GEOMETRY_FUNCTIONS.keys()) # Adding on the Geometry Functions
POSTGIS_TERMS = POSTGIS_OPERATORS.keys() # Getting the operators first
POSTGIS_TERMS += POSTGIS_GEOMETRY_FUNCTIONS.keys() # Adding on the Geometry Functions
POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull')
POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable

View File

@ -6,7 +6,7 @@ from django.db.models.fields import FieldDoesNotExist
from django.utils.datastructures import SortedDict
from django.contrib.gis.db.models.fields import GeometryField
# parse_lookup depends on the spatial database backend.
from django.contrib.gis.db.backend import parse_lookup, ASGML, ASKML, GEOM_SELECT, TRANSFORM, UNION
from django.contrib.gis.db.backend import parse_lookup, ASGML, ASKML, GEOM_SELECT, SPATIAL_BACKEND, TRANSFORM, UNION
from django.contrib.gis.geos import GEOSGeometry
class GeoQ(Q):
@ -51,7 +51,7 @@ class GeoQuerySet(QuerySet):
clone._filters = clone._filters & reduce(operator.and_, map(mapper, args))
return clone
def _get_sql_clause(self):
def _get_sql_clause(self, get_full_query=False):
qn = connection.ops.quote_name
opts = self.model._meta
@ -147,12 +147,61 @@ class GeoQuerySet(QuerySet):
sql.append("ORDER BY " + ", ".join(order_by))
# LIMIT and OFFSET clauses
if SPATIAL_BACKEND != 'oracle':
if self._limit is not None:
sql.append("%s " % connection.ops.limit_offset_sql(self._limit, self._offset))
else:
assert self._offset is None, "'offset' is not allowed without 'limit'"
return select, " ".join(sql), params
else:
# To support limits and offsets, Oracle requires some funky rewriting of an otherwise normal looking query.
select_clause = ",".join(select)
distinct = (self._distinct and "DISTINCT " or "")
if order_by:
order_by_clause = " OVER (ORDER BY %s )" % (", ".join(order_by))
else:
#Oracle's row_number() function always requires an order-by clause.
#So we need to define a default order-by, since none was provided.
order_by_clause = " OVER (ORDER BY %s.%s)" % \
(qn(opts.db_table), qn(opts.fields[0].db_column or opts.fields[0].column))
# limit_and_offset_clause
if self._limit is None:
assert self._offset is None, "'offset' is not allowed without 'limit'"
if self._offset is not None:
offset = int(self._offset)
else:
offset = 0
if self._limit is not None:
limit = int(self._limit)
else:
limit = None
limit_and_offset_clause = ''
if limit is not None:
limit_and_offset_clause = "WHERE rn > %s AND rn <= %s" % (offset, limit+offset)
elif offset:
limit_and_offset_clause = "WHERE rn > %s" % (offset)
if len(limit_and_offset_clause) > 0:
fmt = \
"""SELECT * FROM
(SELECT %s%s,
ROW_NUMBER()%s AS rn
%s)
%s"""
full_query = fmt % (distinct, select_clause,
order_by_clause, ' '.join(sql).strip(),
limit_and_offset_clause)
else:
full_query = None
if get_full_query:
return select, " ".join(sql), params, full_query
else:
return select, " ".join(sql), params
def _clone(self, klass=None, **kwargs):
c = super(GeoQuerySet, self)._clone(klass, **kwargs)
@ -192,8 +241,13 @@ class GeoQuerySet(QuerySet):
if not field_col:
raise TypeError('GML output only available on GeometryFields')
# Adding AsGML function call to SELECT part of the SQL.
return self.extra(select={'gml':'%s(%s,%s,%s)' % (ASGML, field_col, precision, version)})
if SPATIAL_BACKEND == 'oracle':
gml_select = {'gml':'%s(%s)' % (ASGML, field_col)}
else:
gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, field_col, precision, version)}
# Adding GML function call to SELECT part of the SQL.
return self.extra(select=gml_select)
def kml(self, field_name, precision=8):
"""
@ -227,15 +281,19 @@ class GeoQuerySet(QuerySet):
# Setting the key for the field's column with the custom SELECT SQL to
# override the geometry column returned from the database.
self._custom_select[field.column] = \
'(%s(%s, %s)) AS %s' % (TRANSFORM, col, srid,
connection.ops.quote_name(field.column))
if SPATIAL_BACKEND == 'oracle':
custom_sel = '%s(%s, %s)' % (TRANSFORM, col, srid)
else:
custom_sel = '(%s(%s, %s)) AS %s' % \
(TRANSFORM, col, srid, connection.ops.quote_name(field.column))
self._custom_select[field.column] = custom_sel
return self._clone()
def union(self, field_name):
def union(self, field_name, tolerance=0.0005):
"""
Performs an aggregate union on the given geometry field. Returns
None if the GeoQuerySet is empty.
None if the GeoQuerySet is empty. The `tolerance` keyword is for
Oracle backends only.
"""
# Making sure backend supports the Union stored procedure
if not UNION:
@ -254,11 +312,24 @@ class GeoQuerySet(QuerySet):
# Replacing the select with a call to the ST_Union stored procedure
# on the geographic field column.
if SPATIAL_BACKEND == 'oracle':
union_sql = 'SELECT %s' % self._geo_fmt
union_sql = union_sql % ('%s(SDOAGGRTYPE(%s,%s))' % (UNION, field_col, tolerance))
union_sql += sql
else:
union_sql = ('SELECT %s(%s)' % (UNION, field_col)) + sql
# Getting a cursor, executing the query.
cursor = connection.cursor()
cursor.execute(union_sql, params)
# Pulling the HEXEWKB from the returned cursor.
hex = cursor.fetchone()[0]
if hex: return GEOSGeometry(hex)
if SPATIAL_BACKEND == 'oracle':
# On Oracle have to read out WKT from CLOB first.
clob = cursor.fetchone()[0]
if clob: u = clob.read()
else: u = None
else:
u = cursor.fetchone()[0]
if u: return GEOSGeometry(u)
else: return None

View File

@ -29,12 +29,25 @@
http://zcologia.com/news/429/geometries-for-python-update/
"""
from django.contrib.gis.geos.base import GEOSGeometry
from django.contrib.gis.geos.base import GEOSGeometry, wkt_regex, hex_regex
from django.contrib.gis.geos.geometries import Point, LineString, LinearRing, Polygon, HAS_NUMPY
from django.contrib.gis.geos.collections import GeometryCollection, MultiPoint, MultiLineString, MultiPolygon
from django.contrib.gis.geos.error import GEOSException, GEOSGeometryIndexError
from django.contrib.gis.geos.libgeos import geos_version
def fromfile(file_name):
"""
Given a string file name, returns a GEOSGeometry. The file may contain WKB,
WKT, or HEX.
"""
fh = open(file_name, 'rb')
buf = fh.read()
fh.close()
if wkt_regex.match(buf) or hex_regex.match(buf):
return GEOSGeometry(buf)
else:
return GEOSGeometry(buffer(buf))
def fromstr(wkt_or_hex, **kwargs):
"Given a string value (wkt or hex), returns a GEOSGeometry object."
return GEOSGeometry(wkt_or_hex, **kwargs)

View File

@ -120,5 +120,7 @@ class SpatialRefSysMixin(object):
# The SpatialRefSys and GeometryColumns models
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
from django.contrib.gis.db.backend.postgis.models import GeometryColumns, SpatialRefSys
elif settings.DATABASE_ENGINE == 'oracle':
from django.contrib.gis.db.backend.oracle.models import GeometryColumns, SpatialRefSys
else:
raise NotImplementedError('No SpatialRefSys or GeometryColumns models for backend: %s' % settings.DATABASE_ENGINE)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,12 +1,33 @@
import unittest
import os, unittest
from models import Country, City, State, Feature
from django.contrib.gis.geos import *
from django.contrib.gis import gdal
from django.contrib.gis.geos import *
from django.contrib.gis.tests.utils import no_oracle, no_postgis, oracle, postgis
class GeoModelTest(unittest.TestCase):
def test01_initial_sql(self):
"Testing geographic initial SQL."
if oracle:
# Oracle doesn't allow strings longer than 4000 characters
# in SQL files, and I'm stumped on how to use Oracle BFILE's
# in PLSQL, so we set up the larger geometries manually, rather
# than relying on the initial SQL.
# Routine for returning the path to the data files.
data_dir = os.path.join(os.path.dirname(__file__), 'sql')
def get_file(wkt_file):
return os.path.join(data_dir, wkt_file)
co = State(name='Colorado', poly=fromfile(get_file('co.wkt')))
co.save()
ks = State(name='Kansas', poly=fromfile(get_file('ks.wkt')))
ks.save()
tx = Country(name='Texas', mpoly=fromfile(get_file('tx.wkt')))
tx.save()
nz = Country(name='New Zealand', mpoly=fromfile(get_file('nz.wkt')))
nz.save()
# Ensuring that data was loaded from initial SQL.
self.assertEqual(2, Country.objects.count())
self.assertEqual(8, City.objects.count())
@ -80,6 +101,7 @@ class GeoModelTest(unittest.TestCase):
self.assertEqual(ply, State.objects.get(name='NullState').poly)
nullstate.delete()
@no_oracle # Oracle does not support KML.
def test03a_kml(self):
"Testing KML output from the database using GeoManager.kml()."
# Should throw a TypeError when trying to obtain KML from a
@ -98,6 +120,12 @@ class GeoModelTest(unittest.TestCase):
qs = City.objects.all()
self.assertRaises(TypeError, qs.gml, 'name')
ptown = City.objects.gml('point', precision=9).get(name='Pueblo')
if oracle:
# No precision parameter for Oracle :-/
import re
gml_regex = re.compile(r'<gml:Point srsName="SDO:4326" xmlns:gml="http://www.opengis.net/gml"><gml:coordinates decimal="\." cs="," ts=" ">-104.60925199\d+,38.25500\d+ </gml:coordinates></gml:Point>')
self.assertEqual(True, bool(gml_regex.match(ptown.gml)))
else:
self.assertEqual('<gml:Point srsName="EPSG:4326"><gml:coordinates>-104.609252,38.255001</gml:coordinates></gml:Point>', ptown.gml)
def test04_transform(self):
@ -107,14 +135,15 @@ class GeoModelTest(unittest.TestCase):
ptown = fromstr('POINT(992363.390841912 481455.395105533)', srid=2774)
# Asserting the result of the transform operation with the values in
# the pre-transformed points.
# the pre-transformed points. Oracle does not have the 3084 SRID.
if not oracle:
h = City.objects.transform('point', srid=htown.srid).get(name='Houston')
self.assertAlmostEqual(htown.x, h.point.x, 8)
self.assertAlmostEqual(htown.y, h.point.y, 8)
p = City.objects.transform('point', srid=ptown.srid).get(name='Pueblo')
self.assertAlmostEqual(ptown.x, p.point.x, 8)
self.assertAlmostEqual(ptown.y, p.point.y, 8)
self.assertAlmostEqual(ptown.x, p.point.x, 7)
self.assertAlmostEqual(ptown.y, p.point.y, 7)
def test10_contains_contained(self):
"Testing the 'contained', 'contains', and 'bbcontains' lookup types."
@ -124,6 +153,7 @@ class GeoModelTest(unittest.TestCase):
# Seeing what cities are in Texas, should get Houston and Dallas,
# and Oklahoma City because 'contained' only checks on the
# _bounding box_ of the Geometries.
if not oracle:
qs = City.objects.filter(point__contained=texas.mpoly)
self.assertEqual(3, qs.count())
cities = ['Houston', 'Dallas', 'Oklahoma City']
@ -151,19 +181,36 @@ class GeoModelTest(unittest.TestCase):
self.assertEqual(0, len(Country.objects.filter(mpoly__contains=okcity.point.wkt))) # Qeury w/WKT
# OK City is contained w/in bounding box of Texas.
if not oracle:
qs = Country.objects.filter(mpoly__bbcontains=okcity.point)
self.assertEqual(1, len(qs))
self.assertEqual('Texas', qs[0].name)
def test11_lookup_insert_transform(self):
"Testing automatic transform for lookups and inserts."
# San Antonio in 'WGS84' (SRID 4326) and 'NAD83(HARN) / Texas Centric Lambert Conformal' (SRID 3084)
# San Antonio in 'WGS84' (SRID 4326)
sa_4326 = 'POINT (-98.493183 29.424170)'
sa_3084 = 'POINT (1645978.362408288754523 6276356.025927528738976)' # Used ogr.py in gdal 1.4.1 for this transform
# Constructing & querying with a point from a different SRID
wgs_pnt = fromstr(sa_4326, srid=4326) # Our reference point in WGS84
nad_pnt = fromstr(sa_3084, srid=3084)
# Oracle doesn't have SRID 3084, using 41157.
if oracle:
# San Antonio in 'Texas 4205, Southern Zone (1983, meters)' (SRID 41157)
# Used the following Oracle SQL to get this value:
# SELECT SDO_UTIL.TO_WKTGEOMETRY(SDO_CS.TRANSFORM(SDO_GEOMETRY('POINT (-98.493183 29.424170)', 4326), 41157)) FROM DUAL;
nad_wkt = 'POINT (300662.034646583 5416427.45974934)'
nad_srid = 41157
else:
# San Antonio in 'NAD83(HARN) / Texas Centric Lambert Conformal' (SRID 3084)
nad_wkt = 'POINT (1645978.362408288754523 6276356.025927528738976)' # Used ogr.py in gdal 1.4.1 for this transform
nad_srid = 3084
# Constructing & querying with a point from a different SRID. Oracle
# `SDO_OVERLAPBDYINTERSECT` operates differently from
# `ST_Intersects`, so contains is used instead.
nad_pnt = fromstr(nad_wkt, srid=nad_srid)
if oracle:
tx = Country.objects.get(mpoly__contains=nad_pnt)
else:
tx = Country.objects.get(mpoly__intersects=nad_pnt)
self.assertEqual('Texas', tx.name)
@ -177,7 +224,7 @@ class GeoModelTest(unittest.TestCase):
self.assertAlmostEqual(wgs_pnt.y, sa.point.y, 6)
def test12_null_geometries(self):
"Testing NULL geometry support."
"Testing NULL geometry support, and the `isnull` lookup type."
# Querying for both NULL and Non-NULL values.
nullqs = State.objects.filter(poly__isnull=True)
validqs = State.objects.filter(poly__isnull=False)
@ -193,9 +240,12 @@ class GeoModelTest(unittest.TestCase):
self.assertEqual(True, 'Kansas' in state_names)
# Saving another commonwealth w/a NULL geometry.
if not oracle:
# TODO: Fix saving w/NULL geometry on Oracle.
nmi = State(name='Northern Mariana Islands', poly=None)
nmi.save()
@no_oracle # No specific `left` or `right` operators in Oracle.
def test13_left_right(self):
"Testing the 'left' and 'right' lookup types."
# Left: A << B => true if xmax(A) < xmin(B)
@ -240,6 +290,7 @@ class GeoModelTest(unittest.TestCase):
c3 = City.objects.get(point__equals=pnt)
for c in [c1, c2, c3]: self.assertEqual('Houston', c.name)
@no_oracle # Oracle SDO_RELATE() uses a different system.
def test15_relate(self):
"Testing the 'relate' lookup type."
# To make things more interesting, we will have our Texas reference point in

View File

@ -1,8 +1,9 @@
import unittest
from django.contrib.gis.models import SpatialRefSys
from django.contrib.gis.tests.utils import oracle, postgis
test_srs = ({'srid' : 4326,
'auth_name' : 'EPSG',
'auth_name' : ('EPSG', True),
'auth_srid' : 4326,
'srtext' : 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],TOWGS84[0,0,0,0,0,0,0],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]',
'proj4' : '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs ',
@ -12,7 +13,7 @@ test_srs = ({'srid' : 4326,
'eprec' : (1, 1, 9),
},
{'srid' : 32140,
'auth_name' : 'EPSG',
'auth_name' : ('EPSG', False),
'auth_srid' : 32140,
'srtext' : 'PROJCS["NAD83 / Texas South Central",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",30.28333333333333],PARAMETER["standard_parallel_2",28.38333333333333],PARAMETER["latitude_of_origin",27.83333333333333],PARAMETER["central_meridian",-99],PARAMETER["false_easting",600000],PARAMETER["false_northing",4000000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AUTHORITY["EPSG","32140"]]',
'proj4' : '+proj=lcc +lat_1=30.28333333333333 +lat_2=28.38333333333333 +lat_0=27.83333333333333 +lon_0=-99 +x_0=600000 +y_0=4000000 +ellps=GRS80 +datum=NAD83 +units=m +no_defs ',
@ -30,21 +31,32 @@ class SpatialRefSysTest(unittest.TestCase):
for sd in test_srs:
srs = SpatialRefSys.objects.get(srid=sd['srid'])
self.assertEqual(sd['srid'], srs.srid)
self.assertEqual(sd['auth_name'], srs.auth_name)
# Some of the authority names are borked on Oracle, e.g., SRID=32140.
# also, Oracle Spatial seems to add extraneous info to fields, hence the
# the testing with the 'startswith' flag.
auth_name, oracle_flag = sd['auth_name']
if postgis or (oracle and oracle_flag):
self.assertEqual(True, srs.auth_name.startswith(auth_name))
self.assertEqual(sd['auth_srid'], srs.auth_srid)
self.assertEqual(sd['srtext'], srs.srtext)
# No proj.4 and different srtext on oracle backends :(
if postgis:
self.assertEqual(sd['srtext'], srs.wkt)
self.assertEqual(sd['proj4'], srs.proj4text)
def test02_osr(self):
"Testing getting OSR objects from SpatialRefSys model objects."
for sd in test_srs:
sr = SpatialRefSys.objects.get(srid=sd['srid'])
self.assertEqual(sd['spheroid'], sr.spheroid)
self.assertEqual(True, sr.spheroid.startswith(sd['spheroid']))
self.assertEqual(sd['geographic'], sr.geographic)
self.assertEqual(sd['projected'], sr.projected)
self.assertEqual(sd['name'], sr.name)
self.assertEqual(True, sr.name.startswith(sd['name']))
# Testing the SpatialReference object directly.
if postgis:
srs = sr.srs
self.assertEqual(sd['proj4'], srs.proj4)
self.assertEqual(sd['srtext'], srs.wkt)

View File

@ -0,0 +1,20 @@
from django.conf import settings
# function that will pass a test.
def pass_test(*args): return
def no_backend(test_func, backend):
"Use this decorator to disable test on specified backend."
if settings.DATABASE_ENGINE == backend:
return pass_test
else:
return test_func
# Decorators to disable entire test functions for specific
# spatial backends.
def no_oracle(func): return no_backend(func, 'oracle')
def no_postgis(func): return no_backend(func, 'postgresql_psycopg2')
# Shortcut booleans to omit only portions of tests.
oracle = settings.DATABASE_ENGINE == 'oracle'
postgis = settings.DATABASE_ENGINE == 'postgresql_psycopg2'

View File

@ -15,3 +15,4 @@ try:
HAS_GEOIP = True
except:
HAS_GEOIP = False

View File

@ -99,6 +99,7 @@ Example:
"""
from types import StringType, TupleType
from datetime import datetime
from django.contrib.gis.db.backend import SPATIAL_BACKEND
from django.contrib.gis.gdal import \
OGRGeometry, OGRGeomType, SpatialReference, CoordTransform, \
DataSource, OGRException
@ -176,19 +177,19 @@ def check_feature(feat, model_fields, mapping):
elif model_field[:-3] in model_fields: #foreign key
model_type = model_fields[model_field[:-3]]
else:
raise Exception, 'Given mapping field "%s" not in given Model fields!' % model_field
raise Exception('Given mapping field "%s" not in given Model fields!' % model_field)
### Handling if we get a geometry in the Field ###
if ogr_field in ogc_types:
# At this time, no more than one geographic field per model =(
if HAS_GEO:
raise Exception, 'More than one geographic field in mapping not allowed (yet).'
raise Exception('More than one geographic field in mapping not allowed (yet).')
else:
HAS_GEO = ogr_field
# Making sure this geometry field type is a valid Django GIS field.
if not model_type in gis_fields:
raise Exception, 'Unknown Django GIS field type "%s"' % model_type
raise Exception('Unknown Django GIS field type "%s"' % model_type)
# Getting the OGRGeometry, it's type (an integer) and it's name (a string)
geom = feat.geom
@ -202,20 +203,20 @@ def check_feature(feat, model_fields, mapping):
# The geometry type otherwise was expected
pass
else:
raise Exception, 'Invalid mapping geometry; model has %s, feature has %s' % (model_type, gtype)
raise Exception('Invalid mapping geometry; model has %s, feature has %s' % (model_type, gtype))
## Handling other fields
else:
# Making sure the model field is
if not model_type in field_types:
raise Exception, 'Django field type "%s" has no OGR mapping (yet).' % model_type
raise Exception('Django field type "%s" has no OGR mapping (yet).' % model_type)
# Otherwise, we've got an OGR Field. Making sure that an
# index exists for the mapping OGR field.
try:
fi = feat.index(ogr_field)
except:
raise Exception, 'Given mapping OGR field "%s" not in given OGR layer feature!' % ogr_field
raise Exception('Given mapping OGR field "%s" not in given OGR layer feature!' % ogr_field)
def check_layer(layer, fields, mapping):
"Checks the OGR layer by incrementing through and checking each feature."
@ -234,7 +235,7 @@ def check_srs(layer, source_srs):
else:
sr = layer.srs
if not sr:
raise Exception, 'No source reference system defined.'
raise Exception('No source reference system defined.')
else:
return sr
@ -280,9 +281,12 @@ class LayerMapping:
# Getting the GeometryColumn object.
try:
geo_col = GeometryColumns.objects.get(f_table_name=self.model._meta.db_table)
db_table = self.model._meta.db_table
if SPATIAL_BACKEND == 'oracle': db_table = db_table.upper()
gc_kwargs = {GeometryColumns.table_name_col() : db_table}
geo_col = GeometryColumns.objects.get(**gc_kwargs)
except:
raise Exception, 'Geometry column does not exist. (did you run syncdb?)'
raise Exception('Geometry column does not exist. (did you run syncdb?)')
# Getting the coordinate system needed for transformation (with CoordTransform)
try:
@ -292,7 +296,7 @@ class LayerMapping:
# Creating the CoordTransform object
ct = CoordTransform(self.source_srs, target_srs)
except Exception, msg:
raise Exception, 'Could not translate between the data source and model geometry: %s' % msg
raise Exception('Could not translate between the data source and model geometry: %s' % msg)
for feat in self.layer:
# The keyword arguments for model construction

View File

@ -445,14 +445,24 @@ class FormatStylePlaceholderCursor(Database.Cursor):
charset = 'utf-8'
def _format_params(self, params):
sz_kwargs = {}
if isinstance(params, dict):
result = {}
charset = self.charset
for key, value in params.items():
result[smart_str(key, charset)] = smart_str(value, charset)
return result
if hasattr(value, 'oracle_type'): sz_kwargs[key] = value.oracle_type()
else:
return tuple([smart_str(p, self.charset, True) for p in params])
result = {}
for i in xrange(len(params)):
key = 'arg%d' % i
result[key] = smart_str(params[i], self.charset, True)
if hasattr(params[i], 'oracle_type'): sz_kwargs[key] = params[i].oracle_type()
# If any of the parameters had an `oracle_type` method, then we set
# the inputsizes for those parameters using the returned type
if sz_kwargs: self.setinputsizes(**sz_kwargs)
return result
def execute(self, query, params=None):
if params is None:

View File

@ -228,10 +228,11 @@ class Model(object):
# If it does already exist, do an UPDATE.
if cursor.fetchone():
db_values = [f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, False)) for f in non_pks]
placeholders = [f.get_placeholder(raw and getattr(self, f.attname) or f.pre_save(self, False)) for f in non_pks]
if db_values:
cursor.execute("UPDATE %s SET %s WHERE %s=%%s" % \
(qn(self._meta.db_table),
','.join(['%s=%%s' % qn(f.column) for f in non_pks]),
','.join(['%s=%s' % (qn(f.column), placeholders[i]) for i, f in enumerate(non_pks)]),
qn(self._meta.pk.column)),
db_values + self._meta.pk.get_db_prep_lookup('exact', pk_val))
else: