Fixed #24688 -- Added Oracle support for new-style GIS functions.

This commit is contained in:
Jani Tiainen 2015-09-20 13:41:25 +03:00 committed by Tim Graham
parent fe3fc5210f
commit fcf494b48f
6 changed files with 128 additions and 28 deletions

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -61,6 +61,14 @@ class MultiFields(NamedModel):
point = models.PointField()
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']

View File

@ -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>'
)