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"
17 #include "qgsblockingnetworkrequest.h"
18 #include "qgsgeocodercontext.h"
19 #include "qgslogger.h"
20 #include "qgsnetworkaccessmanager.h"
21 #include "qgscoordinatetransform.h"
22 #include <QDateTime>
23 #include <QUrl>
24 #include <QUrlQuery>
25 #include <QMutex>
26 #include <QNetworkRequest>
27 #include <QJsonDocument>
28 #include <QJsonArray>
29 
30 QMutex QgsNominatimGeocoder::sMutex;
31 typedef QMap< QUrl, QList< QgsGeocoderResult > > CachedGeocodeResult;
32 Q_GLOBAL_STATIC( CachedGeocodeResult, sCachedResults )
33 qint64 QgsNominatimGeocoder::sLastRequestTimestamp = 0;
34 
QgsNominatimGeocoder(const QString & countryCodes,const QString & endpoint)35 QgsNominatimGeocoder::QgsNominatimGeocoder( const QString &countryCodes, const QString &endpoint )
36   : QgsGeocoderInterface()
37   , mCountryCodes( countryCodes )
38   , mEndpoint( QStringLiteral( "https://nominatim.qgis.org/search" ) )
39 {
40   if ( !endpoint.isEmpty() )
41     mEndpoint = endpoint;
42 }
43 
flags() const44 QgsGeocoderInterface::Flags QgsNominatimGeocoder::flags() const
45 {
46   return QgsGeocoderInterface::Flag::GeocodesStrings;
47 }
48 
appendedFields() const49 QgsFields QgsNominatimGeocoder::appendedFields() const
50 {
51   QgsFields fields;
52   fields.append( QgsField( QStringLiteral( "osm_type" ), QVariant::String ) );
53   fields.append( QgsField( QStringLiteral( "display_name" ), QVariant::String ) );
54   fields.append( QgsField( QStringLiteral( "place_id" ), QVariant::String ) );
55   fields.append( QgsField( QStringLiteral( "class" ), QVariant::String ) );
56   fields.append( QgsField( QStringLiteral( "type" ), QVariant::String ) );
57   fields.append( QgsField( QStringLiteral( "road" ), QVariant::String ) );
58   fields.append( QgsField( QStringLiteral( "village" ), QVariant::String ) );
59   fields.append( QgsField( QStringLiteral( "city_district" ), QVariant::String ) );
60   fields.append( QgsField( QStringLiteral( "town" ), QVariant::String ) );
61   fields.append( QgsField( QStringLiteral( "city" ), QVariant::String ) );
62   fields.append( QgsField( QStringLiteral( "state" ), QVariant::String ) );
63   fields.append( QgsField( QStringLiteral( "country" ), QVariant::String ) );
64   fields.append( QgsField( QStringLiteral( "postcode" ), QVariant::String ) );
65   return fields;
66 }
67 
wkbType() const68 QgsWkbTypes::Type QgsNominatimGeocoder::wkbType() const
69 {
70   return QgsWkbTypes::Point;
71 }
72 
geocodeString(const QString & string,const QgsGeocoderContext & context,QgsFeedback * feedback) const73 QList<QgsGeocoderResult> QgsNominatimGeocoder::geocodeString( const QString &string, const QgsGeocoderContext &context, QgsFeedback *feedback ) const
74 {
75   QgsRectangle bounds;
76   if ( !context.areaOfInterest().isEmpty() )
77   {
78     QgsGeometry g = context.areaOfInterest();
79     const QgsCoordinateTransform ct( context.areaOfInterestCrs(), QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ), context.transformContext() );
80     try
81     {
82       g.transform( ct );
83       bounds = g.boundingBox();
84     }
85     catch ( QgsCsException & )
86     {
87       QgsDebugMsg( "Could not transform geocode bounds to WGS84" );
88     }
89   }
90 
91   const QUrl url = requestUrl( string, bounds );
92 
93   const QMutexLocker locker( &sMutex );
94   const auto it = sCachedResults()->constFind( url );
95   if ( it != sCachedResults()->constEnd() )
96   {
97     return *it;
98   }
99 
100   while ( QDateTime::currentMSecsSinceEpoch() - sLastRequestTimestamp < 1000 / mRequestsPerSecond )
101   {
102     QThread::msleep( 50 );
103     if ( feedback && feedback->isCanceled() )
104       return QList<QgsGeocoderResult>();
105   }
106 
107   QNetworkRequest request( url );
108   QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsNominatimGeocoder" ) );
109 
110   QgsBlockingNetworkRequest newReq;
111   const QgsBlockingNetworkRequest::ErrorCode errorCode = newReq.get( request, false, feedback );
112 
113   sLastRequestTimestamp = QDateTime::currentMSecsSinceEpoch();
114 
115   if ( errorCode != QgsBlockingNetworkRequest::NoError )
116   {
117     return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( newReq.errorMessage() );
118   }
119 
120   // Parse data
121   QJsonParseError err;
122   const QJsonDocument doc = QJsonDocument::fromJson( newReq.reply().content(), &err );
123   if ( doc.isNull() )
124   {
125     return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( err.errorString() );
126   }
127 
128   const QVariantList results = doc.array().toVariantList();
129   if ( results.isEmpty() )
130   {
131     sCachedResults()->insert( url, QList<QgsGeocoderResult>() );
132     return QList<QgsGeocoderResult>();
133   }
134 
135   QList< QgsGeocoderResult > matches;
136   matches.reserve( results.size() );
137   for ( const QVariant &result : results )
138   {
139     matches << jsonToResult( result.toMap() );
140   }
141 
142   sCachedResults()->insert( url, matches );
143 
144   return matches;
145 }
146 
requestUrl(const QString & address,const QgsRectangle & bounds) const147 QUrl QgsNominatimGeocoder::requestUrl( const QString &address, const QgsRectangle &bounds ) const
148 {
149   QUrl res( mEndpoint );
150   QUrlQuery query;
151   query.addQueryItem( QStringLiteral( "format" ), QStringLiteral( "json" ) );
152   query.addQueryItem( QStringLiteral( "addressdetails" ), QStringLiteral( "1" ) );
153   if ( !bounds.isNull() )
154   {
155     query.addQueryItem( QStringLiteral( "viewbox" ), QStringLiteral( "%1,%2,%3,%4" ).arg( bounds.xMinimum() )
156                         .arg( bounds.yMinimum() )
157                         .arg( bounds.xMaximum() )
158                         .arg( bounds.yMaximum() ) );
159   }
160   if ( !mCountryCodes.isEmpty() )
161   {
162     query.addQueryItem( QStringLiteral( "countrycodes" ), mCountryCodes.toLower() );
163   }
164   query.addQueryItem( QStringLiteral( "q" ), address );
165   res.setQuery( query );
166 
167   return res;
168 }
169 
jsonToResult(const QVariantMap & json) const170 QgsGeocoderResult QgsNominatimGeocoder::jsonToResult( const QVariantMap &json ) const
171 {
172   const double latitude = json.value( QStringLiteral( "lat" ) ).toDouble();
173   const double longitude = json.value( QStringLiteral( "lon" ) ).toDouble();
174 
175   const QgsGeometry geom = QgsGeometry::fromPointXY( QgsPointXY( longitude, latitude ) );
176 
177   QgsGeocoderResult res( json.value( QStringLiteral( "display_name" ) ).toString(),
178                          geom,
179                          QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) );
180 
181   QVariantMap attributes;
182 
183   if ( json.contains( QStringLiteral( "display_name" ) ) )
184     attributes.insert( QStringLiteral( "display_name" ), json.value( QStringLiteral( "display_name" ) ).toString() );
185   if ( json.contains( QStringLiteral( "place_id" ) ) )
186     attributes.insert( QStringLiteral( "place_id" ), json.value( QStringLiteral( "place_id" ) ).toString() );
187   if ( json.contains( QStringLiteral( "osm_type" ) ) )
188     attributes.insert( QStringLiteral( "osm_type" ), json.value( QStringLiteral( "osm_type" ) ).toString() );
189   if ( json.contains( QStringLiteral( "class" ) ) )
190     attributes.insert( QStringLiteral( "class" ), json.value( QStringLiteral( "class" ) ).toString() );
191   if ( json.contains( QStringLiteral( "type" ) ) )
192     attributes.insert( QStringLiteral( "type" ), json.value( QStringLiteral( "type" ) ).toString() );
193 
194   if ( json.contains( QStringLiteral( "address" ) ) )
195   {
196     const QVariantMap address_components = json.value( QStringLiteral( "address" ) ).toMap();
197     if ( address_components.contains( QStringLiteral( "road" ) ) )
198       attributes.insert( QStringLiteral( "road" ), address_components.value( QStringLiteral( "road" ) ).toString() );
199     if ( address_components.contains( QStringLiteral( "village" ) ) )
200       attributes.insert( QStringLiteral( "village" ), address_components.value( QStringLiteral( "village" ) ).toString() );
201     if ( address_components.contains( QStringLiteral( "city_district" ) ) )
202       attributes.insert( QStringLiteral( "city_district" ), address_components.value( QStringLiteral( "city_district" ) ).toString() );
203     if ( address_components.contains( QStringLiteral( "town" ) ) )
204       attributes.insert( QStringLiteral( "town" ), address_components.value( QStringLiteral( "town" ) ).toString() );
205     if ( address_components.contains( QStringLiteral( "city" ) ) )
206       attributes.insert( QStringLiteral( "city" ), address_components.value( QStringLiteral( "city" ) ).toString() );
207     if ( address_components.contains( QStringLiteral( "state" ) ) )
208     {
209       attributes.insert( QStringLiteral( "state" ), address_components.value( QStringLiteral( "state" ) ).toString() );
210       res.setGroup( address_components.value( QStringLiteral( "state" ) ).toString() );
211     }
212     if ( address_components.contains( QStringLiteral( "country" ) ) )
213       attributes.insert( QStringLiteral( "country" ), address_components.value( QStringLiteral( "country" ) ).toString() );
214     if ( address_components.contains( QStringLiteral( "postcode" ) ) )
215       attributes.insert( QStringLiteral( "postcode" ), address_components.value( QStringLiteral( "postcode" ) ).toString() );
216   }
217 
218   if ( json.contains( QStringLiteral( "boundingbox" ) ) )
219   {
220     const QVariantList boundingBox = json.value( QStringLiteral( "boundingbox" ) ).toList();
221     if ( boundingBox.size() == 4 )
222       res.setViewport( QgsRectangle( boundingBox.at( 2 ).toDouble(),
223                                      boundingBox.at( 0 ).toDouble(),
224                                      boundingBox.at( 3 ).toDouble(),
225                                      boundingBox.at( 1 ).toDouble() ) );
226   }
227 
228   res.setAdditionalAttributes( attributes );
229   return res;
230 }
231 
endpoint() const232 QString QgsNominatimGeocoder::endpoint() const
233 {
234   return mEndpoint;
235 }
236 
setEndpoint(const QString & endpoint)237 void QgsNominatimGeocoder::setEndpoint( const QString &endpoint )
238 {
239   mEndpoint = endpoint;
240 }
241 
countryCodes() const242 QString QgsNominatimGeocoder::countryCodes() const
243 {
244   return mCountryCodes;
245 }
246 
setCountryCodes(const QString & countryCodes)247 void QgsNominatimGeocoder::setCountryCodes( const QString &countryCodes )
248 {
249   mCountryCodes = countryCodes;
250 }
251