QGIS API Documentation  3.22.4-Białowieża (ce8e65e95e)
qgsalgorithmimportphotos.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgsalgorithmimportphotos.cpp
3  ------------------
4  begin : March 2018
5  copyright : (C) 2018 by Nyall Dawson
6  email : nyall dot dawson at gmail dot com
7  ***************************************************************************/
8 
9 /***************************************************************************
10  * *
11  * This program is free software; you can redistribute it and/or modify *
12  * it under the terms of the GNU General Public License as published by *
13  * the Free Software Foundation; either version 2 of the License, or *
14  * (at your option) any later version. *
15  * *
16  ***************************************************************************/
17 
19 #include "qgsogrutils.h"
20 #include "qgsvectorlayer.h"
21 #include <QDirIterator>
22 #include <QFileInfo>
23 #include <QRegularExpression>
24 
26 
27 QString QgsImportPhotosAlgorithm::name() const
28 {
29  return QStringLiteral( "importphotos" );
30 }
31 
32 QString QgsImportPhotosAlgorithm::displayName() const
33 {
34  return QObject::tr( "Import geotagged photos" );
35 }
36 
37 QStringList QgsImportPhotosAlgorithm::tags() const
38 {
39  return QObject::tr( "exif,metadata,gps,jpeg,jpg" ).split( ',' );
40 }
41 
42 QString QgsImportPhotosAlgorithm::group() const
43 {
44  return QObject::tr( "Vector creation" );
45 }
46 
47 QString QgsImportPhotosAlgorithm::groupId() const
48 {
49  return QStringLiteral( "vectorcreation" );
50 }
51 
52 void QgsImportPhotosAlgorithm::initAlgorithm( const QVariantMap & )
53 {
54  addParameter( new QgsProcessingParameterFile( QStringLiteral( "FOLDER" ), QObject::tr( "Input folder" ), QgsProcessingParameterFile::Folder ) );
55  addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "RECURSIVE" ), QObject::tr( "Scan recursively" ), false ) );
56 
57  std::unique_ptr< QgsProcessingParameterFeatureSink > output = std::make_unique< QgsProcessingParameterFeatureSink >( QStringLiteral( "OUTPUT" ), QObject::tr( "Photos" ), QgsProcessing::TypeVectorPoint, QVariant(), true );
58  output->setCreateByDefault( true );
59  addParameter( output.release() );
60 
61  std::unique_ptr< QgsProcessingParameterFeatureSink > invalid = std::make_unique< QgsProcessingParameterFeatureSink >( QStringLiteral( "INVALID" ), QObject::tr( "Invalid photos table" ), QgsProcessing::TypeVector, QVariant(), true );
62  invalid->setCreateByDefault( false );
63  addParameter( invalid.release() );
64 }
65 
66 QString QgsImportPhotosAlgorithm::shortHelpString() const
67 {
68  return QObject::tr( "Creates a point layer corresponding to the geotagged locations from JPEG images from a source folder. Optionally the folder can be recursively scanned.\n\n"
69  "The point layer will contain a single PointZ feature per input file from which the geotags could be read. Any altitude information from the geotags will be used "
70  "to set the point's Z value.\n\n"
71  "Optionally, a table of unreadable or non-geotagged photos can also be created." );
72 }
73 
74 QgsImportPhotosAlgorithm *QgsImportPhotosAlgorithm::createInstance() const
75 {
76  return new QgsImportPhotosAlgorithm();
77 }
78 
79 QVariant QgsImportPhotosAlgorithm::parseMetadataValue( const QString &value )
80 {
81  const QRegularExpression numRx( QStringLiteral( "^\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*$" ) );
82  const QRegularExpressionMatch numMatch = numRx.match( value );
83  if ( numMatch.hasMatch() )
84  {
85  return numMatch.captured( 1 ).toDouble();
86  }
87  return value;
88 }
89 
90 bool QgsImportPhotosAlgorithm::extractGeoTagFromMetadata( const QVariantMap &metadata, QgsPointXY &tag )
91 {
92  double x = 0.0;
93  if ( metadata.contains( QStringLiteral( "EXIF_GPSLongitude" ) ) )
94  {
95  bool ok = false;
96  x = metadata.value( QStringLiteral( "EXIF_GPSLongitude" ) ).toDouble( &ok );
97  if ( !ok )
98  return false;
99 
100 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
101  if ( metadata.value( QStringLiteral( "EXIF_GPSLongitudeRef" ) ).toString().rightRef( 1 ).compare( QLatin1String( "W" ), Qt::CaseInsensitive ) == 0
102  || metadata.value( QStringLiteral( "EXIF_GPSLongitudeRef" ) ).toDouble() < 0 )
103 #else
104  if ( QStringView { metadata.value( QStringLiteral( "EXIF_GPSLongitudeRef" ) ).toString() }.right( 1 ).compare( QLatin1String( "W" ), Qt::CaseInsensitive ) == 0
105  || metadata.value( QStringLiteral( "EXIF_GPSLongitudeRef" ) ).toDouble() < 0 )
106 #endif
107  {
108  x = -x;
109  }
110  }
111  else
112  {
113  return false;
114  }
115 
116  double y = 0.0;
117  if ( metadata.contains( QStringLiteral( "EXIF_GPSLatitude" ) ) )
118  {
119  bool ok = false;
120  y = metadata.value( QStringLiteral( "EXIF_GPSLatitude" ) ).toDouble( &ok );
121  if ( !ok )
122  return false;
123 
124 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
125  if ( metadata.value( QStringLiteral( "EXIF_GPSLatitudeRef" ) ).toString().rightRef( 1 ).compare( QLatin1String( "S" ), Qt::CaseInsensitive ) == 0
126  || metadata.value( QStringLiteral( "EXIF_GPSLatitudeRef" ) ).toDouble() < 0 )
127 #else
128  if ( QStringView { metadata.value( QStringLiteral( "EXIF_GPSLatitudeRef" ) ).toString() }.right( 1 ).compare( QLatin1String( "S" ), Qt::CaseInsensitive ) == 0
129  || metadata.value( QStringLiteral( "EXIF_GPSLatitudeRef" ) ).toDouble() < 0 )
130 #endif
131  {
132  y = -y;
133  }
134  }
135  else
136  {
137  return false;
138  }
139 
140  tag = QgsPointXY( x, y );
141  return true;
142 }
143 
144 QVariant QgsImportPhotosAlgorithm::extractAltitudeFromMetadata( const QVariantMap &metadata )
145 {
146  QVariant altitude;
147  if ( metadata.contains( QStringLiteral( "EXIF_GPSAltitude" ) ) )
148  {
149  double alt = metadata.value( QStringLiteral( "EXIF_GPSAltitude" ) ).toDouble();
150  if ( metadata.contains( QStringLiteral( "EXIF_GPSAltitudeRef" ) ) &&
151  ( ( metadata.value( QStringLiteral( "EXIF_GPSAltitudeRef" ) ).type() == QVariant::String && metadata.value( QStringLiteral( "EXIF_GPSAltitudeRef" ) ).toString().right( 1 ) == QLatin1String( "1" ) )
152  || metadata.value( QStringLiteral( "EXIF_GPSAltitudeRef" ) ).toDouble() < 0 ) )
153  alt = -alt;
154  altitude = alt;
155  }
156  return altitude;
157 }
158 
159 QVariant QgsImportPhotosAlgorithm::extractDirectionFromMetadata( const QVariantMap &metadata )
160 {
161  QVariant direction;
162  if ( metadata.contains( QStringLiteral( "EXIF_GPSImgDirection" ) ) )
163  {
164  direction = metadata.value( QStringLiteral( "EXIF_GPSImgDirection" ) ).toDouble();
165  }
166  return direction;
167 }
168 
169 QVariant QgsImportPhotosAlgorithm::extractOrientationFromMetadata( const QVariantMap &metadata )
170 {
171  QVariant orientation;
172  if ( metadata.contains( QStringLiteral( "EXIF_Orientation" ) ) )
173  {
174  switch ( metadata.value( QStringLiteral( "EXIF_Orientation" ) ).toInt() )
175  {
176  case 1:
177  orientation = 0;
178  break;
179  case 2:
180  orientation = 0;
181  break;
182  case 3:
183  orientation = 180;
184  break;
185  case 4:
186  orientation = 180;
187  break;
188  case 5:
189  orientation = 90;
190  break;
191  case 6:
192  orientation = 90;
193  break;
194  case 7:
195  orientation = 270;
196  break;
197  case 8:
198  orientation = 270;
199  break;
200  }
201  }
202  return orientation;
203 }
204 
205 QVariant QgsImportPhotosAlgorithm::extractTimestampFromMetadata( const QVariantMap &metadata )
206 {
207  QVariant ts;
208  if ( metadata.contains( QStringLiteral( "EXIF_DateTimeOriginal" ) ) )
209  {
210  ts = metadata.value( QStringLiteral( "EXIF_DateTimeOriginal" ) );
211  }
212  else if ( metadata.contains( QStringLiteral( "EXIF_DateTimeDigitized" ) ) )
213  {
214  ts = metadata.value( QStringLiteral( "EXIF_DateTimeDigitized" ) );
215  }
216  else if ( metadata.contains( QStringLiteral( "EXIF_DateTime" ) ) )
217  {
218  ts = metadata.value( QStringLiteral( "EXIF_DateTime" ) );
219  }
220 
221  if ( !ts.isValid() )
222  return ts;
223 
224  const QRegularExpression dsRegEx( QStringLiteral( "(\\d+):(\\d+):(\\d+)\\s+(\\d+):(\\d+):(\\d+)" ) );
225  const QRegularExpressionMatch dsMatch = dsRegEx.match( ts.toString() );
226  if ( dsMatch.hasMatch() )
227  {
228  const int year = dsMatch.captured( 1 ).toInt();
229  const int month = dsMatch.captured( 2 ).toInt();
230  const int day = dsMatch.captured( 3 ).toInt();
231  const int hour = dsMatch.captured( 4 ).toInt();
232  const int min = dsMatch.captured( 5 ).toInt();
233  const int sec = dsMatch.captured( 6 ).toInt();
234  return QDateTime( QDate( year, month, day ), QTime( hour, min, sec ) );
235  }
236  else
237  {
238  return QVariant();
239  }
240 }
241 
242 QVariant QgsImportPhotosAlgorithm::parseCoord( const QString &string )
243 {
244  const QRegularExpression coordRx( QStringLiteral( "^\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*$" ) );
245  const QRegularExpressionMatch coordMatch = coordRx.match( string );
246  if ( coordMatch.hasMatch() )
247  {
248  const double hours = coordMatch.captured( 1 ).toDouble();
249  const double minutes = coordMatch.captured( 2 ).toDouble();
250  const double seconds = coordMatch.captured( 3 ).toDouble();
251  return hours + minutes / 60.0 + seconds / 3600.0;
252  }
253  else
254  {
255  return QVariant();
256  }
257 }
258 
259 QVariantMap QgsImportPhotosAlgorithm::parseMetadataList( const QStringList &input )
260 {
261  QVariantMap results;
262  const QRegularExpression splitRx( QStringLiteral( "(.*?)=(.*)" ) );
263  for ( const QString &item : input )
264  {
265  const QRegularExpressionMatch match = splitRx.match( item );
266  if ( !match.hasMatch() )
267  continue;
268 
269  const QString tag = match.captured( 1 );
270  QVariant value = parseMetadataValue( match.captured( 2 ) );
271 
272  if ( tag == QLatin1String( "EXIF_GPSLatitude" ) || tag == QLatin1String( "EXIF_GPSLongitude" ) )
273  value = parseCoord( value.toString() );
274  results.insert( tag, value );
275  }
276  return results;
277 }
278 
279 
280 class SetEditorWidgetForPhotoAttributePostProcessor : public QgsProcessingLayerPostProcessorInterface
281 {
282  public:
283 
285  {
286  if ( QgsVectorLayer *vl = qobject_cast< QgsVectorLayer * >( layer ) )
287  {
288  QVariantMap config;
289  // photo field shows picture viewer
290  config.insert( QStringLiteral( "DocumentViewer" ), 1 );
291  config.insert( QStringLiteral( "FileWidget" ), true );
292  config.insert( QStringLiteral( "UseLink" ), true );
293  config.insert( QStringLiteral( "FullUrl" ), true );
294  vl->setEditorWidgetSetup( vl->fields().lookupField( QStringLiteral( "photo" ) ), QgsEditorWidgetSetup( QStringLiteral( "ExternalResource" ), config ) );
295 
296  config.clear();
297  // path field is a directory link
298  config.insert( QStringLiteral( "FileWidgetButton" ), true );
299  config.insert( QStringLiteral( "StorageMode" ), 1 );
300  config.insert( QStringLiteral( "UseLink" ), true );
301  config.insert( QStringLiteral( "FullUrl" ), true );
302  vl->setEditorWidgetSetup( vl->fields().lookupField( QStringLiteral( "directory" ) ), QgsEditorWidgetSetup( QStringLiteral( "ExternalResource" ), config ) );
303  }
304  }
305 };
306 
307 QVariantMap QgsImportPhotosAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
308 {
309  const QString folder = parameterAsFile( parameters, QStringLiteral( "FOLDER" ), context );
310 
311  const QDir importDir( folder );
312  if ( !importDir.exists() )
313  {
314  throw QgsProcessingException( QObject::tr( "Directory %1 does not exist!" ).arg( folder ) );
315  }
316 
317  const bool recurse = parameterAsBoolean( parameters, QStringLiteral( "RECURSIVE" ), context );
318 
319  QgsFields outFields;
320  outFields.append( QgsField( QStringLiteral( "photo" ), QVariant::String ) );
321  outFields.append( QgsField( QStringLiteral( "filename" ), QVariant::String ) );
322  outFields.append( QgsField( QStringLiteral( "directory" ), QVariant::String ) );
323  outFields.append( QgsField( QStringLiteral( "altitude" ), QVariant::Double ) );
324  outFields.append( QgsField( QStringLiteral( "direction" ), QVariant::Double ) );
325  outFields.append( QgsField( QStringLiteral( "rotation" ), QVariant::Int ) );
326  outFields.append( QgsField( QStringLiteral( "longitude" ), QVariant::String ) );
327  outFields.append( QgsField( QStringLiteral( "latitude" ), QVariant::String ) );
328  outFields.append( QgsField( QStringLiteral( "timestamp" ), QVariant::DateTime ) );
329  QString outputDest;
330  std::unique_ptr< QgsFeatureSink > outputSink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, outputDest, outFields,
331  QgsWkbTypes::PointZ, QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) ) );
332 
333  QgsFields invalidFields;
334  invalidFields.append( QgsField( QStringLiteral( "photo" ), QVariant::String ) );
335  invalidFields.append( QgsField( QStringLiteral( "filename" ), QVariant::String ) );
336  invalidFields.append( QgsField( QStringLiteral( "directory" ), QVariant::String ) );
337  invalidFields.append( QgsField( QStringLiteral( "readable" ), QVariant::Bool ) );
338  QString invalidDest;
339  std::unique_ptr< QgsFeatureSink > invalidSink( parameterAsSink( parameters, QStringLiteral( "INVALID" ), context, invalidDest, invalidFields ) );
340 
341  const QStringList nameFilters { "*.jpeg", "*.jpg" };
342  QStringList files;
343 
344  if ( !recurse )
345  {
346  const QFileInfoList fileInfoList = importDir.entryInfoList( nameFilters, QDir::NoDotAndDotDot | QDir::Files );
347  for ( auto infoIt = fileInfoList.constBegin(); infoIt != fileInfoList.constEnd(); ++infoIt )
348  {
349  files.append( infoIt->absoluteFilePath() );
350  }
351  }
352  else
353  {
354  QDirIterator it( folder, nameFilters, QDir::NoDotAndDotDot | QDir::Files, QDirIterator::Subdirectories );
355  while ( it.hasNext() )
356  {
357  it.next();
358  files.append( it.filePath() );
359  }
360  }
361 
362  auto saveInvalidFile = [&invalidSink, &parameters]( QgsAttributes & attributes, bool readable )
363  {
364  if ( invalidSink )
365  {
366  QgsFeature f;
367  attributes.append( readable );
368  f.setAttributes( attributes );
369  if ( !invalidSink->addFeature( f, QgsFeatureSink::FastInsert ) )
370  throw QgsProcessingException( writeFeatureError( invalidSink.get(), parameters, QStringLiteral( "INVALID" ) ) );
371  }
372  };
373 
374  const double step = files.count() > 0 ? 100.0 / files.count() : 1;
375  int i = 0;
376  for ( const QString &file : files )
377  {
378  i++;
379  if ( feedback->isCanceled() )
380  {
381  break;
382  }
383 
384  feedback->setProgress( i * step );
385 
386  const QFileInfo fi( file );
387  QgsAttributes attributes;
388  attributes << QDir::toNativeSeparators( file )
389  << fi.completeBaseName()
390  << QDir::toNativeSeparators( fi.absolutePath() );
391 
392  const gdal::dataset_unique_ptr hDS( GDALOpen( file.toUtf8().constData(), GA_ReadOnly ) );
393  if ( !hDS )
394  {
395  feedback->reportError( QObject::tr( "Could not open %1" ).arg( QDir::toNativeSeparators( file ) ) );
396  saveInvalidFile( attributes, false );
397  continue;
398  }
399 
400  if ( char **GDALmetadata = GDALGetMetadata( hDS.get(), nullptr ) )
401  {
402  if ( !outputSink )
403  continue;
404 
405  QgsFeature f;
406  const QVariantMap metadata = parseMetadataList( QgsOgrUtils::cStringListToQStringList( GDALmetadata ) );
407 
408  QgsPointXY tag;
409  if ( !extractGeoTagFromMetadata( metadata, tag ) )
410  {
411  // no geotag
412  feedback->reportError( QObject::tr( "Could not retrieve geotag for %1" ).arg( QDir::toNativeSeparators( file ) ) );
413  saveInvalidFile( attributes, true );
414  continue;
415  }
416 
417  const QVariant altitude = extractAltitudeFromMetadata( metadata );
418  const QgsGeometry p = QgsGeometry( new QgsPoint( tag.x(), tag.y(), altitude.toDouble(), 0, QgsWkbTypes::PointZ ) );
419  f.setGeometry( p );
420 
421  attributes
422  << altitude
423  << extractDirectionFromMetadata( metadata )
424  << extractOrientationFromMetadata( metadata )
425  << tag.x()
426  << tag.y()
427  << extractTimestampFromMetadata( metadata );
428  f.setAttributes( attributes );
429  if ( !outputSink->addFeature( f, QgsFeatureSink::FastInsert ) )
430  throw QgsProcessingException( writeFeatureError( outputSink.get(), parameters, QStringLiteral( "OUTPUT" ) ) );
431  }
432  else
433  {
434  feedback->reportError( QObject::tr( "No metadata found in %1" ).arg( QDir::toNativeSeparators( file ) ) );
435  saveInvalidFile( attributes, true );
436  }
437  }
438 
439  QVariantMap outputs;
440  if ( outputSink )
441  {
442  outputs.insert( QStringLiteral( "OUTPUT" ), outputDest );
443 
444  if ( context.willLoadLayerOnCompletion( outputDest ) )
445  {
446  context.layerToLoadOnCompletionDetails( outputDest ).setPostProcessor( new SetEditorWidgetForPhotoAttributePostProcessor() );
447  }
448  }
449 
450  if ( invalidSink )
451  outputs.insert( QStringLiteral( "INVALID" ), invalidDest );
452  return outputs;
453 }
454 
A vector of attributes.
Definition: qgsattributes.h:58
This class represents a coordinate reference system (CRS).
Holder for the widget type and its configuration for a field.
@ FastInsert
Use faster inserts, at the cost of updating the passed features to reflect changes made at the provid...
The feature class encapsulates a single feature including its unique ID, geometry and a list of field...
Definition: qgsfeature.h:56
void setAttributes(const QgsAttributes &attrs)
Sets the feature's attributes.
Definition: qgsfeature.cpp:153
void setGeometry(const QgsGeometry &geometry)
Set the feature's geometry.
Definition: qgsfeature.cpp:163
bool isCanceled() const SIP_HOLDGIL
Tells whether the operation has been canceled already.
Definition: qgsfeedback.h:54
void setProgress(double progress)
Sets the current progress for the feedback object.
Definition: qgsfeedback.h:63
Encapsulate a field in an attribute table or data source.
Definition: qgsfield.h:51
Container of fields for a vector layer.
Definition: qgsfields.h:45
bool append(const QgsField &field, FieldOrigin origin=OriginProvider, int originIndex=-1)
Appends a field. The field must have unique name, otherwise it is rejected (returns false)
Definition: qgsfields.cpp:59
A geometry is the spatial representation of a feature.
Definition: qgsgeometry.h:125
Base class for all map layer types.
Definition: qgsmaplayer.h:73
static QStringList cStringListToQStringList(char **stringList)
Converts a c string list to a QStringList.
A class to represent a 2D point.
Definition: qgspointxy.h:59
double y
Definition: qgspointxy.h:63
Q_GADGET double x
Definition: qgspointxy.h:62
Point geometry type, with support for z-dimension and m-values.
Definition: qgspoint.h:49
void setPostProcessor(QgsProcessingLayerPostProcessorInterface *processor)
Sets the layer post-processor.
Contains information about the context in which a processing algorithm is executed.
bool willLoadLayerOnCompletion(const QString &layer) const
Returns true if the given layer (by ID or datasource) will be loaded into the current project upon co...
QgsProcessingContext::LayerDetails & layerToLoadOnCompletionDetails(const QString &layer)
Returns a reference to the details for a given layer which is loaded on completion of the algorithm o...
Custom exception class for processing related exceptions.
Definition: qgsexception.h:83
Base class for providing feedback from a processing algorithm.
virtual void reportError(const QString &error, bool fatalError=false)
Reports that the algorithm encountered an error while executing.
An interface for layer post-processing handlers for execution following a processing algorithm operat...
virtual void postProcessLayer(QgsMapLayer *layer, QgsProcessingContext &context, QgsProcessingFeedback *feedback)=0
Post-processes the specified layer, following successful execution of a processing algorithm.
A boolean parameter for processing algorithms.
An input file or folder parameter for processing algorithms.
@ TypeVector
Tables (i.e. vector layers with or without geometry). When used for a sink this indicates the sink ha...
Definition: qgsprocessing.h:54
@ TypeVectorPoint
Vector point layers.
Definition: qgsprocessing.h:49
Represents a vector layer which manages a vector based data sets.
std::unique_ptr< std::remove_pointer< GDALDatasetH >::type, GDALDatasetCloser > dataset_unique_ptr
Scoped GDAL dataset.
Definition: qgsogrutils.h:138