1"""
2
3This module defines a ``Comparator`` class for use with geometry and geography
4objects. This is where spatial operators, like ``&&``, ``&<``, are defined.
5Spatial operators very often apply to the bounding boxes of geometries. For
6example, ``geom1 && geom2`` indicates if geom1's bounding box intersects
7geom2's.
8
9Examples
10--------
11
12Select the objects whose bounding boxes are to the left of the
13bounding box of ``POLYGON((-5 45,5 45,5 -45,-5 -45,-5 45))``::
14
15    select([table]).where(table.c.geom.to_left(
16        'POLYGON((-5 45,5 45,5 -45,-5 -45,-5 45))'))
17
18The ``<<`` and ``>>`` operators are a bit specific, because they have
19corresponding Python operator (``__lshift__`` and ``__rshift__``). The
20above ``SELECT`` expression can thus be rewritten like this::
21
22    select([table]).where(
23        table.c.geom << 'POLYGON((-5 45,5 45,5 -45,-5 -45,-5 45))')
24
25Operators can also be used when using the ORM. For example::
26
27    Session.query(Cls).filter(
28        Cls.geom << 'POLYGON((-5 45,5 45,5 -45,-5 -45,-5 45))')
29
30Now some other examples with the ``<#>`` operator.
31
32Select the ten objects that are the closest to ``POINT(0 0)`` (typical
33closed neighbors problem)::
34
35    select([table]).order_by(table.c.geom.distance_box('POINT(0 0)')).limit(10)
36
37Using the ORM::
38
39    Session.query(Cls).order_by(Cls.geom.distance_box('POINT(0 0)')).limit(10)
40
41Reference
42---------
43"""
44
45from sqlalchemy import types as sqltypes
46from sqlalchemy.types import UserDefinedType
47from sqlalchemy.dialects.postgresql import DOUBLE_PRECISION
48from sqlalchemy.sql import operators
49try:
50    from sqlalchemy.sql.functions import _FunctionGenerator
51except ImportError:  # SQLA < 0.9  # pragma: no cover
52    from sqlalchemy.sql.expression import _FunctionGenerator
53
54
55INTERSECTS = operators.custom_op('&&')
56INTERSECTS_ND = operators.custom_op('&&&')
57OVERLAPS_OR_TO_LEFT = operators.custom_op('&<')
58OVERLAPS_OR_TO_RIGHT = operators.custom_op('&>')
59OVERLAPS_OR_BELOW = operators.custom_op('&<|')
60TO_LEFT = operators.custom_op('<<')
61BELOW = operators.custom_op('<<|')
62TO_RIGHT = operators.custom_op('>>')
63CONTAINED = operators.custom_op('@')
64OVERLAPS_OR_ABOVE = operators.custom_op('|&>')
65ABOVE = operators.custom_op('|>>')
66CONTAINS = operators.custom_op('~')
67SAME = operators.custom_op('~=')
68DISTANCE_CENTROID = operators.custom_op('<->')
69DISTANCE_BOX = operators.custom_op('<#>')
70
71
72class BaseComparator(UserDefinedType.Comparator):
73    """
74    A custom comparator base class. It adds the ability to call spatial
75    functions on columns that use this kind of comparator. It also defines
76    functions that map to operators supported by ``Geometry``, ``Geography``
77    and ``Raster`` columns.
78
79    This comparator is used by the :class:`geoalchemy2.types.Raster`.
80    """
81
82    key = None
83
84    def __getattr__(self, name):
85
86        # Function names that don't start with "ST_" are rejected.
87        # This is not to mess up with SQLAlchemy's use of
88        # hasattr/getattr on Column objects.
89
90        if not name.lower().startswith('st_'):
91            raise AttributeError
92
93        # We create our own _FunctionGenerator here, and use it in place of
94        # SQLAlchemy's "func" object. This is to be able to "bind" the
95        # function to the SQL expression. See also GenericFunction.
96
97        func_ = _FunctionGenerator(expr=self.expr)
98        return getattr(func_, name)
99
100    def intersects(self, other):
101        """
102        The ``&&`` operator. A's BBOX intersects B's.
103        """
104        return self.operate(INTERSECTS, other, result_type=sqltypes.Boolean)
105
106    def overlaps_or_to_left(self, other):
107        """
108        The ``&<`` operator. A's BBOX overlaps or is to the left of B's.
109        """
110        return self.operate(OVERLAPS_OR_TO_LEFT, other,
111                            result_type=sqltypes.Boolean)
112
113    def overlaps_or_to_right(self, other):
114        """
115        The ``&>`` operator. A's BBOX overlaps or is to the right of B's.
116        """
117        return self.operate(OVERLAPS_OR_TO_RIGHT, other,
118                            result_type=sqltypes.Boolean)
119
120
121class Comparator(BaseComparator):
122    """
123    A custom comparator class. Used in :class:`geoalchemy2.types.Geometry`
124    and :class:`geoalchemy2.types.Geography`.
125
126    This is where spatial operators like ``<<`` and ``<->`` are defined.
127    """
128
129    def overlaps_or_below(self, other):
130        """
131        The ``&<|`` operator. A's BBOX overlaps or is below B's.
132        """
133        return self.operate(OVERLAPS_OR_BELOW, other,
134                            result_type=sqltypes.Boolean)
135
136    def to_left(self, other):
137        """
138        The ``<<`` operator. A's BBOX is strictly to the left of B's.
139        """
140        return self.operate(TO_LEFT, other, result_type=sqltypes.Boolean)
141
142    def __lshift__(self, other):
143        """
144        The ``<<`` operator. A's BBOX is strictly to the left of B's.
145        Same as ``to_left``, so::
146
147            table.c.geom << 'POINT(1 2)'
148
149        is the same as::
150
151            table.c.geom.to_left('POINT(1 2)')
152        """
153        return self.to_left(other)
154
155    def below(self, other):
156        """
157        The ``<<|`` operator. A's BBOX is strictly below B's.
158        """
159        return self.operate(BELOW, other, result_type=sqltypes.Boolean)
160
161    def to_right(self, other):
162        """
163        The ``>>`` operator. A's BBOX is strictly to the right of B's.
164        """
165        return self.operate(TO_RIGHT, other, result_type=sqltypes.Boolean)
166
167    def __rshift__(self, other):
168        """
169        The ``>>`` operator. A's BBOX is strictly to the left of B's.
170        Same as `to_`right``, so::
171
172            table.c.geom >> 'POINT(1 2)'
173
174        is the same as::
175
176            table.c.geom.to_right('POINT(1 2)')
177        """
178        return self.to_right(other)
179
180    def contained(self, other):
181        """
182        The ``@`` operator. A's BBOX is contained by B's.
183        """
184        return self.operate(CONTAINED, other, result_type=sqltypes.Boolean)
185
186    def overlaps_or_above(self, other):
187        """
188        The ``|&>`` operator. A's BBOX overlaps or is above B's.
189        """
190        return self.operate(OVERLAPS_OR_ABOVE, other,
191                            result_type=sqltypes.Boolean)
192
193    def above(self, other):
194        """
195        The ``|>>`` operator. A's BBOX is strictly above B's.
196        """
197        return self.operate(ABOVE, other, result_type=sqltypes.Boolean)
198
199    def contains(self, other, **kw):
200        """
201        The ``~`` operator. A's BBOX contains B's.
202        """
203        return self.operate(CONTAINS, other, result_type=sqltypes.Boolean)
204
205    def same(self, other):
206        """
207        The ``~=`` operator. A's BBOX is the same as B's.
208        """
209        return self.operate(SAME, other, result_type=sqltypes.Boolean)
210
211    def distance_centroid(self, other):
212        """
213        The ``<->`` operator. The distance between two points.
214        """
215        return self.operate(DISTANCE_CENTROID, other,
216                            result_type=DOUBLE_PRECISION)
217
218    def distance_box(self, other):
219        """
220        The ``<#>`` operator. The distance between bounding box of two
221        geometries.
222        """
223        return self.operate(DISTANCE_BOX, other, result_type=DOUBLE_PRECISION)
224
225    def intersects_nd(self, other):
226        """
227        The ``&&&`` operator returns TRUE if the n-D bounding box of geometry A
228        intersects the n-D bounding box of geometry B.
229        """
230        return self.operate(INTERSECTS_ND, other, result_type=sqltypes.Boolean)
231