mirror of https://github.com/django/django.git
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 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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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>'
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue