Issues (77)

osmalchemy/util/online.py (20 issues)

1
## ~*~ coding: utf-8 ~*~
2
#-
3
# OSMAlchemy - OpenStreetMap to SQLAlchemy bridge
4
# Copyright (c) 2016 Dominik George <[email protected]>
5
# Copyright (c) 2016 Eike Tim Jesinghaus <[email protected]>
6
#
7
# Permission is hereby granted, free of charge, to any person obtaining a copy
8
# of this software and associated documentation files (the "Software"), to deal
9
# in the Software without restriction, including without limitation the rights
10
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
# copies of the Software, and to permit persons to whom the Software is
12
# furnished to do so, subject to the following conditions:
13
#
14
# The above copyright notice and this permission notice shall be included in all
15
# copies or substantial portions of the Software.
16
#
17
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
# SOFTWARE.
24
#
25
# Alternatively, you are free to use OSMAlchemy under Simplified BSD, The
26
# MirOS Licence, GPL-2+, LGPL-2.1+, AGPL-3+ or the same terms as Python
27
# itself.
28
29 1
""" Utility code for OSMAlchemy's overpass code. """
30
31 1
import operator
32 1
import overpass
33 1
import re
0 ignored issues
show
standard import "import re" comes before "import overpass"
Loading history...
34 1
from sqlalchemy.sql.elements import BinaryExpression, BooleanClauseList, BindParameter, Grouping
35
from sqlalchemy.sql.annotation import AnnotatedColumn
0 ignored issues
show
The name AnnotatedColumn does not seem to exist in module sqlalchemy.sql.annotation.
Loading history...
36 1
from sqlalchemy.sql.selectable import Exists
37
38
def _generate_overpass_api(endpoint=None):
39
    """ Create and initialise the Overpass API object.
40
41
    Passing the endpoint argument will override the default
42
    endpoint URL.
43
    """
44
45
    # Create API object with default settings
46
    api = overpass.API()
47
48
    # Change endpoint if desired
49
    if not endpoint is None:
50
        api.endpoint = endpoint
51
52 1
    return api
53
54
def _get_single_element_by_id(api, type_, id_, recurse_down=True):
55
    """ Retrieves a single OpenStreetMap element by its id.
56
57
      api - an initialised Overpass API object
58
      type_ - the element type to query, one of node, way or relation
59
      id_ - the id of the element to retrieve
60
      recurse_down - whether to get child nodes of ways and relations
61
    """
62
63
    # Construct query
64
    query = "%s(%d);%s" % (type_, id_, "(._;>;);" if recurse_down else "")
65
66
    # Run query
67
    result = api.Get(query, responseformat="xml")
68
69
    # Return data
70 1
    return result
71
72
def _get_elements_by_query(api, query, recurse_down=True):
73
    """ Runs a query and returns the resulting OSM XML.
74
75
      api - an initialised Overpass API object
76
      query - the OverpassQL query
77
      recurse_down - whether to get child nodes of ways and relations
78
    """
79
80
    # Run query
81
    result = api.Get("%s%s" % (query, "(._;>;);" if recurse_down else ""), responseformat="xml")
82
83
    # Return data
84
    return result
85 1
86
# Define operator to string mapping
87
_OPS = {operator.eq: "==",
88
        operator.ne: "!=",
89
        operator.lt: "<",
90
        operator.gt: ">",
91
        operator.le: "<=",
92
        operator.ge: ">=",
93
        operator.and_: "&&",
94 1
        operator.or_: "||"}
95
96
def _where_to_tree(clause, target):
0 ignored issues
show
Too many return statements (13/6)
Loading history...
97
    """ Transform an SQLAlchemy whereclause to an expression tree.
98
99
    This function analyses a Query.whereclause object and turns it
100
    into a more general data structure.
101
    """
102
103
    if isinstance(clause, BinaryExpression):
104
        # This is something like "latitude >= 51.0"
105
        left = clause.left
106
        right = clause.right
107
        op = clause.operator
0 ignored issues
show
Coding Style Naming introduced by
The name op does not conform to the variable naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
108
109
        # Left part should be a column
110
        if isinstance(left, AnnotatedColumn):
111
            # Get table class and field
112
            model = left._annotations["parentmapper"].class_
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _annotations was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
113
            field = left
114
115
            # Only use if we are looking for this model
116
            if model is target:
117
                # Store field name
118
                left = field.name
119
            else:
120
                return None
