Skip to content
Permalink
Browse files

[api] Add new class QgsGoogleMapsGeocoder

This geocoder utilises the Google Maps "geocoding" API in order to geocode
strings. The Google Maps service is not publicly available, and accordingly
an API key must be first obtained from Google and specified when constructing
this class.

(The user is responsible for managing their Google Maps API key, and ensuring
that the use of this geocoder does not exceed their usage limits! Excessive use
of the Google Maps geocoder API can result in charges being applied to the API key
holder.)

This is raw underlying API only. It is intended to be exposed to QGIS users only
via plugins, and does not result in any out-of-the-box Google Maps functionality.

In order for a plugin to use it, they must do something like this:

    # create a google maps geocoder
    api_key = 'my api key'
    coder = QgsGoogleMapsGeocoder(api_key)

    # add it to the locator bar
    filter = QgsGeocoderLocatorFilter('Google', 'Google', 'addr', coder, iface.mapCanvas())
    iface.registerLocatorFilter(filter)

Mini FAQ:

Q: Why is this being added to core, instead of a plugin?
A: While a plugin would be perfectly acceptable if we are only targetting QGIS desktop,
the intention here is to allow this underlying, low level class to be reused outside
of QGIS desktop (e.g. on QField) with a minimal amount of duplicate effort.
  • Loading branch information
nyalldawson committed Nov 3, 2020
1 parent 71a755a commit 98d09715a42e9a03b9bf686c9bfa8fe95edce3d1
@@ -0,0 +1,80 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/geocoding/qgsgooglemapsgeocoder.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/



class QgsGoogleMapsGeocoder : QgsGeocoderInterface
{
%Docstring
A geocoder which uses the Google Map geocoding API to retrieve results.

This geocoder utilizes the Google Maps "geocoding" API in order to geocode
strings. The Google Maps service is not publicly available, and accordingly
an API key must be first obtained from Google and specified when constructing
this class.

.. warning::

The user is responsible for managing their Google Maps API key, and ensuring
that the use of this geocoder does not exceed their usage limits! Excessive use
of the Google Maps geocoder API can result in charges being applied to the API key
holder.

.. versionadded:: 3.18
%End

%TypeHeaderCode
#include "qgsgooglemapsgeocoder.h"
%End
public:

QgsGoogleMapsGeocoder( const QString &apiKey, const QString &regionBias = QString() );
%Docstring
Constructor for QgsGoogleMapsGeocoder.

The ``apiKey`` argument must specify a valid Google Maps API key. All use of this
geocoder will be associated with the specified key for Google's billing purposes!

Optionally, a ``regionBias`` can be specified to prioritize results in a certain region.
The ``regionBias`` argument must be set to a two letter country code top-level domain value,
e.g. "gb" for Great Britain.
%End

virtual Flags flags() const;

virtual QgsFields appendedFields() const;

virtual QgsWkbTypes::Type wkbType() const;

virtual QList< QgsGeocoderResult > geocodeString( const QString &string, const QgsGeocoderContext &context, QgsFeedback *feedback = 0 ) const;


QUrl requestUrl( const QString &address, const QgsRectangle &bounds = QgsRectangle() ) const;
%Docstring
Returns the URL generated for geocoding the specified ``address``.
%End

QgsGeocoderResult jsonToResult( const QVariantMap &json ) const;
%Docstring
Converts a JSON result returned from the Google Maps service to a geocoder result object.
%End

void setEndpoint( const QString &endpoint );
%Docstring
Sets a specific API ``endpoint`` to use for requests. This is for internal testing purposes only.
%End

};

