File indexing completed on 2025-01-19 03:53:39
0001 /* ============================================================ 0002 * 0003 * This file is a part of digiKam project 0004 * https://www.digikam.org 0005 * 0006 * Date : 2007-03-22 0007 * Description : Building complex database SQL queries from search descriptions 0008 * 0009 * SPDX-FileCopyrightText: 2007-2012 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de> 0010 * SPDX-FileCopyrightText: 2012-2024 by Gilles Caulier <caulier dot gilles at gmail dot com> 0011 * 0012 * SPDX-License-Identifier: GPL-2.0-or-later 0013 * 0014 * ============================================================ */ 0015 0016 #include "fieldquerybuilder.h" 0017 0018 // C++ includes 0019 0020 #include <cmath> 0021 0022 // Qt includes 0023 0024 #include <QFile> 0025 #include <QDir> 0026 #include <QMap> 0027 #include <QRectF> 0028 #include <QUrl> 0029 #include <QLocale> 0030 #include <QUrlQuery> 0031 0032 // Local includes 0033 0034 #include "itemquerybuilder.h" 0035 #include "digikam_debug.h" 0036 #include "coredbaccess.h" 0037 #include "coredb.h" 0038 #include "geodetictools.h" 0039 0040 namespace Digikam 0041 { 0042 0043 FieldQueryBuilder::FieldQueryBuilder(QString& sql, 0044 SearchXmlCachingReader& reader, 0045 QList<QVariant>* boundValues, 0046 ItemQueryPostHooks* const hooks, 0047 SearchXml::Relation relation) 0048 : sql(sql), 0049 reader(reader), 0050 boundValues(boundValues), 0051 hooks(hooks), 0052 relation(relation) 0053 { 0054 } 0055 0056 QString FieldQueryBuilder::prepareForLike(const QString& str) const 0057 { 0058 if ((relation == SearchXml::Like) || (relation == SearchXml::NotLike)) 0059 { 0060 return QLatin1Char('%') + str + QLatin1Char('%'); 0061 } 0062 else 0063 { 0064 return str; 0065 } 0066 } 0067 0068 void FieldQueryBuilder::addIntField(const QString& name) 0069 { 0070 if ((relation == SearchXml::Interval) || (relation == SearchXml::IntervalOpen)) 0071 { 0072 QList<int> values = reader.valueToIntList(); 0073 0074 if (values.size() != 2) 0075 { 0076 qCWarning(DIGIKAM_DATABASE_LOG) << "Relation Interval requires a list of two values"; 0077 return; 0078 } 0079 0080 sql += QLatin1String(" (") + name + QLatin1Char(' '); 0081 ItemQueryBuilder::addSqlRelation(sql, 0082 relation == SearchXml::Interval ? SearchXml::GreaterThanOrEqual 0083 : SearchXml::GreaterThan); 0084 sql += QLatin1String(" ? AND ") + name + QLatin1Char(' '); 0085 ItemQueryBuilder::addSqlRelation(sql, 0086 relation == SearchXml::Interval ? SearchXml::LessThanOrEqual 0087 : SearchXml::LessThan); 0088 sql += QLatin1String(" ?) "); 0089 0090 *boundValues << values.first() << values.last(); 0091 } 0092 else 0093 { 0094 sql += QLatin1String(" (") + name + QLatin1Char(' '); 0095 ItemQueryBuilder::addSqlRelation(sql, relation); 0096 sql += QLatin1String(" ?) "); 0097 *boundValues << reader.valueToInt(); 0098 } 0099 } 0100 0101 void FieldQueryBuilder::addLongField(const QString& name) 0102 { 0103 if ((relation == SearchXml::Interval) || (relation == SearchXml::IntervalOpen)) 0104 { 0105 QList<qlonglong> values = reader.valueToLongLongList(); 0106 0107 if (values.size() != 2) 0108 { 0109 qCWarning(DIGIKAM_DATABASE_LOG) << "Relation Interval requires a list of two values"; 0110 return; 0111 } 0112 0113 sql += QLatin1String(" (") + name + QLatin1Char(' '); 0114 ItemQueryBuilder::addSqlRelation(sql, 0115 relation == SearchXml::Interval ? SearchXml::GreaterThanOrEqual 0116 : SearchXml::GreaterThan); 0117 sql += QLatin1String(" ? AND ") + name + QLatin1Char(' '); 0118 ItemQueryBuilder::addSqlRelation(sql, 0119 relation == SearchXml::Interval ? SearchXml::LessThanOrEqual 0120 : SearchXml::LessThan); 0121 sql += QLatin1String(" ?) "); 0122 0123 *boundValues << values.first() << values.last(); 0124 } 0125 else 0126 { 0127 sql += QLatin1String(" (") + name + QLatin1Char(' '); 0128 ItemQueryBuilder::addSqlRelation(sql, relation); 0129 sql += QLatin1String(" ?) "); 0130 *boundValues << reader.valueToLongLong(); 0131 } 0132 } 0133 0134 void FieldQueryBuilder::addDoubleField(const QString& name) 0135 { 0136 if ((relation == SearchXml::Interval) || (relation == SearchXml::IntervalOpen)) 0137 { 0138 QList<double> values = reader.valueToDoubleList(); 0139 0140 if (values.size() != 2) 0141 { 0142 qCWarning(DIGIKAM_DATABASE_LOG) << "Relation Interval requires a list of two values"; 0143 return; 0144 } 0145 0146 sql += QLatin1String(" (") + name + QLatin1Char(' '); 0147 ItemQueryBuilder::addSqlRelation(sql, 0148 relation == SearchXml::Interval ? SearchXml::GreaterThanOrEqual 0149 : SearchXml::GreaterThan); 0150 sql += QLatin1String(" ? AND ") + name + QLatin1Char(' '); 0151 ItemQueryBuilder::addSqlRelation(sql, 0152 relation == SearchXml::Interval ? SearchXml::LessThanOrEqual 0153 : SearchXml::LessThan); 0154 sql += QLatin1String(" ?) "); 0155 0156 *boundValues << values.first() << values.last(); 0157 } 0158 else 0159 { 0160 sql += QLatin1String(" (") + name + QLatin1Char(' '); 0161 ItemQueryBuilder::addSqlRelation(sql, relation); 0162 sql += QLatin1String(" ?) "); 0163 *boundValues << reader.valueToDouble(); 0164 } 0165 } 0166 0167 void FieldQueryBuilder::addStringField(const QString& name) 0168 { 0169 sql += QLatin1String(" (") + name + QLatin1Char(' '); 0170 ItemQueryBuilder::addSqlRelation(sql, relation); 0171 0172 if (CoreDbAccess::parameters().isSQLite() && 0173 ((relation == SearchXml::Like) || 0174 (relation == SearchXml::NotLike))) 0175 { 0176 sql += QLatin1String(" ? ESCAPE '\\') "); 0177 } 0178 else 0179 { 0180 sql += QLatin1String(" ?) "); 0181 } 0182 0183 *boundValues << prepareForLike(reader.value()); 0184 } 0185 0186 void FieldQueryBuilder::addDateField(const QString& name) 0187 { 0188 if (relation == SearchXml::Equal) 0189 { 0190 // special case: split in < and > 0191 0192 QDateTime date = QDateTime::fromString(reader.value(), Qt::ISODate); 0193 0194 if (!date.isValid()) 0195 { 0196 qCWarning(DIGIKAM_DATABASE_LOG) << "Date" << reader.value() << "is invalid"; 0197 return; 0198 } 0199 0200 if (date.time() == QTime(0, 0, 0, 0)) 0201 { 0202 // day precision 0203 0204 QDate startDate, endDate; 0205 startDate = date.date().addDays(-1); 0206 endDate = date.date().addDays(1); 0207 *boundValues << startDate.toString(Qt::ISODate) 0208 << endDate.toString(Qt::ISODate); 0209 } 0210 else 0211 { 0212 // sub-day precision 0213 0214 QDateTime startDate, endDate; 0215 int diff; 0216 0217 if (date.time().hour() == 0) 0218 { 0219 diff = 3600; 0220 } 0221 else if (date.time().minute() == 0) 0222 { 0223 diff = 60; 0224 } 0225 else 0226 { 0227 diff = 1; 0228 } 0229 0230 // we spare microseconds for the future 0231 0232 startDate = date.addSecs(-diff); 0233 endDate = date.addSecs(diff); 0234 *boundValues << startDate.toString(Qt::ISODate) 0235 << endDate.toString(Qt::ISODate); 0236 } 0237 0238 sql += QLatin1String(" (") + name + QLatin1Char(' '); 0239 ItemQueryBuilder::addSqlRelation(sql, SearchXml::GreaterThan); 0240 sql += QLatin1String(" ? AND ") + name + QLatin1Char(' '); 0241 ItemQueryBuilder::addSqlRelation(sql, SearchXml::LessThan); 0242 sql += QLatin1String(" ?) "); 0243 } 0244 else if (relation == SearchXml::Interval || relation == SearchXml::IntervalOpen) 0245 { 0246 QList<QString> values = reader.valueToStringList(); 0247 0248 if (values.size() != 2) 0249 { 0250 qCWarning(DIGIKAM_DATABASE_LOG) << "Relation Interval requires a list of two values"; 0251 return; 0252 } 0253 0254 sql += QLatin1String(" (") + name + QLatin1Char(' '); 0255 ItemQueryBuilder::addSqlRelation(sql, 0256 relation == SearchXml::Interval ? SearchXml::GreaterThanOrEqual 0257 : SearchXml::GreaterThan); 0258 sql += QLatin1String(" ? AND ") + name + QLatin1Char(' '); 0259 ItemQueryBuilder::addSqlRelation(sql, 0260 relation == SearchXml::Interval ? SearchXml::LessThanOrEqual 0261 : SearchXml::LessThan); 0262 sql += QLatin1String(" ?) "); 0263 0264 *boundValues << values.first() << values.last(); 0265 } 0266 else 0267 { 0268 sql += QLatin1String(" (") + name + QLatin1Char(' '); 0269 ItemQueryBuilder::addSqlRelation(sql, relation); 0270 sql += QLatin1String(" ?) "); 0271 *boundValues << reader.value(); 0272 } 0273 } 0274 0275 void FieldQueryBuilder::addChoiceIntField(const QString& name) 0276 { 0277 if (relation == SearchXml::OneOf) 0278 { 0279 QList<int> values = reader.valueToIntList(); 0280 bool searchForNull = values.removeAll(-1); 0281 sql += QLatin1String(" (") + name + QLatin1String(" IN ("); 0282 CoreDB::addBoundValuePlaceholders(sql, values.size()); 0283 0284 if (searchForNull) 0285 { 0286 sql += QLatin1String(") OR ") + name + QLatin1String(" IS NULL"); 0287 } 0288 else 0289 { 0290 sql += QLatin1String(") "); 0291 } 0292 0293 Q_FOREACH (int v, values) 0294 { 0295 *boundValues << v; 0296 } 0297 0298 sql += QLatin1String(" ) "); 0299 } 0300 else 0301 { 0302 addIntField(name); 0303 } 0304 } 0305 0306 void FieldQueryBuilder::addLongListField(const QString& name) 0307 { 0308 if (relation == SearchXml::OneOf) 0309 { 0310 QList<qlonglong> values = reader.valueToLongLongList(); 0311 sql += QLatin1String(" (") + name + QLatin1String(" IN ("); 0312 CoreDB::addBoundValuePlaceholders(sql, values.size()); 0313 sql += QLatin1String(") "); 0314 0315 Q_FOREACH (const qlonglong& v, values) 0316 { 0317 *boundValues << v; 0318 } 0319 0320 sql += QLatin1String(" ) "); 0321 } 0322 else 0323 { 0324 addLongField(name); 0325 } 0326 } 0327 0328 void FieldQueryBuilder::addIntBitmaskField(const QString& name) 0329 { 0330 if (relation == SearchXml::OneOf) 0331 { 0332 QList<int> values = reader.valueToIntList(); 0333 bool searchForNull = values.removeAll(-1); 0334 sql += QLatin1String("( "); 0335 bool first = true; 0336 0337 for (int i = 0 ; i < values.size() ; ++i) 0338 { 0339 if (!first) 0340 { 0341 sql += QLatin1String("OR "); 0342 } 0343 0344 first = false; 0345 sql += name + QLatin1String(" & ? "); 0346 } 0347 0348 if (searchForNull) 0349 { 0350 sql += QLatin1String("OR ") + name + QLatin1String(" IS NULL "); 0351 } 0352 0353 Q_FOREACH (int v, values) 0354 { 0355 *boundValues << v; 0356 } 0357 0358 sql += QLatin1String(" ) "); 0359 } 0360 else 0361 { 0362 if (relation == SearchXml::Equal) 0363 { 0364 sql += QLatin1String(" (") + name + QLatin1String(" & ") + QLatin1String(" ?) "); 0365 } 0366 else 0367 { 0368 sql += QLatin1String(" (NOT ") + name + QLatin1String(" & ") + QLatin1String(" ?) "); 0369 } 0370 0371 *boundValues << reader.valueToDouble(); 0372 } 0373 } 0374 0375 void FieldQueryBuilder::addChoiceStringField(const QString& name) 0376 { 0377 if (relation == SearchXml::OneOf) 0378 { 0379 QStringList values = reader.valueToStringList(); 0380 0381 if (values.isEmpty()) 0382 { 0383 qCDebug(DIGIKAM_DATABASE_LOG) << "List for OneOf is empty"; 0384 return; 0385 } 0386 0387 QStringList simpleValues, wildcards; 0388 0389 Q_FOREACH (const QString& value, values) 0390 { 0391 if (value.contains(QLatin1Char('*'))) 0392 { 0393 wildcards << value; 0394 } 0395 else 0396 { 0397 simpleValues << value; 0398 } 0399 } 0400 0401 bool firstCondition = true; 0402 sql += QLatin1String(" ("); 0403 0404 if (!simpleValues.isEmpty()) 0405 { 0406 firstCondition = false; 0407 sql += name + QLatin1String(" IN ("); 0408 CoreDB::addBoundValuePlaceholders(sql, simpleValues.size()); 0409 0410 Q_FOREACH (const QString& value, simpleValues) 0411 { 0412 *boundValues << value; 0413 } 0414 0415 sql += QLatin1String(" ) "); 0416 } 0417 0418 if (!wildcards.isEmpty()) 0419 { 0420 Q_FOREACH (QString wildcard, wildcards) // krazy:exclude=foreach 0421 { 0422 ItemQueryBuilder::addSqlOperator(sql, SearchXml::Or, firstCondition); 0423 firstCondition = false; 0424 wildcard.replace(QLatin1Char('*'), QLatin1Char('%')); 0425 sql += QLatin1Char(' ') + name + QLatin1Char(' '); 0426 ItemQueryBuilder::addSqlRelation(sql, SearchXml::Like); 0427 sql += QLatin1String(" ? "); 0428 *boundValues << wildcard; 0429 } 0430 } 0431 0432 sql += QLatin1String(") "); 0433 } 0434 else 0435 { 0436 QString value = reader.value(); 0437 0438 if (relation == SearchXml::Like && value.contains(QLatin1Char('*'))) 0439 { 0440 // Handle special case: * denotes the place if the wildcard, 0441 // Don't automatically prepend and append %. 0442 0443 sql += QLatin1String(" (") + name + QLatin1Char(' '); 0444 ItemQueryBuilder::addSqlRelation(sql, SearchXml::Like); 0445 sql += QLatin1String(" ?) "); 0446 QString wildcard = reader.value(); 0447 wildcard.replace(QLatin1Char('*'), QLatin1Char('%')); 0448 *boundValues << wildcard; 0449 } 0450 else 0451 { 0452 addStringField(name); 0453 } 0454 } 0455 } 0456 0457 void FieldQueryBuilder::addPosition() 0458 { 0459 if (relation == SearchXml::Near) 0460 { 0461 // First read attributes 0462 0463 QStringView type = reader.attributes().value(QLatin1String("type")); 0464 QStringView distanceString = reader.attributes().value(QLatin1String("distance")); 0465 0466 // Distance in meters 0467 0468 double distance = 100; 0469 0470 if (!distanceString.isEmpty()) 0471 { 0472 distance = distanceString.toString().toDouble(); 0473 } 0474 0475 // Search type, "radius" or "rectangle" 0476 0477 bool radiusSearch = true; 0478 0479 if (type == QLatin1String("radius")) 0480 { 0481 radiusSearch = true; 0482 } 0483 else if (type == QLatin1String("rectangle")) 0484 { 0485 radiusSearch = false; 0486 } 0487 0488 // Get a list of doubles: 0489 // Longitude and Latitude in (decimal) degrees 0490 0491 QList<double> list = reader.valueToDoubleList(); 0492 0493 if (list.size() != 2) 0494 { 0495 qCWarning(DIGIKAM_DATABASE_LOG) << "Relation 'Near' requires a list of two values"; 0496 return; 0497 } 0498 0499 double lon = list.at(0); 0500 double lat = list.at(1); 0501 0502 sql += QLatin1String(" ( "); 0503 0504 // Part 1: Rectangle search. 0505 // Get the coordinates of the (spherical) rectangle enclosing 0506 // the (spherical) circle given by our coordinates and the distance. 0507 // For this one-time computation we use the advanced code 0508 // which assumes the earth is a ellipsoid. 0509 0510 // From the point (lon,lat) we go East, North, West, South, 0511 // and get the coordinates in degrees of a rectangle 0512 // of width and height 2*distance enclosing (lon,lat) 0513 0514 QRectF rect; 0515 GeodeticCalculator calc; 0516 calc.setStartingGeographicPoint(lon, lat); 0517 // go west 0518 calc.setDirection(-90, distance); 0519 rect.setLeft(calc.destinationGeographicPoint().x()); 0520 // go north (from first starting point!) 0521 calc.setDirection(0, distance); 0522 rect.setTop(calc.destinationGeographicPoint().y()); 0523 // go east 0524 calc.setDirection(90, distance); 0525 rect.setRight(calc.destinationGeographicPoint().x()); 0526 // go south 0527 calc.setDirection(180, distance); 0528 rect.setBottom(calc.destinationGeographicPoint().y()); 0529 0530 addRectanglePositionSearch(rect.x(), rect.y(), rect.right(), rect.bottom()); 0531 0532 if (radiusSearch) 0533 { 0534 // Part 2: Use the Haversine formula to filter out from 0535 // the matching pictures those that lie inside the 0536 // actual (spherical) circle. 0537 // This code only assumes that the earth is a sphere. 0538 // But this needs to be computed n times, so it's expensive. 0539 // We refrain from putting this into SQL, but use a post hook. 0540 0541 /* 0542 Reference: www.usenet-replayer.com/faq/comp.infosystems.gis.html 0543 Pseudo code of the formula: 0544 Position 1 (lon1, lat1), position 2 (lon2, lat2), in Radians 0545 d: distance; R: radius of earth. Same unit (assume: meters) 0546 dlon = lon2 - lon1; 0547 dlat = lat2 - lat1; 0548 a = (sin(dlat/2))^2 + cos(lat1) * cos(lat2) * (sin(dlon/2))^2; 0549 c = 2 * arcsin(min(1,sqrt(a))); 0550 d = R * c; 0551 // We precompute c. 0552 */ 0553 0554 class Q_DECL_HIDDEN HaversinePostHook : public ItemQueryPostHook 0555 { 0556 public: 0557 0558 HaversinePostHook(double lat1Deg, double lon1Deg, double radiusOfCurvature, double distance) 0559 { 0560 lat1 = Coordinates::toRadians(lat1Deg); 0561 lon1 = Coordinates::toRadians(lon1Deg); 0562 distanceInRadians = distance / radiusOfCurvature; 0563 cosLat1 = cos(lat1); 0564 } 0565 0566 bool checkPosition(double lat2Deg, double lon2Deg) override 0567 { 0568 double lat2 = Coordinates::toRadians(lat2Deg); 0569 double lon2 = Coordinates::toRadians(lon2Deg); 0570 double dlon = lon2 - lon1; 0571 double dlat = lat2 - lat1; 0572 double a = pow(sin(dlat/2), 2) + cosLat1 * cos(lat2) * pow(sin(dlon/2),2); 0573 double c = 2 * asin(qMin(1.0, sqrt(a))); 0574 0575 return (c < distanceInRadians); 0576 } 0577 0578 double lat1, lon1; 0579 double distanceInRadians; 0580 double cosLat1; 0581 }; 0582 0583 // get radius (of the ellipsoid) in dependence of the latitude. 0584 0585 double R = calc.ellipsoid().radiusOfCurvature(lat); 0586 hooks->addHook(new HaversinePostHook(lat, lon, R, distance)); 0587 } 0588 0589 sql += QLatin1String(" ) "); 0590 } 0591 else if (relation == SearchXml::Inside) 0592 { 0593 // First read attributes 0594 0595 QStringView type = reader.attributes().value(QLatin1String("type")); 0596 0597 // Search type, currently only "rectangle" 0598 0599 if (type != QLatin1String("rectangle")) 0600 { 0601 qCWarning(DIGIKAM_DATABASE_LOG) << "Relation 'Inside' supports no other type than 'rectangle'"; 0602 return; 0603 } 0604 0605 // Get a list of doubles: 0606 // Longitude and Latitude in (decimal) degrees 0607 0608 QList<double> list = reader.valueToDoubleList(); 0609 0610 if (list.size() != 4) 0611 { 0612 qCWarning(DIGIKAM_DATABASE_LOG) << "Relation 'Inside' requires a list of four values"; 0613 return; 0614 } 0615 0616 // the list contains (lon1,lat1), (lon2,lat2) in this order, 0617 // like (x,y), (right,bottom) of a rectangle, 0618 // or like (West,North), (East,South), 0619 // where the searched region contains any lon,lat 0620 // where lon1 < lon < lon2 and lat1 < lat < lat2. 0621 0622 double lon1, lat1, lon2, lat2; 0623 lon1 = list.at(0); 0624 lat1 = list.at(1); 0625 lon2 = list.at(2); 0626 lat2 = list.at(3); 0627 0628 sql += QLatin1String(" ( "); 0629 addRectanglePositionSearch(lon1, lat1, lon2, lat2); 0630 sql += QLatin1String(" ) "); 0631 } 0632 } 0633 0634 void FieldQueryBuilder::addRectanglePositionSearch(double lon1, double lat1, double lon2, double lat2) const 0635 { 0636 // lon1 is always West of lon2. If the rectangle crosses 180 longitude, we have to treat a special case. 0637 0638 if (lon1 <= lon2) 0639 { 0640 sql += QString::fromUtf8(" ImagePositions.LongitudeNumber > ? AND ImagePositions.LatitudeNumber < ? " 0641 " AND ImagePositions.LongitudeNumber < ? AND ImagePositions.LatitudeNumber > ? "); 0642 *boundValues << lon1 << lat1 << lon2 << lat2; 0643 } 0644 else 0645 { 0646 // this effectively means splitting the rectangle is two parts, one East, one West 0647 // to the 180 line. But no need to check for less/greater than -180/180. 0648 0649 sql += QString::fromUtf8(" (ImagePositions.LongitudeNumber > ? OR ImagePositions.LongitudeNumber < ?) " 0650 " AND ImagePositions.LatitudeNumber < ? AND ImagePositions.LatitudeNumber > ? "); 0651 *boundValues << lon1 << lon2 << lat1 << lat2; 0652 } 0653 } 0654 0655 } // namespace Digikam