121
        else:
122
            # Right now, we cannot cope with anything but a column on the left
123
            return None
124
125
        # Right part should be a literal value
126
        if isinstance(right, BindParameter):
127
            # Extract literal value
128
            right = right.value
129
        else:
130
            # Right now, we cannot cope with something else here
131
            return None
132
133
        # Look for a known operator
134
        if op in _OPS.keys():
135
            # Get string representation
136
            op = _OPS[op]
0 ignored issues
show
Coding Style Naming introduced by
The name op does not conform to the variable naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
137
        else:
138
            # Right now, we cannot cope with other operators
139
            return None
140
141
        # Return polish notation tuple of this clause
142
        return (op, left, right)
143
144
    elif isinstance(clause, BooleanClauseList):
145
        # This is an AND or OR operation
146
        op = clause.operator
0 ignored issues
show
Coding Style Naming introduced by
The name op does not conform to the variable naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
147
        clauses = []
148
149
        # Iterate over all the clauses in this operation
150
        for clause in clause.clauses:
151
            # Recursively analyse clauses
152
            res = _where_to_tree(clause, target)
153
            # None is returned for unsupported clauses or operations
154
            if res is not None:
155
                # Append polish notation result to clauses list
156
                clauses.append(res)
157
158
        # Look for a known operator
159
        if op in _OPS.keys():
160
            # Get string representation
161
            op = _OPS[op]
0 ignored issues
show
Coding Style Naming introduced by
The name op does not conform to the variable naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
162
        else:
163
            # Right now, we cannot cope with anything else
164
            return None
165
166
        # Return polish notation tuple of this clause
167
        return (op, clauses)
168
169 1
    elif isinstance(clause, Exists):
170
        # This case is a bit hard to verify.
171
        # We expect this to be the EXISTS sub-clause of something like:
172
        #
173 1
        #  session.query(osmalchemy.node).filter(
174
        #   osmalchemy.node.tags.any(key="name", value="Schwarzrheindorf Kirche")
175 1
        #  ).all()
176
        #
177
        # For now, we stick with simply expecting that until someone
178 1
        # rewrites this entire code.
179
        try:
180
            # Try to get the real conditionals from this weird statement
181
            conditionals = (clause.get_children()[0]._whereclause.clauses[1].
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _whereclause was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
182 1 View Code Duplication
                get_children()[0].get_children()[0]._whereclause.clauses[1].clauses)
0 ignored issues
show
Wrong continued indentation (add 12 spaces).
Loading history...
This code seems to be duplicated in your project.
Loading history...
183
        except:
0 ignored issues
show
Coding Style Best Practice introduced by
General except handlers without types should be used sparingly.

Typically, you would use general except handlers when you intend to specifically handle all types of errors, f.e. when logging. Otherwise, such general error handlers can mask errors in your application that you want to know of.

Loading history...
184
            # Simply return None if we got something unexpected
185 1
            return None
186
187
        key = ""
188 1
        value = ""
189 View Code Duplication
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
190
        for clause in conditionals:
191 1
            if clause.left.name == "key":
192
                key = clause.right.value
193 1
            elif clause.left.name == "value":
194
                value = clause.right.value
195
196 View Code Duplication
        # Check if we got only a key, a key and a value or neither
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
197
        if key and not value:
198
            return ("has", key, "")
199
        elif key and value:
200
            return ("==", key, value)
201
        else:
202
            return None
203 1 View Code Duplication
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
204
    elif isinstance(clause, Grouping):
205 1
        # Ungroup by simply taking the first element of the group
206 1
        # This is not correct in general, but correct for all documented
207
        # use cases.
208
        return _where_to_tree(clause.get_children()[0], target)
209
210
    else:
211 1
        # We hit an unsupported type of clause
212
        return None
213 1
214 1
def _trees_to_overpassql(tree_dict):
215
    """ Transform an expression tree (from _where_to_tree) into OverpassQL. """
216
217
    # Called recursively on all subtrees
218
    def _tree_to_overpassql_recursive(tree, type_):
219 1
        # Empty result string
220
        result = ""
221 1
222 1
        # Test if we got a tree or an atom
223
        if isinstance(tree[1], list):
0 ignored issues
show
Too many nested blocks (6/5)
Loading history...
224
            # We are in a subtree
225
226
            # Store operation of subtree (conjunction/disjunction)
227 1
            op = tree[0]
