QGIS API Documentation 3.99.0-Master (26c88405ac0)
Loading...
Searching...
No Matches
qgsgooglemapsgeocoder.cpp
Go to the documentation of this file.
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
17
20#include "qgsgeocodercontext.h"
21#include "qgslogger.h"
23#include "qgsreadwritelocker.h"
25
26#include <QCryptographicHash>
27#include <QJsonDocument>
28#include <QJsonObject>
29#include <QNetworkRequest>
30#include <QUrl>
31#include <QUrlQuery>
32
33QReadWriteLock QgsGoogleMapsGeocoder::sMutex;
34
35typedef QMap< QUrl, QList< QgsGeocoderResult > > CachedGeocodeResult;
36Q_GLOBAL_STATIC( CachedGeocodeResult, sCachedResultsGM )
37
38
39QgsGoogleMapsGeocoder::QgsGoogleMapsGeocoder( const QString &apiKey, const QString &regionBias )
41 , mApiKey( apiKey )
42 , mRegion( regionBias )
43 , mEndpoint( QStringLiteral( "https://maps.googleapis.com/maps/api/geocode/json" ) )
44{
45
46}
47
52
54{
55 QgsFields fields;
56 fields.append( QgsField( QStringLiteral( "location_type" ), QMetaType::Type::QString ) );
57 fields.append( QgsField( QStringLiteral( "formatted_address" ), QMetaType::Type::QString ) );
58 fields.append( QgsField( QStringLiteral( "place_id" ), QMetaType::Type::QString ) );
59
60 // add more?
61 fields.append( QgsField( QStringLiteral( "street_number" ), QMetaType::Type::QString ) );
62 fields.append( QgsField( QStringLiteral( "route" ), QMetaType::Type::QString ) );
63 fields.append( QgsField( QStringLiteral( "locality" ), QMetaType::Type::QString ) );
64 fields.append( QgsField( QStringLiteral( "administrative_area_level_2" ), QMetaType::Type::QString ) );
65 fields.append( QgsField( QStringLiteral( "administrative_area_level_1" ), QMetaType::Type::QString ) );
66 fields.append( QgsField( QStringLiteral( "country" ), QMetaType::Type::QString ) );
67 fields.append( QgsField( QStringLiteral( "postal_code" ), QMetaType::Type::QString ) );
68 return fields;
69}
70
75
76QList<QgsGeocoderResult> QgsGoogleMapsGeocoder::geocodeString( const QString &string, const QgsGeocoderContext &context, QgsFeedback *feedback ) const
77{
78 QgsRectangle bounds;
79 if ( !context.areaOfInterest().isEmpty() )
80 {
81 QgsGeometry g = context.areaOfInterest();
82 const QgsCoordinateTransform ct( context.areaOfInterestCrs(), QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ), context.transformContext() );
83 try
84 {
85 g.transform( ct );
86 bounds = g.boundingBox();
87 }
88 catch ( QgsCsException & )
89 {
90 QgsDebugError( "Could not transform geocode bounds to WGS84" );
91 }
92 }
93
94 const QUrl url = requestUrl( string, bounds );
95
97 const auto it = sCachedResultsGM()->constFind( url );
98 if ( it != sCachedResultsGM()->constEnd() )
99 {
100 return *it;
101 }
102 locker.unlock();
103
104 QNetworkRequest request( url );
105 QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsGoogleMapsGeocoder" ) );
106
108 const QgsBlockingNetworkRequest::ErrorCode errorCode = newReq.get( request, false, feedback );
109 if ( errorCode != QgsBlockingNetworkRequest::NoError )
110 {
111 return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( newReq.errorMessage() );
112 }
113
114 // Parse data
115 QJsonParseError err;
116 const QJsonDocument doc = QJsonDocument::fromJson( newReq.reply().content(), &err );
117 if ( doc.isNull() )
118 {
119 return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( err.errorString() );
120 }
121 const QVariantMap res = doc.object().toVariantMap();
122 const QString status = res.value( QStringLiteral( "status" ) ).toString();
123 if ( status.isEmpty() || !res.contains( QStringLiteral( "results" ) ) )
124 {
125 return QList<QgsGeocoderResult>();
126 }
127
128 if ( res.contains( QLatin1String( "error_message" ) ) )
129 {
130 return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( res.value( QStringLiteral( "error_message" ) ).toString() );
131 }
132
133 if ( status == QLatin1String( "REQUEST_DENIED" ) || status == QLatin1String( "OVER_QUERY_LIMIT" ) )
134 {
135 return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( QObject::tr( "Request denied -- the API key was rejected" ) );
136 }
137 if ( status != QLatin1String( "OK" ) && status != QLatin1String( "ZERO_RESULTS" ) )
138 {
139 return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( res.value( QStringLiteral( "status" ) ).toString() );
140 }
141
142 // all good!
144
145 const QVariantList results = res.value( QStringLiteral( "results" ) ).toList();
146 if ( results.empty() )
147 {
148 sCachedResultsGM()->insert( url, QList<QgsGeocoderResult>() );
149 return QList<QgsGeocoderResult>();
150 }
151
152 QList< QgsGeocoderResult > matches;
153 matches.reserve( results.size( ) );
154 for ( const QVariant &result : results )
155 {
156 matches << jsonToResult( result.toMap() );
157 }
158 sCachedResultsGM()->insert( url, matches );
159
160 return matches;
161}
162
163QUrl QgsGoogleMapsGeocoder::requestUrl( const QString &address, const QgsRectangle &bounds ) const
164{
165 QUrl res( mEndpoint );
166 QUrlQuery query;
167 if ( !bounds.isNull() )
168 {
169 query.addQueryItem( QStringLiteral( "bounds" ), QStringLiteral( "%1,%2|%3,%4" ).arg( bounds.yMinimum() )
170 .arg( bounds.xMinimum() )
171 .arg( bounds.yMaximum() )
172 .arg( bounds.yMinimum() ) );
173 }
174 if ( !mRegion.isEmpty() )
175 {
176 query.addQueryItem( QStringLiteral( "region" ), mRegion.toLower() );
177 }
178 query.addQueryItem( QStringLiteral( "sensor" ), QStringLiteral( "false" ) );
179 query.addQueryItem( QStringLiteral( "address" ), address );
180 query.addQueryItem( QStringLiteral( "key" ), mApiKey );
181 res.setQuery( query );
182
183
184 if ( res.toString().contains( QLatin1String( "fake_qgis_http_endpoint" ) ) )
185 {
186 // Just for testing with local files instead of http:// resources
187 QString modifiedUrlString = res.toString();
188 // Qt5 does URL encoding from some reason (of the FILTER parameter for example)
189 modifiedUrlString = QUrl::fromPercentEncoding( modifiedUrlString.toUtf8() );
190 modifiedUrlString.replace( QLatin1String( "fake_qgis_http_endpoint/" ), QLatin1String( "fake_qgis_http_endpoint_" ) );
191 QgsDebugMsgLevel( QStringLiteral( "Get %1" ).arg( modifiedUrlString ), 2 );
192 modifiedUrlString = modifiedUrlString.mid( QStringLiteral( "http://" ).size() );
193 QString args = modifiedUrlString.mid( modifiedUrlString.indexOf( '?' ) );
194 if ( modifiedUrlString.size() > 150 )
195 {
196 args = QCryptographicHash::hash( args.toUtf8(), QCryptographicHash::Md5 ).toHex();
197 }
198 else
199 {
200 args.replace( QLatin1String( "?" ), QLatin1String( "_" ) );
201 args.replace( QLatin1String( "&" ), QLatin1String( "_" ) );
202 args.replace( QLatin1String( "<" ), QLatin1String( "_" ) );
203 args.replace( QLatin1String( ">" ), QLatin1String( "_" ) );
204 args.replace( QLatin1String( "'" ), QLatin1String( "_" ) );
205 args.replace( QLatin1String( "\"" ), QLatin1String( "_" ) );
206 args.replace( QLatin1String( " " ), QLatin1String( "_" ) );
207 args.replace( QLatin1String( ":" ), QLatin1String( "_" ) );
208 args.replace( QLatin1String( "/" ), QLatin1String( "_" ) );
209 args.replace( QLatin1String( "\n" ), QLatin1String( "_" ) );
210 }
211#ifdef Q_OS_WIN
212 // Passing "urls" like "http://c:/path" to QUrl 'eats' the : after c,
213 // so we must restore it
214 if ( modifiedUrlString[1] == '/' )
215 {
216 modifiedUrlString = modifiedUrlString[0] + ":/" + modifiedUrlString.mid( 2 );
217 }
218#endif
219 modifiedUrlString = modifiedUrlString.mid( 0, modifiedUrlString.indexOf( '?' ) ) + args;
220 QgsDebugMsgLevel( QStringLiteral( "Get %1 (after laundering)" ).arg( modifiedUrlString ), 2 );
221 res = QUrl::fromLocalFile( modifiedUrlString );
222 }
223
224 return res;
225}
226
228{
229 const QVariantMap geometry = json.value( QStringLiteral( "geometry" ) ).toMap();
230 const QVariantMap location = geometry.value( QStringLiteral( "location" ) ).toMap();
231 const double latitude = location.value( QStringLiteral( "lat" ) ).toDouble();
232 const double longitude = location.value( QStringLiteral( "lng" ) ).toDouble();
233
234 const QgsGeometry geom = QgsGeometry::fromPointXY( QgsPointXY( longitude, latitude ) );
235
236 QgsGeocoderResult res( json.value( QStringLiteral( "formatted_address" ) ).toString(),
237 geom,
238 QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) );
239
240 QVariantMap attributes;
241
242 if ( json.contains( QStringLiteral( "formatted_address" ) ) )
243 attributes.insert( QStringLiteral( "formatted_address" ), json.value( QStringLiteral( "formatted_address" ) ).toString() );
244 if ( json.contains( QStringLiteral( "place_id" ) ) )
245 attributes.insert( QStringLiteral( "place_id" ), json.value( QStringLiteral( "place_id" ) ).toString() );
246 if ( geometry.contains( QStringLiteral( "location_type" ) ) )
247 attributes.insert( QStringLiteral( "location_type" ), geometry.value( QStringLiteral( "location_type" ) ).toString() );
248
249 const QVariantList components = json.value( QStringLiteral( "address_components" ) ).toList();
250 for ( const QVariant &component : components )
251 {
252 const QVariantMap componentMap = component.toMap();
253 const QStringList types = componentMap.value( QStringLiteral( "types" ) ).toStringList();
254
255 for ( const QString &t :
256 {
257 QStringLiteral( "street_number" ),
258 QStringLiteral( "route" ),
259 QStringLiteral( "locality" ),
260 QStringLiteral( "administrative_area_level_2" ),
261 QStringLiteral( "administrative_area_level_1" ),
262 QStringLiteral( "country" ),
263 QStringLiteral( "postal_code" )
264 } )
265 {
266 if ( types.contains( t ) )
267 {
268 attributes.insert( t, componentMap.value( QStringLiteral( "long_name" ) ).toString() );
269 if ( t == QLatin1String( "administrative_area_level_1" ) )
270 res.setGroup( componentMap.value( QStringLiteral( "long_name" ) ).toString() );
271 }
272 }
273 }
274
275 if ( geometry.contains( QStringLiteral( "viewport" ) ) )
276 {
277 const QVariantMap viewport = geometry.value( QStringLiteral( "viewport" ) ).toMap();
278 const QVariantMap northEast = viewport.value( QStringLiteral( "northeast" ) ).toMap();
279 const QVariantMap southWest = viewport.value( QStringLiteral( "southwest" ) ).toMap();
280 res.setViewport( QgsRectangle( southWest.value( QStringLiteral( "lng" ) ).toDouble(),
281 southWest.value( QStringLiteral( "lat" ) ).toDouble(),
282 northEast.value( QStringLiteral( "lng" ) ).toDouble(),
283 northEast.value( QStringLiteral( "lat" ) ).toDouble()
284 ) );
285 }
286
287 res.setAdditionalAttributes( attributes );
288 return res;
289}
290
291void QgsGoogleMapsGeocoder::setEndpoint( const QString &endpoint )
292{
293 mEndpoint = endpoint;
294}
295
297{
298 return mApiKey;
299}
300
302{
303 mApiKey = apiKey;
304}
305
307{
308 return mRegion;
309}
310
312{
313 mRegion = region;
314}
WkbType
The WKB type describes the number of dimensions a geometry has.
Definition qgis.h:277
@ Point
Point.
Definition qgis.h:279
A thread safe class for performing blocking (sync) network requests, with full support for QGIS proxy...
QString errorMessage() const
Returns the error message string, after a get(), post(), head() or put() request has been made.
ErrorCode get(QNetworkRequest &request, bool forceRefresh=false, QgsFeedback *feedback=nullptr, RequestFlags requestFlags=QgsBlockingNetworkRequest::RequestFlags())
Performs a "get" operation on the specified request.
@ NoError
No error was encountered.
QgsNetworkReplyContent reply() const
Returns the content of the network reply, after a get(), post(), head() or put() request has been mad...
Represents a coordinate reference system (CRS).
Handles coordinate transforms between two coordinate systems.
Custom exception class for Coordinate Reference System related exceptions.
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition qgsfeedback.h:44
Encapsulate a field in an attribute table or data source.
Definition qgsfield.h:54
Container of fields for a vector layer.
Definition qgsfields.h:46
bool append(const QgsField &field, Qgis::FieldOrigin origin=Qgis::FieldOrigin::Provider, int originIndex=-1)
Appends a field.
Definition qgsfields.cpp:73
Encapsulates the context of a geocoding operation.
QgsCoordinateTransformContext transformContext() const
Returns the coordinate transform context, which should be used whenever the geocoder constructs a coo...
QgsCoordinateReferenceSystem areaOfInterestCrs() const
Returns the coordinate reference system for the area of interest, which can be used to indicate the d...
QgsGeometry areaOfInterest() const
Returns the optional area of interest, which can be used to indicate the desired geographic area wher...
Interface for geocoders.
Definition qgsgeocoder.h:37
@ GeocodesStrings
Can geocode string input values.
Definition qgsgeocoder.h:44
QFlags< Flag > Flags
Definition qgsgeocoder.h:47
Represents a matching result from a geocoder search.
void setAdditionalAttributes(const QVariantMap &attributes)
Setss additional attributes generated during the geocode, which may be added to features being geocod...
void setGroup(const QString &group)
Sets the optional group value for the result.
void setViewport(const QgsRectangle &viewport)
Sets the suggested viewport for the result, which reflects a recommended map extent for displaying th...
static QgsGeocoderResult errorResult(const QString &errorMessage)
Creates an invalid error result, with the specified errorMessage string.
A geometry is the spatial representation of a feature.
Qgis::GeometryOperationResult transform(const QgsCoordinateTransform &ct, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward, bool transformZ=false)
Transforms this geometry as described by the coordinate transform ct.
static QgsGeometry fromPointXY(const QgsPointXY &point)
Creates a new geometry from a QgsPointXY object.
bool isEmpty() const
Returns true if the geometry is empty (eg a linestring with no vertices, or a collection with no geom...
QgsRectangle boundingBox() const
Returns the bounding box of the geometry.
QList< QgsGeocoderResult > geocodeString(const QString &string, const QgsGeocoderContext &context, QgsFeedback *feedback=nullptr) const override
Geocodes a string.
void setEndpoint(const QString &endpoint)
Sets a specific API endpoint to use for requests.
QgsGeocoderResult jsonToResult(const QVariantMap &json) const
Converts a JSON result returned from the Google Maps service to a geocoder result object.
QgsFields appendedFields() const override
Returns a set of newly created fields which will be appended to existing features during the geocode ...
QString apiKey() const
Returns the API key which will be used when accessing the Google Maps API.
Qgis::WkbType wkbType() const override
Returns the WKB type of geometries returned by the geocoder.
QString region() const
Returns the optional region bias which will be used to prioritize results in a certain region.
QUrl requestUrl(const QString &address, const QgsRectangle &bounds=QgsRectangle()) const
Returns the URL generated for geocoding the specified address.
QgsGoogleMapsGeocoder(const QString &apiKey, const QString &regionBias=QString())
Constructor for QgsGoogleMapsGeocoder.
void setRegion(const QString &region)
Sets the optional region bias which will be used to prioritize results in a certain region.
void setApiKey(const QString &key)
Sets the API key to use when accessing the Google Maps API.
Flags flags() const override
Returns the geocoder's capability flags.
QByteArray content() const
Returns the reply content.
Represents a 2D point.
Definition qgspointxy.h:60
A convenience class that simplifies locking and unlocking QReadWriteLocks.
@ Write
Lock for write.
void unlock()
Unlocks the lock.
void changeMode(Mode mode)
Change the mode of the lock to mode.
A rectangle specified with double values.
double xMinimum
double yMinimum
double yMaximum
Q_GLOBAL_STATIC(QReadWriteLock, sDefinitionCacheLock)
QMap< QUrl, QList< QgsGeocoderResult > > CachedGeocodeResult
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:61
#define QgsDebugError(str)
Definition qgslogger.h:57
#define QgsSetRequestInitiatorClass(request, _class)