summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJani Tiainen <jani.tiainen@tintti.net>2015-09-20 13:41:25 +0300
committerTim Graham <timograham@gmail.com>2015-09-21 13:52:14 -0400
commitfcf494b48fea7c0c55ea29721ba0b2d250351ff8 (patch)
treede5e53ea73a26e772aa5ddbc263248562d000eb9
parentfe3fc5210f0bb334a679ed420152af1c862c0239 (diff)
Fixed #24688 -- Added Oracle support for new-style GIS functions.
-rw-r--r--django/contrib/gis/db/backends/oracle/adapter.py47
-rw-r--r--django/contrib/gis/db/backends/oracle/operations.py25
-rw-r--r--django/contrib/gis/db/models/functions.py71
-rw-r--r--django/contrib/gis/db/models/sql/conversion.py3
-rw-r--r--tests/gis_tests/geoapp/models.py8
-rw-r--r--tests/gis_tests/geoapp/tests.py2
6 files changed, 128 insertions, 28 deletions
diff --git a/django/contrib/gis/db/backends/oracle/adapter.py b/django/contrib/gis/db/backends/oracle/adapter.py
index 60961af817..11eb0424aa 100644
--- a/django/contrib/gis/db/backends/oracle/adapter.py
+++ b/django/contrib/gis/db/backends/oracle/adapter.py
@@ -1,7 +1,54 @@
from cx_Oracle import CLOB
from django.contrib.gis.db.backends.base.adapter import WKTAdapter
+from django.contrib.gis.geos import GeometryCollection, Polygon
+from django.utils.six.moves import range
class OracleSpatialAdapter(WKTAdapter):
input_size = CLOB
+
+ def __init__(self, geom):
+ """
+ Oracle requires that polygon rings are in proper orientation. This
+ affects spatial operations and an invalid orientation may cause
+ failures. Correct orientations are:
+ * Outer ring - counter clockwise
+ * Inner ring(s) - clockwise
+ """
+ if isinstance(geom, Polygon):
+ self._fix_polygon(geom)
+ elif isinstance(geom, GeometryCollection):
+ self._fix_geometry_collection(geom)
+
+ self.wkt = geom.wkt
+ self.srid = geom.srid
+
+ def _fix_polygon(self, poly):
+ # Fix single polygon orientation as described in __init__()
+ if self._isClockwise(poly.exterior_ring):
+ poly.exterior_ring = list(reversed(poly.exterior_ring))
+
+ for i in range(1, len(poly)):
+ if not self._isClockwise(poly[i]):
+ poly[i] = list(reversed(poly[i]))
+
+ return poly
+
+ def _fix_geometry_collection(self, coll):
+ # Fix polygon orientations in geometry collections as described in
+ # __init__()
+ for i, geom in enumerate(coll):
+ if isinstance(geom, Polygon):
+ coll[i] = self._fix_polygon(geom)
+
+ def _isClockwise(self, coords):
+ # A modified shoelace algorithm to determine polygon orientation.
+ # See https://en.wikipedia.org/wiki/Shoelace_formula
+ n = len(coords)
+ area = 0.0
+ for i in range(n):
+ j = (i + 1) % n
+ area += coords[i][0] * coords[j][1]
+ area -= coords[j][0] * coords[i][1]
+ return area < 0.0
diff --git a/django/contrib/gis/db/backends/oracle/operations.py b/django/contrib/gis/db/backends/oracle/operations.py
index 8709202cd2..30e87e63a1 100644
--- a/django/contrib/gis/db/backends/oracle/operations.py
+++ b/django/contrib/gis/db/backends/oracle/operations.py
@@ -70,7 +70,6 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations):
extent = 'SDO_AGGR_MBR'
intersection = 'SDO_GEOM.SDO_INTERSECTION'
length = 'SDO_GEOM.SDO_LENGTH'
- num_geom = 'SDO_UTIL.GETNUMELEM'
num_points = 'SDO_UTIL.GETNUMVERTICES'
perimeter = length
point_on_surface = 'SDO_GEOM.SDO_POINTONSURFACE'
@@ -80,6 +79,23 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations):
union = 'SDO_GEOM.SDO_UNION'
unionagg = 'SDO_AGGR_UNION'
+ function_names = {
+ 'Area': 'SDO_GEOM.SDO_AREA',
+ 'Centroid': 'SDO_GEOM.SDO_CENTROID',
+ 'Difference': 'SDO_GEOM.SDO_DIFFERENCE',
+ 'Distance': 'SDO_GEOM.SDO_DISTANCE',
+ 'Intersection': 'SDO_GEOM.SDO_INTERSECTION',
+ 'Length': 'SDO_GEOM.SDO_LENGTH',
+ 'NumGeometries': 'SDO_UTIL.GETNUMELEM',
+ 'NumPoints': 'SDO_UTIL.GETNUMVERTICES',
+ 'Perimeter': 'SDO_GEOM.SDO_LENGTH',
+ 'PointOnSurface': 'SDO_GEOM.SDO_POINTONSURFACE',
+ 'Reverse': 'SDO_UTIL.REVERSE_LINESTRING',
+ 'SymDifference': 'SDO_GEOM.SDO_XOR',
+ 'Transform': 'SDO_CS.TRANSFORM',
+ 'Union': 'SDO_GEOM.SDO_UNION',
+ }
+
# We want to get SDO Geometries as WKT because it is much easier to
# instantiate GEOS proxies from WKT than SDO_GEOMETRY(...) strings.
# However, this adversely affects performance (i.e., Java is called
@@ -109,6 +125,13 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations):
truncate_params = {'relate': None}
+ unsupported_functions = {
+ 'AsGeoHash', 'AsGeoJSON', 'AsGML', 'AsKML', 'AsSVG',
+ 'BoundingCircle', 'Envelope',
+ 'ForceRHR', 'MemSize', 'Scale',
+ 'SnapToGrid', 'Translate', 'GeoHash',
+ }
+
def geo_quote_name(self, name):
return super(OracleOperations, self).geo_quote_name(name).upper()
diff --git a/django/contrib/gis/db/models/functions.py b/django/contrib/gis/db/models/functions.py
index 792edac929..55324a8eaa 100644
--- a/django/contrib/gis/db/models/functions.py
+++ b/django/contrib/gis/db/models/functions.py
@@ -85,6 +85,9 @@ class GeomValue(Value):
def as_sqlite(self, compiler, connection):
return 'GeomFromText(%%s, %s)' % self.srid, [connection.ops.Adapter(self.value)]
+ def as_oracle(self, compiler, connection):
+ return 'SDO_GEOMETRY(%%s, %s)' % self.srid, [connection.ops.Adapter(self.value)]
+
class GeoFuncWithGeoParam(GeoFunc):
def __init__(self, expression, geom, *expressions, **extra):
@@ -112,27 +115,38 @@ class SQLiteDecimalToFloatMixin(object):
return super(SQLiteDecimalToFloatMixin, self).as_sql(compiler, connection)
-class Area(GeoFunc):
+class OracleToleranceMixin(object):
+ tolerance = 0.05
+
+ def as_oracle(self, compiler, connection):
+ tol = self.extra.get('tolerance', self.tolerance)
+ self.template = "%%(function)s(%%(expressions)s, %s)" % tol
+ return super(OracleToleranceMixin, self).as_sql(compiler, connection)
+
+
+class Area(OracleToleranceMixin, GeoFunc):
def as_sql(self, compiler, connection):
- if connection.ops.oracle:
- self.output_field = AreaField('sq_m') # Oracle returns area in units of meters.
- else:
- if connection.ops.geography:
- # Geography fields support area calculation, returns square meters.
- self.output_field = AreaField('sq_m')
- elif not self.output_field.geodetic(connection):
- # Getting the area units of the geographic field.
- units = self.output_field.units_name(connection)
- if units:
- self.output_field = AreaField(
- AreaMeasure.unit_attname(self.output_field.units_name(connection)))
- else:
- self.output_field = FloatField()
+ if connection.ops.geography:
+ # Geography fields support area calculation, returns square meters.
+ self.output_field = AreaField('sq_m')
+ elif not self.output_field.geodetic(connection):
+ # Getting the area units of the geographic field.
+ units = self.output_field.units_name(connection)
+ if units:
+ self.output_field = AreaField(
+ AreaMeasure.unit_attname(self.output_field.units_name(connection))
+ )
else:
- # TODO: Do we want to support raw number areas for geodetic fields?
- raise NotImplementedError('Area on geodetic coordinate systems not supported.')
+ self.output_field = FloatField()
+ else:
+ # TODO: Do we want to support raw number areas for geodetic fields?
+ raise NotImplementedError('Area on geodetic coordinate systems not supported.')
return super(Area, self).as_sql(compiler, connection)
+ def as_oracle(self, compiler, connection):
+ self.output_field = AreaField('sq_m') # Oracle returns area in units of meters.
+ return super(Area, self).as_oracle(compiler, connection)
+
class AsGeoJSON(GeoFunc):
output_field_class = TextField
@@ -189,11 +203,11 @@ class BoundingCircle(GeoFunc):
super(BoundingCircle, self).__init__(*[expression, num_seg], **extra)
-class Centroid(GeoFunc):
+class Centroid(OracleToleranceMixin, GeoFunc):
pass
-class Difference(GeoFuncWithGeoParam):
+class Difference(OracleToleranceMixin, GeoFuncWithGeoParam):
pass
@@ -215,7 +229,7 @@ class DistanceResultMixin(object):
return value
-class Distance(DistanceResultMixin, GeoFuncWithGeoParam):
+class Distance(DistanceResultMixin, OracleToleranceMixin, GeoFuncWithGeoParam):
output_field_class = FloatField
spheroid = None
@@ -246,6 +260,11 @@ class Distance(DistanceResultMixin, GeoFuncWithGeoParam):
self.function = 'ST_Distance_Sphere'
return super(Distance, self).as_sql(compiler, connection)
+ def as_oracle(self, compiler, connection):
+ if self.spheroid:
+ self.source_expressions.pop(2)
+ return super(Distance, self).as_oracle(compiler, connection)
+
class Envelope(GeoFunc):
pass
@@ -265,11 +284,11 @@ class GeoHash(GeoFunc):
super(GeoHash, self).__init__(*expressions, **extra)
-class Intersection(GeoFuncWithGeoParam):
+class Intersection(OracleToleranceMixin, GeoFuncWithGeoParam):
pass
-class Length(DistanceResultMixin, GeoFunc):
+class Length(DistanceResultMixin, OracleToleranceMixin, GeoFunc):
output_field_class = FloatField
def __init__(self, expr1, spheroid=True, **extra):
@@ -325,7 +344,7 @@ class NumPoints(GeoFunc):
return super(NumPoints, self).as_sql(compiler, connection)
-class Perimeter(DistanceResultMixin, GeoFunc):
+class Perimeter(DistanceResultMixin, OracleToleranceMixin, GeoFunc):
output_field_class = FloatField
def as_postgresql(self, compiler, connection):
@@ -335,7 +354,7 @@ class Perimeter(DistanceResultMixin, GeoFunc):
return super(Perimeter, self).as_sql(compiler, connection)
-class PointOnSurface(GeoFunc):
+class PointOnSurface(OracleToleranceMixin, GeoFunc):
pass
@@ -376,7 +395,7 @@ class SnapToGrid(SQLiteDecimalToFloatMixin, GeoFunc):
super(SnapToGrid, self).__init__(*expressions, **extra)
-class SymDifference(GeoFuncWithGeoParam):
+class SymDifference(OracleToleranceMixin, GeoFuncWithGeoParam):
pass
@@ -412,5 +431,5 @@ class Translate(Scale):
return super(Translate, self).as_sqlite(compiler, connection)
-class Union(GeoFuncWithGeoParam):
+class Union(OracleToleranceMixin, GeoFuncWithGeoParam):
pass
diff --git a/django/contrib/gis/db/models/sql/conversion.py b/django/contrib/gis/db/models/sql/conversion.py
index 28e601613e..dbbdb8b338 100644
--- a/django/contrib/gis/db/models/sql/conversion.py
+++ b/django/contrib/gis/db/models/sql/conversion.py
@@ -2,6 +2,7 @@
This module holds simple classes to convert geospatial values from the
database.
"""
+from __future__ import unicode_literals
from django.contrib.gis.db.models.fields import GeoSelectFormatMixin
from django.contrib.gis.geometry.backend import Geometry
@@ -24,6 +25,8 @@ class AreaField(BaseField):
self.area_att = area_att
def from_db_value(self, value, expression, connection, context):
+ if connection.features.interprets_empty_strings_as_nulls and value == '':
+ value = None
if value is not None:
value = Area(**{self.area_att: value})
return value
diff --git a/tests/gis_tests/geoapp/models.py b/tests/gis_tests/geoapp/models.py
index a819c17ada..d6ca4f5010 100644
--- a/tests/gis_tests/geoapp/models.py
+++ b/tests/gis_tests/geoapp/models.py
@@ -62,6 +62,14 @@ class MultiFields(NamedModel):
poly = models.PolygonField()
class Meta:
+ required_db_features = ['gis_enabled']
+
+
+class UniqueTogetherModel(models.Model):
+ city = models.CharField(max_length=30)
+ point = models.PointField()
+
+ class Meta:
unique_together = ('city', 'point')
required_db_features = ['gis_enabled', 'supports_geometry_field_unique_index']
diff --git a/tests/gis_tests/geoapp/tests.py b/tests/gis_tests/geoapp/tests.py
index 64af08aa77..0d6ee0362f 100644
--- a/tests/gis_tests/geoapp/tests.py
+++ b/tests/gis_tests/geoapp/tests.py
@@ -615,7 +615,7 @@ class GeoQuerySetTest(TestCase):
if oracle:
# No precision parameter for Oracle :-/
gml_regex = re.compile(
- r'^<gml:Point srsName="SDO:4326" xmlns:gml="http://www.opengis.net/gml">'
+ r'^<gml:Point srsName="EPSG:4326" xmlns:gml="http://www.opengis.net/gml">'
r'<gml:coordinates decimal="\." cs="," ts=" ">-104.60925\d+,38.25500\d+ '
r'</gml:coordinates></gml:Point>'
)