0 ignored issues
show
Coding Style Naming introduced by
The name op does not conform to the variable naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
228
229 1
            # Empty bounding box
230 1
            bbox = [None, None, None, None]
231
232
            # List of genrated set names
233
            set_names = []
234
235
            # Iterate over all elements in the conjunction/disjunction
236 1
            for element in tree[1]:
237
                # Check if element is a tree or an atom
238
                if isinstance(element[1], list):
239
                    # Recurse into inner tree
240
                    result_inner_tree = _tree_to_overpassql_recursive(tree[1])
0 ignored issues
show
It seems like a value for argument type_ is missing in the function call.
Loading history...
241
                    # Store resulting query and its name
242
                    result += "%s" % result_inner_tree[1]
243 1
                    set_names.append(result_inner_tree[0])
244
                else:
245
                    # Parse atom
246
247
                    # latitude and longitude comparisons form a bounding box
248
                    if element[1] == "latitude" and element[0] == ">":
249 1
                        # South edge
250
                        if bbox[0] is None:
251 1
                            bbox[0] = float(element[2])
252 1
                        elif op == "&&" and bbox[0] <= element[2]:
253 1
                            bbox[0] = float(element[2])
254
                        elif op == "||" and bbox[0] >= element[2]:
255
                            bbox[0] = float(element[2])
256 1
                    elif element[1] == "latitude" and element[0] == "<":
257
                        # North edge
258 1
                        if bbox[2] is None:
259 1
                            bbox[2] = float(element[2])
260 1
                        elif op == "&&" and bbox[2] >= element[2]:
261 1
                            bbox[2] = float(element[2])
262 1
                        elif op == "||" and bbox[2] <= element[2]:
263 1
                            bbox[2] = float(element[2])
264 1
                    elif element[1] == "longitude" and element[0] == ">":
265 1
                        # West edge
266
                        if bbox[1] is None:
267
                            bbox[1] = float(element[2])
268 1
                        elif op == "&&" and bbox[1] <= element[2]:
269
                            bbox[1] = float(element[2])
270 1
                        elif op == "||" and bbox[1] >= element[2]:
271 1
                            bbox[1] = float(element[2])
272 1
                    elif element[1] == "longitude" and element[0] == "<":
273
                        # East edge
274
                        if bbox[3] is None:
275 1
                            bbox[3] = float(element[2])
276 1
                        elif op == "&&" and bbox[3] >= element[2]:
277
                            bbox[3] = float(element[2])
278 1
                        elif op == "||" and bbox[3] <= element[2]:
279 1
                            bbox[3] = float(element[2])
280 1
                    # Query for an element with specific id
281
                    elif element[1] == "id" and element[0] == "==":
282 1
                        # Build query
283 1
                        if op == "||":
284 1
                            idquery = "%s(%i)" % (type_, element[2])
285 1
                            # Store resulting query and its name
286
                            set_name = "s%i" % id(idquery)
287 1
                            result += "%s->.%s;" % (idquery, set_name)
288
                            set_names.append(set_name)
289
                        elif op == "&&":
290
                            idquery = "(%i)" % (element[2])
291
                            # Store resulting query and its name
292 1
                            set_name = "s%i" % id(idquery)
293
                            result += idquery
294
                            set_names.append(set_name)
295 1
                    elif element[1] == "id":
296
                        # We got an id query, but not with equality comparison
297
                        raise ValueError("id can only be queried with equality")
298 1
                    # Everything else must be a tag query
299
                    else:
300
                        # Check whether it is a comparison or a query for existence
301 1
                        if element[0] == "==":
302
                            # Build query for tag comparison
303
                            if op == "||":
304
                                tagquery = "%s[\"%s\"=\"%s\"]" % (type_, element[1], element[2])
305 1
                            elif op == "&&":
306 1
                                tagquery = "[\"%s\"=\"%s\"]" % (element[1], element[2])
307 1
                        elif element[0] == "has":
308
                            if op == "||":
309 1
                                tagquery = "%s[\"%s\"]" % (type_, element[1])
310
                            elif op == "&&":
311
                                tagquery = "[\"%s\"]" % (element[1])
312
313
                        # Store resulting query and its name
314
                        set_name = "s%i" % id(tagquery)
315 1
                        if op == "||":
316 1
                            result += "%s->.%s;" % (tagquery, set_name)
317 1
                        elif op == "&&":
318
                            result += tagquery
319
                        set_names.append(set_name)
