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