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
introduced
by
![]() |
|||
34 | 1 | from sqlalchemy.sql.elements import BinaryExpression, BooleanClauseList, BindParameter, Grouping |
|
35 | from sqlalchemy.sql.annotation import AnnotatedColumn |
||
0 ignored issues
–
show
|
|||
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
|
|||
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
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. ![]() |
|||
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
It seems like
_annotations was declared protected and should not be accessed from this context.
Prefixing a member variable 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
![]() |
|||
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
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. ![]() |
|||
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
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. ![]() |
|||
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
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. ![]() |
|||
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
It seems like
_whereclause was declared protected and should not be accessed from this context.
Prefixing a member variable 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
![]() |
|||
182 | 1 | View Code Duplication | get_children()[0].get_children()[0]._whereclause.clauses[1].clauses) |
0 ignored issues
–
show
|
|||
183 | except: |
||
0 ignored issues
–
show
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. ![]() |
|||
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
|
|||
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
|
|||
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
|
|||
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
|
|||
224 | # We are in a subtree |
||
225 | |||
226 | # Store operation of subtree (conjunction/disjunction) |
||
227 | 1 | op = tree[0] |
|
0 ignored issues
–
show
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. ![]() |
|||
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
|
|||
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 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. ![]() |
|||
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. ![]() |
|||
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 |