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 ¤tPosition ) : 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 }