LCOV - code coverage report
Current view: top level - core/geocoding - qgsgooglemapsgeocoder.cpp (source / functions) Hit Total Coverage
Test: coverage.info.cleaned Lines: 1 176 0.6 %
Date: 2021-03-26 12:19:53 Functions: 0 0 -
Branches: 0 0 -

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

Generated by: LCOV version 1.14