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