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