QGIS API Documentation  3.24.2-Tisler (13c1a02865)
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 }
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() or post() request has been made.
@ NoError
No error was encountered.
QgsNetworkReplyContent reply() const
Returns the content of the network reply, after a get() or post() request has been made.
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:66
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition: qgsfeedback.h:45
Encapsulate a field in an attribute table or data source.
Definition: qgsfield.h:51
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.
Definition: qgsgeometry.h:125
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.
static QgsGeometry fromPointXY(const QgsPointXY &point) SIP_HOLDGIL
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.
QgsWkbTypes::Type wkbType() const override
Returns the WKB type of geometries returned by the geocoder.
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.
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:59
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 yMaximum() const SIP_HOLDGIL
Returns the y maximum value (top side of rectangle).
Definition: qgsrectangle.h:193
double xMinimum() const SIP_HOLDGIL
Returns the x minimum value (left side of rectangle).
Definition: qgsrectangle.h:188
double yMinimum() const SIP_HOLDGIL
Returns the y minimum value (bottom side of rectangle).
Definition: qgsrectangle.h:198
bool isNull() const
Test if the rectangle is null (all coordinates zero or after call to setMinimal()).
Definition: qgsrectangle.h:479
Type
The WKB type describes the number of dimensions a geometry has.
Definition: qgswkbtypes.h:70
Q_GLOBAL_STATIC(QReadWriteLock, sDefinitionCacheLock)
QMap< QUrl, QList< QgsGeocoderResult > > CachedGeocodeResult
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
#define QgsSetRequestInitiatorClass(request, _class)