Browse Source

fix rate limit timeout issues

by pulling new client code from upstream (1fb8828)
and porting back to QGIS2.18
feature/processing_qgs2
Norwin Roosen 5 days ago
parent
commit
1380296d37

+ 2
- 2
Makefile View File

@@ -38,7 +38,7 @@ PLUGINNAME = OSMtools
38 38
 
39 39
 PY_FILES = \
40 40
 	__init__.py osm_tools.py \
41
-	core gui osmtools_processing
41
+	core gui osmtools_processing utils
42 42
 
43 43
 # translation
44 44
 SOURCES = $(PY_FILES)
@@ -71,7 +71,7 @@ default: deploy
71 71
 compile: $(COMPILED_RESOURCE_FILES)
72 72
 
73 73
 %.py : %.qrc $(RESOURCES_SRC)
74
-	pyrcc4 -o $*.py  $<
74
+	#pyrcc4 -o $*.py  $<
75 75
 
76 76
 %.qm : %.ts
77 77
 	$(LRELEASE) $<

+ 45
- 0
core/__init__.py View File

@@ -0,0 +1,45 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+/***************************************************************************
4
+ OSMtools
5
+                                 A QGIS plugin
6
+ QGIS client to query openrouteservice
7
+                              -------------------
8
+        begin                : 2017-02-01
9
+        git sha              : $Format:%H$
10
+        copyright            : (C) 2017 by Nils Nolde
11
+        email                : nils.nolde@gmail.com
12
+ ***************************************************************************/
13
+
14
+ This plugin provides access to the various APIs from OpenRouteService
15
+ (https://openrouteservice.org), developed and
16
+ maintained by GIScience team at University of Heidelberg, Germany. By using
17
+ this plugin you agree to the ORS terms of service
18
+ (https://openrouteservice.org/terms-of-service/).
19
+
20
+/***************************************************************************
21
+ *                                                                         *
22
+ *   This program is free software; you can redistribute it and/or modify  *
23
+ *   it under the terms of the GNU General Public License as published by  *
24
+ *   the Free Software Foundation; either version 2 of the License, or     *
25
+ *   (at your option) any later version.                                   *
26
+ *                                                                         *
27
+ ***************************************************************************/
28
+"""
29
+
30
+PROFILES = [
31
+            'driving-car',
32
+            'driving-hgv',
33
+            'cycling-regular',
34
+            'cycling-road',
35
+            'cycling-safe',
36
+            'cycling-mountain',
37
+            'cycling-electric',
38
+            'foot-walking',
39
+            'foot-hiking',
40
+            'wheelchair'
41
+            ]
42
+
43
+DIMENSIONS = ['time', 'distance']
44
+
45
+PREFERENCES = ['fastest', 'shortest']

+ 162
- 179
core/client.py View File

@@ -1,82 +1,92 @@
1
-#!/usr/bin/env python3
2 1
 # -*- coding: utf-8 -*-
3 2
 """
4
-Created on Sat Feb 17 13:51:13 2018
5
-
6
-@author: nilsnolde
3
+/***************************************************************************
4
+ OSMtools
5
+                                 A QGIS plugin
6
+ QGIS client to query openrouteservice
7
+                              -------------------
8
+        begin                : 2017-02-01
9
+        git sha              : $Format:%H$
10
+        copyright            : (C) 2017 by Nils Nolde
11
+        email                : nils.nolde@gmail.com
12
+ ***************************************************************************/
13
+
14
+ This plugin provides access to the various APIs from OpenRouteService
15
+ (https://openrouteservice.org), developed and
16
+ maintained by GIScience team at University of Heidelberg, Germany. By using
17
+ this plugin you agree to the ORS terms of service
18
+ (https://openrouteservice.org/terms-of-service/).
19
+
20
+/***************************************************************************
21
+ *                                                                         *
22
+ *   This program is free software; you can redistribute it and/or modify  *
23
+ *   it under the terms of the GNU General Public License as published by  *
24
+ *   the Free Software Foundation; either version 2 of the License, or     *
25
+ *   (at your option) any later version.                                   *
26
+ *                                                                         *
27
+ ***************************************************************************/
7 28
 """
8 29
 
9
-
10 30
 from datetime import datetime, timedelta
11 31
 import requests
12
-import random
13 32
 import time
14
-import collections
15
-from urllib.parse import urlencode
33
+from urllib import urlencode
34
+import random
35
+import json
16 36
 
17
-import OSMtools
18
-from OSMtools.core import exceptions, auxiliary
37
+from PyQt4.QtCore import QObject, pyqtSignal
19 38
 
20
-_USER_AGENT = "ORSClientQGIS/%s".format(OSMtools.__version__)
21
-_RETRIABLE_STATUSES = [503]
22
-_DEFAULT_BASE_URL = "https://api.openrouteservice.org"
39
+from OSMtools.core import networkaccessmanager
40
+from OSMtools.utils import exceptions, configmanager, logger
23 41
 
24
-class Client(object):
42
+_USER_AGENT = "ORSQGISClient@v1"
43
+
44
+
45
+class Client(QObject):
25 46
     """Performs requests to the ORS API services."""
26 47
 
27
-    def __init__(self, iface=None,
28
-                 apiKey=None,
29
-                 retry_timeout=60,
30
-                 requests_kwargs=None,
31
-                 retry_over_query_limit=False):
48
+    def __init__(self,
49
+                 provider=None,
50
+                 retry_timeout=60):
32 51
         """
33
-        :param key: Optional ORS API key. If not provided, it's read from
34
-            config.yml. Requests will fail if no key is in config.yml
35
-        :type key: string
36
-
37
-        :param iface: Optional QGIS interface instance used to push notifications about status
52
+        :param iface: A QGIS interface instance.
38 53
         :type iface: QgisInterface
39 54
 
55
+        :param provider: A openrouteservice provider from config.yml
56
+        :type provider: dict
57
+
40 58
         :param retry_timeout: Timeout across multiple retriable requests, in
41 59
             seconds.
42 60
         :type retry_timeout: int
43
-
44
-        :param requests_kwargs: Extra keyword arguments for the requests
45
-            library, which among other things allow for proxy auth to be
46
-            implemented. See the official requests docs for more info:
47
-            http://docs.python-requests.org/en/latest/api/#main-interface
48
-        :type requests_kwargs: dict
49 61
         """
62
+        QObject.__init__(self)
50 63
 
51
-        base_params = auxiliary.readConfig()
52
-
53
-        (self.key,
54
-         self.base_url,
55
-         self.queries_per_minute) = [v for (k, v) in sorted(base_params.items())]
56
-        self.iface = iface
64
+        self.key = provider['key']
65
+        self.base_url = provider['base_url']
66
+        self.ENV_VARS = provider.get('ENV_VARS')
57 67
 
58
-        if apiKey:
59
-            self.key = apiKey
68
+        # self.session = requests.Session()
69
+        self.nam = networkaccessmanager.NetworkAccessManager(debug=False)
60 70
 
61
-        self.session = requests.Session()
62
-
63
-        self.retry_over_query_limit = retry_over_query_limit
64 71
         self.retry_timeout = timedelta(seconds=retry_timeout)
65
-        self.requests_kwargs = requests_kwargs or {}
66
-        self.requests_kwargs.update({
67
-            "headers": {"User-Agent": _USER_AGENT,
68
-                        'Content-type': 'application/json'}
69
-        })
72
+        self.headers = {
73
+                "User-Agent": _USER_AGENT,
74
+                'Content-type': 'application/json',
75
+                'Authorization': provider['key']
76
+            }
70 77
 
71
-        self.sent_times = collections.deque("", self.queries_per_minute)
78
+        # Save some references to retrieve in client instances
79
+        self.url = None
80
+        self.warnings = None
72 81
 
82
+    overQueryLimit = pyqtSignal()
73 83
     def request(self,
74
-                 url, params,
75
-                 first_request_time=None,
76
-                 retry_counter=0,
77
-                 requests_kwargs=None,
78
-                 post_json=None):
79
-        """Performs HTTP GET/POST with credentials, returning the body as JSON.
84
+                url, params,
85
+                first_request_time=None,
86
+                retry_counter=0,
87
+                post_json=None):
88
+        """Performs HTTP GET/POST with credentials, returning the body as
89
+        JSON.
80 90
 
81 91
         :param url: URL extension for request. Should begin with a slash.
82 92
         :type url: string
@@ -88,20 +98,13 @@ class Client(object):
88 98
             retries have occurred).
89 99
         :type first_request_time: datetime.datetime
90 100
 
91
-        :param retry_counter: The number of this retry, or zero for first attempt.
92
-        :type retry_counter: int
93
-
94
-        :param requests_kwargs: Same extra keywords arg for requests as per
95
-            __init__, but provided here to allow overriding internally on a
96
-            per-request basis.
97
-        :type requests_kwargs: dict
101
+        :param post_json: Parameters for POST endpoints
102
+        :type post_json: dict
98 103
 
99
-        :raises ApiError: when the API returns an error.
100
-        :raises Timeout: if the request timed out.
101
-        :raises TransportError: when something went wrong while trying to
102
-            execute a request.
104
+        :raises OSMtools.utils.exceptions.ApiError: when the API returns an error.
103 105
 
104
-        :rtype: dict from JSON response.
106
+        :returns: openrouteservice response body
107
+        :rtype: dict
105 108
         """
106 109
 
107 110
         if not first_request_time:
@@ -115,7 +118,7 @@ class Client(object):
115 118
             # 0.5 * (1.5 ^ i) is an increased sleep time of 1.5x per iteration,
116 119
             # starting at 0.5s when retry_counter=1. The first retry will occur
117 120
             # at 1, so subtract that first.
118
-            delay_seconds = 1.5 ** (retry_counter - 1)
121
+            delay_seconds = 1.5**(retry_counter - 1)
119 122
 
120 123
             # Jitter this value by 50% and pause.
121 124
             time.sleep(delay_seconds * (random.random() + 0.5))
@@ -123,91 +126,115 @@ class Client(object):
123 126
         authed_url = self._generate_auth_url(url,
124 127
                                              params,
125 128
                                              )
129
+        self.url = self.base_url + authed_url
126 130
 
127 131
         # Default to the client-level self.requests_kwargs, with method-level
128 132
         # requests_kwargs arg overriding.
129
-        requests_kwargs = requests_kwargs or {}
130
-        final_requests_kwargs = dict(self.requests_kwargs, **requests_kwargs)
131
-
132
-        # Check if the time of the nth previous query (where n is
133
-        # queries_per_second) is under a second ago - if so, sleep for
134
-        # the difference.
135
-        if self.sent_times and len(self.sent_times) == self.queries_per_minute:
136
-            elapsed_since_earliest = time.time() - self.sent_times[0]
137
-            if elapsed_since_earliest < 60:
138
-                if self.iface:
139
-                    self.iface.messageBar().pushInfo('Limit exceeded',
140
-                                                    'Request limit of {} per minute exceeded. '
141
-                                                    'Wait for {} seconds'.format(self.queries_per_minute,
142
-                                                                                60 - elapsed_since_earliest))
143
-                else:
144
-                    print "Ratelimit exceeded. Please wait 60s before the next request"
145
-                time.sleep(60 - elapsed_since_earliest)
146
-
147
-        # Determine GET/POST.
148
-        # post_json is so far only sent from matrix call
149
-        requests_method = self.session.get
133
+        # final_requests_kwargs = self.requests_kwargs
134
+
135
+        # Determine GET/POST
136
+        # requests_method = self.session.get
137
+        requests_method = 'GET'
138
+        body = None
150 139
         if post_json is not None:
151
-            requests_method = self.session.post
152
-            final_requests_kwargs["json"] = post_json
140
+            # requests_method = self.session.post
141
+            # final_requests_kwargs["json"] = post_json
142
+            body = post_json
143
+            requests_method = 'POST'
144
+
145
+        logger.log(
146
+            "url: {}\nParameters: {}".format(
147
+                self.url,
148
+                # final_requests_kwargs
149
+                body
150
+            ),
151
+            0
152
+        )
153
+
153 154
         try:
154
-            response = requests_method(self.base_url + authed_url,
155
-                                       **final_requests_kwargs)
156
-        except requests.exceptions.Timeout:
157
-            raise exceptions.Timeout()
158
-        except Exception as e:
159
-            raise #exceptions.TransportError(e)
155
+            # response = requests_method(
156
+            #     self.base_url + authed_url,
157
+            #     **final_requests_kwargs
158
+            # )
159
+            response, content = self.nam.request(self.url,
160
+                                           method=requests_method,
161
+                                           body=body,
162
+                                           headers=self.headers,
163
+                                           blocking=True)
164
+        # except requests.exceptions.Timeout:
165
+        #     raise exceptions.Timeout()
166
+        except networkaccessmanager.RequestsExceptionTimeout:
167
+            raise exceptions.Timeout
168
+
169
+        except networkaccessmanager.RequestsException:
170
+            try:
171
+                # result = self._get_body(response)
172
+                self._check_status()
160 173
 
161
-        if response.status_code in _RETRIABLE_STATUSES:
162
-            # Retry request.
163
-            print('Server down.\nRetrying for the {}th time.'.format(retry_counter + 1))
174
+            except exceptions.OverQueryLimit as e:
164 175
 
165
-            return self.request(url, params, first_request_time,
166
-                                 retry_counter + 1, requests_kwargs, post_json)
176
+                # Let the instances know smth happened
177
+                self.overQueryLimit.emit()
178
+                logger.log("{}: {}".format(e.__class__.__name__, str(e)), 1)
167 179
 
168
-        try:
169
-            result = self._get_body(response)
170
-            self.sent_times.append(time.time())
171
-            return result
172
-        except exceptions._RetriableRequest as e:
173
-            if isinstance(e, exceptions._OverQueryLimit) and not self.retry_over_query_limit:
180
+                return self.request(url, params, first_request_time, retry_counter + 1, post_json)
181
+
182
+            except exceptions.ApiError as e:
183
+                logger.log("Feature ID {} caused a {}: {}".format(params.get('id', 'unknown'), e.__class__.__name__, str(e)), 2)
174 184
                 raise
175 185
 
176
-            if self.iface:
177
-                self.iface.messageBar().pushInfo('Rate limit exceeded.\nRetrying for the {}th time.'.format(retry_counter + 1))
178
-            else:
179
-                print "Ratelimit exceeded. Retrying.."
180
-            return self.request(url, params, first_request_time,
181
-                                 retry_counter + 1, requests_kwargs, post_json)
182
-        except:
183 186
             raise
184 187
 
188
+        # Write env variables if successful
189
+        if self.ENV_VARS:
190
+            for env_var in self.ENV_VARS:
191
+                configmanager.write_env_var(env_var, response.headers.get(self.ENV_VARS[env_var], 'None'))
192
+
193
+        return json.loads(content.decode('utf-8'))
185 194
 
186
-    def _get_body(self, response):
195
+    def _check_status(self):
187 196
         """
188 197
         Casts JSON response to dict
189 198
 
190
-        :param response: The HTTP response of the request.
191
-        :type reponse: JSON object
199
+        :raises OSMtools.utils.exceptions.OverQueryLimitError: when rate limit is exhausted, HTTP 429
200
+        :raises OSMtools.utils.exceptions.ApiError: when the backend API throws an error, HTTP 400
201
+        :raises OSMtools.utils.exceptions.InvalidKey: when API key is invalid (or quota is exceeded), HTTP 403
202
+        :raises OSMtools.utils.exceptions.GenericServerError: all other HTTP errors
192 203
 
193
-        :rtype: dict from JSON
204
+        :returns: response body
205
+        :rtype: dict
194 206
         """
195
-        body = response.json()
196
-        error = body.get('error')
197
-        status_code = response.status_code
198 207
 
199
-        if status_code == 429:
200
-            raise exceptions._OverQueryLimit(
201
-                str(status_code), error)
202
-        if status_code != 200:
203
-            try:
204
-                errorMsg = error['message']
205
-            except TypeError:
206
-                errorMsg = error
207
-            raise exceptions.ApiError(status_code, errorMsg)
208
+        status_code = self.nam.http_call_result.status_code
209
+        message = self.nam.http_call_result.text if self.nam.http_call_result.text != '' else self.nam.http_call_result.reason
208 210
 
209
-        return body
211
+        if status_code == 403:
212
+            raise exceptions.InvalidKey(
213
+                str(status_code),
214
+                # error,
215
+                message
216
+            )
210 217
 
218
+        if status_code == 429:
219
+            raise exceptions.OverQueryLimit(
220
+                str(status_code),
221
+                # error,
222
+                message
223
+            )
224
+        # Internal error message for Bad Request
225
+        if 400 < status_code < 500:
226
+            raise exceptions.ApiError(
227
+                str(status_code),
228
+                # error,
229
+                message
230
+            )
231
+        # Other HTTP errors have different formatting
232
+        if status_code != 200:
233
+            raise exceptions.GenericServerError(
234
+                str(status_code),
235
+                # error,
236
+                message
237
+            )
211 238
 
212 239
     def _generate_auth_url(self, path, params):
213 240
         """Returns the path and query string portion of the request URL, first
@@ -219,8 +246,8 @@ class Client(object):
219 246
         :param params: URL parameters.
220 247
         :type params: dict or list of key/value tuples
221 248
 
249
+        :returns: encoded URL
222 250
         :rtype: string
223
-
224 251
         """
225 252
 
226 253
         if type(params) is dict:
@@ -228,51 +255,7 @@ class Client(object):
228 255
 
229 256
         # Only auto-add API key when using ORS. If own instance, API key must
230 257
         # be explicitly added to params
231
-        if self.key:
232
-            params.append(("api_key", self.key))
233
-            return path + "?" + _urlencode_params(params)
234
-        elif self.base_url != _DEFAULT_BASE_URL:
235
-            return path + "?" + _urlencode_params(params)
236
-
237
-        raise exceptions.ApiError("No API key specified. "
238
-                         "Visit https://go.openrouteservice.org/dev-dashboard/ "
239
-                         "to create one.")
240
-
241
-
242
-def _urlencode_params(params):
243
-    """URL encodes the parameters.
244
-
245
-    :param params: The parameters
246
-    :type params: list of key/value tuples.
247
-
248
-    :rtype: string
249
-    """
250
-    # urlencode does not handle unicode strings in Python 2.
251
-    # Firstly, normalize the values so they get encoded correctly.
252
-    params = [(key, _normalize_for_urlencode(val)) for key, val in params]
253
-    # Secondly, unquote unreserved chars which are incorrectly quoted
254
-    # by urllib.urlencode, causing invalid auth signatures. See GH #72
255
-    # for more info.
256
-    return requests.utils.unquote_unreserved(urlencode(params))
257
-
258
-
259
-try:
260
-    unicode
261
-    # NOTE(cbro): `unicode` was removed in Python 3. In Python 3, NameError is
262
-    # raised here, and caught below.
263
-
264
-    def _normalize_for_urlencode(value):
265
-        """(Python 2) Converts the value to a `str` (raw bytes)."""
266
-        if isinstance(value, unicode):
267
-            return value.encode('utf8')
268
-
269
-        if isinstance(value, str):
270
-            return value
271
-
272
-        return _normalize_for_urlencode(str(value))
273
-
274
-except NameError:
275
-    def _normalize_for_urlencode(value):
276
-        """(Python 3) No-op."""
277
-        # urlencode in Python 3 handles all the types we are passing it.
278
-        return value
258
+        # if self.key:
259
+        #     params.append(("api_key", self.key))
260
+
261
+        return path + "?" + requests.utils.unquote_unreserved(urlencode(params))

+ 396
- 0
core/networkaccessmanager.py View File

@@ -0,0 +1,396 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+***************************************************************************
4
+    An httplib2 replacement that uses QgsNetworkAccessManager
5
+
6
+    ---------------------
7
+    Date                 : August 2016
8
+    Copyright            : (C) 2016 Boundless, http://boundlessgeo.com
9
+    Email                : apasotti at boundlessgeo dot com
10
+***************************************************************************
11
+*                                                                         *
12
+*   This program is free software; you can redistribute it and/or modify  *
13
+*   it under the terms of the GNU General Public License as published by  *
14
+*   the Free Software Foundation; either version 2 of the License, or     *
15
+*   (at your option) any later version.                                   *
16
+*                                                                         *
17
+***************************************************************************
18
+"""
19
+
20
+from builtins import str
21
+from builtins import object
22
+
23
+__author__ = 'Alessandro Pasotti'
24
+__date__ = 'August 2016'
25
+
26
+import re
27
+import io
28
+import urllib.request, urllib.error, urllib.parse
29
+
30
+from qgis.PyQt.QtCore import QUrl, QEventLoop
31
+from qgis.PyQt.QtNetwork import QNetworkRequest, QNetworkReply
32
+
33
+from qgis.core import (
34
+    QgsApplication,
35
+    QgsNetworkAccessManager,
36
+    QgsMessageLog
37
+)
38
+
39
+# FIXME: ignored
40
+DEFAULT_MAX_REDIRECTS = 4
41
+
42
+class RequestsException(Exception):
43
+    pass
44
+
45
+class RequestsExceptionTimeout(RequestsException):
46
+    pass
47
+
48
+class RequestsExceptionConnectionError(RequestsException):
49
+    pass
50
+
51
+class RequestsExceptionUserAbort(RequestsException):
52
+    pass
53
+
54
+class Map(dict):
55
+    """
56
+    Example:
57
+    m = Map({'first_name': 'Eduardo'}, last_name='Pool', age=24, sports=['Soccer'])
58
+    """
59
+    def __init__(self, *args, **kwargs):
60
+        super(Map, self).__init__(*args, **kwargs)
61
+        for arg in args:
62
+            if isinstance(arg, dict):
63
+                for k, v in arg.items():
64
+                    self[k] = v
65
+
66
+        if kwargs:
67
+            for k, v in kwargs.items():
68
+                self[k] = v
69
+
70
+    def __getattr__(self, attr):
71
+        return self.get(attr)
72
+
73
+    def __setattr__(self, key, value):
74
+        self.__setitem__(key, value)
75
+
76
+    def __setitem__(self, key, value):
77
+        super(Map, self).__setitem__(key, value)
78
+        self.__dict__.update({key: value})
79
+
80
+    def __delattr__(self, item):
81
+        self.__delitem__(item)
82
+
83
+    def __delitem__(self, key):
84
+        super(Map, self).__delitem__(key)
85
+        del self.__dict__[key]
86
+
87
+
88
+class Response(Map):
89
+    pass
90
+
91
+class NetworkAccessManager(object):
92
+    """
93
+    This class mimicks httplib2 by using QgsNetworkAccessManager for all
94
+    network calls.
95
+
96
+    The return value is a tuple of (response, content), the first being and
97
+    instance of the Response class, the second being a string that contains
98
+    the response entity body.
99
+
100
+    Parameters
101
+    ----------
102
+    debug : bool
103
+        verbose logging if True
104
+    exception_class : Exception
105
+        Custom exception class
106
+
107
+    Usage 1 (blocking mode)
108
+    -----
109
+    ::
110
+        nam = NetworkAccessManager(authcgf)
111
+        try:
112
+            (response, content) = nam.request('http://www.example.com')
113
+        except RequestsException as e:
114
+            # Handle exception
115
+            pass
116
+
117
+    Usage 2 (Non blocking mode)
118
+    -------------------------
119
+    ::
120
+        NOTE! if blocking mode returns immediatly
121
+              it's up to the caller to manage listeners in case
122
+              of non blocking mode
123
+
124
+        nam = NetworkAccessManager(authcgf)
125
+        try:
126
+            nam.request('http://www.example.com', blocking=False)
127
+            nam.reply.finished.connect(a_signal_listener)
128
+        except RequestsException as e:
129
+            # Handle exception
130
+            pass
131
+
132
+        Get response using method:
133
+        nam.httpResult() that return a dictionary with keys:
134
+            'status' - http code result come from reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
135
+            'status_code' - http code result come from reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
136
+            'status_message' - reply message string from reply.attribute(QNetworkRequest.HttpReasonPhraseAttribute)
137
+            'content' - bytearray returned from reply
138
+            'ok' - request success [True, False]
139
+            'headers' - Dicionary containing the reply header
140
+            'reason' - fomatted message string with reply.errorString()
141
+            'exception' - the exception returne dduring execution
142
+    """
143
+
144
+    def __init__(self, authid=None, disable_ssl_certificate_validation=False, exception_class=None, debug=True):
145
+        self.disable_ssl_certificate_validation = disable_ssl_certificate_validation
146
+        self.authid = authid
147
+        self.reply = None
148
+        self.debug = debug
149
+        self.exception_class = exception_class
150
+        self.on_abort = False
151
+        self.blocking_mode = False
152
+        self.http_call_result = Response({
153
+            'status': 0,
154
+            'status_code': 0,
155
+            'status_message': '',
156
+            'content' : '',
157
+            'ok': False,
158
+            'headers': {},
159
+            'reason': '',
160
+            'exception': None,
161
+        })
162
+
163
+    def msg_log(self, msg):
164
+        if self.debug:
165
+            QgsMessageLog.logMessage(msg, "NetworkAccessManager")
166
+
167
+    def httpResult(self):
168
+        return self.http_call_result
169
+
170
+    def auth_manager(self):
171
+        return QgsApplication.authManager()
172
+
173
+    def request(self, url, method="GET", body=None, headers=None, redirections=DEFAULT_MAX_REDIRECTS, connection_type=None, blocking=True):
174
+        """
175
+        Make a network request by calling QgsNetworkAccessManager.
176
+        redirections argument is ignored and is here only for httplib2 compatibility.
177
+        """
178
+        self.msg_log(u'http_call request: {0}'.format(url))
179
+
180
+        self.blocking_mode = blocking
181
+        req = QNetworkRequest()
182
+        # Avoid double quoting form QUrl
183
+        url = urllib.parse.unquote(url)
184
+        req.setUrl(QUrl(url))
185
+        if headers is not None:
186
+            # This fixes a wierd error with compressed content not being correctly
187
+            # inflated.
188
+            # If you set the header on the QNetworkRequest you are basically telling
189
+            # QNetworkAccessManager "I know what I'm doing, please don't do any content
190
+            # encoding processing".
191
+            # See: https://bugs.webkit.org/show_bug.cgi?id=63696#c1
192
+            try:
193
+                del headers['Accept-Encoding']
194
+            except KeyError:
195
+                pass
196
+            for k, v in list(headers.items()):
197
+                self.msg_log("Setting header %s to %s" % (k, v))
198
+                req.setRawHeader(k.encode(), v.encode())
199
+        if self.authid:
200
+            self.msg_log("Update request w/ authid: {0}".format(self.authid))
201
+            self.auth_manager().updateNetworkRequest(req, self.authid)
202
+        if self.reply is not None and self.reply.isRunning():
203
+            self.reply.close()
204
+        if method.lower() == 'delete':
205
+            func = getattr(QgsNetworkAccessManager.instance(), 'deleteResource')
206
+        else:
207
+            func = getattr(QgsNetworkAccessManager.instance(), method.lower())
208
+        # Calling the server ...
209
+        # Let's log the whole call for debugging purposes:
210
+        self.msg_log("Sending %s request to %s" % (method.upper(), req.url().toString()))
211
+        self.on_abort = False
212
+        headers = {str(h): str(req.rawHeader(h)) for h in req.rawHeaderList()}
213
+        for k, v in list(headers.items()):
214
+            self.msg_log("%s: %s" % (k, v))
215
+        if method.lower() in ['post', 'put']:
216
+            if isinstance(body, io.IOBase):
217
+                body = body.read()
218
+            if isinstance(body, str):
219
+                body = body.encode()
220
+            if isinstance(body, dict):
221
+                body = str(body).encode(encoding='utf-8')
222
+            self.reply = func(req, body)
223
+        else:
224
+            self.reply = func(req)
225
+        if self.authid:
226
+            self.msg_log("Update reply w/ authid: {0}".format(self.authid))
227
+            self.auth_manager().updateNetworkReply(self.reply, self.authid)
228
+
229
+        # necessary to trap local timout manage by QgsNetworkAccessManager
230
+        # calling QgsNetworkAccessManager::abortRequest
231
+        QgsNetworkAccessManager.instance().requestTimedOut.connect(self.requestTimedOut)
232
+
233
+        self.reply.sslErrors.connect(self.sslErrors)
234
+        self.reply.finished.connect(self.replyFinished)
235
+        self.reply.downloadProgress.connect(self.downloadProgress)
236
+
237
+        # block if blocking mode otherwise return immediatly
238
+        # it's up to the caller to manage listeners in case of no blocking mode
239
+        if not self.blocking_mode:
240
+            return (None, None)
241
+
242
+        # Call and block
243
+        self.el = QEventLoop()
244
+        self.reply.finished.connect(self.el.quit)
245
+
246
+        # Catch all exceptions (and clean up requests)
247
+        try:
248
+            self.el.exec_(QEventLoop.ExcludeUserInputEvents)
249
+        except Exception as e:
250
+            raise e
251
+
252
+        if self.reply:
253
+            self.reply.finished.disconnect(self.el.quit)
254
+
255
+        # emit exception in case of error
256
+        if not self.http_call_result.ok:
257
+            if self.http_call_result.exception and not self.exception_class:
258
+                raise self.http_call_result.exception
259
+            else:
260
+                raise self.exception_class(self.http_call_result.reason)
261
+
262
+        return (self.http_call_result, self.http_call_result.content)
263
+
264
+    def downloadProgress(self, bytesReceived, bytesTotal):
265
+        """Keep track of the download progress"""
266
+        #self.msg_log("downloadProgress %s of %s ..." % (bytesReceived, bytesTotal))
267
+        pass
268
+
269
+    def requestTimedOut(self, reply):
270
+        """Trap the timeout. In Async mode requestTimedOut is called after replyFinished"""
271
+        # adapt http_call_result basing on receiving qgs timer timout signal
272
+        self.exception_class = RequestsExceptionTimeout
273
+        self.http_call_result.exception = RequestsExceptionTimeout("Timeout error")
274
+
275
+    def replyFinished(self):
276
+        err = self.reply.error()
277
+        httpStatus = self.reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
278
+        httpStatusMessage = self.reply.attribute(QNetworkRequest.HttpReasonPhraseAttribute)
279
+        self.http_call_result.status_code = httpStatus
280
+        self.http_call_result.status = httpStatus
281
+        self.http_call_result.status_message = httpStatusMessage
282
+        for k, v in self.reply.rawHeaderPairs():
283
+            self.http_call_result.headers[str(k.data(), encoding='utf-8')] = str(v.data(), encoding='utf-8')
284
+            self.http_call_result.headers[str(k.data(), encoding='utf-8').lower()] = str(v.data(), encoding='utf-8')
285
+
286
+        if err != QNetworkReply.NoError:
287
+            # handle error
288
+            # check if errorString is empty, if so, then set err string as
289
+            # reply dump
290
+            if re.match('(.)*server replied: $', self.reply.errorString()):
291
+                errString = self.reply.errorString() + self.http_call_result.content
292
+            else:
293
+                errString = self.reply.errorString()
294
+            # check if self.http_call_result.status_code is available (client abort
295
+            # does not produce http.status_code)
296
+            if self.http_call_result.status_code:
297
+                msg = "Network error #{0}: {1}".format(
298
+                    self.http_call_result.status_code, errString)
299
+            else:
300
+                msg = "Network error: {0}".format(errString)
301
+
302
+            self.http_call_result.reason = msg
303
+            self.http_call_result.text = str(self.reply.readAll().data(), encoding='utf-8')
304
+            self.http_call_result.ok = False
305
+            self.msg_log(msg)
306
+            # set return exception
307
+            if err == QNetworkReply.TimeoutError:
308
+                self.http_call_result.exception = RequestsExceptionTimeout(msg)
309
+
310
+            elif err == QNetworkReply.ConnectionRefusedError:
311
+                self.http_call_result.exception = RequestsExceptionConnectionError(msg)
312
+
313
+            elif err == QNetworkReply.OperationCanceledError:
314
+                # request abort by calling NAM.abort() => cancelled by the user
315
+                if self.on_abort:
316
+                    self.http_call_result.exception = RequestsExceptionUserAbort(msg)
317
+                else:
318
+                    self.http_call_result.exception = RequestsException(msg)
319
+
320
+            else:
321
+                self.http_call_result.exception = RequestsException(msg)
322
+
323
+            # overload exception to the custom exception if available
324
+            if self.exception_class:
325
+                self.http_call_result.exception = self.exception_class(msg)
326
+
327
+        else:
328
+            # Handle redirections
329
+            redirectionUrl = self.reply.attribute(QNetworkRequest.RedirectionTargetAttribute)
330
+            if redirectionUrl is not None and redirectionUrl != self.reply.url():
331
+                if redirectionUrl.isRelative():
332
+                    redirectionUrl = self.reply.url().resolved(redirectionUrl)
333
+
334
+                msg = "Redirected from '{}' to '{}'".format(
335
+                    self.reply.url().toString(), redirectionUrl.toString())
336
+                self.msg_log(msg)
337
+
338
+                self.reply.deleteLater()
339
+                self.reply = None
340
+                self.request(redirectionUrl.toString())
341
+
342
+            # really end request
343
+            else:
344
+                msg = "Network success #{0}".format(self.reply.error())
345
+                self.http_call_result.reason = msg
346
+                self.msg_log(msg)
347
+
348
+                ba = self.reply.readAll()
349
+                self.http_call_result.content = bytes(ba)
350
+                self.http_call_result.text = str(ba.data(), encoding='utf-8')
351
+                self.http_call_result.ok = True
352
+
353
+        # Let's log the whole response for debugging purposes:
354
+        self.msg_log("Got response %s %s from %s" % \
355
+                    (self.http_call_result.status_code,
356
+                     self.http_call_result.status_message,
357
+                     self.reply.url().toString()))
358
+        for k, v in list(self.http_call_result.headers.items()):
359
+            self.msg_log("%s: %s" % (k, v))
360
+        if len(self.http_call_result.content) < 1024:
361
+            self.msg_log("Payload :\n%s" % self.http_call_result.text)
362
+        else:
363
+            self.msg_log("Payload is > 1 KB ...")
364
+
365
+        # clean reply
366
+        if self.reply is not None:
367
+            if self.reply.isRunning():
368
+                self.reply.close()
369
+            self.msg_log("Deleting reply ...")
370
+            # Disconnect all slots
371
+            self.reply.sslErrors.disconnect(self.sslErrors)
372
+            self.reply.finished.disconnect(self.replyFinished)
373
+            self.reply.downloadProgress.disconnect(self.downloadProgress)
374
+            self.reply.deleteLater()
375
+            self.reply = None
376
+        else:
377
+            self.msg_log("Reply was already deleted ...")
378
+
379
+    def sslErrors(self, ssl_errors):
380
+        """
381
+        Handle SSL errors, logging them if debug is on and ignoring them
382
+        if disable_ssl_certificate_validation is set.
383
+        """
384
+        if ssl_errors:
385
+            for v in ssl_errors:
386
+                self.msg_log("SSL Error: %s" % v.errorString())
387
+        if self.disable_ssl_certificate_validation:
388
+            self.reply.ignoreSslErrors()
389
+
390
+    def abort(self):
391
+        """
392
+        Handle request to cancel HTTP call
393
+        """
394
+        if (self.reply and self.reply.isRunning()):
395
+            self.on_abort = True
396
+            self.reply.abort()

+ 11
- 2
osmtools_processing/isochrones.py View File

@@ -104,12 +104,17 @@ class IsochronesGeoAlg(GeoAlgorithm):
104 104
         progress.setInfo('Initializing')
105 105
 
106 106
         simplify = self.getParameterValue(self.IN_SIMPLIFY)
107
-        apiKey = self.getParameterValue(self.IN_KEY)
108 107
         profile = self.getParameterValue(self.IN_PROFILE)
109 108
         metric = self.getParameterValue(self.IN_METRIC)
110 109
         ranges = self.getParameterValue(self.IN_RANGES)
111 110
         ranges = list(map(int, ranges.split(',')))
112
-        client = Client(None, apiKey)
111
+        provider = {
112
+            'key': self.getParameterValue(self.IN_KEY),
113
+            'base_url': 'https://api.openrouteservice.org',
114
+            'timeout': 60,
115
+        }
116
+        client = Client(provider, provider['timeout'])
117
+
113 118
         pointLayer = getObjectFromUri(self.getParameterValue(self.IN_POINTS))
114 119
 
115 120
         # ORS understands WGS84 only, so we convert all points before sending
@@ -123,8 +128,12 @@ class IsochronesGeoAlg(GeoAlgorithm):
123 128
 
124 129
         progress.setInfo('Processing each selected point')
125 130
         responses = []
131
+        POINTS_REQ = 5
132
+        pointsInReq = 0
126 133
         for feature in features(pointLayer):
127 134
 
135
+            # TODO: run 5 points per request!
136
+
128 137
             feature.geometry().transform(transformer)
129 138
             point = feature.geometry().asPoint()
130 139
 

+ 6
- 1
osmtools_processing/isochrones_join.py View File

@@ -107,7 +107,12 @@ class IsochronesJoinGeoAlg(GeoAlgorithm):
107 107
         except ValueError: # a string, so we try a fieldname
108 108
             distFromField = dist
109 109
 
110
-        client = Client(None, apiKey)
110
+        provider = {
111
+            'key': self.getParameterValue(self.IN_KEY),
112
+            'base_url': 'https://api.openrouteservice.org',
113
+            'timeout': 60,
114
+        }
115
+        client = Client(provider, provider['timeout'])
111 116
         pointLayer = getObjectFromUri(self.getParameterValue(self.IN_POINTS))
112 117
 
113 118
         # ORS understands WGS84 only, so we convert all points before sending

+ 28
- 0
utils/__init__.py View File

@@ -0,0 +1,28 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+/***************************************************************************
4
+ OSMtools
5
+                                 A QGIS plugin
6
+ QGIS client to query openrouteservice
7
+                              -------------------
8
+        begin                : 2017-02-01
9
+        git sha              : $Format:%H$
10
+        copyright            : (C) 2017 by Nils Nolde
11
+        email                : nils.nolde@gmail.com
12
+ ***************************************************************************/
13
+
14
+ This plugin provides access to the various APIs from OpenRouteService
15
+ (https://openrouteservice.org), developed and
16
+ maintained by GIScience team at University of Heidelberg, Germany. By using
17
+ this plugin you agree to the ORS terms of service
18
+ (https://openrouteservice.org/terms-of-service/).
19
+
20
+/***************************************************************************
21
+ *                                                                         *
22
+ *   This program is free software; you can redistribute it and/or modify  *
23
+ *   it under the terms of the GNU General Public License as published by  *
24
+ *   the Free Software Foundation; either version 2 of the License, or     *
25
+ *   (at your option) any later version.                                   *
26
+ *                                                                         *
27
+ ***************************************************************************/
28
+"""

+ 70
- 0
utils/configmanager.py View File

@@ -0,0 +1,70 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+/***************************************************************************
4
+ OSMtools
5
+                                 A QGIS plugin
6
+ QGIS client to query openrouteservice
7
+                              -------------------
8
+        begin                : 2017-02-01
9
+        git sha              : $Format:%H$
10
+        copyright            : (C) 2017 by Nils Nolde
11
+        email                : nils.nolde@gmail.com
12
+ ***************************************************************************/
13
+
14
+ This plugin provides access to the various APIs from OpenRouteService
15
+ (https://openrouteservice.org), developed and
16
+ maintained by GIScience team at University of Heidelberg, Germany. By using
17
+ this plugin you agree to the ORS terms of service
18
+ (https://openrouteservice.org/terms-of-service/).
19
+
20
+/***************************************************************************
21
+ *                                                                         *
22
+ *   This program is free software; you can redistribute it and/or modify  *
23
+ *   it under the terms of the GNU General Public License as published by  *
24
+ *   the Free Software Foundation; either version 2 of the License, or     *
25
+ *   (at your option) any later version.                                   *
26
+ *                                                                         *
27
+ ***************************************************************************/
28
+"""
29
+import yaml
30
+import os
31
+
32
+# from OSMtools import CONFIG_PATH
33
+CONFIG_PATH = './orstools.config.yml'
34
+
35
+
36
+def read_config():
37
+    """
38
+    Reads config.yml from file and returns the parsed dict.
39
+
40
+    :returns: Parsed settings dictionary.
41
+    :rtype: dict
42
+    """
43
+    with open(CONFIG_PATH) as f:
44
+        doc = yaml.safe_load(f)
45
+
46
+    return doc
47
+
48
+
49
+def write_config(new_config):
50
+    """
51
+    Dumps new config
52
+
53
+    :param new_config: new provider settings after altering in dialog.
54
+    :type new_config: dict
55
+    """
56
+    with open(CONFIG_PATH, 'w') as f:
57
+        yaml.safe_dump(new_config, f)
58
+
59
+
60
+def write_env_var(key, value):
61
+    """
62
+    Update quota env variables
63
+
64
+    :param key: environment variable to update.
65
+    :type key: str
66
+
67
+    :param value: value for env variable.
68
+    :type value: str
69
+    """
70
+    os.environ[key] = value

+ 138
- 0
utils/convert.py View File

@@ -0,0 +1,138 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+/***************************************************************************
4
+ OSMtools
5
+                                 A QGIS plugin
6
+ QGIS client to query openrouteservice
7
+                              -------------------
8
+        begin                : 2017-02-01
9
+        git sha              : $Format:%H$
10
+        copyright            : (C) 2017 by Nils Nolde
11
+        email                : nils.nolde@gmail.com
12
+ ***************************************************************************/
13
+
14
+ This plugin provides access to the various APIs from OpenRouteService
15
+ (https://openrouteservice.org), developed and
16
+ maintained by GIScience team at University of Heidelberg, Germany. By using
17
+ this plugin you agree to the ORS terms of service
18
+ (https://openrouteservice.org/terms-of-service/).
19
+
20
+/***************************************************************************
21
+ *                                                                         *
22
+ *   This program is free software; you can redistribute it and/or modify  *
23
+ *   it under the terms of the GNU General Public License as published by  *
24
+ *   the Free Software Foundation; either version 2 of the License, or     *
25
+ *   (at your option) any later version.                                   *
26
+ *                                                                         *
27
+ ***************************************************************************/
28
+"""
29
+
30
+
31
+def pipe_list(arg):
32
+    """Convert list of values to pipe-delimited string"""
33
+    if not _is_list(arg):
34
+        raise TypeError(
35
+            "Expected a list or tuple, "
36
+            "but got {}".format(type(arg).__name__))
37
+    return "|".join(map(str, arg))
38
+
39
+
40
+def comma_list(arg):
41
+    """Convert list to comma-separated string"""
42
+    if not _is_list(arg):
43
+        raise TypeError(
44
+            "Expected a list or tuple, "
45
+            "but got {}".format(type(arg).__name__))
46
+    return ",".join(map(str, arg))
47
+
48
+
49
+def _checkBool(boolean):
50
+    """Check whether passed boolean is a string"""
51
+    if boolean not in ["true", "false"]:
52
+        raise ValueError("Give boolean as string 'true' or 'false'.")
53
+
54
+    return
55
+
56
+
57
+def _format_float(arg):
58
+    """Formats a float value to be as short as possible.
59
+
60
+    Trims extraneous trailing zeros and period to give API
61
+    args the best possible chance of fitting within 2000 char
62
+    URL length restrictions.
63
+
64
+    For example:
65
+
66
+    format_float(40) -> "40"
67
+    format_float(40.0) -> "40"
68
+    format_float(40.1) -> "40.1"
69
+    format_float(40.001) -> "40.001"
70
+    format_float(40.0010) -> "40.001"
71
+
72
+    :param arg: The lat or lng float.
73
+    :type arg: float
74
+
75
+    :rtype: string
76
+    """
77
+    return ("{}".format(round(float(arg), 6)).rstrip("0").rstrip("."))
78
+
79
+
80
+def build_coords(arg):
81
+    """Converts one or many lng/lat pair(s) to a comma-separated, pipe
82
+    delimited string. Coordinates will be rounded to 5 digits.
83
+
84
+    For example:
85
+
86
+    convert.build_coords([(151.2069902,-33.8674869),(2.352315,48.513158)])
87
+    # '151.20699,-33.86749|2.35232,48.51316'
88
+
89
+    :param arg: The lat/lon pair(s).
90
+    :type arg: list or tuple
91
+
92
+    :rtype: str
93
+    """
94
+    if _is_list(arg):
95
+        return pipe_list(_concat_coords(arg))
96
+    else:
97
+        raise TypeError(
98
+            "Expected a list or tuple of lng/lat tuples or lists, "
99
+            "but got {}".format(type(arg).__name__))
100
+
101
+
102
+def _concat_coords(arg):
103
+    """Turn the passed coordinate tuple(s) in comma separated coordinate tuple(s).
104
+
105
+    :param arg: coordinate pair(s)
106
+    :type arg: list or tuple
107
+
108
+    :rtype: list of strings
109
+    """
110
+    if all(_is_list(tup) for tup in arg):
111
+        # Check if arg is a list/tuple of lists/tuples
112
+        return [comma_list(map(_format_float, tup)) for tup in arg]
113
+    else:
114
+        return [comma_list(_format_float(coord) for coord in arg)]
115
+
116
+
117
+def _is_list(arg):
118
+    """Checks if arg is list-like."""
119
+    if isinstance(arg, dict):
120
+        return False
121
+    if isinstance(arg, str): # Python 3-only, as str has __iter__
122
+        return False
123
+    return (not _has_method(arg, "strip")
124
+            and _has_method(arg, "__getitem__")
125
+            or _has_method(arg, "__iter__"))
126
+
127
+
128
+def _has_method(arg, method):
129
+    """Returns true if the given object has a method with the given name.
130
+
131
+    :param arg: the object
132
+
133
+    :param method: the method name
134
+    :type method: string
135
+
136
+    :rtype: bool
137
+    """
138
+    return hasattr(arg, method) and callable(getattr(arg, method))

+ 91
- 0
utils/exceptions.py View File

@@ -0,0 +1,91 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+/***************************************************************************
4
+ OSMtools
5
+                                 A QGIS plugin
6
+ QGIS client to query openrouteservice
7
+                              -------------------
8
+        begin                : 2017-02-01
9
+        git sha              : $Format:%H$
10
+        copyright            : (C) 2017 by Nils Nolde
11
+        email                : nils.nolde@gmail.com
12
+ ***************************************************************************/
13
+
14
+ This plugin provides access to the various APIs from OpenRouteService
15
+ (https://openrouteservice.org), developed and
16
+ maintained by GIScience team at University of Heidelberg, Germany. By using
17
+ this plugin you agree to the ORS terms of service
18
+ (https://openrouteservice.org/terms-of-service/).
19
+
20
+/***************************************************************************
21
+ *                                                                         *
22
+ *   This program is free software; you can redistribute it and/or modify  *
23
+ *   it under the terms of the GNU General Public License as published by  *
24
+ *   the Free Software Foundation; either version 2 of the License, or     *
25
+ *   (at your option) any later version.                                   *
26
+ *                                                                         *
27
+ ***************************************************************************/
28
+"""
29
+
30
+"""
31
+Defines exceptions that are thrown by the ORS client.
32
+"""
33
+
34
+
35
+class ApiError(Exception):
36
+    """Represents an exception returned by the remote API."""
37
+    def __init__(self, status, message=None):
38
+        self.status = status
39
+        self.message = message
40
+
41
+    def __str__(self):
42
+        if self.message is None:
43
+            return self.status
44
+        else:
45
+            return "{} ({})".format(self.status, self.message)
46
+
47
+
48
+class InvalidKey(Exception):
49
+    """only called for 403"""
50
+    def __init__(self, status, message):
51
+        self.status = status
52
+        self.message = message
53
+
54
+    def __str__(self):
55
+        if self.message is None:
56
+            return self.status
57
+        else:
58
+            return "{} ({})".format(self.status, self.message)
59
+
60
+
61
+class OverQueryLimit(Exception):
62
+    """Signifies that the request failed because the client exceeded its query rate limit."""
63
+
64
+    def __init__(self, status, message=None):
65
+        self.status = status
66
+        self.message = message
67
+
68
+    def __str__(self):
69
+        if self.message is None:
70
+            return self.status
71
+        else:
72
+            return "{} ({})".format(self.status, self.message)
73
+
74
+
75
+class Timeout(Exception):
76
+    """The request timed out."""
77
+    pass
78
+
79
+
80
+class GenericServerError(Exception):
81
+    """Anything else"""
82
+
83
+    def __init__(self, status, message=None):
84
+        self.status = status
85
+        self.message = message
86
+
87
+    def __str__(self):
88
+        if self.message is None:
89
+            return self.status
90
+        else:
91
+            return "{} ({})".format(self.status, self.message)

+ 51
- 0
utils/logger.py View File

@@ -0,0 +1,51 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+/***************************************************************************
4
+ OSMtools
5
+                                 A QGIS plugin
6
+ QGIS client to query openrouteservice
7
+                              -------------------
8
+        begin                : 2017-02-01
9
+        git sha              : $Format:%H$
10
+        copyright            : (C) 2017 by Nils Nolde
11
+        email                : nils.nolde@gmail.com
12
+ ***************************************************************************/
13
+
14
+ This plugin provides access to the various APIs from OpenRouteService
15
+ (https://openrouteservice.org), developed and
16
+ maintained by GIScience team at University of Heidelberg, Germany. By using
17
+ this plugin you agree to the ORS terms of service
18
+ (https://openrouteservice.org/terms-of-service/).
19
+
20
+/***************************************************************************
21
+ *                                                                         *
22
+ *   This program is free software; you can redistribute it and/or modify  *
23
+ *   it under the terms of the GNU General Public License as published by  *
24
+ *   the Free Software Foundation; either version 2 of the License, or     *
25
+ *   (at your option) any later version.                                   *
26
+ *                                                                         *
27
+ ***************************************************************************/
28
+"""
29
+
30
+from qgis.core import QgsMessageLog
31
+
32
+def log(message, level_in=0):
33
+    """
34
+    Writes to QGIS inbuilt logger accessible through panel.
35
+
36
+    :param message: logging message to write, error or URL.
37
+    :type message: str
38
+
39
+    :param level_in: integer representation of logging level.
40
+    :type level_in: int
41
+    """
42
+    if level_in == 0:
43
+        level = QgsMessageLog.INFO
44
+    elif level_in == 1:
45
+        level = QgsMessageLog.WARNING
46
+    elif level_in == 2:
47
+        level = QgsMessageLog.CRITICAL
48
+    else:
49
+        level = QgsMessageLog.INFO
50
+
51
+    QgsMessageLog.logMessage(message, 'OSMtools', level)

+ 154
- 0
utils/maptools.py View File

@@ -0,0 +1,154 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+/***************************************************************************
4
+ OSMtools
5
+                                 A QGIS plugin
6
+ QGIS client to query openrouteservice
7
+                              -------------------
8
+        begin                : 2017-02-01
9
+        git sha              : $Format:%H$
10
+        copyright            : (C) 2017 by Nils Nolde
11
+        email                : nils.nolde@gmail.com
12
+ ***************************************************************************/
13
+
14
+ This plugin provides access to the various APIs from OpenRouteService
15
+ (https://openrouteservice.org), developed and
16
+ maintained by GIScience team at University of Heidelberg, Germany. By using
17
+ this plugin you agree to the ORS terms of service
18
+ (https://openrouteservice.org/terms-of-service/).
19
+
20
+/***************************************************************************
21
+ *                                                                         *
22
+ *   This program is free software; you can redistribute it and/or modify  *
23
+ *   it under the terms of the GNU General Public License as published by  *
24
+ *   the Free Software Foundation; either version 2 of the License, or     *
25
+ *   (at your option) any later version.                                   *
26
+ *                                                                         *
27
+ ***************************************************************************/
28
+"""
29
+
30
+from PyQt5.QtCore import pyqtSignal
31
+from PyQt5.QtGui import QCursor, QPixmap, QColor
32
+from PyQt5.QtWidgets import QApplication
33
+
34
+from qgis.core import QgsWkbTypes
35
+from qgis.gui import QgsMapToolEmitPoint, QgsRubberBand, QgsVertexMarker
36
+
37
+from OSMtools import RESOURCE_PREFIX, DEFAULT_COLOR
38
+from OSMtools.utils import transform
39
+
40
+
41
+class PointTool(QgsMapToolEmitPoint):
42
+    """Point Map tool to capture mapped coordinates."""
43
+
44
+    def __init__(self, canvas, button):
45
+        """
46
+        :param canvas: current map canvas
47
+        :type: QgsMapCanvas
48
+
49
+        :param button: name of 'Map!' button pressed.
50
+        :type button: str
51
+        """
52
+
53
+        QgsMapToolEmitPoint.__init__(self, canvas)
54
+        self.canvas = canvas
55
+        self.button = button
56
+        self.cursor = QCursor(QPixmap(RESOURCE_PREFIX + 'icon_locate.png').scaledToWidth(48), 24, 24)
57
+
58
+    canvasClicked = pyqtSignal(['QgsPointXY', 'QString'])
59
+    def canvasReleaseEvent(self, event):
60
+        #Get the click and emit a transformed point
61
+
62
+        # mapSettings() was only introduced in QGIS 2.4, keep compatibility
63
+        crsSrc = self.canvas.mapSettings().destinationCrs()
64
+
65
+        point_oldcrs = event.mapPoint()
66
+
67
+        xform = transform.transformToWGS(crsSrc)
68
+        point_newcrs = xform.transform(point_oldcrs)
69
+
70
+        QApplication.restoreOverrideCursor()
71
+
72
+        self.canvasClicked.emit(point_newcrs, self.button)
73
+
74
+    def activate(self):
75
+        QApplication.setOverrideCursor(self.cursor)
76
+
77
+
78
+class LineTool(QgsMapToolEmitPoint):
79
+    """Line Map tool to capture mapped lines."""
80
+
81
+    def __init__(self, canvas):
82
+        """
83
+        :param canvas: current map canvas
84
+        :type canvas: QgsMapCanvas
85
+        """
86
+        self.canvas = canvas
87
+        QgsMapToolEmitPoint.__init__(self, self.canvas)
88
+
89
+        self.rubberBand = QgsRubberBand(self.canvas, False)
90
+        self.rubberBand.setStrokeColor(QColor(DEFAULT_COLOR))
91
+        self.rubberBand.setWidth(3)
92
+
93
+        crsSrc = self.canvas.mapSettings().destinationCrs()
94
+        self.transformer = transform.transformToWGS(crsSrc)
95
+        self.previous_point = None
96
+        self.points = []
97
+        self.markers = []
98
+        self.reset()
99
+
100
+    def reset(self):
101
+        """reset rubberband and captured points."""
102
+
103
+        self.points = []
104
+        # self.isEmittingPoint = False
105
+        self.rubberBand.reset(QgsWkbTypes.LineGeometry)
106
+
107
+    def add_marker(self, point):
108
+        """
109
+        Adds a clicked marker to the map canvas.
110
+
111
+        :param point: point clicked by the user.
112
+        :type point: QgsPointXY
113
+        """
114
+
115
+        new_marker = QgsVertexMarker(self.canvas)
116
+        new_marker.setCenter(point)
117
+        new_marker.setIconType(QgsVertexMarker.ICON_CROSS)
118
+        new_marker.setIconSize(10)
119
+        new_marker.setFillColor(QColor('#485bea'))
120
+        new_marker.setColor(QColor('#000000'))
121
+
122
+        self.markers.append(new_marker)
123
+
124
+    pointDrawn = pyqtSignal(["QgsPointXY", "int"])
125
+    def canvasReleaseEvent(self, e):
126
+        """Add marker to canvas and shows line."""
127
+        new_point = self.toMapCoordinates(e.pos())
128
+        self.add_marker(new_point)
129
+
130
+        self.points.append(new_point)
131
+        self.pointDrawn.emit(self.transformer.transform(new_point), self.points.index(new_point))
132
+        self.showLine()
133
+
134
+    def showLine(self):
135
+        """Builds rubberband from all points and adds it to the map canvas."""
136
+        self.rubberBand.reset(QgsWkbTypes.LineGeometry)
137
+        for point in self.points:
138
+            if point == self.points[-1]:
139
+                self.rubberBand.addPoint(point, True)
140
+            self.rubberBand.addPoint(point, False)
141
+        self.rubberBand.show()
142
+
143
+    doubleClicked = pyqtSignal(['int'])
144
+    def canvasDoubleClickEvent(self, e):
145
+        """Ends line drawing and deletes rubberband and markers from map canvas."""
146
+        self.doubleClicked.emit(len(self.points))
147
+        self.canvas.scene().removeItem(self.rubberBand)
148
+        if self.markers:
149
+            for marker in self.markers:
150
+                self.canvas.scene().removeItem(marker)
151
+
152
+    def deactivate(self):
153
+        super(LineTool, self).deactivate()
154
+        self.deactivated.emit()

+ 49
- 0
utils/transform.py View File

@@ -0,0 +1,49 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+/***************************************************************************
4
+ OSMtools
5
+                                 A QGIS plugin
6
+ QGIS client to query openrouteservice
7
+                              -------------------
8
+        begin                : 2017-02-01
9
+        git sha              : $Format:%H$
10
+        copyright            : (C) 2017 by Nils Nolde
11
+        email                : nils.nolde@gmail.com
12
+ ***************************************************************************/
13
+
14
+ This plugin provides access to the various APIs from OpenRouteService
15
+ (https://openrouteservice.org), developed and
16
+ maintained by GIScience team at University of Heidelberg, Germany. By using
17
+ this plugin you agree to the ORS terms of service
18
+ (https://openrouteservice.org/terms-of-service/).
19
+
20
+/***************************************************************************
21
+ *                                                                         *
22
+ *   This program is free software; you can redistribute it and/or modify  *
23
+ *   it under the terms of the GNU General Public License as published by  *
24
+ *   the Free Software Foundation; either version 2 of the License, or     *
25
+ *   (at your option) any later version.                                   *
26
+ *                                                                         *
27
+ ***************************************************************************/
28
+"""
29
+
30
+from qgis.core import (QgsCoordinateReferenceSystem,
31
+                       QgsCoordinateTransform,
32
+                       QgsProject
33
+                       )
34
+
35
+
36
+def transformToWGS(old_crs):
37
+    """
38
+    Returns a transformer to WGS84
39
+
40
+    :param old_crs: CRS to transfrom from
41
+    :type old_crs: QgsCoordinateReferenceSystem
42
+
43
+    :returns: transformer to use in various modules.
44
+    :rtype: QgsCoordinateTransform
45
+    """
46
+    outCrs = QgsCoordinateReferenceSystem(4326)
47
+    xformer = QgsCoordinateTransform(old_crs, outCrs, QgsProject.instance())
48
+
49
+    return xformer

Loading…
Cancel
Save