QGIS API Documentation  3.20.0-Odense (decaadbb31)
qgsnominatimgeocoder.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgsnominatimgeocoder.cpp
3  ---------------
4  Date : December 2020
5  Copyright : (C) 2020 by Mathieu Pellerin
6  Email : nirvn dot asia 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 "qgsnominatimgeocoder.h"
18 #include "qgsgeocodercontext.h"
19 #include "qgslogger.h"
21 #include <QDateTime>
22 #include <QUrl>
23 #include <QUrlQuery>
24 #include <QMutex>
25 #include <QNetworkRequest>
26 #include <QJsonDocument>
27 #include <QJsonArray>
28 
29 QMutex QgsNominatimGeocoder::sMutex;
30 typedef QMap< QUrl, QList< QgsGeocoderResult > > CachedGeocodeResult;
31 Q_GLOBAL_STATIC( CachedGeocodeResult, sCachedResults )
32 qint64 QgsNominatimGeocoder::sLastRequestTimestamp = 0;
33 
34 QgsNominatimGeocoder::QgsNominatimGeocoder( const QString &countryCodes, const QString &endpoint )
36  , mCountryCodes( countryCodes )
37  , mEndpoint( QStringLiteral( "https://nominatim.qgis.org/search" ) )
38 {
39  if ( !endpoint.isEmpty() )
40  mEndpoint = endpoint;
41 }
42 
43 QgsGeocoderInterface::Flags QgsNominatimGeocoder::flags() const
44 {
46 }
47 
49 {
50  QgsFields fields;
51  fields.append( QgsField( QStringLiteral( "osm_type" ), QVariant::String ) );
52  fields.append( QgsField( QStringLiteral( "display_name" ), QVariant::String ) );
53  fields.append( QgsField( QStringLiteral( "place_id" ), QVariant::String ) );
54  fields.append( QgsField( QStringLiteral( "class" ), QVariant::String ) );
55  fields.append( QgsField( QStringLiteral( "type" ), QVariant::String ) );
56  fields.append( QgsField( QStringLiteral( "road" ), QVariant::String ) );
57  fields.append( QgsField( QStringLiteral( "village" ), QVariant::String ) );
58  fields.append( QgsField( QStringLiteral( "city_district" ), QVariant::String ) );
59  fields.append( QgsField( QStringLiteral( "town" ), QVariant::String ) );
60  fields.append( QgsField( QStringLiteral( "city" ), QVariant::String ) );
61  fields.append( QgsField( QStringLiteral( "state" ), QVariant::String ) );
62  fields.append( QgsField( QStringLiteral( "country" ), QVariant::String ) );
63  fields.append( QgsField( QStringLiteral( "postcode" ), QVariant::String ) );
64  return fields;
65 }
66 
68 {
69  return QgsWkbTypes::Point;
70 }
71 
72 QList<QgsGeocoderResult> QgsNominatimGeocoder::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  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 
92  QMutexLocker locker( &sMutex );
93  auto it = sCachedResults()->constFind( url );
94  if ( it != sCachedResults()->constEnd() )
95  {
96  return *it;
97  }
98 
99  while ( QDateTime::currentMSecsSinceEpoch() - sLastRequestTimestamp < 1000 / mRequestsPerSecond )
100  {
101  QThread::msleep( 50 );
102  if ( feedback && feedback->isCanceled() )
103  return QList<QgsGeocoderResult>();
104  }
105 
106  QNetworkRequest request( url );
107  QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsNominatimGeocoder" ) );
108 
110  const QgsBlockingNetworkRequest::ErrorCode errorCode = newReq.get( request, false, feedback );
111 
112  sLastRequestTimestamp = QDateTime::currentMSecsSinceEpoch();
113 
114  if ( errorCode != QgsBlockingNetworkRequest::NoError )
115  {
116  return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( newReq.errorMessage() );
117  }
118 
119  // Parse data
120  QJsonParseError err;
121  QJsonDocument doc = QJsonDocument::fromJson( newReq.reply().content(), &err );
122  if ( doc.isNull() )
123  {
124  return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( err.errorString() );
125  }
126 
127  const QVariantList results = doc.array().toVariantList();
128  if ( results.isEmpty() )
129  {
130  sCachedResults()->insert( url, QList<QgsGeocoderResult>() );
131  return QList<QgsGeocoderResult>();
132  }
133 
134  QList< QgsGeocoderResult > matches;
135  matches.reserve( results.size() );
136  for ( const QVariant &result : results )
137  {
138  matches << jsonToResult( result.toMap() );
139  }
140 
141  sCachedResults()->insert( url, matches );
142 
143  return matches;
144 }
145 
146 QUrl QgsNominatimGeocoder::requestUrl( const QString &address, const QgsRectangle &bounds ) const
147 {
148  QUrl res( mEndpoint );
149  QUrlQuery query;
150  query.addQueryItem( QStringLiteral( "format" ), QStringLiteral( "json" ) );
151  query.addQueryItem( QStringLiteral( "addressdetails" ), QStringLiteral( "1" ) );
152  if ( !bounds.isNull() )
153  {
154  query.addQueryItem( QStringLiteral( "viewbox" ), QStringLiteral( "%1,%2,%3,%4" ).arg( bounds.xMinimum() )
155  .arg( bounds.yMinimum() )
156  .arg( bounds.xMaximum() )
157  .arg( bounds.yMaximum() ) );
158  }
159  if ( !mCountryCodes.isEmpty() )
160  {
161  query.addQueryItem( QStringLiteral( "countrycodes" ), mCountryCodes.toLower() );
162  }
163  query.addQueryItem( QStringLiteral( "q" ), address );
164  res.setQuery( query );
165 
166  return res;
167 }
168 
170 {
171  const double latitude = json.value( QStringLiteral( "lat" ) ).toDouble();
172  const double longitude = json.value( QStringLiteral( "lon" ) ).toDouble();
173 
174  const QgsGeometry geom = QgsGeometry::fromPointXY( QgsPointXY( longitude, latitude ) );
175 
176  QgsGeocoderResult res( json.value( QStringLiteral( "display_name" ) ).toString(),
177  geom,
178  QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) );
179 
180  QVariantMap attributes;
181 
182  if ( json.contains( QStringLiteral( "display_name" ) ) )
183  attributes.insert( QStringLiteral( "display_name" ), json.value( QStringLiteral( "display_name" ) ).toString() );
184  if ( json.contains( QStringLiteral( "place_id" ) ) )
185  attributes.insert( QStringLiteral( "place_id" ), json.value( QStringLiteral( "place_id" ) ).toString() );
186  if ( json.contains( QStringLiteral( "osm_type" ) ) )
187  attributes.insert( QStringLiteral( "osm_type" ), json.value( QStringLiteral( "osm_type" ) ).toString() );
188  if ( json.contains( QStringLiteral( "class" ) ) )
189  attributes.insert( QStringLiteral( "class" ), json.value( QStringLiteral( "class" ) ).toString() );
190  if ( json.contains( QStringLiteral( "type" ) ) )
191  attributes.insert( QStringLiteral( "type" ), json.value( QStringLiteral( "type" ) ).toString() );
192 
193  if ( json.contains( QStringLiteral( "address" ) ) )
194  {
195  const QVariantMap address_components = json.value( QStringLiteral( "address" ) ).toMap();
196  if ( address_components.contains( QStringLiteral( "road" ) ) )
197  attributes.insert( QStringLiteral( "road" ), address_components.value( QStringLiteral( "road" ) ).toString() );
198  if ( address_components.contains( QStringLiteral( "village" ) ) )
199  attributes.insert( QStringLiteral( "village" ), address_components.value( QStringLiteral( "village" ) ).toString() );
200  if ( address_components.contains( QStringLiteral( "city_district" ) ) )
201  attributes.insert( QStringLiteral( "city_district" ), address_components.value( QStringLiteral( "city_district" ) ).toString() );
202  if ( address_components.contains( QStringLiteral( "town" ) ) )
203  attributes.insert( QStringLiteral( "town" ), address_components.value( QStringLiteral( "town" ) ).toString() );
204  if ( address_components.contains( QStringLiteral( "city" ) ) )
205  attributes.insert( QStringLiteral( "city" ), address_components.value( QStringLiteral( "city" ) ).toString() );
206  if ( address_components.contains( QStringLiteral( "state" ) ) )
207  {
208  attributes.insert( QStringLiteral( "state" ), address_components.value( QStringLiteral( "state" ) ).toString() );
209  res.setGroup( address_components.value( QStringLiteral( "state" ) ).toString() );
210  }
211  if ( address_components.contains( QStringLiteral( "country" ) ) )
212  attributes.insert( QStringLiteral( "country" ), address_components.value( QStringLiteral( "country" ) ).toString() );
213  if ( address_components.contains( QStringLiteral( "postcode" ) ) )
214  attributes.insert( QStringLiteral( "postcode" ), address_components.value( QStringLiteral( "postcode" ) ).toString() );
215  }
216 
217  if ( json.contains( QStringLiteral( "boundingbox" ) ) )
218  {
219  QVariantList boundingBox = json.value( QStringLiteral( "boundingbox" ) ).toList();
220  if ( boundingBox.size() == 4 )
221  res.setViewport( QgsRectangle( boundingBox.at( 2 ).toDouble(),
222  boundingBox.at( 0 ).toDouble(),
223  boundingBox.at( 3 ).toDouble(),
224  boundingBox.at( 1 ).toDouble() ) );
225  }
226 
227  res.setAdditionalAttributes( attributes );
228  return res;
229 }
230 
232 {
233  return mEndpoint;
234 }
235 
236 void QgsNominatimGeocoder::setEndpoint( const QString &endpoint )
237 {
238  mEndpoint = endpoint;
239 }
240 
242 {
243  return mCountryCodes;
244 }
245 
246 void QgsNominatimGeocoder::setCountryCodes( const QString &countryCodes )
247 {
248  mCountryCodes = countryCodes;
249 }
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
bool isCanceled() const SIP_HOLDGIL
Tells whether the operation has been canceled already.
Definition: qgsfeedback.h:54
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.
QByteArray content() const
Returns the reply content.
QList< QgsGeocoderResult > geocodeString(const QString &string, const QgsGeocoderContext &context, QgsFeedback *feedback=nullptr) const override
Geocodes a string.
QgsGeocoderResult jsonToResult(const QVariantMap &json) const
Converts a JSON result returned from the Nominatim service to a geocoder result object.
QgsNominatimGeocoder(const QString &countryCodes=QString(), const QString &endpoint=QString())
Constructor for QgsNominatimGeocoder.
QString countryCodes() const
Returns the optional region bias which will be used to prioritize results in a certain region.
QString endpoint() const
Returns the API endpoint used for requests.
QgsFields appendedFields() const override
Returns a set of newly created fields which will be appended to existing features during the geocode ...
void setEndpoint(const QString &endpoint)
Sets a specific API endpoint to use for requests.
Flags flags() const override
Returns the geocoder's capability flags.
void setCountryCodes(const QString &countryCodes)
Sets the optional region bias which will be used to prioritize results in a certain region.
QgsWkbTypes::Type wkbType() const override
Returns the WKB type of geometries returned by the geocoder.
QUrl requestUrl(const QString &address, const QgsRectangle &bounds=QgsRectangle()) const
Returns the URL generated for geocoding the specified address.
A class to represent a 2D point.
Definition: qgspointxy.h:59
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 xMaximum() const SIP_HOLDGIL
Returns the x maximum value (right side of rectangle).
Definition: qgsrectangle.h:183
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)
QMap< QUrl, QList< QgsGeocoderResult > > CachedGeocodeResult