File indexing completed on 2024-04-28 03:49:27

0001 // SPDX-License-Identifier: LGPL-2.1-or-later
0002 //
0003 // SPDX-FileCopyrightText: 2010 Dennis Nienhüser <nienhueser@kde.org>
0004 //
0005 
0006 #include "AlternativeRoutesModel.h"
0007 
0008 #include "GeoDataLatLonAltBox.h"
0009 #include "GeoDataDocument.h"
0010 #include "GeoDataFolder.h"
0011 #include "GeoDataExtendedData.h"
0012 #include "GeoDataLineString.h"
0013 #include "GeoDataPlacemark.h"
0014 
0015 #include <QElapsedTimer>
0016 #include <QTimer>
0017 #include <QPainter>
0018 
0019 namespace Marble {
0020 
0021 class Q_DECL_HIDDEN AlternativeRoutesModel::Private
0022 {
0023 public:
0024     Private();
0025 
0026     /**
0027       * Returns true if there exists a route with high similarity to the given one
0028       */
0029     bool filter( const GeoDataDocument* document ) const;
0030 
0031     /**
0032       * Returns a similarity measure in the range of [0..1]. Two routes with a similarity of 0 can
0033       * be treated as totally different (e.g. different route requests), two routes with a similarity
0034       * of 1 are considered equal. Otherwise the routes overlap to an extent indicated by the
0035       * similarity value -- the higher, the more they do overlap.
0036       * @note: The direction of routes is important; reversed routes are not considered equal
0037       */
0038     static qreal similarity( const GeoDataDocument* routeA, const GeoDataDocument* routeB );
0039 
0040     /**
0041       * Returns the distance between the given polygon and the given point
0042       */
0043     static qreal distance( const GeoDataLineString &wayPoints, const GeoDataCoordinates &position );
0044 
0045     /**
0046       * Returns the bearing of the great circle path defined by the coordinates one and two
0047       * Based on https://www.movable-type.co.uk/scripts/latlong.html
0048       */
0049     static qreal bearing( const GeoDataCoordinates &one, const GeoDataCoordinates &two );
0050 
0051     /**
0052       * Returns the distance between the given point and the line segment (not line) defined
0053       * by the two coordinates lineA and lineB
0054       */
0055     static qreal distance( const GeoDataCoordinates &satellite, const GeoDataCoordinates &lineA, const GeoDataCoordinates &lineB );
0056 
0057     /**
0058       * Returns the point reached when traveling the given distance from start with the given direction
0059       */
0060     static GeoDataCoordinates coordinates( const GeoDataCoordinates &start, qreal distance, qreal bearing );
0061 
0062     /**
0063       * Returns the similarity between routeA and routeB. This method is not symmetric, i.e. in
0064       * general unidirectionalSimilarity(a,b) != unidirectionalSimilarity(b,a)
0065       */
0066     static qreal unidirectionalSimilarity( const GeoDataDocument* routeA, const GeoDataDocument* routeB );
0067 
0068     /**
0069       * (Primitive) scoring for routes
0070       */
0071     static bool higherScore( const GeoDataDocument* one, const GeoDataDocument* two );
0072 
0073     /**
0074       * Returns true if the given route contains instructions (placemarks with turn instructions)
0075       */
0076     static qreal instructionScore( const GeoDataDocument* document );
0077 
0078     static const GeoDataLineString* waypoints( const GeoDataDocument* document );
0079 
0080     static int nonZero( const QImage &image );
0081 
0082     static QPolygonF polygon( const GeoDataLineString &lineString, qreal x, qreal y, qreal sx, qreal sy );
0083 
0084     /** The currently shown alternative routes (model data) */
0085     QVector<GeoDataDocument*> m_routes;
0086 
0087     /** Pending route data (waiting for other results to come in) */
0088     QVector<GeoDataDocument*> m_restrainedRoutes;
0089 
0090     /** Counts the time between route request and first result */
0091     QElapsedTimer m_responseTime;
0092 
0093     int m_currentIndex;
0094 };
0095 
0096 
0097 AlternativeRoutesModel::Private::Private() :
0098         m_currentIndex( -1 )
0099 {
0100     // nothing to do
0101 }
0102 
0103 int AlternativeRoutesModel::Private::nonZero( const QImage &image )
0104 {
0105   QRgb const black = qRgb( 0, 0, 0 );
0106   int count = 0;
0107   for ( int y = 0; y < image.height(); ++y ) {
0108       QRgb* destLine = (QRgb*) image.scanLine( y );
0109       for ( int x = 0; x < image.width(); ++x ) {
0110           count += destLine[x] == black ? 0 : 1;
0111       }
0112   }
0113   return count;
0114 }
0115 
0116 QPolygonF AlternativeRoutesModel::Private::polygon( const GeoDataLineString &lineString, qreal x, qreal y, qreal sx, qreal sy )
0117 {
0118     QPolygonF poly;
0119     for ( int i = 0; i < lineString.size(); ++i ) {
0120         poly << QPointF( qAbs( ( lineString)[i].longitude() - x ) * sx,
0121                          qAbs( ( lineString)[i].latitude()  - y ) * sy );
0122     }
0123     return poly;
0124 }
0125 
0126 bool AlternativeRoutesModel::Private::filter( const GeoDataDocument* document ) const
0127 {
0128     for ( int i=0; i<m_routes.size(); ++i ) {
0129         qreal similarity = Private::similarity( document, m_routes.at( i ) );
0130         if ( similarity > 0.8 ) {
0131             return true;
0132         }
0133     }
0134 
0135     return false;
0136 }
0137 
0138 qreal AlternativeRoutesModel::Private::similarity( const GeoDataDocument* routeA, const GeoDataDocument* routeB )
0139 {
0140     return qMax<qreal>( unidirectionalSimilarity( routeA, routeB ),
0141                         unidirectionalSimilarity( routeB, routeA ) );
0142 }
0143 
0144 qreal AlternativeRoutesModel::Private::distance( const GeoDataLineString &wayPoints, const GeoDataCoordinates &position )
0145 {
0146     Q_ASSERT( !wayPoints.isEmpty() );
0147     qreal minDistance = 0;
0148     for ( int i = 1; i < wayPoints.size(); ++i ) {
0149         qreal dist = distance( position, wayPoints.at( i-1 ), wayPoints.at( i ) );
0150         if ( minDistance <= 0 || dist < minDistance ) {
0151             minDistance = dist;
0152         }
0153     }
0154 
0155     return minDistance;
0156 }
0157 
0158 qreal AlternativeRoutesModel::Private::bearing( const GeoDataCoordinates &one, const GeoDataCoordinates &two )
0159 {
0160     qreal delta = two.longitude() - one.longitude();
0161     qreal lat1 = one.latitude();
0162     qreal lat2 = two.latitude();
0163     return fmod( atan2( sin ( delta ) * cos ( lat2 ),
0164                  cos( lat1 ) * sin( lat2 ) - sin( lat1 ) * cos( lat2 ) * cos ( delta ) ), 2 * M_PI );
0165 }
0166 
0167 GeoDataCoordinates AlternativeRoutesModel::Private::coordinates( const GeoDataCoordinates &start, qreal distance, qreal bearing )
0168 {
0169     qreal lat1 = start.latitude();
0170     qreal lon1 = start.longitude();
0171     qreal lat2 = asin( sin( lat1 ) * cos( distance ) + cos( lat1 ) * sin( distance ) * cos( bearing ) );
0172     qreal lon2 = lon1 + atan2( sin( bearing ) * sin( distance ) * cos( lat1 ), cos( distance ) - sin( lat1 ) * sin( lat2 ) );
0173     return GeoDataCoordinates( lon2, lat2 );
0174 }
0175 
0176 qreal AlternativeRoutesModel::Private::distance( const GeoDataCoordinates &satellite, const GeoDataCoordinates &lineA, const GeoDataCoordinates &lineB )
0177 {
0178     const qreal dist = lineA.sphericalDistanceTo(satellite);
0179     qreal bearA = bearing( lineA, satellite );
0180     qreal bearB = bearing( lineA, lineB );
0181     qreal result = asin( sin ( dist ) * sin( bearB - bearA ) );
0182     Q_ASSERT(qMax<qreal>(satellite.sphericalDistanceTo(lineA), satellite.sphericalDistanceTo(lineB)) >= qAbs<qreal>(result));
0183 
0184     result = acos( cos( dist ) / cos( result ) );
0185     /** @todo: This is a naive approach. Look into the maths. */
0186     const qreal final = qMin<qreal>(satellite.sphericalDistanceTo(lineA), satellite.sphericalDistanceTo(lineB));
0187     if ( result >= 0 && result <= lineA.sphericalDistanceTo(lineB)) {
0188         GeoDataCoordinates nearest = coordinates( lineA, result, bearB );
0189         return qMin<qreal>(final, satellite.sphericalDistanceTo(nearest));
0190     } else {
0191         return final;
0192     }
0193 }
0194 
0195 qreal AlternativeRoutesModel::Private::unidirectionalSimilarity( const GeoDataDocument* routeA, const GeoDataDocument* routeB )
0196 {
0197     const GeoDataLineString* waypointsA = waypoints( routeA );
0198     const GeoDataLineString* waypointsB = waypoints( routeB );
0199     if ( !waypointsA || !waypointsB )
0200     {
0201         return 0.0;
0202     }
0203 
0204     QImage image( 64, 64, QImage::Format_ARGB32_Premultiplied );
0205     image.fill( qRgb( 0, 0, 0 ) );
0206     GeoDataLatLonBox box = GeoDataLatLonBox::fromLineString( *waypointsA );
0207     box = box.united( GeoDataLatLonBox::fromLineString( *waypointsB ) );
0208     if ( !box.width() || !box.height() ) {
0209       return 0.0;
0210     }
0211 
0212     qreal const sw = image.width() / box.width();
0213     qreal const sh = image.height() / box.height();
0214 
0215     QPainter painter( &image );
0216     painter.setPen( QColor( Qt::white ) );
0217 
0218     painter.drawPoints( Private::polygon( *waypointsA, box.west(), box.north(), sw, sh ) );
0219     int const countA = Private::nonZero( image );
0220 
0221     painter.drawPoints( Private::polygon( *waypointsB, box.west(), box.north(), sw, sh ) );
0222     int const countB = Private::nonZero( image );
0223     Q_ASSERT( countA <= countB );
0224     return countB ? 1.0 - qreal( countB - countA ) / countB : 0;
0225 }
0226 
0227 bool AlternativeRoutesModel::Private::higherScore( const GeoDataDocument* one, const GeoDataDocument* two )
0228 {
0229     qreal instructionScoreA = instructionScore( one );
0230     qreal instructionScoreB = instructionScore( two );
0231     if ( instructionScoreA != instructionScoreB ) {
0232         return instructionScoreA > instructionScoreB;
0233     }
0234 
0235     qreal lengthA = waypoints( one )->length( EARTH_RADIUS );
0236     qreal lengthB = waypoints( two )->length( EARTH_RADIUS );
0237 
0238     return lengthA < lengthB;
0239 }
0240 
0241 qreal AlternativeRoutesModel::Private::instructionScore( const GeoDataDocument* document )
0242 {
0243     bool hasInstructions = false;
0244 
0245     QStringList blacklist = QStringList() << "" << "Route" << "Tessellated";
0246     QVector<GeoDataFolder*> folders = document->folderList();
0247     for( const GeoDataFolder *folder: folders ) {
0248         for( const GeoDataPlacemark *placemark: folder->placemarkList() ) {
0249             if ( !blacklist.contains( placemark->name() ) ) {
0250                 hasInstructions = true;
0251                 break;
0252             }
0253         }
0254     }
0255 
0256     for( const GeoDataPlacemark *placemark: document->placemarkList() ) {
0257         if ( !blacklist.contains( placemark->name() ) ) {
0258             hasInstructions = true;
0259 
0260             if (placemark->extendedData().contains(QStringLiteral("turnType"))) {
0261                 return 1.0;
0262             }
0263         }
0264     }
0265 
0266     return hasInstructions ? 0.5 : 0.0;
0267 }
0268 
0269 const GeoDataLineString* AlternativeRoutesModel::Private::waypoints( const GeoDataDocument* document )
0270 {
0271     QVector<GeoDataFolder*> folders = document->folderList();
0272     for( const GeoDataFolder *folder: folders ) {
0273         for( const GeoDataPlacemark *placemark: folder->placemarkList() ) {
0274             const GeoDataGeometry* geometry = placemark->geometry();
0275             const GeoDataLineString* lineString = dynamic_cast<const GeoDataLineString*>( geometry );
0276             if ( lineString ) {
0277                 return lineString;
0278             }
0279         }
0280     }
0281 
0282     for( const GeoDataPlacemark *placemark: document->placemarkList() ) {
0283         const GeoDataGeometry* geometry = placemark->geometry();
0284         const GeoDataLineString* lineString = dynamic_cast<const GeoDataLineString*>( geometry );
0285         if ( lineString ) {
0286             return lineString;
0287         }
0288     }
0289 
0290     return nullptr;
0291 }
0292 
0293 AlternativeRoutesModel::AlternativeRoutesModel( QObject *parent ) :
0294         QAbstractListModel( parent ),
0295         d( new Private() )
0296 {
0297     // nothing to do
0298 }
0299 
0300 AlternativeRoutesModel::~AlternativeRoutesModel()
0301 {
0302     clear();
0303     delete d;
0304 }
0305 
0306 int AlternativeRoutesModel::rowCount ( const QModelIndex & ) const
0307 {
0308     return d->m_routes.size();
0309 }
0310 
0311 QVariant AlternativeRoutesModel::headerData ( int, Qt::Orientation, int ) const
0312 {
0313     return QVariant();
0314 }
0315 
0316 QVariant AlternativeRoutesModel::data ( const QModelIndex &index, int role ) const
0317 {
0318     QVariant result;
0319 
0320     if ( role == Qt::DisplayRole && index.column() == 0 && index.row() >= 0 && index.row() < d->m_routes.size() ) {
0321         result = d->m_routes.at( index.row() )->name();
0322     }
0323 
0324     return result;
0325 }
0326 
0327 const GeoDataDocument *AlternativeRoutesModel::route(int index) const
0328 {
0329     if ( index >= 0 && index < d->m_routes.size() ) {
0330         return d->m_routes.at(index);
0331     }
0332 
0333     return nullptr;
0334 }
0335 
0336 void AlternativeRoutesModel::newRequest( RouteRequest * )
0337 {
0338     d->m_responseTime.start();
0339     d->m_currentIndex = -1;
0340     clear();
0341 }
0342 
0343 void AlternativeRoutesModel::addRestrainedRoutes()
0344 {
0345     Q_ASSERT( d->m_routes.isEmpty() );
0346     std::sort( d->m_restrainedRoutes.begin(), d->m_restrainedRoutes.end(), Private::higherScore );
0347 
0348     for( GeoDataDocument* route: d->m_restrainedRoutes ) {
0349         if ( !d->filter( route ) ) {
0350             int affected = d->m_routes.size();
0351             beginInsertRows( QModelIndex(), affected, affected );
0352 //            GeoDataDocument* base = d->m_routes.isEmpty() ? 0 : d->m_routes.first();
0353             d->m_routes.push_back( route );
0354             endInsertRows();
0355         }
0356     }
0357 
0358     d->m_restrainedRoutes.clear();
0359     Q_ASSERT( !d->m_routes.isEmpty() );
0360     setCurrentRoute( 0 );
0361 }
0362 
0363 void AlternativeRoutesModel::addRoute( GeoDataDocument* document, WritePolicy policy )
0364 {
0365     if (policy != Instant) {
0366         if (d->m_routes.isEmpty()) {
0367             d->m_restrainedRoutes.push_back(document);
0368 
0369             if (d->m_restrainedRoutes.isEmpty()) {
0370                 // First
0371                 const int responseTime = d->m_responseTime.elapsed();
0372                 const int timeout = qMin<int>(500, qMax<int>(50, responseTime * 2));
0373                 QTimer::singleShot(timeout, this, SLOT(addRestrainedRoutes()));
0374 
0375                 return;
0376             }
0377         }
0378 
0379         for ( int i=0; i<d->m_routes.size(); ++i ) {
0380             qreal similarity = Private::similarity( document, d->m_routes.at( i ) );
0381             if ( similarity > 0.8 ) {
0382                 if ( Private::higherScore( document, d->m_routes.at( i ) ) ) {
0383                     d->m_routes[i] = document;
0384                     QModelIndex changed = index( i );
0385                     emit dataChanged( changed, changed );
0386                 }
0387 
0388                 return;
0389             }
0390         }
0391     }
0392 
0393     const int affected = d->m_routes.size();
0394     beginInsertRows(QModelIndex(), affected, affected);
0395     d->m_routes.push_back(document);
0396     endInsertRows();
0397 }
0398 
0399 const GeoDataLineString* AlternativeRoutesModel::waypoints( const GeoDataDocument* document )
0400 {
0401     return Private::waypoints( document );
0402 }
0403 
0404 void AlternativeRoutesModel::setCurrentRoute( int index )
0405 {
0406     if ( index >= 0 && index < rowCount() && d->m_currentIndex != index ) {
0407         d->m_currentIndex = index;
0408         emit currentRouteChanged( currentRoute() );
0409         emit currentRouteChanged( d->m_currentIndex );
0410     }
0411 }
0412 
0413 const GeoDataDocument *AlternativeRoutesModel::currentRoute() const
0414 {
0415     const GeoDataDocument *result = nullptr;
0416     if ( d->m_currentIndex >= 0 && d->m_currentIndex < rowCount() ) {
0417         result = d->m_routes[d->m_currentIndex];
0418     }
0419 
0420     return result;
0421 }
0422 
0423 void AlternativeRoutesModel::clear()
0424 {
0425     beginResetModel();
0426     QVector<GeoDataDocument*> routes = d->m_routes;
0427     d->m_currentIndex = -1;
0428     d->m_routes.clear();
0429     qDeleteAll(routes);
0430     endResetModel();
0431 }
0432 
0433 } // namespace Marble
0434 
0435 #include "moc_AlternativeRoutesModel.cpp"