File indexing completed on 2024-06-23 03:50:27

0001 // SPDX-License-Identifier: LGPL-2.1-or-later
0002 //
0003 // SPDX-FileCopyrightText: 2012 Dennis Nienhüser <nienhueser@kde.org>
0004 //
0005 
0006 #include "MapQuestRunner.h"
0007 
0008 #include "MarbleDebug.h"
0009 #include "MarbleLocale.h"
0010 #include "GeoDataDocument.h"
0011 #include "GeoDataPlacemark.h"
0012 #include "GeoDataExtendedData.h"
0013 #include "GeoDataData.h"
0014 #include "GeoDataLineString.h"
0015 #include "HttpDownloadManager.h"
0016 #include "routing/Maneuver.h"
0017 #include "routing/RouteRequest.h"
0018 
0019 #include <QString>
0020 #include <QUrl>
0021 #include <QTime>
0022 #include <QTimer>
0023 #include <QDomDocument>
0024 
0025 #include <QUrlQuery>
0026 
0027 namespace Marble
0028 {
0029 
0030 MapQuestRunner::MapQuestRunner( QObject *parent ) :
0031     RoutingRunner( parent ),
0032     m_networkAccessManager(),
0033     m_request()
0034 {
0035     connect( &m_networkAccessManager, SIGNAL(finished(QNetworkReply*)),
0036              this, SLOT(retrieveData(QNetworkReply*)) );
0037 }
0038 
0039 MapQuestRunner::~MapQuestRunner()
0040 {
0041     // nothing to do
0042 }
0043 
0044 void MapQuestRunner::retrieveRoute( const RouteRequest *route )
0045 {
0046     if ( route->size() < 2 ) {
0047         return;
0048     }
0049 
0050     QHash<QString, QVariant> settings = route->routingProfile().pluginSettings()["mapquest"];
0051 
0052     if (settings.value(QStringLiteral("appKey")).toString().isEmpty()) {
0053         return;
0054     }
0055 
0056     QString url = "http://open.mapquestapi.com/directions/v1/route?callback=renderAdvancedNarrative&outFormat=xml&narrativeType=text&shapeFormat=raw&generalize=0";
0057     GeoDataCoordinates::Unit const degree = GeoDataCoordinates::Degree;
0058     append(&url, "from", QString::number(route->source().latitude(degree), 'f', 6) + QLatin1Char(',') + QString::number(route->source().longitude(degree), 'f', 6));
0059     for ( int i=1; i<route->size(); ++i ) {
0060         append(&url, "to", QString::number(route->at(i).latitude(degree), 'f', 6) + QLatin1Char(',') + QString::number(route->at(i).longitude(degree), 'f', 6));
0061     }
0062 
0063     QString const unit = MarbleGlobal::getInstance()->locale()->measurementSystem() == MarbleLocale::MetricSystem ? "k" : "m";
0064     append( &url, "units", unit );
0065 
0066     if (settings[QStringLiteral("noMotorways")].toInt()) {
0067         append( &url, "avoids", "Limited Access" );
0068     }
0069     if (settings[QStringLiteral("noTollroads")].toInt()) {
0070         append( &url, "avoids", "Toll road" );
0071     }
0072     if (settings[QStringLiteral("noFerries")].toInt()) {
0073         append( &url, "avoids", "Ferry" );
0074     }
0075 
0076     if (!settings[QStringLiteral("preference")].toString().isEmpty()) {
0077         append(&url, QStringLiteral("routeType"), settings[QStringLiteral("preference")].toString());
0078     }
0079 
0080     const QString ascendingSetting = settings[QStringLiteral("ascending")].toString();
0081     const QString descendingSetting = settings[QStringLiteral("descending")].toString();
0082     if (!ascendingSetting.isEmpty() && !descendingSetting.isEmpty()) {
0083         if (ascendingSetting == QLatin1String("AVOID_UP_HILL") &&
0084             descendingSetting == QLatin1String("AVOID_DOWN_HILL")) {
0085             append(&url, QStringLiteral("roadGradeStrategy"), QStringLiteral("AVOID_ALL_HILLS"));
0086         }
0087         else if (ascendingSetting == QLatin1String("FAVOR_UP_HILL") &&
0088                  descendingSetting == QLatin1String("FAVOR_DOWN_HILL")) {
0089             append(&url, QStringLiteral("roadGradeStrategy"), QStringLiteral("FAVOR_ALL_HILLS"));
0090         }
0091         else if (ascendingSetting == QLatin1String("DEFAULT_STRATEGY") &&
0092                  descendingSetting == QLatin1String("DEFAULT_STRATEGY")) {
0093             append(&url, QStringLiteral("roadGradeStrategy"), QStringLiteral("DEFAULT_STRATEGY"));
0094         }
0095         else if (ascendingSetting == QLatin1String("DEFAULT_STRATEGY")) {
0096             append(&url, QStringLiteral("roadGradeStrategy"), descendingSetting);
0097         }
0098         else if (descendingSetting == QLatin1String("DEFAULT_STRATEGY")) {
0099             append(&url, QStringLiteral("roadGradeStrategy"), ascendingSetting);
0100         }
0101         else if (descendingSetting == QLatin1String("AVOID_DOWN_HILL")) {
0102             append(&url, QStringLiteral("roadGradeStrategy"), descendingSetting);
0103         }
0104         else if (ascendingSetting == QLatin1String("AVOID_UP_HILL")) {
0105             append(&url, QStringLiteral("roadGradeStrategy"), ascendingSetting);
0106         }
0107     }
0108 
0109     QUrl qurl(url);
0110     // FIXME: verify that this works with special characters.
0111     QUrlQuery urlQuery;
0112     urlQuery.addQueryItem(QStringLiteral("key"), settings.value(QStringLiteral("appKey")).toByteArray());
0113     qurl.setQuery(urlQuery);
0114     m_request.setUrl( qurl );
0115     m_request.setRawHeader( "User-Agent", HttpDownloadManager::userAgent( "Browser", "MapQuestRunner" ) );
0116 
0117     QEventLoop eventLoop;
0118 
0119     QTimer timer;
0120     timer.setSingleShot( true );
0121     timer.setInterval( 15000 );
0122 
0123     connect( &timer, SIGNAL(timeout()),
0124              &eventLoop, SLOT(quit()));
0125     connect( this, SIGNAL(routeCalculated(GeoDataDocument*)),
0126              &eventLoop, SLOT(quit()) );
0127 
0128     // @todo FIXME Must currently be done in the main thread, see bug 257376
0129     QTimer::singleShot( 0, this, SLOT(get()) );
0130     timer.start();
0131 
0132     eventLoop.exec();
0133 }
0134 
0135 void MapQuestRunner::get()
0136 {
0137     QNetworkReply *reply = m_networkAccessManager.get( m_request );
0138     connect( reply, SIGNAL(error(QNetworkReply::NetworkError)),
0139              this, SLOT(handleError(QNetworkReply::NetworkError)), Qt::DirectConnection );
0140 }
0141 
0142 void MapQuestRunner::retrieveData( QNetworkReply *reply )
0143 {
0144     if ( reply->isFinished() ) {
0145         QByteArray data = reply->readAll();
0146         reply->deleteLater();
0147         //mDebug() << "Download completed: " << data;
0148         GeoDataDocument* document = parse( data );
0149 
0150         if ( !document ) {
0151             mDebug() << "Failed to parse the downloaded route data" << data;
0152         }
0153 
0154         emit routeCalculated( document );
0155     }
0156 }
0157 
0158 void MapQuestRunner::handleError( QNetworkReply::NetworkError error )
0159 {
0160     mDebug() << " Error when retrieving mapquest.org route: " << error;
0161 }
0162 
0163 void MapQuestRunner::append(QString *input, const QString &key, const QString &value)
0164 {
0165     *input += QLatin1Char('&') + key + QLatin1Char('=') + value;
0166 }
0167 
0168 int MapQuestRunner::maneuverType( int mapQuestId )
0169 {
0170     /** @todo FIXME: review 10, 11 */
0171     switch( mapQuestId ) {
0172     case 0: return Maneuver::Straight ; // straight
0173     case 1: return Maneuver::SlightRight ; // slight right
0174     case 2: return Maneuver::Right ; // right
0175     case 3: return Maneuver::SharpRight ; // sharp right
0176     case 4: return Maneuver::TurnAround ; // reverse
0177     case 5: return Maneuver::SharpLeft ; // sharp left
0178     case 6: return Maneuver::Left ; // left
0179     case 7: return Maneuver::SlightLeft ; // slight left
0180     case 8: return Maneuver::TurnAround ; // right u-turn
0181     case 9: return Maneuver::TurnAround ; // left u-turn
0182     case 10: return Maneuver::Merge ; // right merge
0183     case 11: return Maneuver::Merge ; // left merge
0184     case 12: return Maneuver::Merge ; // right on ramp
0185     case 13: return Maneuver::Merge ; // left on ramp
0186     case 14: return Maneuver::ExitRight ; // right off ramp
0187     case 15: return Maneuver::ExitLeft ; // left off ramp
0188     case 16: return Maneuver::Right ; // right fork
0189     case 17: return Maneuver::Left ; // left fork
0190     case 18: return Maneuver::Continue ; // straight fork
0191     }
0192 
0193     return Maneuver::Unknown;
0194 }
0195 
0196 GeoDataDocument* MapQuestRunner::parse( const QByteArray &content ) const
0197 {
0198     QDomDocument xml;
0199     if ( !xml.setContent( content ) ) {
0200         mDebug() << "Cannot parse xml file with routing instructions.";
0201         return nullptr;
0202     }
0203 
0204     // mDebug() << xml.toString(2);
0205     QDomElement root = xml.documentElement();
0206 
0207     GeoDataDocument* result = new GeoDataDocument();
0208     result->setName(QStringLiteral("MapQuest"));
0209     GeoDataPlacemark* routePlacemark = new GeoDataPlacemark;
0210     routePlacemark->setName(QStringLiteral("Route"));
0211 
0212     GeoDataLineString* routeWaypoints = new GeoDataLineString;
0213     QDomNodeList shapePoints = root.elementsByTagName(QStringLiteral("shapePoints"));
0214     if ( shapePoints.size() == 1 ) {
0215         QDomNodeList geometry = shapePoints.at(0).toElement().elementsByTagName(QStringLiteral("latLng"));
0216         for ( int i=0; i<geometry.size(); ++i ) {
0217             double const lat = geometry.item(i).namedItem(QStringLiteral("lat")).toElement().text().toDouble();
0218             double const lon = geometry.item(i).namedItem(QStringLiteral("lng")).toElement().text().toDouble();
0219             GeoDataCoordinates const position( lon, lat, 0.0, GeoDataCoordinates::Degree );
0220             routeWaypoints->append( position );
0221         }
0222     }
0223     routePlacemark->setGeometry( routeWaypoints );
0224 
0225     QTime time;
0226     time = time.addSecs(root.elementsByTagName(QStringLiteral("time")).at(0).toElement().text().toInt());
0227     qreal length = routeWaypoints->length( EARTH_RADIUS );
0228     const QString name = nameString( "MQ", length, time );
0229     const GeoDataExtendedData data = routeData( length, time );
0230     routePlacemark->setExtendedData( data );
0231     result->setName( name );
0232     result->append( routePlacemark );
0233 
0234     QMap<int,int> mapping;
0235     QDomNodeList maneuvers = root.elementsByTagName(QStringLiteral("maneuverIndexes"));
0236     if ( maneuvers.size() == 1 ) {
0237         maneuvers = maneuvers.at( 0 ).childNodes();
0238         for ( int i=0; i<maneuvers.size(); ++i ) {
0239             mapping[i] = maneuvers.at( i ).toElement().text().toInt();
0240             if ( mapping[i] == routeWaypoints->size() ) {
0241                 --mapping[i];
0242             }
0243         }
0244     }
0245 
0246     QDomNodeList instructions = root.elementsByTagName(QStringLiteral("maneuver"));
0247     unsigned int const lastInstruction = qMax<int>( 0, instructions.length()-1 ); // ignore the last 'Welcome to xy' instruction
0248     for ( unsigned int i = 0; i < lastInstruction; ++i ) {
0249         QDomElement node = instructions.item( i ).toElement();
0250 
0251         QDomNodeList maneuver = node.elementsByTagName(QStringLiteral("turnType"));
0252         QDomNodeList textNodes = node.elementsByTagName(QStringLiteral("narrative"));
0253         QDomNodeList points = node.elementsByTagName(QStringLiteral("startPoint"));
0254         QDomNodeList streets = node.elementsByTagName(QStringLiteral("streets"));
0255 
0256         Q_ASSERT( mapping.contains( i ) );
0257         if ( textNodes.size() == 1 && maneuver.size() == 1 && points.size() == 1 && mapping.contains( i ) ) {
0258             GeoDataPlacemark* instruction = new GeoDataPlacemark;
0259             instruction->setName( textNodes.at( 0 ).toElement().text() );
0260 
0261             GeoDataExtendedData extendedData;
0262             GeoDataData turnType;
0263             turnType.setName(QStringLiteral("turnType"));
0264             turnType.setValue( maneuverType( maneuver.at( 0 ).toElement().text().toInt() ) );
0265             extendedData.addValue( turnType );
0266             if ( streets.size() == 1 ) {
0267                 GeoDataData roadName;
0268                 roadName.setName(QStringLiteral("roadName"));
0269                 roadName.setValue( streets.at( 0 ).toElement().text() );
0270                 extendedData.addValue( roadName );
0271             }
0272             instruction->setExtendedData( extendedData );
0273 
0274             int const start = mapping[i];
0275             int const end = mapping.contains(i+1) ? mapping[i+1] : routeWaypoints->size()-1;
0276             if ( start >= 0 && start < routeWaypoints->size() && end < routeWaypoints->size() ) {
0277                 instruction->setName( textNodes.item( 0 ).toElement().text() );
0278                 GeoDataLineString *lineString = new GeoDataLineString;
0279                 for ( int j=start; j<=end; ++j ) {
0280                     *lineString << GeoDataCoordinates( routeWaypoints->at( j ).longitude(), routeWaypoints->at( j ).latitude() );
0281                 }
0282 
0283                 if ( !lineString->isEmpty() ) {
0284                     instruction->setGeometry( lineString );
0285                     result->append( instruction );
0286                 }
0287             }
0288         }
0289     }
0290 
0291     if ( routeWaypoints->size() < 1 ) {
0292         delete result;
0293         result = nullptr;
0294     }
0295 
0296     return result;
0297 }
0298 
0299 
0300 } // namespace Marble
0301 
0302 #include "moc_MapQuestRunner.cpp"