File indexing completed on 2024-04-28 03:50:32

0001 // SPDX-License-Identifier: LGPL-2.1-or-later
0002 //
0003 // SPDX-FileCopyrightText: 2011 Dennis Nienhüser <nienhueser@kde.org>
0004 // SPDX-FileCopyrightText: 2013 Bernhard Beschow <bbeschow@cs.tu-berlin.de>
0005 //
0006 
0007 #include "OsmDatabase.h"
0008 
0009 #include "DatabaseQuery.h"
0010 #include "GeoDataLatLonAltBox.h"
0011 #include "MarbleDebug.h"
0012 #include "MarbleMath.h"
0013 #include "MarbleLocale.h"
0014 #include "MarbleModel.h"
0015 #include "PositionTracking.h"
0016 
0017 #include <QDataStream>
0018 #include <QElapsedTimer>
0019 
0020 #include <QSqlDatabase>
0021 #include <QSqlQuery>
0022 #include <QSqlError>
0023 
0024 namespace Marble {
0025 
0026 namespace {
0027 
0028 class PlacemarkSmallerDistance
0029 {
0030 public:
0031     PlacemarkSmallerDistance( const GeoDataCoordinates &currentPosition ) :
0032         m_currentPosition( currentPosition )
0033     {}
0034 
0035     bool operator()( const OsmPlacemark &a, const OsmPlacemark &b ) const
0036     {
0037         return distanceSphere( a.longitude() * DEG2RAD, a.latitude() * DEG2RAD,
0038                                m_currentPosition.longitude(), m_currentPosition.latitude() )
0039              < distanceSphere( b.longitude() * DEG2RAD, b.latitude() * DEG2RAD,
0040                                m_currentPosition.longitude(), m_currentPosition.latitude() );
0041     }
0042 
0043 private:
0044     GeoDataCoordinates m_currentPosition;
0045 };
0046 
0047 class PlacemarkHigherScore
0048 {
0049 public:
0050     PlacemarkHigherScore( const DatabaseQuery *currentQuery ) :
0051         m_currentQuery( currentQuery )
0052     {}
0053 
0054     bool operator()( const OsmPlacemark &a, const OsmPlacemark &b ) const
0055     {
0056         return a.matchScore( m_currentQuery ) > b.matchScore( m_currentQuery );
0057     }
0058 
0059 private:
0060     const DatabaseQuery *const m_currentQuery;
0061 };
0062 
0063 }
0064 
0065 OsmDatabase::OsmDatabase( const QStringList &databaseFiles ) :
0066     m_databaseFiles( databaseFiles )
0067 {
0068 }
0069 
0070 QVector<OsmPlacemark> OsmDatabase::find( const DatabaseQuery &userQuery )
0071 {
0072     if ( m_databaseFiles.isEmpty() ) {
0073         return QVector<OsmPlacemark>();
0074     }
0075 
0076     QSqlDatabase database = QSqlDatabase::addDatabase( "QSQLITE", QString( "marble/local-osm-search-%1" ).arg( reinterpret_cast<size_t>( this ) ) );
0077 
0078     QVector<OsmPlacemark> result;
0079     QElapsedTimer timer;
0080     timer.start();
0081     for( const QString &databaseFile: m_databaseFiles ) {
0082         database.setDatabaseName( databaseFile );
0083         if ( !database.open() ) {
0084             qWarning() << "Failed to connect to database" << databaseFile;
0085         }
0086 
0087         QString regionRestriction;
0088         if ( !userQuery.region().isEmpty() ) {
0089             QElapsedTimer regionTimer;
0090             regionTimer.start();
0091             // Nested set model to support region hierarchies, see https://en.wikipedia.org/wiki/Nested_set_model
0092             const QString regionsQueryString = QLatin1String("SELECT lft, rgt FROM regions WHERE name LIKE '%") + userQuery.region() + QLatin1String("%';");
0093             QSqlQuery regionsQuery( regionsQueryString, database );
0094             if ( regionsQuery.lastError().isValid() ) {
0095                 qWarning() << regionsQuery.lastError() << "in" << databaseFile << "with query" << regionsQuery.lastQuery();
0096             }
0097             regionRestriction = " AND (";
0098             int regionCount = 0;
0099             while ( regionsQuery.next() ) {
0100                 if ( regionCount > 0 ) {
0101                     regionRestriction += QLatin1String(" OR ");
0102                 }
0103                 regionRestriction += QLatin1String(" (regions.lft >= ") + regionsQuery.value( 0 ).toString() +
0104                                      QLatin1String(" AND regions.lft <= ") + regionsQuery.value( 1 ).toString() + QLatin1Char(')');
0105                 regionCount++;
0106             }
0107             regionRestriction += QLatin1Char(')');
0108 
0109             mDebug() << "region query in" << databaseFile << "with query" << regionsQueryString
0110                      << "took" << regionTimer.elapsed() << "ms for" << regionCount << "results";
0111 
0112             if ( regionCount == 0 ) {
0113                 continue;
0114             }
0115         }
0116 
0117         QString queryString;
0118 
0119         queryString = " SELECT regions.name,"
0120                 " places.name, places.number,"
0121                 " places.category, places.lon, places.lat"
0122                 " FROM regions, places";
0123 
0124         if ( userQuery.queryType() == DatabaseQuery::CategorySearch ) {
0125             queryString += QLatin1String(" WHERE regions.id = places.region");
0126             if( userQuery.category() == OsmPlacemark::UnknownCategory ) {
0127                 // search for all pois which are not street nor address
0128                 queryString += QLatin1String(" AND places.category <> 0 AND places.category <> 6");
0129             } else {
0130                 // search for specific category
0131                 queryString += QLatin1String(" AND places.category = %1");
0132                 queryString = queryString.arg( (qint32) userQuery.category() );
0133             }
0134             if ( userQuery.position().isValid() && userQuery.region().isEmpty() ) {
0135                 // sort by distance
0136                 queryString += QLatin1String(" ORDER BY ((places.lat-%1)*(places.lat-%1)+(places.lon-%2)*(places.lon-%2))");
0137                 GeoDataCoordinates position = userQuery.position();
0138                 queryString = queryString.arg( position.latitude( GeoDataCoordinates::Degree ), 0, 'f', 8 )
0139                         .arg( position.longitude( GeoDataCoordinates::Degree ), 0, 'f', 8 );
0140             } else {
0141                 queryString += regionRestriction;
0142             }
0143         } else if ( userQuery.queryType() == DatabaseQuery::BroadSearch ) {
0144             queryString += QLatin1String(" WHERE regions.id = places.region"
0145                     " AND places.name ") + wildcardQuery(userQuery.searchTerm());
0146         } else {
0147             queryString += QLatin1String(" WHERE regions.id = places.region"
0148                     "   AND places.name ") + wildcardQuery(userQuery.street());
0149             if ( !userQuery.houseNumber().isEmpty() ) {
0150                 queryString += QLatin1String(" AND places.number ") + wildcardQuery(userQuery.houseNumber());
0151             } else {
0152                 queryString += QLatin1String(" AND places.number IS NULL");
0153             }
0154             queryString += regionRestriction;
0155         }
0156 
0157         queryString += QLatin1String(" LIMIT 50;");
0158 
0159         /** @todo: sort/filter results from several databases */
0160 
0161         QSqlQuery query( database );
0162         query.setForwardOnly( true );
0163         QElapsedTimer queryTimer;
0164         queryTimer.start();
0165         if ( !query.exec( queryString ) ) {
0166             qWarning() << query.lastError() << "in" << databaseFile << "with query" << query.lastQuery();
0167             continue;
0168         }
0169 
0170         int resultCount = 0;
0171         while ( query.next() ) {
0172             OsmPlacemark placemark;
0173             if ( userQuery.resultFormat() == DatabaseQuery::DistanceFormat ) {
0174                 GeoDataCoordinates coordinates( query.value(4).toFloat(), query.value(5).toFloat(), 0.0, GeoDataCoordinates::Degree );
0175                 placemark.setAdditionalInformation( formatDistance( coordinates, userQuery.position() ) );
0176             } else {
0177                 placemark.setAdditionalInformation( query.value( 0 ).toString() );
0178             }
0179             placemark.setName( query.value(1).toString() );
0180             placemark.setHouseNumber( query.value(2).toString() );
0181             placemark.setCategory( (OsmPlacemark::OsmCategory) query.value(3).toInt() );
0182             placemark.setLongitude( query.value(4).toFloat() );
0183             placemark.setLatitude( query.value(5).toFloat() );
0184 
0185             result.push_back( placemark );
0186             resultCount++;
0187         }
0188 
0189         mDebug() << "query in" << databaseFile << "with query" << queryString
0190                  << "took" << queryTimer.elapsed() << "ms for" << resultCount << "results";
0191     }
0192 
0193     mDebug() << "Offline OSM search query took" << timer.elapsed() << "ms for" << result.count() << "results.";
0194 
0195     std::sort( result.begin(), result.end() );
0196     makeUnique( result );
0197 
0198     if ( userQuery.position().isValid() ) {
0199         const PlacemarkSmallerDistance placemarkSmallerDistance( userQuery.position() );
0200         std::sort( result.begin(), result.end(), placemarkSmallerDistance );
0201     } else {
0202         const PlacemarkHigherScore placemarkHigherScore( &userQuery );
0203         std::sort( result.begin(), result.end(), placemarkHigherScore );
0204     }
0205 
0206     if ( result.size() > 50 ) {
0207         result.remove( 50, result.size()-50 );
0208     }
0209 
0210     return result;
0211 }
0212 
0213 void OsmDatabase::makeUnique( QVector<OsmPlacemark> &placemarks )
0214 {
0215     for ( int i=1; i<placemarks.size(); ++i ) {
0216         if ( placemarks[i-1] == placemarks[i] ) {
0217             placemarks.remove( i );
0218             --i;
0219         }
0220     }
0221 }
0222 
0223 QString OsmDatabase::formatDistance( const GeoDataCoordinates &a, const GeoDataCoordinates &b )
0224 {
0225     qreal distance = EARTH_RADIUS * a.sphericalDistanceTo(b);
0226 
0227     int precision = 0;
0228     QString distanceUnit = QLatin1String( "m" );
0229 
0230     if ( MarbleGlobal::getInstance()->locale()->measurementSystem() == MarbleLocale::ImperialSystem ) {
0231         precision = 1;
0232         distanceUnit = "mi";
0233         distance *= METER2KM;
0234         distance *= KM2MI;
0235     } else if (MarbleGlobal::getInstance()->locale()->measurementSystem() ==
0236                MarbleLocale::MetricSystem) {
0237         if ( distance >= 1000 ) {
0238             distance /= 1000;
0239             distanceUnit = "km";
0240             precision = 1;
0241         } else if ( distance >= 200 ) {
0242             distance = 50 * qRound( distance / 50 );
0243         } else if ( distance >= 100 ) {
0244             distance = 25 * qRound( distance / 25 );
0245         } else {
0246             distance = 10 * qRound( distance / 10 );
0247         }
0248     } else if (MarbleGlobal::getInstance()->locale()->measurementSystem() ==
0249                MarbleLocale::NauticalSystem) {
0250         precision = 2;
0251         distanceUnit = "nm";
0252         distance *= METER2KM;
0253         distance *= KM2NM;
0254     }
0255 
0256     QString const fuzzyDistance = QString( "%1 %2" ).arg( distance, 0, 'f', precision ).arg( distanceUnit );
0257 
0258     int direction = 180 + bearing( a, b ) * RAD2DEG;
0259 
0260     QString heading = QObject::tr( "north" );
0261     if ( direction > 337 ) {
0262         heading = QObject::tr( "north" );
0263     } else if ( direction > 292 ) {
0264         heading = QObject::tr( "north-west" );
0265     } else if ( direction > 247 ) {
0266         heading = QObject::tr( "west" );
0267     } else if ( direction > 202 ) {
0268         heading = QObject::tr( "south-west" );
0269     } else if ( direction > 157 ) {
0270         heading = QObject::tr( "south" );
0271     } else if ( direction > 112 ) {
0272         heading = QObject::tr( "south-east" );
0273     } else if ( direction > 67 ) {
0274         heading = QObject::tr( "east" );
0275     } else if ( direction > 22 ) {
0276         heading = QObject::tr( "north-east" );
0277     }
0278 
0279     return fuzzyDistance + QLatin1Char(' ') + heading;
0280 }
0281 
0282 qreal OsmDatabase::bearing( const GeoDataCoordinates &a, const GeoDataCoordinates &b )
0283 {
0284     qreal delta = b.longitude() - a.longitude();
0285     qreal lat1 = a.latitude();
0286     qreal lat2 = b.latitude();
0287     return fmod( atan2( sin ( delta ) * cos ( lat2 ),
0288                        cos( lat1 ) * sin( lat2 ) - sin( lat1 ) * cos( lat2 ) * cos ( delta ) ), 2 * M_PI );
0289 }
0290 
0291 QString OsmDatabase::wildcardQuery( const QString &term )
0292 {
0293     QString result = term;
0294     if (term.contains(QLatin1Char('*'))) {
0295         return QLatin1String(" LIKE '") + result.replace(QLatin1Char('*'), QLatin1Char('%')) + QLatin1Char('\'');
0296     } else {
0297         return QLatin1String(" = '") + result + QLatin1Char('\'');
0298     }
0299 }
0300 
0301 }