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

0001 // SPDX-License-Identifier: LGPL-2.1-or-later
0002 //
0003 // SPDX-FileCopyrightText: 2010 Dennis Nienhüser <nienhueser@kde.org>
0004 //
0005 
0006 #include "OpenRouteServiceRunner.h"
0007 
0008 #include "MarbleDebug.h"
0009 #include "GeoDataDocument.h"
0010 #include "GeoDataPlacemark.h"
0011 #include "GeoDataData.h"
0012 #include "GeoDataExtendedData.h"
0013 #include "GeoDataLineString.h"
0014 #include "routing/RouteRequest.h"
0015 
0016 #include <QUrl>
0017 #include <QUrlQuery>
0018 #include <QTime>
0019 #include <QTimer>
0020 #include <QDomDocument>
0021 
0022 namespace Marble
0023 {
0024 
0025 OpenRouteServiceRunner::OpenRouteServiceRunner( QObject *parent ) :
0026         RoutingRunner( parent ),
0027         m_networkAccessManager()
0028 {
0029     connect( &m_networkAccessManager, SIGNAL(finished(QNetworkReply*)),
0030              this, SLOT(retrieveData(QNetworkReply*)));
0031 }
0032 
0033 void OpenRouteServiceRunner::retrieveRoute( const RouteRequest *route )
0034 {
0035     if ( route->size() < 2 ) {
0036         return;
0037     }
0038 
0039     GeoDataCoordinates source = route->source();
0040     GeoDataCoordinates destination = route->destination();
0041     QHash<QString, QVariant> settings = route->routingProfile().pluginSettings()["openrouteservice"];
0042 
0043     QUrlQuery queries;
0044     queries.addQueryItem("api_key", "ee0b8233adff52ce9fd6afc2a2859a28");
0045 
0046     QString unit = "KM";
0047     QString preference = "Fastest";
0048     if (settings.contains(QStringLiteral("preference"))) {
0049         preference = settings[QStringLiteral("preference")].toString();
0050     }
0051     if (preference == QLatin1String("Pedestrian")) {
0052         unit = QStringLiteral("M");
0053     }
0054 
0055     queries.addQueryItem("start", formatCoordinates(source));
0056     QStringList via;
0057     for (int i = 1; i < route->size()-1; ++i) {
0058         via << formatCoordinates(route->at(i));
0059     }
0060     queries.addQueryItem("via", via.join(' '));
0061     queries.addQueryItem("end", formatCoordinates(destination));
0062 
0063     queries.addQueryItem("distunit", unit);
0064     if (preference == "Fastest" || preference == "Shortest" || preference == "Recommended") {
0065         queries.addQueryItem("routepref", "Car");
0066         queries.addQueryItem("weighting", preference);
0067     } else {
0068         queries.addQueryItem("routepref", preference);
0069         queries.addQueryItem("weighting", "Recommended");
0070     }
0071 
0072     QString const motorways = settings.value("noMotorways").toInt() == 0 ? "false" : "true";
0073     queries.addQueryItem("noMotorways", motorways);
0074     QString const tollways = settings.value("noTollways").toInt() == 0 ? "false" : "true";
0075     queries.addQueryItem("noTollways", tollways);
0076     queries.addQueryItem("noUnpavedroads", "false");
0077     queries.addQueryItem("noSteps", "false");
0078     QString const ferries = settings.value("noFerries").toInt() == 0 ? "false" : "true";
0079     queries.addQueryItem("noFerries", ferries);
0080     queries.addQueryItem("instructions", "true");
0081     queries.addQueryItem("lang", "en");
0082 
0083     QUrl url = QUrl( "http://openls.geog.uni-heidelberg.de/route" );
0084     // QUrlQuery strips empty value pairs, but OpenRouteService does not work without
0085     QString const trailer = route->size() == 2 ? "&via=" : QString();
0086     url.setQuery(queries.toString() + trailer);
0087 
0088     m_request = QNetworkRequest( url );
0089 
0090     QEventLoop eventLoop;
0091     QTimer timer;
0092     timer.setSingleShot( true );
0093     timer.setInterval( 15000 );
0094 
0095     connect( &timer, SIGNAL(timeout()),
0096              &eventLoop, SLOT(quit()));
0097     connect( this, SIGNAL(routeCalculated(GeoDataDocument*)),
0098              &eventLoop, SLOT(quit()));
0099 
0100     // @todo FIXME Must currently be done in the main thread, see bug 257376
0101     QTimer::singleShot( 0, this, SLOT(get()));
0102     timer.start();
0103 
0104     eventLoop.exec();
0105 }
0106 
0107 void OpenRouteServiceRunner::get()
0108 {
0109     QNetworkReply *reply = m_networkAccessManager.get(m_request);
0110     connect( reply, SIGNAL(error(QNetworkReply::NetworkError)),
0111              this, SLOT(handleError(QNetworkReply::NetworkError)), Qt::DirectConnection);
0112 }
0113 
0114 QString OpenRouteServiceRunner::formatCoordinates(const GeoDataCoordinates &coordinates)
0115 {
0116     return QStringLiteral("%1,%2")
0117             .arg(coordinates.longitude(GeoDataCoordinates::Degree ), 0, 'f', 8)
0118             .arg(coordinates.latitude(GeoDataCoordinates::Degree ), 0, 'f', 8);
0119 }
0120 
0121 void OpenRouteServiceRunner::retrieveData( QNetworkReply *reply )
0122 {
0123     if ( reply->isFinished() ) {
0124         QByteArray data = reply->readAll();
0125         reply->deleteLater();
0126         //mDebug() << "Download completed: " << data;
0127         GeoDataDocument* document = parse( data );
0128 
0129         if ( !document ) {
0130             mDebug() << "Failed to parse the downloaded route data" << data;
0131         }
0132 
0133         emit routeCalculated( document );
0134     }
0135 }
0136 
0137 void OpenRouteServiceRunner::handleError( QNetworkReply::NetworkError error )
0138 {
0139     mDebug() << " Error when retrieving openrouteservice.org route: " << error;
0140 }
0141 
0142 GeoDataDocument* OpenRouteServiceRunner::parse( const QByteArray &content ) const
0143 {
0144     QDomDocument xml;
0145     if ( !xml.setContent( content ) ) {
0146         mDebug() << "Cannot parse xml file with routing instructions.";
0147         return nullptr;
0148     }
0149 
0150     QDomElement root = xml.documentElement();
0151 
0152     GeoDataDocument* result = new GeoDataDocument();
0153     result->setName(QStringLiteral("OpenRouteService"));
0154 
0155     QDomNodeList errors = root.elementsByTagName(QStringLiteral("xls:Error"));
0156     if ( errors.size() > 0 ) {
0157         return nullptr;
0158         // Returning early because fallback routing providers are used now
0159         // The code below can be used to parse OpenGis errors reported by ORS
0160         // and may be useful in the future
0161 
0162         for (int i=0 ; i < errors.length(); ++i ) {
0163             QDomNode node = errors.item( i );
0164             QString errorMessage = node.attributes().namedItem(QStringLiteral("message")).nodeValue();
0165             QRegExp regexp = QRegExp( "^(.*) Please Check your Position: (-?[0-9]+.[0-9]+) (-?[0-9]+.[0-9]+) !" );
0166             if ( regexp.indexIn( errorMessage ) == 0 ) {
0167                 if ( regexp.capturedTexts().size() == 4 ) {
0168                     GeoDataPlacemark* placemark = new GeoDataPlacemark;
0169                     placemark->setName( regexp.capturedTexts().at( 1 ) );
0170                     GeoDataCoordinates position;
0171                     position.setLongitude( regexp.capturedTexts().at( 2 ).toDouble(), GeoDataCoordinates::Degree );
0172                     position.setLatitude( regexp.capturedTexts().at( 3 ).toDouble(), GeoDataCoordinates::Degree );
0173                     placemark->setCoordinate( position );
0174                     result->append( placemark );
0175                 }
0176             } else {
0177                 mDebug() << "Error message " << errorMessage << " not parsable.";
0178                 /** @todo: How to handle this now with plugins? */
0179 //                QString message = tr( "Sorry, a problem occurred when calculating the route. Try adjusting start and destination points." );
0180 //                QPointer<QMessageBox> messageBox = new QMessageBox( QMessageBox::Warning, "Route Error", message );
0181 //                messageBox->setDetailedText( errorMessage );
0182 //                messageBox->exec();
0183 //                delete messageBox;
0184             }
0185         }
0186     }
0187 
0188     GeoDataPlacemark* routePlacemark = new GeoDataPlacemark;
0189     routePlacemark->setName(QStringLiteral("Route"));
0190     QTime time;
0191     QDomNodeList summary = root.elementsByTagName(QStringLiteral("xls:RouteSummary"));
0192     if ( summary.size() > 0 ) {
0193         QDomNodeList timeNodeList = summary.item(0).toElement().elementsByTagName(QStringLiteral("xls:TotalTime"));
0194         if ( timeNodeList.size() == 1 ) {
0195             QRegExp regexp = QRegExp( "^P(?:(\\d+)D)?T(?:(\\d+)H)?(?:(\\d+)M)?(\\d+)S" );
0196             if ( regexp.indexIn( timeNodeList.item( 0 ).toElement().text() ) == 0 ) {
0197                 QStringList matches = regexp.capturedTexts();
0198                 unsigned int hours( 0 ), minutes( 0 ), seconds( 0 );
0199                 switch ( matches.size() ) {
0200                 case 5:
0201                     // days    = regexp.cap( matches.size() - 4 ).toInt();
0202                     Q_FALLTHROUGH();
0203                 case 4:
0204                     hours   = regexp.cap( matches.size() - 3 ).toInt();
0205                     Q_FALLTHROUGH();
0206                 case 3:
0207                     minutes = regexp.cap( matches.size() - 2 ).toInt();
0208                     Q_FALLTHROUGH();
0209                 case 2:
0210                     seconds = regexp.cap( matches.size() - 1 ).toInt();
0211                     break;
0212                 default:
0213                     mDebug() << "Unable to parse time string " << timeNodeList.item( 0 ).toElement().text();
0214                 }
0215 
0216                 time = QTime( hours, minutes, seconds, 0 );
0217             }
0218         }
0219     }
0220 
0221     GeoDataLineString* routeWaypoints = new GeoDataLineString;
0222     QDomNodeList geometry = root.elementsByTagName(QStringLiteral("xls:RouteGeometry"));
0223     if ( geometry.size() > 0 ) {
0224         QDomNodeList waypoints = geometry.item( 0 ).toElement().elementsByTagName( "gml:pos" );
0225         for (int i=0 ; i < waypoints.length(); ++i ) {
0226             QDomNode node = waypoints.item( i );
0227             const QStringList content = node.toElement().text().split(QLatin1Char(' '));
0228             if ( content.length() == 2 ) {
0229                 GeoDataCoordinates position;
0230                 position.setLongitude( content.at( 0 ).toDouble(), GeoDataCoordinates::Degree );
0231                 position.setLatitude( content.at( 1 ).toDouble(), GeoDataCoordinates::Degree );
0232                 routeWaypoints->append( position );
0233             }
0234         }
0235     }
0236     routePlacemark->setGeometry( routeWaypoints );
0237 
0238     qreal length = routeWaypoints->length( EARTH_RADIUS );
0239     const QString name = nameString( "ORS", length, time );
0240     const GeoDataExtendedData data = routeData( length, time );
0241     routePlacemark->setExtendedData( data );
0242     result->setName( name );
0243 
0244     result->append( routePlacemark );
0245 
0246     QDomNodeList instructionList = root.elementsByTagName(QStringLiteral("xls:RouteInstructionsList"));
0247     if ( instructionList.size() > 0 ) {
0248         QDomNodeList instructions = instructionList.item(0).toElement().elementsByTagName(QStringLiteral("xls:RouteInstruction"));
0249         for (int i=0 ; i < instructions.length(); ++i ) {
0250             QDomElement node = instructions.item( i ).toElement();
0251 
0252             QDomNodeList textNodes = node.elementsByTagName(QStringLiteral("xls:Instruction"));
0253             QDomNodeList positions = node.elementsByTagName(QStringLiteral("gml:pos"));
0254 
0255             if ( textNodes.size() > 0 && positions.size() > 0 ) {
0256                 const QStringList content = positions.at(0).toElement().text().split(QLatin1Char(' '));
0257                 if ( content.length() == 2 ) {
0258                     GeoDataLineString *lineString = new GeoDataLineString;
0259 
0260                     for( int i = 0; i < positions.count(); ++i ) {
0261                          const QStringList pointList = positions.at(i).toElement().text().split(QLatin1Char(' '));
0262                          GeoDataCoordinates position;
0263                          position.setLongitude( pointList.at( 0 ).toDouble(), GeoDataCoordinates::Degree );
0264                          position.setLatitude( pointList.at( 1 ).toDouble(), GeoDataCoordinates::Degree );
0265                          lineString->append( position );
0266                     }
0267 
0268                     GeoDataPlacemark* instruction = new GeoDataPlacemark;
0269 
0270                     QString const text = textNodes.item( 0 ).toElement().text().remove(QRegExp("<[^>]*>"));
0271                     GeoDataExtendedData extendedData;
0272                     GeoDataData turnTypeData;
0273                     turnTypeData.setName(QStringLiteral("turnType"));
0274                     QString road;
0275                     RoutingInstruction::TurnType turnType = parseTurnType( text, &road );
0276                     turnTypeData.setValue( turnType );
0277                     extendedData.addValue( turnTypeData );
0278                     if ( !road.isEmpty() ) {
0279                         GeoDataData roadName;
0280                         roadName.setName(QStringLiteral("roadName"));
0281                         roadName.setValue( road );
0282                         extendedData.addValue( roadName );
0283                     }
0284 
0285                     QString const instructionText = turnType == RoutingInstruction::Unknown ? text : RoutingInstruction::generateRoadInstruction( turnType, road );
0286                     instruction->setName( instructionText );
0287                     instruction->setExtendedData( extendedData );
0288                     instruction->setGeometry( lineString );
0289                     result->append( instruction );
0290                 }
0291             }
0292         }
0293     }
0294 
0295     return result;
0296 }
0297 
0298 RoutingInstruction::TurnType OpenRouteServiceRunner::parseTurnType( const QString &text, QString *road )
0299 {
0300     QRegExp syntax( "^(Go|Drive|Turn) (half left|left|sharp left|straight forward|half right|right|sharp right)( on )?(.*)?$", Qt::CaseSensitive, QRegExp::RegExp2 );
0301     QString instruction;
0302     if ( syntax.indexIn( text ) == 0 ) {
0303         if ( syntax.captureCount() > 1 ) {
0304             instruction = syntax.cap( 2 );
0305             if ( syntax.captureCount() == 4 ) {
0306                 *road = syntax.cap( 4 ).remove(QLatin1String( " - Arrived at destination!"));
0307             }
0308         }
0309     }
0310 
0311     if (instruction == QLatin1String("Continue")) {
0312         return RoutingInstruction::Straight;
0313     } else if (instruction == QLatin1String("half right")) {
0314         return RoutingInstruction::SlightRight;
0315     } else if (instruction == QLatin1String("right")) {
0316         return RoutingInstruction::Right;
0317     } else if (instruction == QLatin1String("sharp right")) {
0318         return RoutingInstruction::SharpRight;
0319     } else if (instruction == QLatin1String("straight forward")) {
0320         return RoutingInstruction::Straight;
0321     } else if (instruction == QLatin1String("turn")) {
0322         return RoutingInstruction::TurnAround;
0323     } else if (instruction == QLatin1String("sharp left")) {
0324         return RoutingInstruction::SharpLeft;
0325     } else if (instruction == QLatin1String("left")) {
0326         return RoutingInstruction::Left;
0327     } else if (instruction == QLatin1String("half left")) {
0328         return RoutingInstruction::SlightLeft;
0329     }
0330 
0331     return RoutingInstruction::Unknown;
0332 }
0333 
0334 } // namespace Marble
0335 
0336 #include "moc_OpenRouteServiceRunner.cpp"