QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
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#include "qgsgeocodercontext.h"
18#include "qgslogger.h"
22#include "qgsreadwritelocker.h"
24#include <QUrl>
25#include <QUrlQuery>
26#include <QNetworkRequest>
27#include <QJsonDocument>
28#include <QJsonObject>
29
30QReadWriteLock QgsGoogleMapsGeocoder::sMutex;
31
32typedef QMap< QUrl, QList< QgsGeocoderResult > > CachedGeocodeResult;
33Q_GLOBAL_STATIC( CachedGeocodeResult, sCachedResults )
34
35
36QgsGoogleMapsGeocoder::QgsGoogleMapsGeocoder( const QString &apiKey, const QString &regionBias )
38 , mApiKey( apiKey )
39 , mRegion( regionBias )
40 , mEndpoint( QStringLiteral( "https://maps.googleapis.com/maps/api/geocode/json" ) )
41{
42
43}
44
46{
48}
49
51{
52 QgsFields fields;
53 fields.append( QgsField( QStringLiteral( "location_type" ), QVariant::String ) );
54 fields.append( QgsField( QStringLiteral( "formatted_address" ), QVariant::String ) );
55 fields.append( QgsField( QStringLiteral( "place_id" ), QVariant::String ) );
56
57 // add more?
58 fields.append( QgsField( QStringLiteral( "street_number" ), QVariant::String ) );
59 fields.append( QgsField( QStringLiteral( "route" ), QVariant::String ) );
60 fields.append( QgsField( QStringLiteral( "locality" ), QVariant::String ) );
61 fields.append( QgsField( QStringLiteral( "administrative_area_level_2" ), QVariant::String ) );
62 fields.append( QgsField( QStringLiteral( "administrative_area_level_1" ), QVariant::String ) );
63 fields.append( QgsField( QStringLiteral( "country" ), QVariant::String ) );
64 fields.append( QgsField( QStringLiteral( "postal_code" ), QVariant::String ) );
65 return fields;
66}
67
69{
71}
72
73QList<QgsGeocoderResult> QgsGoogleMapsGeocoder::geocodeString( const QString &string, const QgsGeocoderContext &context, QgsFeedback *feedback ) const
74{
75 QgsRectangle bounds;
76 if ( !context.areaOfInterest().isEmpty() )
77 {
78 QgsGeometry g = context.areaOfInterest();
79 const QgsCoordinateTransform ct( context.areaOfInterestCrs(), QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ), context.transformContext() );
80 try
81 {
82 g.transform( ct );
83 bounds = g.boundingBox();
84 }
85 catch ( QgsCsException & )
86 {
87 QgsDebugError( "Could not transform geocode bounds to WGS84" );
88 }
89 }
90
91 const QUrl url = requestUrl( string, bounds );
92
94 const auto it = sCachedResults()->constFind( url );
95 if ( it != sCachedResults()->constEnd() )
96 {
97 return *it;
98 }
99 locker.unlock();
100
101 QNetworkRequest request( url );
102 QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsGoogleMapsGeocoder" ) );
103
105 const QgsBlockingNetworkRequest::ErrorCode errorCode = newReq.get( request, false, feedback );
106 if ( errorCode != QgsBlockingNetworkRequest::NoError )
107 {
108 return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( newReq.errorMessage() );
109 }
110
111 // Parse data
112 QJsonParseError err;
113 const QJsonDocument doc = QJsonDocument::fromJson( newReq.reply().content(), &err );
114 if ( doc.isNull() )
115 {
116 return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( err.errorString() );
117 }
118 const QVariantMap res = doc.object().toVariantMap();
119 const QString status = res.value( QStringLiteral( "status" ) ).toString();
120 if ( status.isEmpty() || !res.contains( QStringLiteral( "results" ) ) )
121 {
122 return QList<QgsGeocoderResult>();
123 }
124
125 if ( res.contains( QLatin1String( "error_message" ) ) )
126 {
127 return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( res.value( QStringLiteral( "error_message" ) ).toString() );
128 }
129
130 if ( status == QLatin1String( "REQUEST_DENIED" ) || status == QLatin1String( "OVER_QUERY_LIMIT" ) )
131 {
132 return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( QObject::tr( "Request denied -- the API key was rejected" ) );
133 }
134 if ( status != QLatin1String( "OK" ) && status != QLatin1String( "ZERO_RESULTS" ) )
135 {
136 return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( res.value( QStringLiteral( "status" ) ).toString() );
137 }
138
139 // all good!
141
142 const QVariantList results = res.value( QStringLiteral( "results" ) ).toList();
143 if ( results.empty() )
144 {
145 sCachedResults()->insert( url, QList<QgsGeocoderResult>() );
146 return QList<QgsGeocoderResult>();
147 }
148
149 QList< QgsGeocoderResult > matches;
150 matches.reserve( results.size( ) );
151 for ( const QVariant &result : results )
152 {
153 matches << jsonToResult( result.toMap() );
154 }
155 sCachedResults()->insert( url, matches );
156
157 return matches;
158}
159
160QUrl QgsGoogleMapsGeocoder::requestUrl( const QString &address, const QgsRectangle &bounds ) const
161{
162 QUrl res( mEndpoint );
163 QUrlQuery query;
164 if ( !bounds.isNull() )
165 {
166 query.addQueryItem( QStringLiteral( "bounds" ), QStringLiteral( "%1,%2|%3,%4" ).arg( bounds.yMinimum() )
167 .arg( bounds.xMinimum() )
168 .arg( bounds.yMaximum() )
169 .arg( bounds.yMinimum() ) );
170 }
171 if ( !mRegion.isEmpty() )
172 {
173 query.addQueryItem( QStringLiteral( "region" ), mRegion.toLower() );
174 }
175 query.addQueryItem( QStringLiteral( "sensor" ), QStringLiteral( "false" ) );
176 query.addQueryItem( QStringLiteral( "address" ), address );
177 query.addQueryItem( QStringLiteral( "key" ), mApiKey );
178 res.setQuery( query );
179
180
181 if ( res.toString().contains( QLatin1String( "fake_qgis_http_endpoint" ) ) )
182 {
183 // Just for testing with local files instead of http:// resources
184 QString modifiedUrlString = res.toString();
185 // Qt5 does URL encoding from some reason (of the FILTER parameter for example)
186 modifiedUrlString = QUrl::fromPercentEncoding( modifiedUrlString.toUtf8() );
187 modifiedUrlString.replace( QLatin1String( "fake_qgis_http_endpoint/" ), QLatin1String( "fake_qgis_http_endpoint_" ) );
188 QgsDebugMsgLevel( QStringLiteral( "Get %1" ).arg( modifiedUrlString ), 2 );
189 modifiedUrlString = modifiedUrlString.mid( QStringLiteral( "http://" ).size() );
190 QString args = modifiedUrlString.mid( modifiedUrlString.indexOf( '?' ) );
191 if ( modifiedUrlString.size() > 150 )
192 {
193 args = QCryptographicHash::hash( args.toUtf8(), QCryptographicHash::Md5 ).toHex();
194 }
195 else
196 {
197 args.replace( QLatin1String( "?" ), QLatin1String( "_" ) );
198 args.replace( QLatin1String( "&" ), QLatin1String( "_" ) );
199 args.replace( QLatin1String( "<" ), QLatin1String( "_" ) );
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( "\n" ), QLatin1String( "_" ) );
207 }
208#ifdef Q_OS_WIN
209 // Passing "urls" like "http://c:/path" to QUrl 'eats' the : after c,
210 // so we must restore it
211 if ( modifiedUrlString[1] == '/' )
212 {
213 modifiedUrlString = modifiedUrlString[0] + ":/" + modifiedUrlString.mid( 2 );
214 }
215#endif
216 modifiedUrlString = modifiedUrlString.mid( 0, modifiedUrlString.indexOf( '?' ) ) + args;
217 QgsDebugMsgLevel( QStringLiteral( "Get %1 (after laundering)" ).arg( modifiedUrlString ), 2 );
218 res = QUrl::fromLocalFile( modifiedUrlString );
219 }
220
221 return res;
222}
223
225{
226 const QVariantMap geometry = json.value( QStringLiteral( "geometry" ) ).toMap();
227 const QVariantMap location = geometry.value( QStringLiteral( "location" ) ).toMap();
228 const double latitude = location.value( QStringLiteral( "lat" ) ).toDouble();
229 const double longitude = location.value( QStringLiteral( "lng" ) ).toDouble();
230
231 const QgsGeometry geom = QgsGeometry::fromPointXY( QgsPointXY( longitude, latitude ) );
232
233 QgsGeocoderResult res( json.value( QStringLiteral( "formatted_address" ) ).toString(),
234 geom,
235 QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) );
236
237 QVariantMap attributes;
238
239 if ( json.contains( QStringLiteral( "formatted_address" ) ) )
240 attributes.insert( QStringLiteral( "formatted_address" ), json.value( QStringLiteral( "formatted_address" ) ).toString() );
241 if ( json.contains( QStringLiteral( "place_id" ) ) )
242 attributes.insert( QStringLiteral( "place_id" ), json.value( QStringLiteral( "place_id" ) ).toString() );
243 if ( geometry.contains( QStringLiteral( "location_type" ) ) )
244 attributes.insert( QStringLiteral( "location_type" ), geometry.value( QStringLiteral( "location_type" ) ).toString() );
245
246 const QVariantList components = json.value( QStringLiteral( "address_components" ) ).toList();
247 for ( const QVariant &component : components )
248 {
249 const QVariantMap componentMap = component.toMap();
250 const QStringList types = componentMap.value( QStringLiteral( "types" ) ).toStringList();
251
252 for ( const QString &t :
253 {
254 QStringLiteral( "street_number" ),
255 QStringLiteral( "route" ),
256 QStringLiteral( "locality" ),
257 QStringLiteral( "administrative_area_level_2" ),
258 QStringLiteral( "administrative_area_level_1" ),
259 QStringLiteral( "country" ),
260 QStringLiteral( "postal_code" )
261 } )
262 {
263 if ( types.contains( t ) )
264 {
265 attributes.insert( t, componentMap.value( QStringLiteral( "long_name" ) ).toString() );
266 if ( t == QLatin1String( "administrative_area_level_1" ) )
267 res.setGroup( componentMap.value( QStringLiteral( "long_name" ) ).toString() );
268 }
269 }
270 }
271
272 if ( geometry.contains( QStringLiteral( "viewport" ) ) )
273 {
274 const QVariantMap viewport = geometry.value( QStringLiteral( "viewport" ) ).toMap();
275 const QVariantMap northEast = viewport.value( QStringLiteral( "northeast" ) ).toMap();
276 const QVariantMap southWest = viewport.value( QStringLiteral( "southwest" ) ).toMap();
277 res.setViewport( QgsRectangle( southWest.value( QStringLiteral( "lng" ) ).toDouble(),
278 southWest.value( QStringLiteral( "lat" ) ).toDouble(),
279 northEast.value( QStringLiteral( "lng" ) ).toDouble(),
280 northEast.value( QStringLiteral( "lat" ) ).toDouble()
281 ) );
282 }
283
284 res.setAdditionalAttributes( attributes );
285 return res;
286}
287
288void QgsGoogleMapsGeocoder::setEndpoint( const QString &endpoint )
289{
290 mEndpoint = endpoint;
291}
292
294{
295 return mApiKey;
296}
297
298void QgsGoogleMapsGeocoder::setApiKey( const QString &apiKey )
299{
300 mApiKey = apiKey;
301}
302
304{
305 return mRegion;
306}
307
308void QgsGoogleMapsGeocoder::setRegion( const QString &region )
309{
310 mRegion = region;
311}
WkbType
The WKB type describes the number of dimensions a geometry has.
Definition: qgis.h:182
A thread safe class for performing blocking (sync) network requests, with full support for QGIS proxy...
ErrorCode get(QNetworkRequest &request, bool forceRefresh=false, QgsFeedback *feedback=nullptr)
Performs a "get" operation on the specified request.
QString errorMessage() const
Returns the error message string, after a get(), post(), head() or put() request has been made.
@ 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...
This class represents a coordinate reference system (CRS).
Class for doing transforms between two map coordinate systems.
Custom exception class for Coordinate Reference System related exceptions.
Definition: qgsexception.h:67
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:53
Container of fields for a vector layer.
Definition: qgsfields.h:45
bool append(const QgsField &field, FieldOrigin origin=OriginProvider, int originIndex=-1)
Appends a field. The field must have unique name, otherwise it is rejected (returns false)
Definition: qgsfields.cpp:59
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.
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.
Definition: qgsgeometry.h:162
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.
A geocoder which uses the Google Map geocoding API to retrieve results.
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.
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.
A class to represent a 2D point.
Definition: qgspointxy.h:60
The QgsReadWriteLocker class is a convenience class that simplifies locking and unlocking QReadWriteL...
@ Write
Lock for write.
@ Read
Lock for read.
void unlock()
Unlocks the lock.
void changeMode(Mode mode)
Change the mode of the lock to mode.
A rectangle specified with double values.
Definition: qgsrectangle.h:42
double xMinimum() const
Returns the x minimum value (left side of rectangle).
Definition: qgsrectangle.h:201
double yMinimum() const
Returns the y minimum value (bottom side of rectangle).
Definition: qgsrectangle.h:211
bool isNull() const
Test if the rectangle is null (holding no spatial information).
Definition: qgsrectangle.h:505
double yMaximum() const
Returns the y maximum value (top side of rectangle).
Definition: qgsrectangle.h:206
Q_GLOBAL_STATIC(QReadWriteLock, sDefinitionCacheLock)
QMap< QUrl, QList< QgsGeocoderResult > > CachedGeocodeResult
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugError(str)
Definition: qgslogger.h:38
#define QgsSetRequestInitiatorClass(request, _class)