Commit fcf494b4 authored by Jani Tiainen's avatar Jani Tiainen Committed by Tim Graham
Browse files

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

parent fe3fc521
Loading
Loading
Loading
Loading
+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
+24 −1
Original line number Diff line number Diff line
@@ -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()

+45 −26
Original line number Diff line number Diff line
@@ -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,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')
@@ -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:
@@ -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
@@ -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
+3 −0
Original line number Diff line number Diff line
@@ -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
+8 −0
Original line number Diff line number Diff line
@@ -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