QGIS API Documentation  3.16.0-Hannover (43b64b13f3)
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
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  config.insert( QStringLiteral( "UseLink" ), true );
240  config.insert( QStringLiteral( "FullUrl" ), true );
241  vl->setEditorWidgetSetup( vl->fields().lookupField( QStringLiteral( "photo" ) ), QgsEditorWidgetSetup( QStringLiteral( "ExternalResource" ), config ) );
242 
243  config.clear();
244  // path field is a directory link
245  config.insert( QStringLiteral( "FileWidgetButton" ), true );
246  config.insert( QStringLiteral( "StorageMode" ), 1 );
247  config.insert( QStringLiteral( "UseLink" ), true );
248  config.insert( QStringLiteral( "FullUrl" ), true );
249  vl->setEditorWidgetSetup( vl->fields().lookupField( QStringLiteral( "directory" ) ), QgsEditorWidgetSetup( QStringLiteral( "ExternalResource" ), config ) );
250  }
251  }
252 };
253 
254 QVariantMap QgsImportPhotosAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
255 {
256  QString folder = parameterAsFile( parameters, QStringLiteral( "FOLDER" ), context );
257 
258  QDir importDir( folder );
259  if ( !importDir.exists() )
260  {
261  throw QgsProcessingException( QObject::tr( "Directory %1 does not exist!" ).arg( folder ) );
262  }
263 
264  bool recurse = parameterAsBoolean( parameters, QStringLiteral( "RECURSIVE" ), context );
265 
266  QgsFields outFields;
267  outFields.append( QgsField( QStringLiteral( "photo" ), QVariant::String ) );
268  outFields.append( QgsField( QStringLiteral( "filename" ), QVariant::String ) );
269  outFields.append( QgsField( QStringLiteral( "directory" ), QVariant::String ) );
270  outFields.append( QgsField( QStringLiteral( "altitude" ), QVariant::Double ) );
271  outFields.append( QgsField( QStringLiteral( "direction" ), QVariant::Double ) );
272  outFields.append( QgsField( QStringLiteral( "longitude" ), QVariant::String ) );
273  outFields.append( QgsField( QStringLiteral( "latitude" ), QVariant::String ) );
274  outFields.append( QgsField( QStringLiteral( "timestamp" ), QVariant::DateTime ) );
275  QString outputDest;
276  std::unique_ptr< QgsFeatureSink > outputSink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, outputDest, outFields,
277  QgsWkbTypes::PointZ, QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) ) );
278 
279  QgsFields invalidFields;
280  invalidFields.append( QgsField( QStringLiteral( "photo" ), QVariant::String ) );
281  invalidFields.append( QgsField( QStringLiteral( "filename" ), QVariant::String ) );
282  invalidFields.append( QgsField( QStringLiteral( "directory" ), QVariant::String ) );
283  invalidFields.append( QgsField( QStringLiteral( "readable" ), QVariant::Bool ) );
284  QString invalidDest;
285  std::unique_ptr< QgsFeatureSink > invalidSink( parameterAsSink( parameters, QStringLiteral( "INVALID" ), context, invalidDest, invalidFields ) );
286 
287  QStringList nameFilters { "*.jpeg", "*.jpg" };
288  QStringList files;
289 
290  if ( !recurse )
291  {
292  QFileInfoList fileInfoList = importDir.entryInfoList( nameFilters, QDir::NoDotAndDotDot | QDir::Files );
293  for ( auto infoIt = fileInfoList.constBegin(); infoIt != fileInfoList.constEnd(); ++infoIt )
294  {
295  files.append( infoIt->absoluteFilePath() );
296  }
297  }
298  else
299  {
300  QDirIterator it( folder, nameFilters, QDir::NoDotAndDotDot | QDir::Files, QDirIterator::Subdirectories );
301  while ( it.hasNext() )
302  {
303  it.next();
304  files.append( it.filePath() );
305  }
306  }
307 
308  auto saveInvalidFile = [&invalidSink]( QgsAttributes & attributes, bool readable )
309  {
310  if ( invalidSink )
311  {
312  QgsFeature f;
313  attributes.append( readable );
314  f.setAttributes( attributes );
315  invalidSink->addFeature( f, QgsFeatureSink::FastInsert );
316  }
317  };
318 
319  double step = files.count() > 0 ? 100.0 / files.count() : 1;
320  int i = 0;
321  for ( const QString &file : files )
322  {
323  i++;
324  if ( feedback->isCanceled() )
325  {
326  break;
327  }
328 
329  feedback->setProgress( i * step );
330 
331  QFileInfo fi( file );
332  QgsAttributes attributes;
333  attributes << QDir::toNativeSeparators( file )
334  << fi.completeBaseName()
335  << QDir::toNativeSeparators( fi.absolutePath() );
336 
337  gdal::dataset_unique_ptr hDS( GDALOpen( file.toUtf8().constData(), GA_ReadOnly ) );
338  if ( !hDS )
339  {
340  feedback->reportError( QObject::tr( "Could not open %1" ).arg( QDir::toNativeSeparators( file ) ) );
341  saveInvalidFile( attributes, false );
342  continue;
343  }
344 
345  if ( char **GDALmetadata = GDALGetMetadata( hDS.get(), nullptr ) )
346  {
347  if ( !outputSink )
348  continue;
349 
350  QgsFeature f;
351  QVariantMap metadata = parseMetadataList( QgsOgrUtils::cStringListToQStringList( GDALmetadata ) );
352 
353  QgsPointXY tag;
354  if ( !extractGeoTagFromMetadata( metadata, tag ) )
355  {
356  // no geotag
357  feedback->reportError( QObject::tr( "Could not retrieve geotag for %1" ).arg( QDir::toNativeSeparators( file ) ) );
358  saveInvalidFile( attributes, true );
359  continue;
360  }
361 
362  QVariant altitude = extractAltitudeFromMetadata( metadata );
363  QgsGeometry p = QgsGeometry( new QgsPoint( tag.x(), tag.y(), altitude.toDouble(), 0, QgsWkbTypes::PointZ ) );
364  f.setGeometry( p );
365 
366  attributes
367  << altitude
368  << extractDirectionFromMetadata( metadata )
369  << tag.x()
370  << tag.y()
371  << extractTimestampFromMetadata( metadata );
372  f.setAttributes( attributes );
373  outputSink->addFeature( f, QgsFeatureSink::FastInsert );
374  }
375  else
376  {
377  feedback->reportError( QObject::tr( "No metadata found in %1" ).arg( QDir::toNativeSeparators( file ) ) );
378  saveInvalidFile( attributes, true );
379  }
380  }
381 
382  QVariantMap outputs;
383  if ( outputSink )
384  {
385  outputs.insert( QStringLiteral( "OUTPUT" ), outputDest );
386 
387  if ( context.willLoadLayerOnCompletion( outputDest ) )
388  {
389  context.layerToLoadOnCompletionDetails( outputDest ).setPostProcessor( new SetEditorWidgetForPhotoAttributePostProcessor() );
390  }
391  }
392 
393  if ( invalidSink )
394  outputs.insert( QStringLiteral( "INVALID" ), invalidDest );
395  return outputs;
396 }
397 
QgsFeedback::setProgress
void setProgress(double progress)
Sets the current progress for the feedback object.
Definition: qgsfeedback.h:62
QgsPointXY::y
double y
Definition: qgspointxy.h:48
QgsProcessingContext::LayerDetails::setPostProcessor
void setPostProcessor(QgsProcessingLayerPostProcessorInterface *processor)
Sets the layer post-processor.
Definition: qgsprocessingcontext.cpp:166
QgsProcessingContext::willLoadLayerOnCompletion
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...
Definition: qgsprocessingcontext.h:329
QgsEditorWidgetSetup
Holder for the widget type and its configuration for a field.
Definition: qgseditorwidgetsetup.h:29
QgsPoint
Point geometry type, with support for z-dimension and m-values.
Definition: qgspoint.h:38
QgsProcessingFeedback
Base class for providing feedback from a processing algorithm.
Definition: qgsprocessingfeedback.h:38
QgsPointXY::x
Q_GADGET double x
Definition: qgspointxy.h:47
QgsProcessingFeedback::reportError
virtual void reportError(const QString &error, bool fatalError=false)
Reports that the algorithm encountered an error while executing.
Definition: qgsprocessingfeedback.cpp:39
QgsProcessingParameterFile::Folder
@ Folder
Parameter is a folder.
Definition: qgsprocessingparameters.h:1696
QgsFields
Container of fields for a vector layer.
Definition: qgsfields.h:45
qgsogrutils.h
QgsFields::append
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
QgsProcessing::TypeVectorPoint
@ TypeVectorPoint
Vector point layers.
Definition: qgsprocessing.h:48
QgsProcessing::TypeVector
@ TypeVector
Tables (i.e. vector layers with or without geometry). When used for a sink this indicates the sink ha...
Definition: qgsprocessing.h:53
QgsFeature::setGeometry
void setGeometry(const QgsGeometry &geometry)
Set the feature's geometry.
Definition: qgsfeature.cpp:139
gdal::dataset_unique_ptr
std::unique_ptr< std::remove_pointer< GDALDatasetH >::type, GDALDatasetCloser > dataset_unique_ptr
Scoped GDAL dataset.
Definition: qgsogrutils.h:134
QgsProcessingContext
Contains information about the context in which a processing algorithm is executed.
Definition: qgsprocessingcontext.h:44
qgsalgorithmimportphotos.h
QgsWkbTypes::PointZ
@ PointZ
Definition: qgswkbtypes.h:86
QgsProcessingLayerPostProcessorInterface
An interface for layer post-processing handlers for execution following a processing algorithm operat...
Definition: qgsprocessingcontext.h:660
QgsProcessingParameterFile
An input file or folder parameter for processing algorithms.
Definition: qgsprocessingparameters.h:1689
QgsCoordinateReferenceSystem
This class represents a coordinate reference system (CRS).
Definition: qgscoordinatereferencesystem.h:206
qgsvectorlayer.h
QgsPointXY
A class to represent a 2D point.
Definition: qgspointxy.h:44
QgsFeedback::isCanceled
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition: qgsfeedback.h:53
QgsProcessingParameterBoolean
A boolean parameter for processing algorithms.
Definition: qgsprocessingparameters.h:1507
QgsGeometry
A geometry is the spatial representation of a feature.
Definition: qgsgeometry.h:124
QgsVectorLayer
Represents a vector layer which manages a vector based data sets.
Definition: qgsvectorlayer.h:387
QgsMapLayer
Base class for all map layer types.
Definition: qgsmaplayer.h:83
QgsProcessingContext::layerToLoadOnCompletionDetails
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...
Definition: qgsprocessingcontext.h:366
QgsProcessingLayerPostProcessorInterface::postProcessLayer
virtual void postProcessLayer(QgsMapLayer *layer, QgsProcessingContext &context, QgsProcessingFeedback *feedback)=0
Post-processes the specified layer, following successful execution of a processing algorithm.
QgsAttributes
A vector of attributes.
Definition: qgsattributes.h:58
QgsFeature
The feature class encapsulates a single feature including its id, geometry and a list of field/values...
Definition: qgsfeature.h:56
QgsFeature::setAttributes
void setAttributes(const QgsAttributes &attrs)
Sets the feature's attributes.
Definition: qgsfeature.cpp:129
QgsProcessingException
Custom exception class for processing related exceptions.
Definition: qgsexception.h:83
QgsOgrUtils::cStringListToQStringList
static QStringList cStringListToQStringList(char **stringList)
Converts a c string list to a QStringList.
Definition: qgsogrutils.cpp:711
QgsFeatureSink::FastInsert
@ FastInsert
Use faster inserts, at the cost of updating the passed features to reflect changes made at the provid...
Definition: qgsfeaturesink.h:70
QgsField
Encapsulate a field in an attribute table or data source.
Definition: qgsfield.h:50