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