/************************************************************************
* This file has been generated automatically from *
* *
* src/core/geocoding/qgsgooglemapsgeocoder.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
@@ -316,6 +316,7 @@
%Include auto_generated/geocoding/qgsgeocoder.sip
%Include auto_generated/geocoding/qgsgeocodercontext.sip
%Include auto_generated/geocoding/qgsgeocoderresult.sip
%Include auto_generated/geocoding/qgsgooglemapsgeocoder.sip
%Include auto_generated/geometry/qgsabstractgeometry.sip
%Include auto_generated/geometry/qgsbox3d.sip
%Include auto_generated/geometry/qgscircle.sip
@@ -35,6 +35,7 @@ SET(QGIS_CORE_SRCS
geocoding/qgsgeocoder.cpp
geocoding/qgsgeocodercontext.cpp
geocoding/qgsgeocoderresult.cpp
geocoding/qgsgooglemapsgeocoder.cpp

gps/qgsgpsconnection.cpp
gps/qgsgpsconnectionregistry.cpp
@@ -1159,6 +1160,7 @@ SET(QGIS_CORE_HDRS
geocoding/qgsgeocoder.h
geocoding/qgsgeocodercontext.h
geocoding/qgsgeocoderresult.h
geocoding/qgsgooglemapsgeocoder.h

geometry/qgsabstractgeometry.h
geometry/qgsbox3d.h
@@ -0,0 +1,253 @@
/***************************************************************************
qgsgooglemapsgeocoder.cpp
---------------
Date : November 2020
Copyright : (C) 2020 by Nyall Dawson
Email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/

#include "qgsgooglemapsgeocoder.h"
#include "qgsgeocodercontext.h"
#include "qgslogger.h"
#include "qgsnetworkaccessmanager.h"
#include "qgsblockingnetworkrequest.h"
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkRequest>
#include <QJsonDocument>
#include <QJsonObject>

QgsGoogleMapsGeocoder::QgsGoogleMapsGeocoder( const QString &apiKey, const QString &regionBias )
: QgsGeocoderInterface()
, mApiKey( apiKey )
, mRegion( regionBias )
, mEndpoint( QStringLiteral( "https://maps.googleapis.com/maps/api/geocode/json" ) )
{

}

QgsGeocoderInterface::Flags QgsGoogleMapsGeocoder::flags() const
{
return QgsGeocoderInterface::Flag::GeocodesStrings;
}

QgsFields QgsGoogleMapsGeocoder::appendedFields() const
{
QgsFields fields;
fields.append( QgsField( QStringLiteral( "location_type" ), QVariant::String ) );
fields.append( QgsField( QStringLiteral( "formatted_address" ), QVariant::String ) );
fields.append( QgsField( QStringLiteral( "place_id" ), QVariant::String ) );

// add more?
fields.append( QgsField( QStringLiteral( "street_number" ), QVariant::String ) );
fields.append( QgsField( QStringLiteral( "route" ), QVariant::String ) );
fields.append( QgsField( QStringLiteral( "locality" ), QVariant::String ) );
fields.append( QgsField( QStringLiteral( "administrative_area_level_2" ), QVariant::String ) );
fields.append( QgsField( QStringLiteral( "administrative_area_level_1" ), QVariant::String ) );
fields.append( QgsField( QStringLiteral( "country" ), QVariant::String ) );
fields.append( QgsField( QStringLiteral( "postal_code" ), QVariant::String ) );
return fields;
}

QgsWkbTypes::Type QgsGoogleMapsGeocoder::wkbType() const
{
return QgsWkbTypes::Point;
}

QList<QgsGeocoderResult> QgsGoogleMapsGeocoder::geocodeString( const QString &string, const QgsGeocoderContext &context, QgsFeedback *feedback ) const
{
QgsRectangle bounds;
if ( !context.areaOfInterest().isEmpty() )
{
QgsGeometry g = context.areaOfInterest();
QgsCoordinateTransform ct( context.areaOfInterestCrs(), QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ), context.transformContext() );
try
{
g.transform( ct );
bounds = g.boundingBox();
}
catch ( QgsCsException & )
{
QgsDebugMsg( "Could not transform geocode bounds to WGS84" );
}
}

const QUrl url = requestUrl( string, bounds );

QNetworkRequest request( url );
QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsGoogleMapsGeocoder" ) );

QgsBlockingNetworkRequest newReq;
const QgsBlockingNetworkRequest::ErrorCode errorCode = newReq.get( request, false, feedback );
if ( errorCode != QgsBlockingNetworkRequest::NoError )
{
return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( newReq.errorMessage() );
}

// Parse data
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson( newReq.reply().content(), &err );
if ( doc.isNull() )
{
return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( err.errorString() );
}
const QVariantMap res = doc.object().toVariantMap();
const QString status = res.value( QStringLiteral( "status" ) ).toString();
if ( status.isEmpty() || !res.contains( QStringLiteral( "results" ) ) )
{
return QList<QgsGeocoderResult>();
}

if ( res.contains( QStringLiteral( "error_message" ) ) )
{
return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( res.value( QStringLiteral( "error_message" ) ).toString() );
}

if ( status == QStringLiteral( "REQUEST_DENIED" ) || status == QStringLiteral( "OVER_QUERY_LIMIT" ) )
{
return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( QObject::tr( "Request denied -- the API key was rejected" ) );
}
if ( status != QStringLiteral( "OK" ) )
{
return QList<QgsGeocoderResult>() << QgsGeocoderResult::errorResult( res.value( QStringLiteral( "status" ) ).toString() );
}

// all good!
const QVariantList results = res.value( QStringLiteral( "results" ) ).toList();
if ( results.empty() )
{
return QList<QgsGeocoderResult>();
}

QList< QgsGeocoderResult > matches;
matches.reserve( results.size( ) );
for ( const QVariant &result : results )
{
matches << jsonToResult( result.toMap() );
}
return matches;
}

QUrl QgsGoogleMapsGeocoder::requestUrl( const QString &address, const QgsRectangle &bounds ) const
{
QUrl res( mEndpoint );
QUrlQuery query;
if ( !bounds.isNull() )
{
query.addQueryItem( QStringLiteral( "bounds" ), QStringLiteral( "%1,%2|%3,%4" ).arg( bounds.yMinimum() )
.arg( bounds.xMinimum() )
.arg( bounds.yMaximum() )
.arg( bounds.yMinimum() ) );
}
if ( !mRegion.isEmpty() )
{
query.addQueryItem( QStringLiteral( "region" ), mRegion.toLower() );
}
query.addQueryItem( QStringLiteral( "sensor" ), QStringLiteral( "false" ) );
query.addQueryItem( QStringLiteral( "address" ), address );
query.addQueryItem( QStringLiteral( "key" ), mApiKey );
res.setQuery( query );


if ( res.toString().contains( QLatin1String( "fake_qgis_http_endpoint" ) ) )
{
// Just for testing with local files instead of http:// resources
QString modifiedUrlString = res.toString();
// Qt5 does URL encoding from some reason (of the FILTER parameter for example)
modifiedUrlString = QUrl::fromPercentEncoding( modifiedUrlString.toUtf8() );
modifiedUrlString.replace( QLatin1String( "fake_qgis_http_endpoint/" ), QLatin1String( "fake_qgis_http_endpoint_" ) );
QgsDebugMsg( QStringLiteral( "Get %1" ).arg( modifiedUrlString ) );
modifiedUrlString = modifiedUrlString.mid( QStringLiteral( "http://" ).size() );
QString args = modifiedUrlString.mid( modifiedUrlString.indexOf( '?' ) );
if ( modifiedUrlString.size() > 150 )
{
args = QCryptographicHash::hash( args.toUtf8(), QCryptographicHash::Md5 ).toHex();
}
else
{
args.replace( QLatin1String( "?" ), QLatin1String( "_" ) );
args.replace( QLatin1String( "&" ), QLatin1String( "_" ) );
args.replace( QLatin1String( "<" ), QLatin1String( "_" ) );
args.replace( QLatin1String( ">" ), QLatin1String( "_" ) );
args.replace( QLatin1String( "'" ), QLatin1String( "_" ) );
args.replace( QLatin1String( "\"" ), QLatin1String( "_" ) );
args.replace( QLatin1String( " " ), QLatin1String( "_" ) );
args.replace( QLatin1String( ":" ), QLatin1String( "_" ) );
args.replace( QLatin1String( "/" ), QLatin1String( "_" ) );
args.replace( QLatin1String( "\n" ), QLatin1String( "_" ) );
}
#ifdef Q_OS_WIN
// Passing "urls" like "http://c:/path" to QUrl 'eats' the : after c,
// so we must restore it
if ( modifiedUrlString[1] == '/' )
{
modifiedUrlString = modifiedUrlString[0] + ":/" + modifiedUrlString.mid( 2 );
}
#endif
modifiedUrlString = modifiedUrlString.mid( 0, modifiedUrlString.indexOf( '?' ) ) + args;
QgsDebugMsg( QStringLiteral( "Get %1 (after laundering)" ).arg( modifiedUrlString ) );
res = QUrl::fromLocalFile( modifiedUrlString );
}

return res;
}

QgsGeocoderResult QgsGoogleMapsGeocoder::jsonToResult( const QVariantMap &json ) const
{
const QVariantMap geometry = json.value( QStringLiteral( "geometry" ) ).toMap();
const QVariantMap location = geometry.value( QStringLiteral( "location" ) ).toMap();
const double latitude = location.value( QStringLiteral( "lat" ) ).toDouble();
const double longitude = location.value( QStringLiteral( "lng" ) ).toDouble();

const QgsGeometry geom = QgsGeometry::fromPointXY( QgsPointXY( longitude, latitude ) );

QgsGeocoderResult res( json.value( QStringLiteral( "formatted_address" ) ).toString(),
geom,
QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) );

QVariantMap attributes;

if ( json.contains( QStringLiteral( "formatted_address" ) ) )
attributes.insert( QStringLiteral( "formatted_address" ), json.value( QStringLiteral( "formatted_address" ) ).toString() );
if ( json.contains( QStringLiteral( "place_id" ) ) )
attributes.insert( QStringLiteral( "place_id" ), json.value( QStringLiteral( "place_id" ) ).toString() );
if ( geometry.contains( QStringLiteral( "location_type" ) ) )
attributes.insert( QStringLiteral( "location_type" ), geometry.value( QStringLiteral( "location_type" ) ).toString() );

const QVariantList components = json.value( QStringLiteral( "address_components" ) ).toList();
for ( const QVariant &component : components )
{
const QVariantMap componentMap = component.toMap();
const QStringList types = componentMap.value( QStringLiteral( "types" ) ).toStringList();

for ( const QString &t :
{
QStringLiteral( "street_number" ),
QStringLiteral( "route" ),
QStringLiteral( "locality" ),
QStringLiteral( "administrative_area_level_2" ),
QStringLiteral( "administrative_area_level_1" ),
QStringLiteral( "country" ),
QStringLiteral( "postal_code" )
} )
{
if ( types.contains( t ) )
attributes.insert( t, componentMap.value( QStringLiteral( "long_name" ) ).toString() );
}
}

res.setAdditionalAttributes( attributes );
return res;
}

void QgsGoogleMapsGeocoder::setEndpoint( const QString &endpoint )
{
mEndpoint = endpoint;
}

0 comments on commit 98d0971

Please sign in to comment.
You can’t perform that action at this time.