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