Branch data Line data Source code
1 : : /***************************************************************************
2 : : qgsgooglemapsgeocoder.cpp
3 : : ---------------
4 : : Date : November 2020
5 : : Copyright : (C) 2020 by Nyall Dawson
6 : : Email : nyall dot dawson at gmail dot com
7 : : ***************************************************************************
8 : : * *
9 : : * This program is free software; you can redistribute it and/or modify *
10 : : * it under the terms of the GNU General Public License as published by *
11 : : * the Free Software Foundation; either version 2 of the License, or *
12 : : * (at your option) any later version. *
13 : : * *
14 : : ***************************************************************************/
15 : :
16 : : #include "qgsgooglemapsgeocoder.h"
17 : : #include "qgsgeocodercontext.h"
18 : : #include "qgslogger.h"
19 : : #include "qgsnetworkaccessmanager.h"
20 : : #include "qgsblockingnetworkrequest.h"
21 : : #include "qgsreadwritelocker.h"
22 : : #include <QUrl>
23 : : #include <QUrlQuery>
24 : : #include <QNetworkRequest>
25 : : #include <QJsonDocument>
26 : : #include <QJsonObject>
27 : :
28 : 5 : QReadWriteLock QgsGoogleMapsGeocoder::sMutex;
29 : :
30 : : typedef QMap< QUrl, QList< QgsGeocoderResult > > CachedGeocodeResult;
31 : 0 : Q_GLOBAL_STATIC( CachedGeocodeResult, sCachedResults )
32 : :
33 : :
34 : 0 : QgsGoogleMapsGeocoder::QgsGoogleMapsGeocoder( const QString &apiKey, const QString ®ionBias )
35 : 0 : : QgsGeocoderInterface()
36 : 0 : , mApiKey( apiKey )
37 : 0 : , mRegion( regionBias )
38 : 0 : , mEndpoint( QStringLiteral( "https://maps.googleapis.com/maps/api/geocode/json" ) )
39 : 0 : {
40 : :
41 : 0 : }
42 : :
43 : 0 : QgsGeocoderInterface::Flags QgsGoogleMapsGeocoder::flags() const
44 : : {
45 : 0 : return QgsGeocoderInterface::Flag::GeocodesStrings;
46 : : }
47 : :
48 : 0 : QgsFields QgsGoogleMapsGeocoder::appendedFields() const
49 : : {
50 : 0 : QgsFields fields;
51 : 0 : fields.append( QgsField( QStringLiteral( "location_type" ), QVariant::String ) );
52 : 0 : fields.append( QgsField( QStringLiteral( "formatted_address" ), QVariant::String ) );
53 : 0 : fields.append( QgsField( QStringLiteral( "place_id" ), QVariant::String ) );
54 : :
55 : : // add more?
56 : 0 : fields.append( QgsField( QStringLiteral( "street_number" ), QVariant::String ) );
57 : 0 : fields.append( QgsField( QStringLiteral( "route" ), QVariant::String ) );
58 : 0 : fields.append( QgsField( QStringLiteral( "locality" ), QVariant::String ) );
59 : 0 : fields.append( QgsField( QStringLiteral( "administrative_area_level_2" ), QVariant::String ) );
60 : 0 : fields.append( QgsField( QStringLiteral( "administrative_area_level_1" ), QVariant::String ) );
61 : 0 : fields.append( QgsField( QStringLiteral( "country" ), QVariant::String ) );
62 : 0 : fields.append( QgsField( QStringLiteral( "postal_code" ), QVariant::String ) );
63 : 0 : return fields;
64 : 0 : }
65 : :
66 : 0 : QgsWkbTypes::Type QgsGoogleMapsGeocoder::wkbType() const
67 : : {
68 : 0 : return QgsWkbTypes::Point;
69 : : }
70 : :
71 : 0 : QList<QgsGeocoderResult> QgsGoogleMapsGeocoder::geocodeString( const QString &string, const QgsGeocoderContext &context, QgsFeedback *feedback ) const
72 : : {
73 : 0 : QgsRectangle bounds;
74 : 0 : if ( !context.areaOfInterest().isEmpty() )
75 : : {
76 : 0 : QgsGeometry g = context.areaOfInterest();
77 : 0 : QgsCoordinateTransform ct( context.areaOfInterestCrs(), QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ), context.transformContext() );
78 : : try
79 : : {
80 : 0 : g.transform( ct );
81 : 0 : bounds = g.boundingBox();
82 : 0 : }
83 : : catch ( QgsCsException & )
84 : : {
85 : 0 : QgsDebugMsg( "Could not transform geocode bounds to WGS84" );
86 : 0 : }
87 : 0 : }
88 : :
89 : 0 : const QUrl url = requestUrl( string, bounds );
90 : :
91 : 0 : QgsReadWriteLocker locker( sMutex, QgsReadWriteLocker::Read );
92 : 0 : auto it = sCachedResults()->constFind( url );
93 : 0 : if ( it != sCachedResults()->constEnd() )
94 : : {
95 : 0 : return *it;
96 : : }
97 : 0 : locker.unlock();
98 : :
99 : 0 : QNetworkRequest request( url );
100 : 0 : QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsGoogleMapsGeocoder" ) );
101 : :
102 : 0 : QgsBlockingNetworkRequest newReq;
103 : 0 : const QgsBlockingNetworkRequest::ErrorCode errorCode = newReq.get( request, false, feedback );
104 : 0 : if ( errorCode != QgsBlockingNetworkRequest::NoError )
105 : : {
106 : 0 : return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( newReq.errorMessage() );
107 : : }
108 : :
109 : : // Parse data
110 : : QJsonParseError err;
111 : 0 : QJsonDocument doc = QJsonDocument::fromJson( newReq.reply().content(), &err );
112 : 0 : if ( doc.isNull() )
113 : : {
114 : 0 : return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( err.errorString() );
115 : : }
116 : 0 : const QVariantMap res = doc.object().toVariantMap();
117 : 0 : const QString status = res.value( QStringLiteral( "status" ) ).toString();
118 : 0 : if ( status.isEmpty() || !res.contains( QStringLiteral( "results" ) ) )
119 : : {
120 : 0 : return QList<QgsGeocoderResult>();
121 : : }
122 : :
123 : 0 : if ( res.contains( QLatin1String( "error_message" ) ) )
124 : : {
125 : 0 : return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( res.value( QStringLiteral( "error_message" ) ).toString() );
126 : : }
127 : :
128 : 0 : if ( status == QLatin1String( "REQUEST_DENIED" ) || status == QLatin1String( "OVER_QUERY_LIMIT" ) )
129 : : {
130 : 0 : return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( QObject::tr( "Request denied -- the API key was rejected" ) );
131 : : }
132 : 0 : if ( status != QLatin1String( "OK" ) && status != QLatin1String( "ZERO_RESULTS" ) )
133 : : {
134 : 0 : return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( res.value( QStringLiteral( "status" ) ).toString() );
135 : : }
136 : :
137 : : // all good!
138 : 0 : locker.changeMode( QgsReadWriteLocker::Write );
139 : :
140 : 0 : const QVariantList results = res.value( QStringLiteral( "results" ) ).toList();
141 : 0 : if ( results.empty() )
142 : : {
143 : 0 : sCachedResults()->insert( url, QList<QgsGeocoderResult>() );
144 : 0 : return QList<QgsGeocoderResult>();
145 : : }
146 : :
147 : 0 : QList< QgsGeocoderResult > matches;
148 : 0 : matches.reserve( results.size( ) );
149 : 0 : for ( const QVariant &result : results )
150 : : {
151 : 0 : matches << jsonToResult( result.toMap() );
152 : : }
153 : 0 : sCachedResults()->insert( url, matches );
154 : :
155 : 0 : return matches;
156 : 0 : }
157 : :
158 : 0 : QUrl QgsGoogleMapsGeocoder::requestUrl( const QString &address, const QgsRectangle &bounds ) const
159 : : {
160 : 0 : QUrl res( mEndpoint );
161 : 0 : QUrlQuery query;
162 : 0 : if ( !bounds.isNull() )
163 : : {
164 : 0 : query.addQueryItem( QStringLiteral( "bounds" ), QStringLiteral( "%1,%2|%3,%4" ).arg( bounds.yMinimum() )
165 : 0 : .arg( bounds.xMinimum() )
166 : 0 : .arg( bounds.yMaximum() )
167 : 0 : .arg( bounds.yMinimum() ) );
168 : 0 : }
169 : 0 : if ( !mRegion.isEmpty() )
170 : : {
171 : 0 : query.addQueryItem( QStringLiteral( "region" ), mRegion.toLower() );
172 : 0 : }
173 : 0 : query.addQueryItem( QStringLiteral( "sensor" ), QStringLiteral( "false" ) );
174 : 0 : query.addQueryItem( QStringLiteral( "address" ), address );
175 : 0 : query.addQueryItem( QStringLiteral( "key" ), mApiKey );
176 : 0 : res.setQuery( query );
177 : :
178 : :
179 : 0 : if ( res.toString().contains( QLatin1String( "fake_qgis_http_endpoint" ) ) )
180 : : {
181 : : // Just for testing with local files instead of http:// resources
182 : 0 : QString modifiedUrlString = res.toString();
183 : : // Qt5 does URL encoding from some reason (of the FILTER parameter for example)
184 : 0 : modifiedUrlString = QUrl::fromPercentEncoding( modifiedUrlString.toUtf8() );
185 : 0 : modifiedUrlString.replace( QLatin1String( "fake_qgis_http_endpoint/" ), QLatin1String( "fake_qgis_http_endpoint_" ) );
186 : 0 : QgsDebugMsg( QStringLiteral( "Get %1" ).arg( modifiedUrlString ) );
187 : 0 : modifiedUrlString = modifiedUrlString.mid( QStringLiteral( "http://" ).size() );
188 : 0 : QString args = modifiedUrlString.mid( modifiedUrlString.indexOf( '?' ) );
189 : 0 : if ( modifiedUrlString.size() > 150 )
190 : : {
191 : 0 : args = QCryptographicHash::hash( args.toUtf8(), QCryptographicHash::Md5 ).toHex();
192 : 0 : }
193 : : else
194 : : {
195 : 0 : args.replace( QLatin1String( "?" ), QLatin1String( "_" ) );
196 : 0 : args.replace( QLatin1String( "&" ), QLatin1String( "_" ) );
197 : 0 : args.replace( QLatin1String( "<" ), QLatin1String( "_" ) );
198 : 0 : args.replace( QLatin1String( ">" ), QLatin1String( "_" ) );
199 : 0 : args.replace( QLatin1String( "'" ), QLatin1String( "_" ) );
200 : 0 : args.replace( QLatin1String( "\"" ), QLatin1String( "_" ) );
201 : 0 : args.replace( QLatin1String( " " ), QLatin1String( "_" ) );
202 : 0 : args.replace( QLatin1String( ":" ), QLatin1String( "_" ) );
203 : 0 : args.replace( QLatin1String( "/" ), QLatin1String( "_" ) );
204 : 0 : args.replace( QLatin1String( "\n" ), QLatin1String( "_" ) );
205 : : }
206 : : #ifdef Q_OS_WIN
207 : : // Passing "urls" like "http://c:/path" to QUrl 'eats' the : after c,
208 : : // so we must restore it
209 : : if ( modifiedUrlString[1] == '/' )
210 : : {
211 : : modifiedUrlString = modifiedUrlString[0] + ":/" + modifiedUrlString.mid( 2 );
212 : : }
213 : : #endif
214 : 0 : modifiedUrlString = modifiedUrlString.mid( 0, modifiedUrlString.indexOf( '?' ) ) + args;
215 : 0 : QgsDebugMsg( QStringLiteral( "Get %1 (after laundering)" ).arg( modifiedUrlString ) );
216 : 0 : res = QUrl::fromLocalFile( modifiedUrlString );
217 : 0 : }
218 : :
219 : 0 : return res;
220 : 0 : }
221 : :
222 : 0 : QgsGeocoderResult QgsGoogleMapsGeocoder::jsonToResult( const QVariantMap &json ) const
223 : : {
224 : 0 : const QVariantMap geometry = json.value( QStringLiteral( "geometry" ) ).toMap();
225 : 0 : const QVariantMap location = geometry.value( QStringLiteral( "location" ) ).toMap();
226 : 0 : const double latitude = location.value( QStringLiteral( "lat" ) ).toDouble();
227 : 0 : const double longitude = location.value( QStringLiteral( "lng" ) ).toDouble();
228 : :
229 : 0 : const QgsGeometry geom = QgsGeometry::fromPointXY( QgsPointXY( longitude, latitude ) );
230 : :
231 : 0 : QgsGeocoderResult res( json.value( QStringLiteral( "formatted_address" ) ).toString(),
232 : : geom,
233 : 0 : QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) );
234 : :
235 : 0 : QVariantMap attributes;
236 : :
237 : 0 : if ( json.contains( QStringLiteral( "formatted_address" ) ) )
238 : 0 : attributes.insert( QStringLiteral( "formatted_address" ), json.value( QStringLiteral( "formatted_address" ) ).toString() );
239 : 0 : if ( json.contains( QStringLiteral( "place_id" ) ) )
240 : 0 : attributes.insert( QStringLiteral( "place_id" ), json.value( QStringLiteral( "place_id" ) ).toString() );
241 : 0 : if ( geometry.contains( QStringLiteral( "location_type" ) ) )
242 : 0 : attributes.insert( QStringLiteral( "location_type" ), geometry.value( QStringLiteral( "location_type" ) ).toString() );
243 : :
244 : 0 : const QVariantList components = json.value( QStringLiteral( "address_components" ) ).toList();
245 : 0 : for ( const QVariant &component : components )
246 : : {
247 : 0 : const QVariantMap componentMap = component.toMap();
248 : 0 : const QStringList types = componentMap.value( QStringLiteral( "types" ) ).toStringList();
249 : :
250 : 0 : for ( const QString &t :
251 : 0 : {
252 : 0 : QStringLiteral( "street_number" ),
253 : 0 : QStringLiteral( "route" ),
254 : 0 : QStringLiteral( "locality" ),
255 : 0 : QStringLiteral( "administrative_area_level_2" ),
256 : 0 : QStringLiteral( "administrative_area_level_1" ),
257 : 0 : QStringLiteral( "country" ),
258 : 0 : QStringLiteral( "postal_code" )
259 : : } )
260 : : {
261 : 0 : if ( types.contains( t ) )
262 : : {
263 : 0 : attributes.insert( t, componentMap.value( QStringLiteral( "long_name" ) ).toString() );
264 : 0 : if ( t == QLatin1String( "administrative_area_level_1" ) )
265 : 0 : res.setGroup( componentMap.value( QStringLiteral( "long_name" ) ).toString() );
266 : 0 : }
267 : : }
268 : 0 : }
269 : :
270 : 0 : if ( geometry.contains( QStringLiteral( "viewport" ) ) )
271 : : {
272 : 0 : const QVariantMap viewport = geometry.value( QStringLiteral( "viewport" ) ).toMap();
273 : 0 : const QVariantMap northEast = viewport.value( QStringLiteral( "northeast" ) ).toMap();
274 : 0 : const QVariantMap southWest = viewport.value( QStringLiteral( "southwest" ) ).toMap();
275 : 0 : res.setViewport( QgsRectangle( southWest.value( QStringLiteral( "lng" ) ).toDouble(),
276 : 0 : southWest.value( QStringLiteral( "lat" ) ).toDouble(),
277 : 0 : northEast.value( QStringLiteral( "lng" ) ).toDouble(),
278 : 0 : northEast.value( QStringLiteral( "lat" ) ).toDouble()
279 : : ) );
280 : 0 : }
281 : :
282 : 0 : res.setAdditionalAttributes( attributes );
283 : 0 : return res;
284 : 0 : }
285 : :
286 : 0 : void QgsGoogleMapsGeocoder::setEndpoint( const QString &endpoint )
287 : : {
288 : 0 : mEndpoint = endpoint;
289 : 0 : }
290 : :
291 : 0 : QString QgsGoogleMapsGeocoder::apiKey() const
292 : : {
293 : 0 : return mApiKey;
294 : : }
295 : :
296 : 0 : void QgsGoogleMapsGeocoder::setApiKey( const QString &apiKey )
297 : : {
298 : 0 : mApiKey = apiKey;
299 : 0 : }
300 : :
301 : 0 : QString QgsGoogleMapsGeocoder::region() const
302 : : {
303 : 0 : return mRegion;
304 : : }
305 : :
306 : 0 : void QgsGoogleMapsGeocoder::setRegion( const QString ®ion )
307 : : {
308 : 0 : mRegion = region;
309 : 0 : }
|