Fixed #24688 -- Added Oracle support for new-style GIS functions.
This commit is contained in:
parent
fe3fc5210f
commit
fcf494b48f
|
@ -1,7 +1,54 @@
|
||||||
from cx_Oracle import CLOB
|
from cx_Oracle import CLOB
|
||||||
|
|
||||||
from django.contrib.gis.db.backends.base.adapter import WKTAdapter
|
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):
|
class OracleSpatialAdapter(WKTAdapter):
|
||||||
input_size = CLOB
|
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
|
||||||
|
|
|
@ -70,7 +70,6 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations):
|
||||||
extent = 'SDO_AGGR_MBR'
|
extent = 'SDO_AGGR_MBR'
|
||||||
intersection = 'SDO_GEOM.SDO_INTERSECTION'
|
intersection = 'SDO_GEOM.SDO_INTERSECTION'
|
||||||
length = 'SDO_GEOM.SDO_LENGTH'
|
length = 'SDO_GEOM.SDO_LENGTH'
|
||||||
num_geom = 'SDO_UTIL.GETNUMELEM'
|
|
||||||
num_points = 'SDO_UTIL.GETNUMVERTICES'
|
num_points = 'SDO_UTIL.GETNUMVERTICES'
|
||||||
perimeter = length
|
perimeter = length
|
||||||
point_on_surface = 'SDO_GEOM.SDO_POINTONSURFACE'
|
point_on_surface = 'SDO_GEOM.SDO_POINTONSURFACE'
|
||||||
|
@ -80,6 +79,23 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations):
|
||||||
union = 'SDO_GEOM.SDO_UNION'
|
union = 'SDO_GEOM.SDO_UNION'
|
||||||
unionagg = 'SDO_AGGR_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
|
# We want to get SDO Geometries as WKT because it is much easier to
|
||||||
# instantiate GEOS proxies from WKT than SDO_GEOMETRY(...) strings.
|
# instantiate GEOS proxies from WKT than SDO_GEOMETRY(...) strings.
|
||||||
# However, this adversely affects performance (i.e., Java is called
|
# However, this adversely affects performance (i.e., Java is called
|
||||||
|
@ -109,6 +125,13 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations):
|
||||||
|
|
||||||
truncate_params = {'relate': None}
|
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):
|
def geo_quote_name(self, name):
|
||||||
return super(OracleOperations, self).geo_quote_name(name).upper()
|
return super(OracleOperations, self).geo_quote_name(name).upper()
|
||||||
|
|
||||||
|
|
|
@ -85,6 +85,9 @@ class GeomValue(Value):
|
||||||
def as_sqlite(self, compiler, connection):
|
def as_sqlite(self, compiler, connection):
|
||||||
return 'GeomFromText(%%s, %s)' % self.srid, [connection.ops.Adapter(self.value)]
|
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):
|
class GeoFuncWithGeoParam(GeoFunc):
|
||||||
def __init__(self, expression, geom, *expressions, **extra):
|
def __init__(self, expression, geom, *expressions, **extra):
|
||||||
|
@ -112,27 +115,38 @@ class SQLiteDecimalToFloatMixin(object):
|
||||||
return super(SQLiteDecimalToFloatMixin, self).as_sql(compiler, connection)
|
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):
|
def as_sql(self, compiler, connection):
|
||||||
if connection.ops.oracle:
|
if connection.ops.geography:
|
||||||
self.output_field = AreaField('sq_m') # Oracle returns area in units of meters.
|
# Geography fields support area calculation, returns square meters.
|
||||||
else:
|
self.output_field = AreaField('sq_m')
|
||||||
if connection.ops.geography:
|
elif not self.output_field.geodetic(connection):
|
||||||
# Geography fields support area calculation, returns square meters.
|
# Getting the area units of the geographic field.
|
||||||
self.output_field = AreaField('sq_m')
|
units = self.output_field.units_name(connection)
|
||||||
elif not self.output_field.geodetic(connection):
|
if units:
|
||||||
# Getting the area units of the geographic field.
|
self.output_field = AreaField(
|
||||||
units = self.output_field.units_name(connection)
|
AreaMeasure.unit_attname(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()
|
|
||||||
else:
|
else:
|
||||||
# TODO: Do we want to support raw number areas for geodetic fields?
|
self.output_field = FloatField()
|
||||||
raise NotImplementedError('Area on geodetic coordinate systems not supported.')
|
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)
|
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):
|
class AsGeoJSON(GeoFunc):
|
||||||
output_field_class = TextField
|
output_field_class = TextField
|
||||||
|
@ -189,11 +203,11 @@ class BoundingCircle(GeoFunc):
|
||||||
super(BoundingCircle, self).__init__(*[expression, num_seg], **extra)
|
super(BoundingCircle, self).__init__(*[expression, num_seg], **extra)
|
||||||
|
|
||||||
|
|
||||||
class Centroid(GeoFunc):
|
class Centroid(OracleToleranceMixin, GeoFunc):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Difference(GeoFuncWithGeoParam):
|
class Difference(OracleToleranceMixin, GeoFuncWithGeoParam):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -215,7 +229,7 @@ class DistanceResultMixin(object):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class Distance(DistanceResultMixin, GeoFuncWithGeoParam):
|
class Distance(DistanceResultMixin, OracleToleranceMixin, GeoFuncWithGeoParam):
|
||||||
output_field_class = FloatField
|
output_field_class = FloatField
|
||||||
spheroid = None
|
spheroid = None
|
||||||
|
|
||||||
|
@ -246,6 +260,11 @@ class Distance(DistanceResultMixin, GeoFuncWithGeoParam):
|
||||||
self.function = 'ST_Distance_Sphere'
|
self.function = 'ST_Distance_Sphere'
|
||||||
return super(Distance, self).as_sql(compiler, connection)
|
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):
|
class Envelope(GeoFunc):
|
||||||
pass
|
pass
|
||||||
|
@ -265,11 +284,11 @@ class GeoHash(GeoFunc):
|
||||||
super(GeoHash, self).__init__(*expressions, **extra)
|
super(GeoHash, self).__init__(*expressions, **extra)
|
||||||
|
|
||||||
|
|
||||||
class Intersection(GeoFuncWithGeoParam):
|
class Intersection(OracleToleranceMixin, GeoFuncWithGeoParam):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Length(DistanceResultMixin, GeoFunc):
|
class Length(DistanceResultMixin, OracleToleranceMixin, GeoFunc):
|
||||||
output_field_class = FloatField
|
output_field_class = FloatField
|
||||||
|
|
||||||
def __init__(self, expr1, spheroid=True, **extra):
|
def __init__(self, expr1, spheroid=True, **extra):
|
||||||
|
@ -325,7 +344,7 @@ class NumPoints(GeoFunc):
|
||||||
return super(NumPoints, self).as_sql(compiler, connection)
|
return super(NumPoints, self).as_sql(compiler, connection)
|
||||||
|
|
||||||
|
|
||||||
class Perimeter(DistanceResultMixin, GeoFunc):
|
class Perimeter(DistanceResultMixin, OracleToleranceMixin, GeoFunc):
|
||||||
output_field_class = FloatField
|
output_field_class = FloatField
|
||||||
|
|
||||||
def as_postgresql(self, compiler, connection):
|
def as_postgresql(self, compiler, connection):
|
||||||
|
@ -335,7 +354,7 @@ class Perimeter(DistanceResultMixin, GeoFunc):
|
||||||
return super(Perimeter, self).as_sql(compiler, connection)
|
return super(Perimeter, self).as_sql(compiler, connection)
|
||||||
|
|
||||||
|
|
||||||
class PointOnSurface(GeoFunc):
|
class PointOnSurface(OracleToleranceMixin, GeoFunc):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -376,7 +395,7 @@ class SnapToGrid(SQLiteDecimalToFloatMixin, GeoFunc):
|
||||||
super(SnapToGrid, self).__init__(*expressions, **extra)
|
super(SnapToGrid, self).__init__(*expressions, **extra)
|
||||||
|
|
||||||
|
|
||||||
class SymDifference(GeoFuncWithGeoParam):
|
class SymDifference(OracleToleranceMixin, GeoFuncWithGeoParam):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -412,5 +431,5 @@ class Translate(Scale):
|
||||||
return super(Translate, self).as_sqlite(compiler, connection)
|
return super(Translate, self).as_sqlite(compiler, connection)
|
||||||
|
|
||||||
|
|
||||||
class Union(GeoFuncWithGeoParam):
|
class Union(OracleToleranceMixin, GeoFuncWithGeoParam):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
This module holds simple classes to convert geospatial values from the
|
This module holds simple classes to convert geospatial values from the
|
||||||
database.
|
database.
|
||||||
"""
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.contrib.gis.db.models.fields import GeoSelectFormatMixin
|
from django.contrib.gis.db.models.fields import GeoSelectFormatMixin
|
||||||
from django.contrib.gis.geometry.backend import Geometry
|
from django.contrib.gis.geometry.backend import Geometry
|
||||||
|
@ -24,6 +25,8 @@ class AreaField(BaseField):
|
||||||
self.area_att = area_att
|
self.area_att = area_att
|
||||||
|
|
||||||
def from_db_value(self, value, expression, connection, context):
|
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:
|
if value is not None:
|
||||||
value = Area(**{self.area_att: value})
|
value = Area(**{self.area_att: value})
|
||||||
return value
|
return value
|
||||||
|
|
|
@ -61,6 +61,14 @@ class MultiFields(NamedModel):
|
||||||
point = models.PointField()
|
point = models.PointField()
|
||||||
poly = models.PolygonField()
|
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:
|
class Meta:
|
||||||
unique_together = ('city', 'point')
|
unique_together = ('city', 'point')
|
||||||
required_db_features = ['gis_enabled', 'supports_geometry_field_unique_index']
|
required_db_features = ['gis_enabled', 'supports_geometry_field_unique_index']
|
||||||
|
|
|
@ -615,7 +615,7 @@ class GeoQuerySetTest(TestCase):
|
||||||
if oracle:
|
if oracle:
|
||||||
# No precision parameter for Oracle :-/
|
# No precision parameter for Oracle :-/
|
||||||
gml_regex = re.compile(
|
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 decimal="\." cs="," ts=" ">-104.60925\d+,38.25500\d+ '
|
||||||
r'</gml:coordinates></gml:Point>'
|
r'</gml:coordinates></gml:Point>'
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue