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"