source: subversion/applications/editors/merkaartor/GeoImageDock.cpp @ 13954

Last change on this file since 13954 was 13943, checked in by schluessler, 11 years ago

FIX : Readonly layers are asked to be made writeable when loading geotagged images to them
FIX : added missing "Picture"-tag setting when loading images based on their image-timestamp

File size: 16.1 KB
Line 
1#include "GeoImageDock.h"
2
3#include "Map/TrackPoint.h"
4#include "Map/MapLayer.h"
5#include "Command/DocumentCommands.h"
6#include "LayerWidget.h"
7
8#include <QtGui/QInputDialog>
9#include <QtGui/QMessageBox>
10#include <QtGui/QProgressDialog>
11#include <QtGui/QClipboard>
12#include <QtGui/QRadioButton>
13#include <QtGui/QTimeEdit>
14#include <QtGui/QDialogButtonBox>
15
16
17GeoImageDock::GeoImageDock(MainWindow *aMain)
18        : QDockWidget(aMain), Main(aMain)
19{
20        curImage = -1;
21        setWindowTitle(tr("Geo Images"));
22        Image = new ImageView(this);
23        setWidget(Image);
24        setObjectName("geoImageDock");
25
26        setContextMenuPolicy(Qt::ActionsContextMenu);
27
28        QAction *remImages = new QAction(tr("Remove Images"), this);
29        QAction *toClipboard = new QAction(tr("Copy filename to clipboard"), this);
30
31        addAction(remImages);
32        addAction(toClipboard);
33
34        connect(remImages, SIGNAL(triggered()), this, SLOT(removeImages()));
35        connect(toClipboard, SIGNAL(triggered()), this, SLOT(toClipboard()));
36}
37
38GeoImageDock::~GeoImageDock(void)
39{
40        delete widget();
41}
42
43void GeoImageDock::setImage(TrackPoint *Pt)
44{
45        if (!Pt) {
46                Image->setImage("");
47                curImage = -1;
48                return;
49        }
50
51        int ImageId;
52        QString id = Pt->id();
53        for (ImageId = 0; ImageId < usedTrackPoints.size(); ImageId++) // search for an entry in our list
54                if (usedTrackPoints.at(ImageId).first == id)
55                        break;
56
57        if (ImageId == curImage)
58                return;
59
60        if (ImageId == usedTrackPoints.size()) { // haven't found it
61                Image->setImage("");
62                curImage = -1;
63                return;
64        }
65
66        Image->setImage(usedTrackPoints.at(ImageId).second.first);
67        curImage = ImageId;
68}
69
70void GeoImageDock::removeImages(void)
71{
72        int i;
73
74        for (i = 0; i < usedTrackPoints.size(); i++) {
75                TrackPoint *Pt = dynamic_cast<TrackPoint*>(Main->document()->getFeature(usedTrackPoints.at(i).first));
76                if (!Pt) {
77                        qWarning("This should not happen. See %s::%d!", __FILE__, __LINE__);
78                        continue;
79                }
80                if (usedTrackPoints.at(i).second.second) {
81                        Pt->layer()->remove(Pt);
82                        delete Pt;
83                }
84                else
85                        Pt->clearTag("Picture");
86        }
87
88        usedTrackPoints.clear();
89        curImage = -1;
90        Image->setImage("");
91
92        Main->view()->invalidate(true, false);
93}
94       
95void GeoImageDock::toClipboard(void)
96{
97        if (curImage != -1) {
98                QClipboard *clipboard = QApplication::clipboard();
99
100                clipboard->setText(usedTrackPoints.at(curImage).second.first);
101        }
102}
103
104void GeoImageDock::loadImages(QStringList fileNames)
105{
106        QString file, latS, lonS;
107        QDateTime time;
108        int offset = -1, timeQuestion = 0, noMatchQuestion = 0;
109
110        MapDocument *theDocument = Main->document();
111        MapView *theView = Main->view();
112
113        Exiv2::Image::AutoPtr image;
114        Exiv2::ExifData exifData;
115
116        MapLayer *theLayer;
117        { // retrieve the target layer from the user
118                QStringList layers;
119                QList<int> layerId;
120                unsigned int i;
121                MapLayer *layer;
122                MapLayer *singleLayer = NULL;
123                MapLayer *singleTrackLayer = NULL;
124                int trackLayersCount = 0;
125                for (i=0;i<theDocument->layerSize();i++) {
126                        layer = theDocument->getLayer(i);
127                        if (layer->className() == "TrackMapLayer") {
128                                trackLayersCount++;
129                                if (!singleTrackLayer)
130                                        singleTrackLayer = layer;
131                        }
132                        if (layer->className() == "TrackMapLayer" || layer->className() == "DrawingMapLayer") {
133                                if (!singleLayer)
134                                        singleLayer = layer;
135                                layers.append(theDocument->getLayer(i)->name());
136                                layerId.append(i);
137                        }
138                }
139
140                if (layers.size() == 0) {
141                        QMessageBox::critical(this, tr("No layers"), tr("No suitable layer found. Please first download data from OSM server or open a track."));
142                        return;
143                }
144
145                // Select single layer if there is only one
146                if (layers.size() == 1)
147                {
148                        theLayer = singleLayer;
149                }
150                // Select single track layer if there is only one
151                else if (trackLayersCount == 1)
152                {
153                        theLayer = singleTrackLayer;
154                }
155                // Now ask the user what layer to add the photos to
156                else
157                {
158                        bool ok;
159                        QString name = QInputDialog::getItem(NULL, tr("Load geotagged Images"),
160                         tr("Select the layer to which the images belong:"), layers, 0, false, &ok);
161                        if (ok && !name.isEmpty())
162                                theLayer = theDocument->getLayer(layerId.at(layers.indexOf(name)));
163                        else
164                                return;
165                }
166        }
167
168        if (theLayer->isReadonly()) { // nodes from readonly layers can not be selected and therefore associated images can not be displayed
169                if (QMessageBox::question(this, tr("Layer is readonly"),
170                 tr("The used layer is not writeable. Should it be made writeable?\nIf not, you can't load images that belongs to it."),
171                 QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Yes) == QMessageBox::Yes)
172                        theLayer->getWidget()->setLayerReadonly(false); // this makes/updates both the widget and the layer with readonly = false
173                else
174                        return;
175        }
176
177        QProgressDialog progress(tr("Loading Images ..."), tr("Abort loading"), 0, fileNames.size());
178        progress.setWindowModality(Qt::WindowModal);
179        progress.show();
180
181        foreach(file, fileNames) {
182                progress.setValue(fileNames.indexOf(file));
183
184                if (!QFile::exists(file))
185                        WARNING(tr("No such file"), tr("Can't find image \"%1\"."));
186
187                try {
188                        image = Exiv2::ImageFactory::open(file.toStdString());
189                }
190                catch (Exiv2::Error error)
191                        WARNING(tr("Exiv2"), tr("Error while opening \"%2\":\n%1").arg(error.what()));
192                if (image.get() == 0)
193                        WARNING(tr("Exiv2"), tr("Error while loading EXIF-data from \"%1\"."));
194
195                image->readMetadata();
196
197                exifData = image->exifData();
198                if (!exifData.empty()) {
199                        latS = QString::fromStdString(exifData["Exif.GPSInfo.GPSLatitude"].toString());
200                        lonS = QString::fromStdString(exifData["Exif.GPSInfo.GPSLongitude"].toString());
201
202                        if (latS.isEmpty() || lonS.isEmpty()) {
203                                QString timeStamp = QString::fromStdString(exifData["Exif.Image.DateTime"].toString());
204                                if (timeStamp.isEmpty())
205                                        timeStamp = QString::fromStdString(exifData["Exif.Photo.DateTimeOriginal"].toString());
206
207                                if (!timeStamp.isEmpty())
208                                        time = QDateTime::fromString(timeStamp, "yyyy:MM:dd hh:mm:ss");
209                        }
210                }
211                if (exifData.empty() || ((latS.isEmpty() || lonS.isEmpty()) && time.isNull()) ) {
212                        QUESTION(tr("No EXIF"), tr("No EXIF header found in image \"%1\".\nDo you want to revert to improper file timestamp?").arg(file), timeQuestion);
213
214                        QFileInfo fileInfo(file);
215                        time = fileInfo.created();
216                }
217
218                if (!latS.isEmpty() && !lonS.isEmpty()) {
219                        double lat = 0.0, lon = 0.0, *cur;
220                        QString curS;
221                        int i;
222                        curS = latS;
223                        cur = &lat;
224                        for (i=0;i<=1;i++) { // parse latS and lonS. format: "h/d m/d s/d" (with d as divider)
225                                QList<int> p;
226                                p.append(curS.indexOf("/"));
227                                p.append(curS.indexOf(" ", p.last()));
228                                p.append(curS.indexOf("/", p.last()));
229                                p.append(curS.indexOf(" ", p.last()));
230                                p.append(curS.indexOf("/", p.last()));
231                                p.append(curS.indexOf(" ", p.last()));
232
233                                *cur = (double)curS.left(p.at(0)).toInt() / (double)curS.mid(p.at(0)+1, p.at(1)-p.at(0)-1).toInt() + // hours
234                                 (double)curS.mid(p.at(1)+1, p.at(2)-p.at(1)-1).toInt() / (double)curS.mid(p.at(2)+1, p.at(3)-p.at(2)-1).toInt() / 60.0 + // minutes
235                                 (double)curS.mid(p.at(3)+1, p.at(4)-p.at(3)-1).toInt() / (double)curS.mid(p.at(4)+1, p.at(5)-p.at(4)-1).toInt() / 60.0 / 60.0; // seconds
236                                       
237                                curS = lonS;
238                                cur = &lon;
239                        }
240
241                        latS.clear(); // clear these to be empty for the next image
242                        lonS.clear();
243
244                        Coord newPos(angToInt(lat), angToInt(lon));
245                        TrackPoint *Pt;
246                        VisibleFeatureIterator it(Main->document());
247                        for (; !it.isEnd(); ++it) // use existing TrackPoint if there is one in small distance
248                                if ((Pt = qobject_cast<TrackPoint*>(it.get())) &&
249                                 Pt->position().distanceFrom(newPos) <= .002)
250                                        break;
251                        if (it.isEnd())
252                                Pt = new TrackPoint(newPos);
253
254                        Pt->setTag("Picture", "GeoTagged");
255                        usedTrackPoints << qMakePair(Pt->id(), qMakePair(file, it.isEnd()));
256                        if (it.isEnd())
257                                theLayer->add(Pt);
258                                //new AddFeatureCommand(theLayer, Pt, false);
259                } else if (!time.isNull()) {
260       
261                        if (offset == -1) { // ask the user to specify an offset for the images
262                                QDialog dialog(this);
263                                dialog.setWindowTitle(tr("Specify offset"));
264
265                                QLabel position(tr("Position images more to the:"), &dialog);
266                                QRadioButton positive(tr("end of the track"), &dialog);
267                                QRadioButton negative(tr("beginning of the track"), &dialog);
268                                QTimeEdit timeEdit(&dialog);
269                                QDialogButtonBox buttons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &dialog);
270
271                                timeEdit.setDisplayFormat(tr("hh:mm:ss"));
272
273                                connect(&buttons, SIGNAL(accepted()), &dialog, SLOT(accept()));
274                                connect(&buttons, SIGNAL(rejected()), &dialog, SLOT(reject()));
275
276                                QVBoxLayout layout(&dialog); // very important to first declare the QVBoxLayout.
277                                QHBoxLayout radioLayout; // otherwise there would be a segmentation fault when return;
278                                QHBoxLayout timeLayout;
279
280                                radioLayout.addWidget(&positive);
281                                radioLayout.addWidget(&negative);
282                                timeLayout.addStretch();
283                                timeLayout.addWidget(&timeEdit); // center and make as small as possible
284                                timeLayout.addStretch();
285
286                                layout.addWidget(&position);
287                                layout.addLayout(&radioLayout);
288                                layout.addLayout(&timeLayout);
289                                layout.addWidget(&buttons);
290
291                                dialog.setLayout(&layout);
292
293                                if (dialog.exec()) { // we have to change the sign here because secsTo returns negative value
294                                        if (positive.isChecked())
295                                                offset = - timeEdit.time().secsTo(QTime(0, 0, 0));
296                                        else if (negative.isChecked())
297                                                offset = timeEdit.time().secsTo(QTime(0, 0, 0));
298                                        else
299                                                offset = 0;
300                                } else {
301                                        theView->invalidate(true, false);
302                                        return;
303                                }
304                        }
305
306                        time = time.addSecs(offset);
307
308                        MapFeature *feature = NULL;
309                        TrackPoint *Pt, *bestPt = NULL;
310                        int a, secondsTo = (unsigned int)-1 / 2;
311                        unsigned int u;
312
313                        for (u=0; u<theLayer->size(); u++) {
314                                feature = theLayer->get(u);
315                                if ((Pt = dynamic_cast<TrackPoint*>(feature))) {
316                                        a = time.secsTo(Pt->time().toLocalTime());
317                                        if (abs(a) < abs(secondsTo)) {
318                                                secondsTo = a;
319                                                bestPt = Pt;
320                                        }
321                                }
322                        }
323
324                        if (!bestPt)
325                                WARNING(tr("No TrackPoints"), tr("No TrackPoints found for image \"%1\""));
326
327                        if (abs(secondsTo) >= 15) {
328                                QTime difference = QTime().addSecs(abs(secondsTo));
329                                QString display;
330                                if (difference.hour() == 0)
331                                        if (difference.minute() == 0)
332                                                display = difference.toString(tr("ss 'seconds'"));
333                                        else
334                                                display = difference.toString(tr("mm 'minutes and' ss 'seconds'"));
335                                else
336                                        display = difference.toString(tr("hh 'hours,' mm 'minutes and' ss 'seconds'"));
337                                QUESTION(tr("Wrong image?"), secondsTo > 0 ?
338                                 tr("Image \"%1\" was taken %2 before the next trackpoint was recorded.\nDo you still want to use it?").arg(file).arg(display) :
339                                 tr("Image \"%1\" was taken %2 after the last trackpoint was recorded.\nDo you still want to use it?").arg(file).arg(display),
340                                 noMatchQuestion);
341                        }
342
343                        usedTrackPoints << qMakePair(bestPt->id(), qMakePair(file, false));
344                        bestPt->setTag("Picture", "GeoTagged");
345       
346                        time = QDateTime(); // empty time to be null for the next image
347                } else
348                        WARNING(tr("No geo informations"), tr("Image \"%1\" is not a geotagged image."));
349
350                if (progress.wasCanceled()) {
351                        theView->invalidate(true, false);
352                        return;
353                }
354                qApp->processEvents();
355        }
356
357        progress.setValue(fileNames.size());
358
359        theView->invalidate(true, false);
360
361}
362
363void GeoImageDock::addGeoDataToImage(Coord position, const QString & file)
364{
365        Exiv2::Image::AutoPtr image;
366
367        try {
368                image = Exiv2::ImageFactory::open(file.toStdString());
369        }
370        catch (Exiv2::Error error) {
371                QMessageBox::warning(this, tr("Exiv2"), tr("Error while opening \"%1\":\n%2").arg(file).arg(error.what()), QMessageBox::Ok);
372                return;
373        }
374        if (image.get() == 0) {
375                QMessageBox::warning(this, tr("Exiv2"), tr("Error while loading EXIF-data from \"%1\".").arg(file), QMessageBox::Ok);
376                return;
377        }
378
379        image->readMetadata();
380
381        double lat = intToAng(position.lat());
382        double lon = intToAng(position.lon());
383
384        QString hourFormat("%1/1 %2/1 %3/100");
385
386        int h = lat / 1; // translate angle to hours, minutes and seconds
387        int m = (lat - h) * 60 / 1;
388        int s = (lat - h - m/60.0) * 60 * 60 * 100 / 1; // multiply with 100 because of divider in hourFormat
389        Exiv2::ValueType<Exiv2::URational> vlat;
390        vlat.read(hourFormat.arg(h).arg(m).arg(s).toStdString()); // fill vlat with string
391        Exiv2::ExifKey klat("Exif.GPSInfo.GPSLatitude");
392        Exiv2::ExifData::iterator pos;
393        while ((pos = image->exifData().findKey(klat)) != image->exifData().end())
394                image->exifData().erase(pos);
395        image->exifData().add(klat, &vlat); // add key with value
396
397        h = lon / 1; // translate angle to hours, minutes and seconds
398        m = (lon - h) * 60 / 1;
399        s = (lon - h - m/60.0) * 60 * 60 * 100 / 1; // multiply with 100 because of divider in hourFormat
400        Exiv2::ValueType<Exiv2::URational> vlon;
401        vlon.read(hourFormat.arg(h).arg(m).arg(s).toStdString()); // fil vlon with string
402        Exiv2::ExifKey klon("Exif.GPSInfo.GPSLongitude");
403        while ((pos = image->exifData().findKey(klon)) != image->exifData().end())
404                image->exifData().erase(pos);
405        image->exifData().add(klon, &vlon); // add key with value
406
407        image->writeMetadata(); // store it
408
409        loadImages(QStringList(file)); // loadImages now can load the data and display the image
410
411        return;
412}
413
414
415// *** ImageView *** //
416
417ImageView::ImageView(QWidget *parent)
418        : QWidget(parent)
419{
420        zoomLevel = 1.0;
421}
422
423ImageView::~ImageView()
424{
425}
426
427void ImageView::setImage(QString filename)
428{
429        name = filename;
430        if (!name.isEmpty())
431                image.load(name);
432        else
433                image = QImage();
434        area = QRectF(QPoint(0, 0), image.size());
435        zoomLevel = 1.0;
436        resizeEvent(NULL);
437        update();
438}
439
440void ImageView::paintEvent(QPaintEvent * /* e */)
441{
442        QPainter P(this);
443
444        P.setRenderHint(QPainter::SmoothPixmapTransform);
445        P.drawImage(rect, image, area, Qt::OrderedDither); // draw the image
446
447        QRect text = QFontMetrics(P.font()).boundingRect(name); // calculate size of filename
448        text.translate(-text.topLeft()); // move topLeft to (0, 0)
449        if (text.width() > width())
450                text.setWidth(width()); // max size is width()
451
452        P.fillRect(text, QColor(255, 255, 255, 192)); // draw the text background
453
454        if (text.width() == width()) { // draw a cutting text ("...") in front of the cutted filename
455                QRect cutting = QFontMetrics(P.font()).boundingRect("...");
456                cutting.translate(-cutting.topLeft()); // move topLeft to (0, 0)
457                text.setWidth(width() - cutting.width());
458                text.translate(QPoint(cutting.width(), 0));
459                P.drawText(cutting, "...");
460        }
461
462        P.drawText(text, Qt::AlignRight, name);
463}
464
465void ImageView::resizeEvent(QResizeEvent * /* e */)
466{
467        if (image.height() == 0 || image.width() == 0) return;
468        rect = geometry();
469        rect.translate(-rect.topLeft());
470        double aspect = (double)image.height() / (double)image.width();
471
472        if (aspect * (double)rect.width() > rect.height()) rect.setWidth((int)((double)rect.height() / aspect));
473        else rect.setHeight((int)((double)rect.width() * aspect));
474}
475
476void ImageView::mouseDoubleClickEvent(QMouseEvent * /* e */)
477{
478        if (QApplication::keyboardModifiers() == Qt::ControlModifier)
479                zoom(-1);
480        else
481                zoom(1);
482}
483
484void ImageView::mousePressEvent(QMouseEvent * e)
485{
486        if (e->button() & Qt::RightButton)
487                QWidget::mousePressEvent(e);
488        else mousePos = e->pos();
489}
490       
491void ImageView::mouseMoveEvent(QMouseEvent * e)
492{
493        if (geometry().width() == 0 || geometry().height() == 0) return;
494        area.translate((double)(mousePos.x() - e->pos().x()) / (double)rect.width() * area.width(),
495                (double)(mousePos.y() - e->pos().y()) / (double)rect.height() * area.height());
496        mousePos = e->pos();
497        update();
498}
499
500void ImageView::wheelEvent(QWheelEvent *e)
501{
502        zoom(e->delta() / 8.0 / 360.0 * 10.0); // one wheel rotation is about 10 steps
503}
504
505void ImageView::zoom(double levelStep)
506{
507        if (name.isEmpty())
508                return;
509
510        // zoomValue (in percent) increases/decreases following this function: 100 * sqrt(2)^x
511        // round about it results in -> 100% 150% 200% 300% 400% 550% 800% (see zooming values e.g. in gimp)
512        double newZoom = zoomLevel * pow(sqrt(2), levelStep);
513        if (newZoom > 256 || newZoom < 0.8) // only zoom up to 25600 % or down to 80%
514                return;
515        QPoint zoomValue((1 / newZoom) * image.width(), (1 / newZoom) * image.height());
516
517        QPointF center = area.center();
518        area.setWidth(zoomValue.x());
519        area.setHeight(zoomValue.y());
520        area.moveCenter(center);
521
522        if (levelStep > 0 ) {
523                QPoint cursor = mapFromGlobal(QCursor::pos());
524                area.translate(((double)cursor.x() - (double)rect.width() / 2.0) / (double)rect.width() * (((1 / zoomLevel)-(1 / newZoom)) * (double)image.width()),
525                        ((double)cursor.y() - (double)rect.height() / 2.0) / (double)rect.height() * (((1 / zoomLevel)-(1/newZoom))*(double)image.height()) );
526        }
527        zoomLevel = newZoom;
528        update();
529}
530
531
Note: See TracBrowser for help on using the repository browser.