QGIS API Documentation  3.26.3-Buenos Aires (65e4edfdad)
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 
16 #include "qgsgooglemapsgeocoder.h"
17 #include "qgsgeocodercontext.h"
18 #include "qgslogger.h"
21 #include "qgsreadwritelocker.h"
22 #include "qgscoordinatetransform.h"
23 #include <QUrl>
24 #include <QUrlQuery>
25 #include <QNetworkRequest>
26 #include <QJsonDocument>
27 #include <QJsonObject>
28 
29 QReadWriteLock QgsGoogleMapsGeocoder::sMutex;
30 
31 typedef QMap< QUrl, QList< QgsGeocoderResult > > CachedGeocodeResult;
32 Q_GLOBAL_STATIC( CachedGeocodeResult, sCachedResults )
33 
34 
35 QgsGoogleMapsGeocoder::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 
44 QgsGeocoderInterface::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 
68 {
69  return QgsWkbTypes::Point;
70 }
71 
72 QList<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  QgsDebugMsg( "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 
159 QUrl 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  QgsDebugMsg( QStringLiteral( "Get %1" ).arg( modifiedUrlString ) );
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  QgsDebugMsg( QStringLiteral( "Get %1 (after laundering)" ).arg( modifiedUrlString ) );
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 
287 void QgsGoogleMapsGeocoder::setEndpoint( const QString &endpoint )
288 {
289  mEndpoint = endpoint;
290 }
291 
293 {
294  return mApiKey;
295 }
296 
297 void QgsGoogleMapsGeocoder::setApiKey( const QString &apiKey )
298 {
299  mApiKey = apiKey;
300 }
301 
303 {
304  return mRegion;
305 }
306 
307 void QgsGoogleMapsGeocoder::setRegion( const QString &region )
308 {
309  mRegion = region;
310 }
QgsReadWriteLocker::changeMode
void changeMode(Mode mode)
Change the mode of the lock to mode.
Definition: qgsreadwritelocker.cpp:30
QgsGoogleMapsGeocoder::region
QString region() const
Returns the optional region bias which will be used to prioritize results in a certain region.
Definition: qgsgooglemapsgeocoder.cpp:302
qgsreadwritelocker.h
QgsReadWriteLocker::Read
@ Read
Lock for read.
Definition: qgsreadwritelocker.h:75
QgsGoogleMapsGeocoder::jsonToResult
QgsGeocoderResult jsonToResult(const QVariantMap &json) const
Converts a JSON result returned from the Google Maps service to a geocoder result object.
Definition: qgsgooglemapsgeocoder.cpp:223
QgsWkbTypes::Point
@ Point
Definition: qgswkbtypes.h:72
QgsGeometry::transform
Qgis::GeometryOperationResult transform(const QgsCoordinateTransform &ct, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward, bool transformZ=false) SIP_THROW(QgsCsException)
Transforms this geometry as described by the coordinate transform ct.
Definition: qgsgeometry.cpp:3128
qgsblockingnetworkrequest.h
CachedGeocodeResult
QMap< QUrl, QList< QgsGeocoderResult > > CachedGeocodeResult
Definition: qgsgooglemapsgeocoder.cpp:31
QgsGoogleMapsGeocoder::wkbType
QgsWkbTypes::Type wkbType() const override
Returns the WKB type of geometries returned by the geocoder.
Definition: qgsgooglemapsgeocoder.cpp:67
QgsFields
Container of fields for a vector layer.
Definition: qgsfields.h:44
QgsRectangle::yMinimum
double yMinimum() const SIP_HOLDGIL
Returns the y minimum value (bottom side of rectangle).
Definition: qgsrectangle.h:198
QgsBlockingNetworkRequest::reply
QgsNetworkReplyContent reply() const
Returns the content of the network reply, after a get(), post(), head() or put() request has been mad...
Definition: qgsblockingnetworkrequest.h:193
QgsGeometry::fromPointXY
static QgsGeometry fromPointXY(const QgsPointXY &point) SIP_HOLDGIL
Creates a new geometry from a QgsPointXY object.
Definition: qgsgeometry.cpp:176
QgsWkbTypes::Type
Type
The WKB type describes the number of dimensions a geometry has.
Definition: qgswkbtypes.h:69
QgsGoogleMapsGeocoder
A geocoder which uses the Google Map geocoding API to retrieve results.
Definition: qgsgooglemapsgeocoder.h:40
QgsDebugMsg
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
QgsFields::append
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
QgsGeocoderResult::setAdditionalAttributes
void setAdditionalAttributes(const QVariantMap &attributes)
Setss additional attributes generated during the geocode, which may be added to features being geocod...
Definition: qgsgeocoderresult.h:163
Q_GLOBAL_STATIC
Q_GLOBAL_STATIC(QReadWriteLock, sDefinitionCacheLock)
QgsReadWriteLocker::unlock
void unlock()
Unlocks the lock.
Definition: qgsreadwritelocker.cpp:45
QgsRectangle
A rectangle specified with double values.
Definition: qgsrectangle.h:41
QgsSetRequestInitiatorClass
#define QgsSetRequestInitiatorClass(request, _class)
Definition: qgsnetworkaccessmanager.h:45
QgsReadWriteLocker
The QgsReadWriteLocker class is a convenience class that simplifies locking and unlocking QReadWriteL...
Definition: qgsreadwritelocker.h:40
QgsGeocoderResult::setGroup
void setGroup(const QString &group)
Sets the optional group value for the result.
Definition: qgsgeocoderresult.cpp:38
QgsBlockingNetworkRequest::ErrorCode
ErrorCode
Error codes.
Definition: qgsblockingnetworkrequest.h:52
QgsCsException
Custom exception class for Coordinate Reference System related exceptions.
Definition: qgsexception.h:65
QgsFeedback
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition: qgsfeedback.h:44
qgsnetworkaccessmanager.h
QgsGoogleMapsGeocoder::appendedFields
QgsFields appendedFields() const override
Returns a set of newly created fields which will be appended to existing features during the geocode ...
Definition: qgsgooglemapsgeocoder.cpp:49
QgsReadWriteLocker::Write
@ Write
Lock for write.
Definition: qgsreadwritelocker.h:76
QgsGoogleMapsGeocoder::flags
Flags flags() const override
Returns the geocoder's capability flags.
Definition: qgsgooglemapsgeocoder.cpp:44
QgsGoogleMapsGeocoder::setApiKey
void setApiKey(const QString &key)
Sets the API key to use when accessing the Google Maps API.
Definition: qgsgooglemapsgeocoder.cpp:297
QgsGeocoderResult::setViewport
void setViewport(const QgsRectangle &viewport)
Sets the suggested viewport for the result, which reflects a recommended map extent for displaying th...
Definition: qgsgeocoderresult.h:147
qgscoordinatetransform.h
QgsBlockingNetworkRequest::NoError
@ NoError
No error was encountered.
Definition: qgsblockingnetworkrequest.h:54
QgsGeometry::isEmpty
bool isEmpty() const
Returns true if the geometry is empty (eg a linestring with no vertices, or a collection with no geom...
Definition: qgsgeometry.cpp:379
QgsGeocoderContext::transformContext
QgsCoordinateTransformContext transformContext() const
Returns the coordinate transform context, which should be used whenever the geocoder constructs a coo...
Definition: qgsgeocodercontext.h:60
QgsGeocoderInterface::Flag::GeocodesStrings
@ GeocodesStrings
Can geocode string input values.
QgsGoogleMapsGeocoder::setEndpoint
void setEndpoint(const QString &endpoint)
Sets a specific API endpoint to use for requests.
Definition: qgsgooglemapsgeocoder.cpp:287
QgsBlockingNetworkRequest::errorMessage
QString errorMessage() const
Returns the error message string, after a get(), post(), head() or put() request has been made.
Definition: qgsblockingnetworkrequest.h:188
QgsGeocoderContext::areaOfInterest
QgsGeometry areaOfInterest() const
Returns the optional area of interest, which can be used to indicate the desired geographic area wher...
Definition: qgsgeocodercontext.h:84
QgsRectangle::xMinimum
double xMinimum() const SIP_HOLDGIL
Returns the x minimum value (left side of rectangle).
Definition: qgsrectangle.h:188
QgsCoordinateReferenceSystem
This class represents a coordinate reference system (CRS).
Definition: qgscoordinatereferencesystem.h:211
QgsPointXY
A class to represent a 2D point.
Definition: qgspointxy.h:58
QgsGeocoderResult::errorResult
static QgsGeocoderResult errorResult(const QString &errorMessage)
Creates an invalid error result, with the specified errorMessage string.
Definition: qgsgeocoderresult.cpp:18
QgsGeocoderContext
Encapsulates the context of a geocoding operation.
Definition: qgsgeocodercontext.h:31
QgsBlockingNetworkRequest::get
ErrorCode get(QNetworkRequest &request, bool forceRefresh=false, QgsFeedback *feedback=nullptr)
Performs a "get" operation on the specified request.
Definition: qgsblockingnetworkrequest.cpp:58
QgsRectangle::yMaximum
double yMaximum() const SIP_HOLDGIL
Returns the y maximum value (top side of rectangle).
Definition: qgsrectangle.h:193
QgsGeocoderResult
Represents a matching result from a geocoder search.
Definition: qgsgeocoderresult.h:40
QgsGeometry
A geometry is the spatial representation of a feature.
Definition: qgsgeometry.h:124
QgsGeocoderContext::areaOfInterestCrs
QgsCoordinateReferenceSystem areaOfInterestCrs() const
Returns the coordinate reference system for the area of interest, which can be used to indicate the d...
Definition: qgsgeocodercontext.h:109
QgsGoogleMapsGeocoder::apiKey
QString apiKey() const
Returns the API key which will be used when accessing the Google Maps API.
Definition: qgsgooglemapsgeocoder.cpp:292
QgsGeometry::boundingBox
QgsRectangle boundingBox() const
Returns the bounding box of the geometry.
Definition: qgsgeometry.cpp:1080
QgsNetworkReplyContent::content
QByteArray content() const
Returns the reply content.
Definition: qgsnetworkreply.h:171
QgsGoogleMapsGeocoder::setRegion
void setRegion(const QString &region)
Sets the optional region bias which will be used to prioritize results in a certain region.
Definition: qgsgooglemapsgeocoder.cpp:307
qgsgooglemapsgeocoder.h
qgslogger.h
QgsCoordinateTransform
Class for doing transforms between two map coordinate systems.
Definition: qgscoordinatetransform.h:57
QgsGoogleMapsGeocoder::requestUrl
QUrl requestUrl(const QString &address, const QgsRectangle &bounds=QgsRectangle()) const
Returns the URL generated for geocoding the specified address.
Definition: qgsgooglemapsgeocoder.cpp:159
QgsRectangle::isNull
bool isNull() const
Test if the rectangle is null (all coordinates zero or after call to setMinimal()).
Definition: qgsrectangle.h:479
QgsBlockingNetworkRequest
A thread safe class for performing blocking (sync) network requests, with full support for QGIS proxy...
Definition: qgsblockingnetworkrequest.h:46
QgsGeocoderInterface
Interface for geocoders.
Definition: qgsgeocoder.h:36
QgsGoogleMapsGeocoder::geocodeString
QList< QgsGeocoderResult > geocodeString(const QString &string, const QgsGeocoderContext &context, QgsFeedback *feedback=nullptr) const override
Geocodes a string.
Definition: qgsgooglemapsgeocoder.cpp:72
qgsgeocodercontext.h
QgsField
Encapsulate a field in an attribute table or data source.
Definition: qgsfield.h:50