Loading django/contrib/gis/db/backends/oracle/adapter.py +47 −0 Original line number Diff line number Diff line 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 django/contrib/gis/db/backends/oracle/operations.py +24 −1 Original line number Diff line number Diff line Loading @@ -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' Loading @@ -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 Loading Loading @@ -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() Loading django/contrib/gis/db/models/functions.py +45 −26 Original line number Diff line number Diff line Loading @@ -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): Loading Loading @@ -112,11 +115,17 @@ 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') Loading @@ -125,7 +134,8 @@ class Area(GeoFunc): units = self.output_field.units_name(connection) if units: self.output_field = AreaField( AreaMeasure.unit_attname(self.output_field.units_name(connection))) AreaMeasure.unit_attname(self.output_field.units_name(connection)) ) else: self.output_field = FloatField() else: Loading @@ -133,6 +143,10 @@ class Area(GeoFunc): 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 Loading Loading @@ -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 Loading @@ -215,7 +229,7 @@ class DistanceResultMixin(object): return value class Distance(DistanceResultMixin, GeoFuncWithGeoParam): class Distance(DistanceResultMixin, OracleToleranceMixin, GeoFuncWithGeoParam): output_field_class = FloatField spheroid = None Loading Loading @@ -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 Loading @@ -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): Loading Loading @@ -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): Loading @@ -335,7 +354,7 @@ class Perimeter(DistanceResultMixin, GeoFunc): return super(Perimeter, self).as_sql(compiler, connection) class PointOnSurface(GeoFunc): class PointOnSurface(OracleToleranceMixin, GeoFunc): pass Loading Loading @@ -376,7 +395,7 @@ class SnapToGrid(SQLiteDecimalToFloatMixin, GeoFunc): super(SnapToGrid, self).__init__(*expressions, **extra) class SymDifference(GeoFuncWithGeoParam): class SymDifference(OracleToleranceMixin, GeoFuncWithGeoParam): pass Loading Loading @@ -412,5 +431,5 @@ class Translate(Scale): return super(Translate, self).as_sqlite(compiler, connection) class Union(GeoFuncWithGeoParam): class Union(OracleToleranceMixin, GeoFuncWithGeoParam): pass django/contrib/gis/db/models/sql/conversion.py +3 −0 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading tests/gis_tests/geoapp/models.py +8 −0 Original line number Diff line number Diff line Loading @@ -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'] Loading Loading
django/contrib/gis/db/backends/oracle/adapter.py +47 −0 Original line number Diff line number Diff line 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
django/contrib/gis/db/backends/oracle/operations.py +24 −1 Original line number Diff line number Diff line Loading @@ -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' Loading @@ -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 Loading Loading @@ -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() Loading
django/contrib/gis/db/models/functions.py +45 −26 Original line number Diff line number Diff line Loading @@ -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): Loading Loading @@ -112,11 +115,17 @@ 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') Loading @@ -125,7 +134,8 @@ class Area(GeoFunc): units = self.output_field.units_name(connection) if units: self.output_field = AreaField( AreaMeasure.unit_attname(self.output_field.units_name(connection))) AreaMeasure.unit_attname(self.output_field.units_name(connection)) ) else: self.output_field = FloatField() else: Loading @@ -133,6 +143,10 @@ class Area(GeoFunc): 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 Loading Loading @@ -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 Loading @@ -215,7 +229,7 @@ class DistanceResultMixin(object): return value class Distance(DistanceResultMixin, GeoFuncWithGeoParam): class Distance(DistanceResultMixin, OracleToleranceMixin, GeoFuncWithGeoParam): output_field_class = FloatField spheroid = None Loading Loading @@ -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 Loading @@ -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): Loading Loading @@ -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): Loading @@ -335,7 +354,7 @@ class Perimeter(DistanceResultMixin, GeoFunc): return super(Perimeter, self).as_sql(compiler, connection) class PointOnSurface(GeoFunc): class PointOnSurface(OracleToleranceMixin, GeoFunc): pass Loading Loading @@ -376,7 +395,7 @@ class SnapToGrid(SQLiteDecimalToFloatMixin, GeoFunc): super(SnapToGrid, self).__init__(*expressions, **extra) class SymDifference(GeoFuncWithGeoParam): class SymDifference(OracleToleranceMixin, GeoFuncWithGeoParam): pass Loading Loading @@ -412,5 +431,5 @@ class Translate(Scale): return super(Translate, self).as_sqlite(compiler, connection) class Union(GeoFuncWithGeoParam): class Union(OracleToleranceMixin, GeoFuncWithGeoParam): pass
django/contrib/gis/db/models/sql/conversion.py +3 −0 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading
tests/gis_tests/geoapp/models.py +8 −0 Original line number Diff line number Diff line Loading @@ -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'] Loading