File indexing completed on 2025-01-05 03:58:31

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2010-03-21
0007  * Description : A container to hold GPS information about an item.
0008  *
0009  * SPDX-FileCopyrightText: 2010-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0010  * SPDX-FileCopyrightText: 2010-2014 by Michael G. Hansen <mike at mghansen dot de>
0011  *
0012  * SPDX-License-Identifier: GPL-2.0-or-later
0013  *
0014  * ============================================================ */
0015 
0016 #include "gpsitemcontainer.h"
0017 
0018 // Qt includes
0019 
0020 #include <QBrush>
0021 #include <QFileInfo>
0022 #include <QScopedPointer>
0023 #include <QLocale>
0024 
0025 // KDE includes
0026 
0027 #include <klocalizedstring.h>
0028 
0029 // Local includes
0030 
0031 #include "digikam_debug.h"
0032 #include "gpsitemmodel.h"
0033 #include "dmetadata.h"
0034 #include "metaenginesettings.h"
0035 
0036 namespace Digikam
0037 {
0038 
0039 bool setExifXmpTagDataVariant(DMetadata* const meta, const char* const exifTagName,
0040                               const char* const xmpTagName, const QVariant& value)
0041 {
0042     bool success = meta->setExifTagVariant(exifTagName, value);
0043 
0044     if (success)
0045     {
0046         /**
0047          * @todo Here we save all data types as XMP Strings.
0048          * Is that okay or do we have to store them as some other type?
0049          */
0050 
0051 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
0052 
0053         switch (static_cast<QMetaType::Type>(value.typeId()))
0054 
0055 #else
0056 
0057         switch (value.type())
0058 
0059 #endif
0060 
0061         {
0062             case QVariant::Int:
0063             case QVariant::UInt:
0064             case QVariant::Bool:
0065             case QVariant::LongLong:
0066             case QVariant::ULongLong:
0067                 success = meta->setXmpTagString(xmpTagName, QString::number(value.toInt()));
0068                 break;
0069 
0070             case QVariant::Double:
0071             {
0072                 long num, den;
0073                 meta->convertToRationalSmallDenominator(value.toDouble(), &num, &den);
0074                 success = meta->setXmpTagString(xmpTagName, QString::fromLatin1("%1/%2").arg(num).arg(den));
0075                 break;
0076             }
0077 
0078             case QVariant::List:
0079             {
0080                 long num             = 0;
0081                 long den             = 1;
0082                 QList<QVariant> list = value.toList();
0083 
0084                 if (list.size() >= 1)
0085                 {
0086                     num = list[0].toInt();
0087                 }
0088 
0089                 if (list.size() >= 2)
0090                 {
0091                     den = list[1].toInt();
0092                 }
0093 
0094                 success = meta->setXmpTagString(xmpTagName, QString::fromLatin1("%1/%2").arg(num).arg(den));
0095                 break;
0096             }
0097 
0098             case QVariant::Date:
0099             case QVariant::DateTime:
0100             {
0101                 QDateTime dateTime = value.toDateTime();
0102 
0103                 if (!dateTime.isValid())
0104                 {
0105                     success = false;
0106                     break;
0107                 }
0108 
0109                 success = meta->setXmpTagString(xmpTagName, dateTime.toString(QLatin1String("yyyy:MM:dd hh:mm:ss")));
0110                 break;
0111             }
0112 
0113             case QVariant::String:
0114             case QVariant::Char:
0115                 success = meta->setXmpTagString(xmpTagName, value.toString());
0116                 break;
0117 
0118             case QVariant::ByteArray:
0119 
0120                 /// @todo I don't know a straightforward way to convert a byte array to XMP
0121 
0122                 success = false;
0123                 break;
0124 
0125             default:
0126                 success = false;
0127                 break;
0128         }
0129     }
0130 
0131     return success;
0132 }
0133 
0134 GPSItemContainer::GPSItemContainer(const QUrl& url)
0135     : m_model       (nullptr),
0136       m_url         (url),
0137       m_dateTime    (),
0138       m_dirty       (false),
0139       m_gpsData     (),
0140       m_savedState  (),
0141       m_tagListDirty(false),
0142       m_tagList     (),
0143       m_savedTagList(),
0144       m_writeXmpTags(true),
0145       m_writeMetaLoc(true)
0146 {
0147 }
0148 
0149 GPSItemContainer::~GPSItemContainer()
0150 {
0151 }
0152 
0153 DMetadata* GPSItemContainer::getMetadataForFile() const
0154 {
0155     QScopedPointer<DMetadata> meta(new DMetadata);
0156 
0157     if (!meta->load(m_url.toLocalFile()))
0158     {
0159         // It is possible that no sidecar file has yet been created.
0160         // If writing to sidecar file is activated, we ignore the loading error of the metadata.
0161 
0162         if (MetaEngineSettings::instance()->settings().metadataWritingMode == DMetadata::WRITE_TO_FILE_ONLY)
0163         {
0164             return nullptr;
0165         }
0166     }
0167 
0168     return meta.take();
0169 }
0170 
0171 int getWarningLevelFromGPSDataContainer(const GPSDataContainer& data)
0172 {
0173     if      (data.hasDop())
0174     {
0175         const int dopValue = data.getDop();
0176 
0177         if (dopValue < 2)
0178         {
0179             return 1;
0180         }
0181 
0182         if (dopValue < 4)
0183         {
0184             return 2;
0185         }
0186 
0187         if (dopValue < 10)
0188         {
0189             return 3;
0190         }
0191 
0192         return 4;
0193     }
0194     else if (data.hasFixType())
0195     {
0196         if (data.getFixType() < 3)
0197         {
0198             return 4;
0199         }
0200     }
0201     else if (data.hasNSatellites())
0202     {
0203         if (data.getNSatellites() < 4)
0204         {
0205             return 4;
0206         }
0207     }
0208 
0209     // no warning level
0210 
0211     return -1;
0212 }
0213 
0214 bool GPSItemContainer::loadImageData()
0215 {
0216     QScopedPointer<DMetadata> meta(getMetadataForFile());
0217 
0218     if (meta && !m_dateTime.isValid())
0219     {
0220         m_dateTime = meta->getItemDateTime();
0221     }
0222 
0223     if (!m_dateTime.isValid())
0224     {
0225         // Get date from filesystem.
0226 
0227         QFileInfo info(m_url.toLocalFile());
0228 
0229         QDateTime ctime = info.birthTime();
0230         QDateTime mtime = info.lastModified();
0231 
0232         if (ctime.isNull() || mtime.isNull())
0233         {
0234             m_dateTime = qMax(ctime, mtime);
0235         }
0236         else
0237         {
0238             m_dateTime = qMin(ctime, mtime);
0239         }
0240 
0241         m_dateTime.setTimeSpec(Qt::UTC);
0242     }
0243 
0244     if (!meta)
0245     {
0246         return false;
0247     }
0248 
0249     // The way we read the coordinates here is problematic
0250     // if the coordinates were in the file initially, but
0251     // the user deleted them in the database. Then we still load
0252     // them from the file. On the other hand, we can not clear
0253     // the coordinates, because then we would loose them if
0254     // they are only stored in the database.
0255 /*
0256     m_gpsData.clear();
0257 */
0258 
0259     if (!m_gpsData.hasCoordinates())
0260     {
0261         // could not load the coordinates from the interface,
0262         // read them directly from the file
0263 
0264         double lat, lng;
0265         bool haveCoordinates = meta->getGPSLatitudeNumber(&lat) && meta->getGPSLongitudeNumber(&lng);
0266 
0267         if (haveCoordinates)
0268         {
0269             GeoCoordinates coordinates(lat, lng);
0270             double alt;
0271 
0272             if (meta->getGPSAltitude(&alt))
0273             {
0274                 coordinates.setAlt(alt);
0275             }
0276 
0277             m_gpsData.setCoordinates(coordinates);
0278         }
0279     }
0280 
0281     /**
0282      * @todo It seems that exiv2 provides EXIF entries if XMP sidecar entries exist,
0283      * therefore no need to read XMP as well?
0284      */
0285 
0286     // read the remaining GPS information from the file:
0287 
0288     const QByteArray speedRef  = meta->getExifTagData("Exif.GPSInfo.GPSSpeedRef");
0289     bool success               = !speedRef.isEmpty();
0290     long num, den;
0291     success                   &= meta->getExifTagRational("Exif.GPSInfo.GPSSpeed", num, den);
0292 
0293     if (success)
0294     {
0295         // be relaxed about 0/0
0296 
0297         if ((num == 0.0) && (den == 0.0))
0298         {
0299             den = 1.0;
0300         }
0301 
0302         const qreal speedInRef = qreal(num)/qreal(den);
0303         qreal FactorToMetersPerSecond;
0304 
0305         if      (speedRef.startsWith('K'))
0306         {
0307             // km/h = 1000 * 3600
0308 
0309             FactorToMetersPerSecond = 1.0 / 3.6;
0310         }
0311         else if (speedRef.startsWith('M'))
0312         {
0313             // TODO: someone please check that this is the 'right' mile
0314             // miles/hour = 1609.344 meters / hour = 1609.344 meters / 3600 seconds
0315 
0316             FactorToMetersPerSecond = 1.0 / (1609.344 / 3600.0);
0317         }
0318         else if (speedRef.startsWith('N'))
0319         {
0320             // speed is in knots.
0321             // knot = one nautical mile / hour = 1852 meters / hour = 1852 meters / 3600 seconds
0322 
0323             FactorToMetersPerSecond = 1.0 / (1852.0 / 3600.0);
0324         }
0325         else
0326         {
0327             success = false;
0328         }
0329 
0330         if (success)
0331         {
0332             const qreal speedInMetersPerSecond = speedInRef * FactorToMetersPerSecond;
0333             m_gpsData.setSpeed(speedInMetersPerSecond);
0334         }
0335     }
0336 
0337     // number of satellites
0338 
0339     const QString gpsSatellitesString = meta->getExifTagString("Exif.GPSInfo.GPSSatellites");
0340     bool satellitesOkay               = !gpsSatellitesString.isEmpty();
0341 
0342     if (satellitesOkay)
0343     {
0344         /**
0345          * @todo Here we only accept a single integer denoting the number of satellites used
0346          *       but not detailed information about all satellites.
0347          */
0348 
0349         const int nSatellites = gpsSatellitesString.toInt(&satellitesOkay);
0350 
0351         if (satellitesOkay)
0352         {
0353             m_gpsData.setNSatellites(nSatellites);
0354         }
0355     }
0356 
0357     // fix type / measure mode
0358 
0359     const QByteArray gpsMeasureModeByteArray = meta->getExifTagData("Exif.GPSInfo.GPSMeasureMode");
0360     bool measureModeOkay                     = !gpsMeasureModeByteArray.isEmpty();
0361 
0362     if (measureModeOkay)
0363     {
0364         const int measureMode = gpsMeasureModeByteArray.toInt(&measureModeOkay);
0365 
0366         if (measureModeOkay)
0367         {
0368             if ((measureMode == 2) || (measureMode == 3))
0369             {
0370                 m_gpsData.setFixType(measureMode);
0371             }
0372         }
0373     }
0374 
0375     // read the DOP value:
0376 
0377     success = meta->getExifTagRational("Exif.GPSInfo.GPSDOP", num, den);
0378 
0379     if (success)
0380     {
0381         // be relaxed about 0/0
0382 
0383         if ((num == 0.0) && (den == 0.0))
0384         {
0385             den = 1.0;
0386         }
0387 
0388         const qreal dop = qreal(num)/qreal(den);
0389 
0390         m_gpsData.setDop(dop);
0391     }
0392 
0393     // mark us as not-dirty, because the data was just loaded:
0394 
0395     m_dirty      = false;
0396     m_savedState = m_gpsData;
0397 
0398     emitDataChanged();
0399 
0400     return true;
0401 }
0402 
0403 QVariant GPSItemContainer::data(const int column, const int role) const
0404 {
0405     if      ((column == ColumnFilename) && (role == Qt::DisplayRole))
0406     {
0407         return m_url.fileName();
0408     }
0409     else if ((column == ColumnDateTime) && (role == Qt::DisplayRole))
0410     {
0411         if (m_dateTime.isValid())
0412         {
0413             return QLocale().toString(m_dateTime, QLocale::ShortFormat);
0414         }
0415 
0416         return i18n("Not available");
0417     }
0418     else if (role == RoleCoordinates)
0419     {
0420         return QVariant::fromValue(m_gpsData.getCoordinates());
0421     }
0422     else if ((column == ColumnLatitude) && (role == Qt::DisplayRole))
0423     {
0424         if (!m_gpsData.getCoordinates().hasLatitude())
0425         {
0426             return QString();
0427         }
0428 
0429         return QLocale().toString(m_gpsData.getCoordinates().lat(), 'g', 7);
0430     }
0431     else if ((column == ColumnLongitude) && (role == Qt::DisplayRole))
0432     {
0433         if (!m_gpsData.getCoordinates().hasLongitude())
0434         {
0435             return QString();
0436         }
0437 
0438         return QLocale().toString(m_gpsData.getCoordinates().lon(), 'g', 7);
0439     }
0440     else if ((column == ColumnAltitude) && (role == Qt::DisplayRole))
0441     {
0442         if (!m_gpsData.getCoordinates().hasAltitude())
0443         {
0444             return QString();
0445         }
0446 
0447         return QLocale().toString(m_gpsData.getCoordinates().alt(), 'g', 7);
0448     }
0449     else if (column == ColumnAccuracy)
0450     {
0451         if (role == Qt::DisplayRole)
0452         {
0453             if (m_gpsData.hasDop())
0454             {
0455                 return i18n("DOP: %1", m_gpsData.getDop());
0456             }
0457 
0458             if (m_gpsData.hasFixType())
0459             {
0460                 return i18n("Fix: %1d", m_gpsData.getFixType());
0461             }
0462 
0463             if (m_gpsData.hasNSatellites())
0464             {
0465                 return i18n("#Sat: %1", m_gpsData.getNSatellites());
0466             }
0467         }
0468         else if (role == Qt::BackgroundRole)
0469         {
0470             const int warningLevel = getWarningLevelFromGPSDataContainer(m_gpsData);
0471 
0472             switch (warningLevel)
0473             {
0474                 case 1:
0475                     return QBrush(Qt::green);
0476 
0477                 case 2:
0478                     return QBrush(Qt::yellow);
0479 
0480                 case 3:
0481                     // orange
0482                     return QBrush(QColor(0xff, 0x80, 0x00));
0483 
0484                 case 4:
0485                     return QBrush(Qt::red);
0486 
0487                 default:
0488                     break;
0489             }
0490         }
0491     }
0492     else if ((column == ColumnDOP) && (role == Qt::DisplayRole))
0493     {
0494         if (!m_gpsData.hasDop())
0495         {
0496             return QString();
0497         }
0498 
0499         return QString::number(m_gpsData.getDop());
0500     }
0501     else if ((column == ColumnFixType) && (role == Qt::DisplayRole))
0502     {
0503         if (!m_gpsData.hasFixType())
0504         {
0505             return QString();
0506         }
0507 
0508         return i18n("%1d", m_gpsData.getFixType());
0509     }
0510     else if ((column == ColumnNSatellites) && (role == Qt::DisplayRole))
0511     {
0512         if (!m_gpsData.hasNSatellites())
0513         {
0514             return QString();
0515         }
0516 
0517         return QString::number(m_gpsData.getNSatellites());
0518     }
0519     else if ((column == ColumnSpeed) && (role == Qt::DisplayRole))
0520     {
0521         if (!m_gpsData.hasSpeed())
0522         {
0523             return QString();
0524         }
0525 
0526         return QLocale().toString(m_gpsData.getSpeed());
0527     }
0528     else if ((column == ColumnStatus) && (role == Qt::DisplayRole))
0529     {
0530         if (m_dirty || m_tagListDirty)
0531         {
0532             return i18n("Modified");
0533         }
0534 
0535         return QString();
0536     }
0537     else if ((column == ColumnTags) && (role == Qt::DisplayRole))
0538     {
0539         if (!m_tagList.isEmpty())
0540         {
0541 
0542             QString myTagsList;
0543 
0544             for (int i = 0 ; i < m_tagList.count() ; ++i)
0545             {
0546                 QString myTag;
0547 
0548                 for (int j = 0 ; j < m_tagList[i].count() ; ++j)
0549                 {
0550                     myTag.append(QLatin1Char('/') + m_tagList[i].at(j).tagName);
0551 
0552                     if (j == 0)
0553                     {
0554                         myTag.remove(0, 1);
0555                     }
0556                 }
0557 
0558                 if (!myTagsList.isEmpty())
0559                 {
0560                     myTagsList.append(QLatin1String(", "));
0561                 }
0562 
0563                 myTagsList.append(myTag);
0564             }
0565 
0566             return myTagsList;
0567         }
0568 
0569         return QString();
0570     }
0571 
0572     return QVariant();
0573 }
0574 
0575 void GPSItemContainer::setCoordinates(const GeoCoordinates& newCoordinates)
0576 {
0577     m_gpsData.setCoordinates(newCoordinates);
0578     m_dirty = true;
0579 
0580     emitDataChanged();
0581 }
0582 
0583 void GPSItemContainer::setModel(GPSItemModel* const model)
0584 {
0585     m_model = model;
0586 }
0587 
0588 void GPSItemContainer::emitDataChanged()
0589 {
0590     if (m_model)
0591     {
0592         m_model->itemChanged(this);
0593     }
0594 }
0595 
0596 void GPSItemContainer::setHeaderData(GPSItemModel* const model)
0597 {
0598     model->setColumnCount(ColumnGPSItemContainerCount);
0599     model->setHeaderData(ColumnThumbnail,   Qt::Horizontal, i18n("Thumbnail"),      Qt::DisplayRole);
0600     model->setHeaderData(ColumnFilename,    Qt::Horizontal, i18n("Filename"),       Qt::DisplayRole);
0601     model->setHeaderData(ColumnDateTime,    Qt::Horizontal, i18n("Date and time"),  Qt::DisplayRole);
0602     model->setHeaderData(ColumnLatitude,    Qt::Horizontal, i18n("Latitude"),       Qt::DisplayRole);
0603     model->setHeaderData(ColumnLongitude,   Qt::Horizontal, i18n("Longitude"),      Qt::DisplayRole);
0604     model->setHeaderData(ColumnAltitude,    Qt::Horizontal, i18n("Altitude"),       Qt::DisplayRole);
0605     model->setHeaderData(ColumnAccuracy,    Qt::Horizontal, i18n("Accuracy"),       Qt::DisplayRole);
0606     model->setHeaderData(ColumnDOP,         Qt::Horizontal, i18n("DOP"),            Qt::DisplayRole);
0607     model->setHeaderData(ColumnFixType,     Qt::Horizontal, i18n("Fix type"),       Qt::DisplayRole);
0608     model->setHeaderData(ColumnNSatellites, Qt::Horizontal, i18n("# satellites"),   Qt::DisplayRole);
0609     model->setHeaderData(ColumnSpeed,       Qt::Horizontal, i18n("Speed"),          Qt::DisplayRole);
0610     model->setHeaderData(ColumnStatus,      Qt::Horizontal, i18n("Status"),         Qt::DisplayRole);
0611     model->setHeaderData(ColumnTags,        Qt::Horizontal, i18n("Tags"),           Qt::DisplayRole);
0612 }
0613 
0614 bool GPSItemContainer::lessThan(const GPSItemContainer* const otherItem, const int column) const
0615 {
0616     switch (column)
0617     {
0618         case ColumnThumbnail:
0619             return false;
0620 
0621         case ColumnFilename:
0622             return (m_url < otherItem->m_url);
0623 
0624         case ColumnDateTime:
0625             return (m_dateTime < otherItem->m_dateTime);
0626 
0627         case ColumnAltitude:
0628         {
0629             if (!m_gpsData.hasAltitude())
0630             {
0631                 return false;
0632             }
0633 
0634             if (!otherItem->m_gpsData.hasAltitude())
0635             {
0636                 return true;
0637             }
0638 
0639             return (m_gpsData.getCoordinates().alt() < otherItem->m_gpsData.getCoordinates().alt());
0640         }
0641 
0642         case ColumnNSatellites:
0643         {
0644             if (!m_gpsData.hasNSatellites())
0645             {
0646                 return false;
0647             }
0648 
0649             if (!otherItem->m_gpsData.hasNSatellites())
0650             {
0651                 return true;
0652             }
0653 
0654             return (m_gpsData.getNSatellites() < otherItem->m_gpsData.getNSatellites());
0655         }
0656 
0657         case ColumnAccuracy:
0658         {
0659             const int myWarning    = getWarningLevelFromGPSDataContainer(m_gpsData);
0660             const int otherWarning = getWarningLevelFromGPSDataContainer(otherItem->m_gpsData);
0661 
0662             if (myWarning < 0)
0663             {
0664                 return false;
0665             }
0666 
0667             if (otherWarning < 0)
0668             {
0669                 return true;
0670             }
0671 
0672             if (myWarning != otherWarning)
0673             {
0674                 return (myWarning < otherWarning);
0675             }
0676 
0677             // TODO: this may not be the best way to sort images with equal warning levels
0678             //       but it works for now
0679 
0680             if (m_gpsData.hasDop() != otherItem->m_gpsData.hasDop())
0681             {
0682                 return !m_gpsData.hasDop();
0683             }
0684 
0685             if (m_gpsData.hasDop() && otherItem->m_gpsData.hasDop())
0686             {
0687                 return (m_gpsData.getDop() < otherItem->m_gpsData.getDop());
0688             }
0689 
0690             if (m_gpsData.hasFixType() != otherItem->m_gpsData.hasFixType())
0691             {
0692                 return m_gpsData.hasFixType();
0693             }
0694 
0695             if (m_gpsData.hasFixType() && otherItem->m_gpsData.hasFixType())
0696             {
0697                 return (m_gpsData.getFixType() > otherItem->m_gpsData.getFixType());
0698             }
0699 
0700             if (m_gpsData.hasNSatellites() != otherItem->m_gpsData.hasNSatellites())
0701             {
0702                 return m_gpsData.hasNSatellites();
0703             }
0704 
0705             if (m_gpsData.hasNSatellites() && otherItem->m_gpsData.hasNSatellites())
0706             {
0707                 return (m_gpsData.getNSatellites() > otherItem->m_gpsData.getNSatellites());
0708             }
0709 
0710             return false;
0711         }
0712 
0713         case ColumnDOP:
0714         {
0715             if (!m_gpsData.hasDop())
0716             {
0717                 return false;
0718             }
0719 
0720             if (!otherItem->m_gpsData.hasDop())
0721             {
0722                 return true;
0723             }
0724 
0725             return (m_gpsData.getDop() < otherItem->m_gpsData.getDop());
0726         }
0727 
0728         case ColumnFixType:
0729         {
0730             if (!m_gpsData.hasFixType())
0731             {
0732                 return false;
0733             }
0734 
0735             if (!otherItem->m_gpsData.hasFixType())
0736             {
0737                 return true;
0738             }
0739 
0740             return (m_gpsData.getFixType() < otherItem->m_gpsData.getFixType());
0741         }
0742 
0743         case ColumnSpeed:
0744         {
0745             if (!m_gpsData.hasSpeed())
0746             {
0747                 return false;
0748             }
0749 
0750             if (!otherItem->m_gpsData.hasSpeed())
0751             {
0752                 return true;
0753             }
0754 
0755             return (m_gpsData.getSpeed() < otherItem->m_gpsData.getSpeed());
0756         }
0757 
0758         case ColumnLatitude:
0759         {
0760             if (!m_gpsData.hasCoordinates())
0761             {
0762                 return false;
0763             }
0764 
0765             if (!otherItem->m_gpsData.hasCoordinates())
0766             {
0767                 return true;
0768             }
0769 
0770             return (m_gpsData.getCoordinates().lat() < otherItem->m_gpsData.getCoordinates().lat());
0771         }
0772 
0773         case ColumnLongitude:
0774         {
0775             if (!m_gpsData.hasCoordinates())
0776             {
0777                 return false;
0778             }
0779 
0780             if (!otherItem->m_gpsData.hasCoordinates())
0781             {
0782                 return true;
0783             }
0784 
0785             return (m_gpsData.getCoordinates().lon() < otherItem->m_gpsData.getCoordinates().lon());
0786         }
0787 
0788         case ColumnStatus:
0789         {
0790             return (m_dirty && !otherItem->m_dirty);
0791         }
0792 
0793         default:
0794         {
0795             return false;
0796         }
0797     }
0798 }
0799 
0800 SaveProperties GPSItemContainer::saveProperties() const
0801 {
0802     SaveProperties p;
0803 
0804     // do we have gps information?
0805 
0806     if (m_gpsData.hasCoordinates())
0807     {
0808         p.shouldWriteCoordinates = true;
0809         p.latitude               = m_gpsData.getCoordinates().lat();
0810         p.longitude              = m_gpsData.getCoordinates().lon();
0811 
0812         if (m_gpsData.hasAltitude())
0813         {
0814             p.shouldWriteAltitude = true;
0815             p.altitude            = m_gpsData.getCoordinates().alt();
0816         }
0817         else
0818         {
0819             p.shouldRemoveAltitude = true;
0820         }
0821     }
0822     else
0823     {
0824         p.shouldRemoveCoordinates = true;
0825     }
0826 
0827     return p;
0828 }
0829 
0830 QString GPSItemContainer::saveChanges()
0831 {
0832     SaveProperties p = saveProperties();
0833 
0834     QString returnString;
0835 
0836     // first try to write the information to the image file
0837 
0838     bool success = true;
0839     QScopedPointer<DMetadata> meta(getMetadataForFile());
0840 
0841     if (!meta)
0842     {
0843         // TODO: more verbosity!
0844 
0845         returnString = i18n("Failed to open file.");
0846         success      = false;
0847     }
0848     else
0849     {
0850         if (p.shouldWriteCoordinates)
0851         {
0852             if (p.shouldWriteAltitude)
0853             {
0854                 success = meta->setGPSInfo(p.altitude, p.latitude, p.longitude);
0855             }
0856             else
0857             {
0858                 success = meta->setGPSInfo(nullptr,
0859                                            p.latitude, p.longitude);
0860             }
0861 
0862             // write all other GPS information here too
0863 
0864             if (success && m_gpsData.hasSpeed())
0865             {
0866                 success = setExifXmpTagDataVariant(meta.data(),
0867                                                    "Exif.GPSInfo.GPSSpeedRef",
0868                                                    "Xmp.exif.GPSSpeedRef",
0869                                                    QVariant(QLatin1String("K")));
0870 
0871                 if (success)
0872                 {
0873                     const qreal speedInMetersPerSecond   = m_gpsData.getSpeed();
0874 
0875                     // km/h = 0.001 * m / ( s * 1/(60*60) ) = 3.6 * m/s
0876 
0877                     const qreal speedInKilometersPerHour = 3.6 * speedInMetersPerSecond;
0878                     success                              = setExifXmpTagDataVariant(meta.data(),
0879                                                                                     "Exif.GPSInfo.GPSSpeed",
0880                                                                                     "Xmp.exif.GPSSpeed",
0881                                                                                     QVariant(speedInKilometersPerHour));
0882                 }
0883             }
0884 
0885             if (success && m_gpsData.hasNSatellites())
0886             {
0887                 /**
0888                  * @todo According to the EXIF 2.2 spec, GPSSatellites is a free form field which can either hold only the
0889                  * number of satellites or more details about each satellite used. For now, we just write
0890                  * the number of satellites. Are we using the correct format for the number of satellites here?
0891                  */
0892                 success = setExifXmpTagDataVariant(meta.data(),
0893                                                    "Exif.GPSInfo.GPSSatellites", "Xmp.exif.GPSSatellites",
0894                                                    QVariant(QString::number(m_gpsData.getNSatellites())));
0895             }
0896 
0897             if (success && m_gpsData.hasFixType())
0898             {
0899                 success = setExifXmpTagDataVariant(meta.data(),
0900                                                    "Exif.GPSInfo.GPSMeasureMode", "Xmp.exif.GPSMeasureMode",
0901                                                    QVariant(QString::number(m_gpsData.getFixType())));
0902             }
0903 
0904             // write DOP
0905 
0906             if (success && m_gpsData.hasDop())
0907             {
0908                 success = setExifXmpTagDataVariant(meta.data(),
0909                                                    "Exif.GPSInfo.GPSDOP",
0910                                                    "Xmp.exif.GPSDOP",
0911                                                    QVariant(m_gpsData.getDop()));
0912             }
0913 
0914 
0915             if (!success)
0916             {
0917                 returnString = i18n("Failed to add GPS info to image.");
0918             }
0919         }
0920 
0921         if (success && p.shouldRemoveCoordinates)
0922         {
0923             // TODO: remove only the altitude if requested
0924 
0925             success = meta->removeGPSInfo();
0926 
0927             if (!success)
0928             {
0929                 returnString = i18n("Failed to remove GPS info from image");
0930             }
0931         }
0932 
0933         if (success && !m_tagList.isEmpty() && (m_writeXmpTags || m_writeMetaLoc))
0934         {
0935             if (m_writeXmpTags)
0936             {
0937                 QStringList tagSeq = meta->getXmpTagStringSeq("Xmp.digiKam.TagsList", false);
0938 
0939                 for (int i = 0 ; i < m_tagList.count() ; ++i)
0940                 {
0941                     QList<TagData> currentTagList = m_tagList[i];
0942                     QString tag;
0943 
0944                     for (int j = 0 ; j < currentTagList.count() ; ++j)
0945                     {
0946                         if (currentTagList[j].tipName != QLatin1String("{Country code}"))
0947                         {
0948                             tag.append(QLatin1Char('/') + currentTagList[j].tagName);
0949                         }
0950                     }
0951 
0952                     tag.remove(0, 1);
0953 
0954                     if (!tag.isEmpty() && !tagSeq.contains(tag))
0955                     {
0956                         tagSeq.append(tag);
0957                     }
0958                 }
0959 
0960                 success = meta->setXmpTagStringSeq("Xmp.digiKam.TagsList", tagSeq);
0961 
0962                 if (!success)
0963                 {
0964                     returnString = i18n("Failed to save tags to file.");
0965                 }
0966                 else
0967                 {
0968                     success = meta->setXmpTagStringSeq("Xmp.dc.subject", tagSeq);
0969 
0970                     if (!success)
0971                     {
0972                         returnString = i18n("Failed to save tags to file.");
0973                     }
0974                 }
0975             }
0976 
0977             if (success && m_writeMetaLoc)
0978             {
0979                 IptcCoreLocationInfo locationInfo;
0980 
0981                 for (int i = 0 ; i < m_tagList.count() ; ++i)
0982                 {
0983                     QList<TagData> currentTagList = m_tagList[i];
0984                     QString tag;
0985 
0986                     for (int j = 0 ; j < currentTagList.count() ; ++j)
0987                     {
0988                         setLocationInfo(currentTagList[j], locationInfo);
0989                     }
0990                 }
0991 
0992                 success = meta->setIptcCoreLocation(locationInfo);
0993 
0994                 if (!success)
0995                 {
0996                     returnString = i18n("Failed to save tags to file.");
0997                 }
0998             }
0999         }
1000     }
1001 
1002     if (success)
1003     {
1004         success = meta->save(m_url.toLocalFile());
1005 
1006         if (!success)
1007         {
1008             returnString = i18n("Unable to save changes to file");
1009         }
1010         else
1011         {
1012             m_dirty        = false;
1013             m_savedState   = m_gpsData;
1014             m_tagListDirty = false;
1015             m_savedTagList = m_tagList;
1016         }
1017     }
1018 
1019     if (returnString.isEmpty())
1020     {
1021         // mark all changes as not dirty and tell the model:
1022 
1023         emitDataChanged();
1024     }
1025 
1026     return returnString;
1027 }
1028 
1029 /**
1030  * @brief Restore the gps data to @p container. Sets m_dirty to false if container equals savedState.
1031  */
1032 void GPSItemContainer::restoreGPSData(const GPSDataContainer& container)
1033 {
1034     m_dirty   = !(container == m_savedState);
1035     m_gpsData = container;
1036 
1037     emitDataChanged();
1038 }
1039 
1040 void GPSItemContainer::restoreRGTagList(const QList<QList<TagData> >& tagList)
1041 {
1042     // TODO: override == operator
1043 
1044     if (tagList.count() != m_savedTagList.count())
1045     {
1046         m_tagListDirty = true;
1047     }
1048     else
1049     {
1050         for (int i = 0 ; i < tagList.count() ; ++i)
1051         {
1052             bool foundNotEqual = false;
1053 
1054             if (tagList[i].count() != m_savedTagList[i].count())
1055             {
1056                 m_tagListDirty = true;
1057                 break;
1058             }
1059 
1060             for (int j = 0 ; j < tagList[i].count() ; ++j)
1061             {
1062                 if (tagList[i].at(j).tagName != m_savedTagList[i].at(j).tagName)
1063                 {
1064                     foundNotEqual = true;
1065                     break;
1066                 }
1067             }
1068 
1069             if (foundNotEqual)
1070             {
1071                 m_tagListDirty = true;
1072                 break;
1073             }
1074         }
1075     }
1076 
1077     m_tagList = tagList;
1078 
1079     emitDataChanged();
1080 }
1081 
1082 bool GPSItemContainer::isDirty() const
1083 {
1084     return m_dirty;
1085 }
1086 
1087 QUrl GPSItemContainer::url() const
1088 {
1089     return m_url;
1090 }
1091 
1092 QDateTime GPSItemContainer::dateTime() const
1093 {
1094     return m_dateTime;
1095 }
1096 
1097 GeoCoordinates GPSItemContainer::coordinates() const
1098 {
1099     return m_gpsData.getCoordinates();
1100 }
1101 
1102 GPSDataContainer GPSItemContainer::gpsData() const
1103 {
1104     return m_gpsData;
1105 }
1106 
1107 void GPSItemContainer::setGPSData(const GPSDataContainer& container)
1108 {
1109     m_gpsData = container;
1110     m_dirty   = true;
1111 
1112     emitDataChanged();
1113 }
1114 
1115 void GPSItemContainer::setTagList(const QList<QList<TagData> >& externalTagList)
1116 {
1117     m_tagList      = externalTagList;
1118     m_tagListDirty = true;
1119 
1120     emitDataChanged();
1121 }
1122 
1123 bool GPSItemContainer::isTagListDirty() const
1124 {
1125     return m_tagListDirty;
1126 }
1127 
1128 QList<QList<TagData> > GPSItemContainer::getTagList() const
1129 {
1130     return m_tagList;
1131 }
1132 
1133 void GPSItemContainer::setLocationInfo(const TagData& tagData, IptcCoreLocationInfo& locationInfo)
1134 {
1135     if      (tagData.tipName == QLatin1String("{Country}"))
1136     {
1137         locationInfo.country       = tagData.tagName;
1138     }
1139     else if (tagData.tipName == QLatin1String("{Country code}"))
1140     {
1141         locationInfo.countryCode   = tagData.tagName;
1142     }
1143     else if (tagData.tipName == QLatin1String("{State}"))
1144     {
1145         locationInfo.provinceState = tagData.tagName;
1146     }
1147     else if (locationInfo.provinceState.isEmpty() &&
1148              (tagData.tipName == QLatin1String("{State district}")))
1149     {
1150         locationInfo.provinceState = tagData.tagName;
1151     }
1152     else if (locationInfo.provinceState.isEmpty() &&
1153              (tagData.tipName == QLatin1String("{County}")))
1154     {
1155         locationInfo.provinceState = tagData.tagName;
1156     }
1157     else if (tagData.tipName == QLatin1String("{City}"))
1158     {
1159         locationInfo.city          = tagData.tagName;
1160     }
1161     else if (locationInfo.city.isEmpty() &&
1162              (tagData.tipName == QLatin1String("{City district}")))
1163     {
1164         locationInfo.city          = tagData.tagName;
1165     }
1166     else if (locationInfo.city.isEmpty() &&
1167              (tagData.tipName == QLatin1String("{Suburb}")))
1168     {
1169         locationInfo.city          = tagData.tagName;
1170     }
1171     else if (locationInfo.city.isEmpty() &&
1172              (tagData.tipName == QLatin1String("{Town}")))
1173     {
1174         locationInfo.city          = tagData.tagName;
1175     }
1176     else if (locationInfo.city.isEmpty() &&
1177              (tagData.tipName == QLatin1String("{Village}")))
1178     {
1179         locationInfo.city          = tagData.tagName;
1180     }
1181     else if (locationInfo.city.isEmpty() &&
1182              (tagData.tipName == QLatin1String("{Hamlet}")))
1183     {
1184         locationInfo.city          = tagData.tagName;
1185     }
1186     else if (tagData.tipName == QLatin1String("{Street}"))
1187     {
1188         locationInfo.location      = tagData.tagName;
1189     }
1190     else if (!locationInfo.location.isEmpty() &&
1191              (tagData.tipName == QLatin1String("{House number}")))
1192     {
1193         locationInfo.location.append(QLatin1Char(' ') + tagData.tagName);
1194     }
1195 }
1196 
1197 } // namespace Digikam