1
/** ===========================================================
4
* This file is a part of kipi-plugins project
5
* <a href="http://www.kipi-plugins.org">http://www.kipi-plugins.org</a>
8
* @brief An item to hold information about an image.
10
* @author Copyright (C) 2010 by Michael G. Hansen
11
* <a href="mailto:mike at mghansen dot de">mike at mghansen dot de</a>
13
* This program is free software; you can redistribute it
14
* and/or modify it under the terms of the GNU General
15
* Public License as published by the Free Software Foundation;
16
* either version 2, or (at your option) any later version.
18
* This program is distributed in the hope that it will be useful,
19
* but WITHOUT ANY WARRANTY; without even the implied warranty of
20
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
* GNU General Public License for more details.
23
* ============================================================ */
25
#include "kipiimageitem.h"
30
#include <QScopedPointer>
40
#include <libkexiv2/version.h>
41
#include <libkexiv2/kexiv2.h>
45
#include "kipiimagemodel.h"
47
using namespace KExiv2Iface;
49
namespace KIPIGPSSyncPlugin
52
bool KExiv2SetExifXmpTagDataVariant(KExiv2Iface::KExiv2* const exiv2Iface, const char* const exifTagName, const char* const xmpTagName, const QVariant& value)
54
bool success = exiv2Iface->setExifTagVariant(exifTagName, value);
58
/** @todo Here we save all data types as XMP Strings. Is that okay or do we have to store them as some other type?
65
case QVariant::LongLong:
66
case QVariant::ULongLong:
67
success = exiv2Iface->setXmpTagString(xmpTagName, QString::number(value.toInt()));
70
case QVariant::Double:
73
exiv2Iface->convertToRationalSmallDenominator(value.toDouble(), &num, &den);
74
success = exiv2Iface->setXmpTagString(xmpTagName, QString("%1/%2").arg(num).arg(den));
79
long num = 0, den = 1;
80
QList<QVariant> list = value.toList();
82
num = list[0].toInt();
84
den = list[1].toInt();
85
success = exiv2Iface->setXmpTagString(xmpTagName, QString("%1/%2").arg(num).arg(den));
90
case QVariant::DateTime:
92
QDateTime dateTime = value.toDateTime();
93
if(!dateTime.isValid())
99
success = exiv2Iface->setXmpTagString(xmpTagName, dateTime.toString(QString("yyyy:MM:dd hh:mm:ss")));
103
case QVariant::String:
105
success = exiv2Iface->setXmpTagString(xmpTagName, value.toString());
108
case QVariant::ByteArray:
109
/// @todo I don't know a straightforward way to convert a byte array to XMP
122
KipiImageItem::KipiImageItem(KIPI::Interface* const interface, const KUrl& url)
123
: m_interface(interface),
130
m_tagListDirty(false),
137
KipiImageItem::~KipiImageItem()
141
KExiv2Iface::KExiv2* KipiImageItem::getExiv2ForFile()
143
QScopedPointer<KExiv2Iface::KExiv2> exiv2Iface(new KExiv2Iface::KExiv2);
147
exiv2Iface->setWriteRawFiles(m_interface->hostSetting("WriteMetadataToRAW").toBool());
148
exiv2Iface->setUpdateFileTimeStamp(m_interface->hostSetting("WriteMetadataUpdateFiletimeStamp").toBool());
149
exiv2Iface->setUseXMPSidecar4Reading(m_interface->hostSetting("UseXMPSidecar4Reading").toBool());
150
exiv2Iface->setMetadataWritingMode(m_interface->hostSetting("MetadataWritingMode").toInt());
154
exiv2Iface->setUseXMPSidecar4Reading(true);
155
exiv2Iface->setMetadataWritingMode(KExiv2::WRITETOSIDECARONLY4READONLYFILES);
158
if (!exiv2Iface->load(m_url.path()))
163
return exiv2Iface.take();
166
int getWarningLevelFromGPSDataContainer(const GPSDataContainer& data)
170
const int dopValue = data.getDop();
179
else if (data.hasFixType())
181
if (data.getFixType()<3)
184
else if (data.hasNSatellites())
186
if (data.getNSatellites()<4)
194
bool KipiImageItem::loadImageData(const bool fromInterface, const bool fromFile)
196
if (fromInterface && m_interface)
198
// try to load the GPS data from the KIPI interface:
199
QMap<QString, QVariant> attributes;
200
KIPI::ImageInfo info = m_interface->info(m_url);
201
attributes = info.attributes();
203
if (attributes.contains("latitude") &&
204
attributes.contains("longitude"))
206
m_gpsData.setLatLon(attributes["latitude"].toDouble(), attributes["longitude"].toDouble());
207
if (attributes.contains("altitude"))
209
m_gpsData.setAltitude(attributes["altitude"].toDouble());
213
m_dateTime = info.time(KIPI::FromInfo);
218
QScopedPointer<KExiv2Iface::KExiv2> exiv2Iface(getExiv2ForFile());
223
if (!m_dateTime.isValid())
225
m_dateTime = exiv2Iface->getImageDateTime();
230
if (!m_gpsData.hasCoordinates())
232
// could not load the coordinates from the interface,
233
// read them directly from the file
236
bool haveCoordinates = exiv2Iface->getGPSLatitudeNumber(&lat) && exiv2Iface->getGPSLongitudeNumber(&lng);
239
KGeoMap::GeoCoordinates coordinates(lat, lng);
241
if (exiv2Iface->getGPSAltitude(&alt))
243
coordinates.setAlt(alt);
245
m_gpsData.setCoordinates(coordinates);
249
/** @todo It seems that exiv2 provides EXIF entries if XMP sidecar entries exist,
250
* therefore no need to read XMP as well?
252
// read the remaining GPS information from the file:
253
const QByteArray speedRef = exiv2Iface->getExifTagData("Exif.GPSInfo.GPSSpeedRef");
254
bool success = !speedRef.isEmpty();
256
success&= exiv2Iface->getExifTagRational("Exif.GPSInfo.GPSSpeed", num, den);
259
// be relaxed about 0/0
260
if ((num==0.0)&&(den==0.0))
263
const qreal speedInRef = qreal(num)/qreal(den);
265
qreal FactorToMetersPerSecond;
266
if (speedRef.startsWith('K'))
268
// km/h = 1000 * 3600
269
FactorToMetersPerSecond = 1.0/3.6;
271
else if (speedRef.startsWith('M'))
273
// TODO: someone please check that this is the 'right' mile
274
// miles/hour = 1609.344 meters / hour = 1609.344 meters / 3600 seconds
275
FactorToMetersPerSecond = 1.0 / (1609.344 / 3600.0);
277
else if (speedRef.startsWith('N'))
279
// speed is in knots.
280
// knot = one nautic mile / hour = 1852 meters / hour = 1852 meters / 3600 seconds
281
FactorToMetersPerSecond = 1.0 / (1852.0 / 3600.0);
290
const qreal speedInMetersPerSecond = speedInRef * FactorToMetersPerSecond;
291
m_gpsData.setSpeed(speedInMetersPerSecond);
295
// number of satellites
296
const QString gpsSatellitesString = exiv2Iface->getExifTagString("Exif.GPSInfo.GPSSatellites");
297
bool satellitesOkay = !gpsSatellitesString.isEmpty();
301
* @todo Here we only accept a single integer denoting the number of satellites used
302
* but not detailed information about all satellites.
304
const int nSatellites = gpsSatellitesString.toInt(&satellitesOkay);
307
m_gpsData.setNSatellites(nSatellites);
311
// fix type / measure mode
312
const QByteArray gpsMeasureModeByteArray = exiv2Iface->getExifTagData("Exif.GPSInfo.GPSMeasureMode");
313
bool measureModeOkay = !gpsMeasureModeByteArray.isEmpty();
316
const int measureMode = gpsMeasureModeByteArray.toInt(&measureModeOkay);
319
if ((measureMode==2)||(measureMode==3))
321
m_gpsData.setFixType(measureMode);
326
// read the DOP value:
327
success= exiv2Iface->getExifTagRational("Exif.GPSInfo.GPSDOP", num, den);
330
// be relaxed about 0/0
331
if ((num==0.0)&&(den==0.0))
334
const qreal dop = qreal(num)/qreal(den);
336
m_gpsData.setDop(dop);
341
// mark us as not-dirty, because the data was just loaded:
343
m_savedState = m_gpsData;
350
QVariant KipiImageItem::data(const int column, const int role) const
352
if ((column==ColumnFilename)&&(role==Qt::DisplayRole))
354
return m_url.fileName();
356
else if ((column==ColumnDateTime)&&(role==Qt::DisplayRole))
358
if (m_dateTime.isValid())
360
return m_dateTime.toString(Qt::LocalDate);
362
return i18n("Not available");
364
else if (role==RoleCoordinates)
366
return QVariant::fromValue(m_gpsData.getCoordinates());
368
else if ((column==ColumnLatitude)&&(role==Qt::DisplayRole))
370
if (!m_gpsData.getCoordinates().hasLatitude())
373
return KGlobal::locale()->formatNumber(m_gpsData.getCoordinates().lat(), 7);
375
else if ((column==ColumnLongitude)&&(role==Qt::DisplayRole))
377
if (!m_gpsData.getCoordinates().hasLongitude())
380
return KGlobal::locale()->formatNumber(m_gpsData.getCoordinates().lon(), 7);
382
else if ((column==ColumnAltitude)&&(role==Qt::DisplayRole))
384
if (!m_gpsData.getCoordinates().hasAltitude())
387
return KGlobal::locale()->formatNumber(m_gpsData.getCoordinates().alt());
389
else if (column==ColumnAccuracy)
391
if (role==Qt::DisplayRole)
393
if (m_gpsData.hasDop())
395
return i18n("DOP: %1", m_gpsData.getDop());
398
if (m_gpsData.hasFixType())
400
return i18n("Fix: %1d", m_gpsData.getFixType());
403
if (m_gpsData.hasNSatellites())
405
return i18n("#Sat: %1", m_gpsData.getNSatellites());
408
else if (role==Qt::BackgroundRole)
410
const int warningLevel = getWarningLevelFromGPSDataContainer(m_gpsData);
411
switch (warningLevel)
414
return QBrush(Qt::green);
416
return QBrush(Qt::yellow);
419
return QBrush(QColor(0xff, 0x80, 0x00));
421
return QBrush(Qt::red);
427
else if ((column==ColumnDOP)&&(role==Qt::DisplayRole))
429
if (!m_gpsData.hasDop())
432
return KGlobal::locale()->formatNumber(m_gpsData.getDop());
434
else if ((column==ColumnFixType)&&(role==Qt::DisplayRole))
436
if (!m_gpsData.hasFixType())
439
return i18n("%1d", m_gpsData.getFixType());
441
else if ((column==ColumnNSatellites)&&(role==Qt::DisplayRole))
443
if (!m_gpsData.hasNSatellites())
446
return KGlobal::locale()->formatNumber(m_gpsData.getNSatellites(), 0);
448
else if ((column==ColumnSpeed)&&(role==Qt::DisplayRole))
450
if (!m_gpsData.hasSpeed())
453
return KGlobal::locale()->formatNumber(m_gpsData.getSpeed());
455
else if ((column==ColumnStatus)&&(role==Qt::DisplayRole))
457
if (m_dirty || m_tagListDirty)
459
return i18n("Modified");
464
else if ((column==ColumnTags)&&(role==Qt::DisplayRole))
466
if (!m_tagList.isEmpty())
470
for (int i=0; i<m_tagList.count(); ++i)
473
for (int j=0; j<m_tagList[i].count(); ++j)
475
myTag.append(QString("/") + m_tagList[i].at(j).tagName);
480
if (!myTagsList.isEmpty())
481
myTagsList.append(", ");
482
myTagsList.append(myTag);
495
void KipiImageItem::setCoordinates(const KGeoMap::GeoCoordinates& newCoordinates)
497
m_gpsData.setCoordinates(newCoordinates);
502
void KipiImageItem::setModel(KipiImageModel* const model)
507
void KipiImageItem::emitDataChanged()
511
m_model->itemChanged(this);
515
void KipiImageItem::setHeaderData(KipiImageModel* const model)
517
model->setColumnCount(ColumnGPSImageItemCount);
518
model->setHeaderData(ColumnThumbnail, Qt::Horizontal, i18n("Thumbnail"), Qt::DisplayRole);
519
model->setHeaderData(ColumnFilename, Qt::Horizontal, i18n("Filename"), Qt::DisplayRole);
520
model->setHeaderData(ColumnDateTime, Qt::Horizontal, i18n("Date and time"), Qt::DisplayRole);
521
model->setHeaderData(ColumnLatitude, Qt::Horizontal, i18n("Latitude"), Qt::DisplayRole);
522
model->setHeaderData(ColumnLongitude, Qt::Horizontal, i18n("Longitude"), Qt::DisplayRole);
523
model->setHeaderData(ColumnAltitude, Qt::Horizontal, i18n("Altitude"), Qt::DisplayRole);
524
model->setHeaderData(ColumnAccuracy, Qt::Horizontal, i18n("Accuracy"), Qt::DisplayRole);
525
model->setHeaderData(ColumnDOP, Qt::Horizontal, i18n("DOP"), Qt::DisplayRole);
526
model->setHeaderData(ColumnFixType, Qt::Horizontal, i18n("Fix type"), Qt::DisplayRole);
527
model->setHeaderData(ColumnNSatellites, Qt::Horizontal, i18n("# satellites"), Qt::DisplayRole);
528
model->setHeaderData(ColumnSpeed, Qt::Horizontal, i18n("Speed"), Qt::DisplayRole);
529
model->setHeaderData(ColumnStatus, Qt::Horizontal, i18n("Status"), Qt::DisplayRole);
530
model->setHeaderData(ColumnTags, Qt::Horizontal, i18n("Tags"), Qt::DisplayRole);
533
bool KipiImageItem::lessThan(const KipiImageItem* const otherItem, const int column) const
537
case ColumnThumbnail:
541
return m_url < otherItem->m_url;
544
return m_dateTime < otherItem->m_dateTime;
548
if (!m_gpsData.hasAltitude())
551
if (!otherItem->m_gpsData.hasAltitude())
554
return m_gpsData.getCoordinates().alt() < otherItem->m_gpsData.getCoordinates().alt();
557
case ColumnNSatellites:
559
if (!m_gpsData.hasNSatellites())
562
if (!otherItem->m_gpsData.hasNSatellites())
565
return m_gpsData.getNSatellites() < otherItem->m_gpsData.getNSatellites();
570
const int myWarning = getWarningLevelFromGPSDataContainer(m_gpsData);
571
const int otherWarning = getWarningLevelFromGPSDataContainer(otherItem->m_gpsData);
579
if (myWarning!=otherWarning)
580
return myWarning < otherWarning;
582
// TODO: this may not be the best way to sort images with equal warning levels
583
// but it works for now
585
if (m_gpsData.hasDop()!=otherItem->m_gpsData.hasDop())
586
return !m_gpsData.hasDop();
587
if (m_gpsData.hasDop()&&otherItem->m_gpsData.hasDop())
589
return m_gpsData.getDop()<otherItem->m_gpsData.getDop();
592
if (m_gpsData.hasFixType()!=otherItem->m_gpsData.hasFixType())
593
return m_gpsData.hasFixType();
594
if (m_gpsData.hasFixType()&&otherItem->m_gpsData.hasFixType())
596
return m_gpsData.getFixType()>otherItem->m_gpsData.getFixType();
599
if (m_gpsData.hasNSatellites()!=otherItem->m_gpsData.hasNSatellites())
600
return m_gpsData.hasNSatellites();
601
if (m_gpsData.hasNSatellites()&&otherItem->m_gpsData.hasNSatellites())
603
return m_gpsData.getNSatellites()>otherItem->m_gpsData.getNSatellites();
611
if (!m_gpsData.hasDop())
614
if (!otherItem->m_gpsData.hasDop())
617
return m_gpsData.getDop() < otherItem->m_gpsData.getDop();
622
if (!m_gpsData.hasFixType())
625
if (!otherItem->m_gpsData.hasFixType())
628
return m_gpsData.getFixType() < otherItem->m_gpsData.getFixType();
633
if (!m_gpsData.hasSpeed())
636
if (!otherItem->m_gpsData.hasSpeed())
639
return m_gpsData.getSpeed() < otherItem->m_gpsData.getSpeed();
644
if (!m_gpsData.hasCoordinates())
647
if (!otherItem->m_gpsData.hasCoordinates())
650
return m_gpsData.getCoordinates().lat() < otherItem->m_gpsData.getCoordinates().lat();
653
case ColumnLongitude:
655
if (!m_gpsData.hasCoordinates())
658
if (!otherItem->m_gpsData.hasCoordinates())
661
return m_gpsData.getCoordinates().lon() < otherItem->m_gpsData.getCoordinates().lon();
666
return m_dirty && !otherItem->m_dirty;
674
QString KipiImageItem::saveChanges(const bool toInterface, const bool toFile)
676
Q_UNUSED(toInterface);
679
// determine what is to be done first
680
bool shouldRemoveCoordinates = false;
681
bool shouldRemoveAltitude = false;
682
bool shouldWriteCoordinates = false;
683
bool shouldWriteAltitude = false;
688
// do we have gps information?
689
if (m_gpsData.hasCoordinates())
691
shouldWriteCoordinates = true;
692
latitude = m_gpsData.getCoordinates().lat();
693
longitude = m_gpsData.getCoordinates().lon();
695
if (m_gpsData.hasAltitude())
697
shouldWriteAltitude = true;
698
altitude = m_gpsData.getCoordinates().alt();
702
shouldRemoveAltitude = true;
707
shouldRemoveCoordinates = true;
708
shouldRemoveAltitude = true;
711
QString returnString;
713
// first try to write the information to the image file
714
bool success = false;
715
QScopedPointer<KExiv2Iface::KExiv2> exiv2Iface(getExiv2ForFile());
718
// TODO: more verbosity!
719
returnString = i18n("Failed to open file.");
723
if (shouldWriteCoordinates)
725
if (shouldWriteAltitude)
727
success = exiv2Iface->setGPSInfo(altitude, latitude, longitude);
731
success = exiv2Iface->setGPSInfo(static_cast<const double* const>(0), latitude, longitude);
734
// write all other GPS information here too
735
if (success && m_gpsData.hasSpeed())
737
success = KExiv2SetExifXmpTagDataVariant(exiv2Iface.data(), "Exif.GPSInfo.GPSSpeedRef", "Xmp.exif.GPSSpeedRef", QVariant(QString("K")));
741
const qreal speedInMetersPerSecond = m_gpsData.getSpeed();
743
// km/h = 0.001 * m / ( s * 1/(60*60) ) = 3.6 * m/s
744
const qreal speedInKilometersPerHour = 3.6 * speedInMetersPerSecond;
745
success = KExiv2SetExifXmpTagDataVariant(exiv2Iface.data(), "Exif.GPSInfo.GPSSpeed", "Xmp.exif.GPSSpeed", QVariant(speedInKilometersPerHour));
749
if (success && m_gpsData.hasNSatellites())
752
* @todo According to the EXIF 2.2 spec, GPSSatellites is a free form field which can either hold only the
753
* number of satellites or more details about each satellite used. For now, we just write
754
* the number of satellites. Are we using the correct format for the number of satellites here?
756
success = KExiv2SetExifXmpTagDataVariant(exiv2Iface.data(),
757
"Exif.GPSInfo.GPSSatellites", "Xmp.exif.GPSSatellites",
758
QVariant(QString::number(m_gpsData.getNSatellites())));
761
if (success && m_gpsData.hasFixType())
763
success = KExiv2SetExifXmpTagDataVariant(exiv2Iface.data(),
764
"Exif.GPSInfo.GPSMeasureMode", "Xmp.exif.GPSMeasureMode",
765
QVariant(QString::number(m_gpsData.getFixType())));
769
if (success && m_gpsData.hasDop())
771
success = KExiv2SetExifXmpTagDataVariant(
773
"Exif.GPSInfo.GPSDOP",
775
QVariant(m_gpsData.getDop())
782
returnString = i18n("Failed to add GPS info to image.");
785
if (shouldRemoveCoordinates)
787
// TODO: remove only the altitude if requested
788
success = exiv2Iface->removeGPSInfo();
791
returnString = i18n("Failed to remove GPS info from image");
795
if (!m_tagList.isEmpty() && m_writeXmpTags)
800
for (int i=0; i<m_tagList.count(); ++i)
802
QList<TagData> currentTagList = m_tagList[i];
805
for (int j=0; j<currentTagList.count(); ++j)
807
tag.append(QString("/") + currentTagList[j].tagName);
814
bool success = exiv2Iface->setXmpTagStringSeq("Xmp.digiKam.TagsList", tagSeq, true);
817
returnString = i18n("Failed to save tags to file.");
819
success = exiv2Iface->setXmpTagStringSeq("Xmp.dc.subject", tagSeq, true);
822
returnString = i18n("Failed to save tags to file.");
829
success = exiv2Iface->save(m_url.path());
832
returnString = i18n("Unable to save changes to file");
837
m_savedState = m_gpsData;
838
m_tagListDirty = false;
839
m_savedTagList = m_tagList;
843
// now tell the interface about the changes
844
// TODO: remove the altitude if it is not available
847
if (shouldWriteCoordinates)
849
QMap<QString, QVariant> attributes;
850
attributes.insert("latitude", latitude);
851
attributes.insert("longitude", longitude);
852
if (shouldWriteAltitude)
854
attributes.insert("altitude", altitude);
857
KIPI::ImageInfo info = m_interface->info(m_url);
858
info.addAttributes(attributes);
861
if (shouldRemoveCoordinates)
863
QStringList listToRemove;
864
listToRemove << "gpslocation";
865
KIPI::ImageInfo info = m_interface->info(m_url);
866
info.delAttributes(listToRemove);
869
if (!m_tagList.isEmpty())
871
QMap<QString, QVariant> attributes;
872
QStringList tagsPath;
874
for (int i=0; i<m_tagList.count(); ++i)
877
QString singleTagPath;
878
QList<TagData> currentTagPath = m_tagList[i];
879
for (int j=0; j<currentTagPath.count(); ++j)
881
singleTagPath.append(QString("%1").arg("/") + currentTagPath[j].tagName);
884
singleTagPath.remove(0,1);
888
tagsPath.append(singleTagPath);
891
attributes.insert("tagspath", tagsPath);
892
KIPI::ImageInfo info = m_interface->info(m_url);
893
info.addAttributes(attributes);
897
if (returnString.isEmpty())
899
// mark all changes as not dirty and tell the model:
907
* @brief Restore the gps data to @p container. Sets m_dirty to false if container equals savedState.
909
void KipiImageItem::restoreGPSData(const GPSDataContainer& container)
911
m_dirty = !(container == m_savedState);
912
m_gpsData = container;
916
void KipiImageItem::restoreRGTagList(const QList<QList<TagData> >& tagList)
918
//TODO: override == operator
920
if (tagList.count() != m_savedTagList.count())
921
m_tagListDirty = true;
924
for (int i=0; i<tagList.count(); ++i)
926
bool foundNotEqual = false;
928
if (tagList[i].count() != m_savedTagList[i].count())
930
m_tagListDirty = true;
934
for (int j=0; j<tagList[i].count(); ++j)
936
if (tagList[i].at(j).tagName != m_savedTagList[i].at(j).tagName)
938
foundNotEqual = true;
945
m_tagListDirty = true;
955
} /* KIPIGPSSyncPlugin */