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