File indexing completed on 2024-04-28 15:39:06

0001 // SPDX-FileCopyrightText: 2020-2022 Tobias Leupold <tl at stonemx dot de>
0002 //
0003 // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 
0005 // Local includes
0006 #include "ImagesModel.h"
0007 #include "KGeoTag.h"
0008 #include "Coordinates.h"
0009 
0010 // KDE includes
0011 #include <KLocalizedString>
0012 #include <KExiv2/KExiv2>
0013 
0014 // Qt includes
0015 #include <QFileInfo>
0016 #include <QFont>
0017 
0018 // C++ includes
0019 #include <utility>
0020 
0021 ImagesModel::ImagesModel(QObject *parent, bool splitImagesList, int thumbnailSize, int previewSize)
0022     : QAbstractListModel(parent),
0023       m_splitImagesList(splitImagesList),
0024       m_thumbnailSize(QSize(thumbnailSize, thumbnailSize)),
0025       m_previewSize(QSize(previewSize, previewSize))
0026 {
0027     m_timeZone = QTimeZone::systemTimeZone();
0028 }
0029 
0030 void ImagesModel::setSplitImagesList(bool state)
0031 {
0032     m_splitImagesList = state;
0033     Q_EMIT dataChanged(index(0, 0), index(rowCount(), 0), { Qt::DisplayRole });
0034 }
0035 
0036 int ImagesModel::rowCount(const QModelIndex &) const
0037 {
0038     return m_paths.count();
0039 }
0040 
0041 QVariant ImagesModel::data(const QModelIndex &index, int role) const
0042 {
0043     if (! index.isValid() || index.row() > m_paths.count()) {
0044         return QVariant();
0045     }
0046 
0047     const auto &path = m_paths.at(index.row());
0048     const auto &data = m_imageData[path];
0049 
0050     if (role == Qt::DisplayRole) {
0051         const QString associatedMarker = (! m_splitImagesList && data.coordinates.isSet())
0052             ? i18nc("Marker for an associated file", "\u2713\u2009")
0053             : QString();
0054         const QString changedmarker = data.coordinates != data.lastSavedCoordinates
0055             ? i18nc("Marker for a file with a pending change", "\u2009*")
0056             : QString();
0057         return i18nc("Pattern for a display filename with a \"pending change\" and an "
0058                      "\"associated\" marker. The first option is the \"associated\" marker, the "
0059                      "second one is the filename and the third one the \"pending change\" marker.",
0060                      "%1%2%3",
0061                      associatedMarker, data.fileName, changedmarker);
0062 
0063     } else if (role == Qt::DecorationRole) {
0064         return data.thumbnail;
0065 
0066     } else if (role == Qt::ForegroundRole) {
0067         switch (data.matchType) {
0068         case KGeoTag::NotMatched:
0069             return m_colorScheme.foreground();
0070         case KGeoTag::ExactMatch:
0071             return m_colorScheme.foreground(KColorScheme::PositiveText);
0072         case KGeoTag::InterpolatedMatch:
0073             return m_colorScheme.foreground(KColorScheme::NeutralText);
0074         case KGeoTag::ManuallySet:
0075             return m_colorScheme.foreground(KColorScheme::LinkText);
0076         }
0077 
0078     } else if (! m_splitImagesList && role == Qt::FontRole) {
0079         QFont font;
0080         if (data.coordinates.isSet()) {
0081             font.setBold(true);
0082         }
0083         return font;
0084 
0085     } else if (role == KGeoTag::PathRole) {
0086         return path;
0087 
0088     } else if (role == KGeoTag::DateRole) {
0089         return data.date;
0090 
0091     } else if (role == KGeoTag::CoordinatesRole) {
0092         QVariant coordinates;
0093         coordinates.setValue(data.coordinates);
0094         return coordinates;
0095 
0096     } else if (role == KGeoTag::ThumbnailRole) {
0097         return data.thumbnail;
0098 
0099     } else if (role == KGeoTag::PreviewRole) {
0100         return data.preview;
0101 
0102     } else if (role == KGeoTag::MatchTypeRole) {
0103         QVariant matchType;
0104         matchType.setValue(data.matchType);
0105         return matchType;
0106 
0107     } else if (role == KGeoTag::ChangedRole) {
0108         return data.originalCoordinates != data.coordinates;
0109 
0110     }
0111 
0112     return QVariant();
0113 }
0114 
0115 ImagesModel::LoadResult ImagesModel::addImage(const QString &path)
0116 {
0117     // Check if we already have the image
0118     if (m_paths.contains(path)) {
0119         return LoadResult::AlreadyLoaded;
0120     }
0121 
0122     // Read the image
0123     QImage image = QImage(path);
0124     if (image.isNull()) {
0125         return LoadResult::LoadingImageFailed;
0126     }
0127 
0128     // Read the exif data
0129     auto exif = KExiv2Iface::KExiv2();
0130     exif.setUseXMPSidecar4Reading(true);
0131     if (! exif.load(path)) {
0132         return LoadResult::LoadingMetadataFailed;
0133     }
0134 
0135     // Prepare the images's data struct
0136     ImageData data;
0137 
0138     // Add the filename
0139     const QFileInfo info(path);
0140     data.fileName = info.fileName();
0141 
0142     // Read the date
0143     data.date = exif.getImageDateTime();
0144 
0145     // If no date could be read from the metadata, fall back to file properties
0146     if (! data.date.isValid()) {
0147         // First try to get the file's initial creation date
0148         data.date = info.birthTime();
0149 
0150         // If that fails, fall back to the file's mtime
0151         if (! data.date.isValid()) {
0152             data.date = info.lastModified();
0153         }
0154     }
0155 
0156     // Apply the currently set timezone
0157     data.date.setTimeZone(m_timeZone);
0158 
0159     // Strip out milliseconds if the image provides them to allow seconds-exact matching
0160     const auto msec = data.date.time().msec();
0161     if (msec != 0) {
0162         data.date = data.date.addMSecs(msec * -1);
0163     }
0164 
0165     // Try to read gps information
0166     double altitude;
0167     double latitude;
0168     double longitude;
0169     if (exif.getGPSInfo(altitude, latitude, longitude)) {
0170         data.originalCoordinates = Coordinates(longitude, latitude, altitude, true);
0171         data.lastSavedCoordinates = data.originalCoordinates;
0172         data.coordinates = data.originalCoordinates;
0173     }
0174 
0175     // Fix the image's orientation
0176     exif.rotateExifQImage(image, exif.getImageOrientation());
0177 
0178     // Create a smaller thumbnail
0179     data.thumbnail = QPixmap::fromImage(image.scaled(m_thumbnailSize, Qt::KeepAspectRatio,
0180                                                      Qt::SmoothTransformation));
0181 
0182     // Create a bigger preview (to be scaled according to the view size)
0183     data.preview = image.scaled(m_previewSize, Qt::KeepAspectRatio);
0184 
0185     // Find the correct row for the new image (sorted by date)
0186     int row = 0;
0187     for (const QString &path : m_paths) {
0188         if (m_imageData.value(path).date > data.date) {
0189             break;
0190         }
0191         row++;
0192     }
0193 
0194     // Add the image
0195 
0196     beginInsertRows(QModelIndex(), row, row);
0197     m_paths.insert(row, path);
0198     m_imageData.insert(path, data);
0199     endInsertRows();
0200 
0201     const auto modelIndex = index(row, 0);
0202     Q_EMIT dataChanged(modelIndex, modelIndex, { Qt::DisplayRole });
0203 
0204     return LoadResult::LoadingSucceeded;
0205 }
0206 
0207 void ImagesModel::emitDataChanged(const QString &path)
0208 {
0209     const auto modelIndex = indexFor(path);
0210     Q_EMIT dataChanged(modelIndex, modelIndex, { Qt::DisplayRole });
0211 }
0212 
0213 const QVector<QString> &ImagesModel::allImages() const
0214 {
0215     return m_paths;
0216 }
0217 
0218 QVector<QString> ImagesModel::imagesWithPendingChanges() const
0219 {
0220     QVector<QString> images;
0221     for (const auto &path : std::as_const(m_paths)) {
0222         const auto &data = m_imageData[path];
0223         if (data.coordinates != data.lastSavedCoordinates) {
0224             images.append(path);
0225         }
0226     }
0227     return images;
0228 }
0229 
0230 QVector<QString> ImagesModel::processedSavedImages() const
0231 {
0232     QVector<QString> images;
0233     for (const auto &path : std::as_const(m_paths)) {
0234         const auto &data = m_imageData[path];
0235         if (data.changed && data.coordinates == data.lastSavedCoordinates) {
0236             images.append(path);
0237         }
0238     }
0239     return images;
0240 }
0241 
0242 QVector<QString> ImagesModel::imagesLoadedTagged() const
0243 {
0244     QVector<QString> images;
0245     for (const auto &path : std::as_const(m_paths)) {
0246         const auto &data = m_imageData[path];
0247         if (data.originalCoordinates.isSet()) {
0248             images.append(path);
0249         }
0250     }
0251     return images;
0252 }
0253 
0254 QDateTime ImagesModel::date(const QString &path) const
0255 {
0256     return m_imageData.value(path).date;
0257 }
0258 
0259 bool ImagesModel::contains(const QString &path) const
0260 {
0261     return m_paths.contains(path);
0262 }
0263 
0264 Coordinates ImagesModel::coordinates(const QString &path) const
0265 {
0266     return m_imageData.value(path).coordinates;
0267 }
0268 
0269 void ImagesModel::setCoordinates(const QString &path, const Coordinates &coordinates,
0270                                  KGeoTag::MatchType matchType)
0271 {
0272     auto &data = m_imageData[path];
0273     data.matchType = matchType;
0274     data.coordinates = coordinates;
0275     data.changed = true;
0276     emitDataChanged(path);
0277 }
0278 
0279 void ImagesModel::setElevation(const QString &path, double elevation)
0280 {
0281     auto &data = m_imageData[path];
0282     data.coordinates.setAlt(elevation);
0283     data.changed = true;
0284 }
0285 
0286 void ImagesModel::resetChanges(const QString &path)
0287 {
0288     auto &data = m_imageData[path];
0289     data.coordinates = data.originalCoordinates;
0290     data.matchType = KGeoTag::NotMatched;
0291     emitDataChanged(path);
0292 }
0293 
0294 QModelIndex ImagesModel::indexFor(const QString &path) const
0295 {
0296     return index(m_paths.indexOf(path), 0);
0297 }
0298 
0299 void ImagesModel::setSaved(const QString &path)
0300 {
0301     auto &data = m_imageData[path];
0302     data.lastSavedCoordinates = data.coordinates;
0303 }
0304 
0305 KGeoTag::MatchType ImagesModel::matchType(const QString &path) const
0306 {
0307     return m_imageData.value(path).matchType;
0308 }
0309 
0310 void ImagesModel::setImagesTimeZone(const QByteArray &id)
0311 {
0312     m_timeZone = QTimeZone(id);
0313 
0314     for (const auto &path : m_paths) {
0315         m_imageData[path].date.setTimeZone(m_timeZone);
0316     }
0317 }
0318 
0319 bool ImagesModel::hasPendingChanges(const QString &path) const
0320 {
0321     const auto &data = m_imageData[path];
0322     return data.coordinates != data.lastSavedCoordinates;
0323 }
0324 
0325 void ImagesModel::removeImages(const QVector<QString> &paths)
0326 {
0327     for (const auto &path : paths) {
0328         const auto row = m_paths.indexOf(path);
0329         const auto modelIndex = index(row, 0);
0330         beginRemoveRows(QModelIndex(), row, row);
0331         m_paths.remove(row);
0332         m_imageData.remove(path);
0333         Q_EMIT dataChanged(modelIndex, modelIndex, { Qt::DisplayRole });
0334         endRemoveRows();
0335     }
0336 }
0337 
0338 void ImagesModel::removeAllImages()
0339 {
0340     const auto lastRow = m_paths.count() - 1;
0341     const auto firstModelIndex = index(0, 0);
0342     const auto lastModelIndex = index(lastRow, 0);
0343     beginRemoveRows(QModelIndex(), 0, lastRow);
0344     m_paths.clear();
0345     m_imageData.clear();
0346     Q_EMIT dataChanged(firstModelIndex, lastModelIndex, { Qt::DisplayRole });
0347     endRemoveRows();
0348 }