320 1
321 1
            # Check if any component of the bounding box was set
322
            if bbox != [None, None, None, None]:
323 1
                # Amend minima/maxima
324
                if bbox[0] is None:
325 1
                    bbox[0] = -90.0
326
                if bbox[1] is None:
327
                    bbox[1] = -180.0
328 1
                if bbox[2] is None:
329 1
                    bbox[2] = 90.0
330 1
                if bbox[3] is None:
331 1
                    bbox[3] = 180.0
332 1
333 1
                # Build query
334 1
                if op == "||":
335
                    bboxquery = "%s(%s,%s,%s,%s)" % (type_, bbox[0], bbox[1], bbox[2], bbox[3])
336
                elif op == "&&":
337 1
                    bboxquery = "(%s,%s,%s,%s)" % (bbox[0], bbox[1], bbox[2], bbox[3])
338
                # Store resulting query and its name
339
                set_name = "s%i" % id(bboxquery)
340
                if op == "||":
341
                    result += "%s->.%s;" % (bboxquery, set_name)
342
                elif op == "&&":
343
                    result += bboxquery
344
                set_names.append(set_name)
345
346
            # Build conjunction or disjunction according to current operation
347
            if len(set_names) > 1:
348
                if op == "&&":
349
                    # Conjunction, build an intersection
350
                    result = "%s%s" % (type_, result)
351
                elif op == "||":
352
                    # Disjunction, build a union
353
                    result += "("
354
                    for set_name in set_names:
355
                        result += ".%s;" % set_name
356
                    result += ")"
357
            else:
358
                if op == "||":
359
                    result += "(.%s;)" % set_names[0]
360
                elif op == "&&":
361
                    result = "%s%s" % (type_, result)
362
        else:
363
            # We got a bare atom
364
365
            # latitude and longitude are components of a bounding box query
366
            if tree[1] == "latitude" and tree[0] == ">":
367
                # South edge
368
                result = "%s(%s,-180.0,90.0,180.0)" % (type_, tree[2])
369
            elif tree[1] == "latitude" and tree[0] == "<":
370
                # West edge
371
                result = "%s(-90.0,-180.0,%s,180.0)" % (type_, tree[2])
372
            elif tree[1] == "longitude" and tree[0] == ">":
373
                # North edge
374
                result = "%s(-90.0,%s,-90.0,180.0)" % (type_, tree[2])
375
            elif tree[1] == "longitude" and tree[0] == "<":
376
                # East edge
377
                result = "%s(-90.0,-180.0,-90.0,%s)" % (type_, tree[2])
378
            # Query for an id
379
            elif tree[1] == "id" and tree[0] == "==":
380
                result = "%s(%i)" % (type_, tree[2])
381
            elif tree[1] == "id":
382
                # We got an id query, but not with equality comparison
383
                raise ValueError("id can only be queried with equality")
384
            # Everything else must be a tag query
385
            else:
386
                result = "%s[\"%s\"=\"%s\"]" % (type_, tree[1], tree[2])
387
388
        # generate a name for the complete set and return it, along with the query
389
        set_name = id(result)
390
        result += "->.s%i;" % set_name
391
        return (set_name, result)
392
393
    # Run tree transformation for each type in the input tree
394
    results = []
395
    for type_ in tree_dict.keys():
396
        # Get real type name (OSMNode→node,…)
397
        real_type = type_[3:].lower()
398
        # Do transformation and store query and name
399
        results.append(_tree_to_overpassql_recursive(tree_dict[type_], real_type))
400
401
    # Build finally resulting query in a union
402
    full_result = ""
403
    set_names = "("
404
    for result in results:
405
        full_result += result[1]
406
        set_names += ".s%s; " % result[0]
407
    set_names = "%s);" % set_names.strip()
408
    full_result += set_names
409
410
    # Return final query
411
    return full_result
412
413
def _normalise_overpassql(oql):
414
    """ Normalise an OverpassQL expression.
0 ignored issues
show
A suspicious escape sequence \. was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
415
416
    Takes an OverpassQL string as argument and strips all set names of the form \.s[0-9]+
417
    """
418
419
    def replace_setname(match):
0 ignored issues
show
This function should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
420
        if not match.group().startswith('"'):
421
            return re.sub(r"\.s[0-9]+", ".s", match.group())
422
        else:
423
            return match.group()
424
425
    return re.sub(r'"[^"]*"|([^"]*)', replace_setname, oql)
426