QGIS API Documentation 3.41.0-Master (af5edcb665c)
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#include "qgsogrutils.h"
20#include "qgsvectorlayer.h"
21#include <QDirIterator>
22#include <QFileInfo>
23#include <QRegularExpression>
24
26
27QString QgsImportPhotosAlgorithm::name() const
28{
29 return QStringLiteral( "importphotos" );
30}
31
32QString QgsImportPhotosAlgorithm::displayName() const
33{
34 return QObject::tr( "Import geotagged photos" );
35}
36
37QStringList QgsImportPhotosAlgorithm::tags() const
38{
39 return QObject::tr( "exif,metadata,gps,jpeg,jpg" ).split( ',' );
40}
41
42QString QgsImportPhotosAlgorithm::group() const
43{
44 return QObject::tr( "Vector creation" );
45}
46
47QString QgsImportPhotosAlgorithm::groupId() const
48{
49 return QStringLiteral( "vectorcreation" );
50}
51
52void QgsImportPhotosAlgorithm::initAlgorithm( const QVariantMap & )
53{
54 addParameter( new QgsProcessingParameterFile( QStringLiteral( "FOLDER" ), QObject::tr( "Input folder" ), Qgis::ProcessingFileParameterBehavior::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" ), Qgis::ProcessingSourceType::VectorPoint, 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" ), Qgis::ProcessingSourceType::Vector, QVariant(), true );
62 invalid->setCreateByDefault( false );
63 addParameter( invalid.release() );
64}
65
66QString QgsImportPhotosAlgorithm::shortHelpString() const
67{
68 return QObject::tr( "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"
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
74QgsImportPhotosAlgorithm *QgsImportPhotosAlgorithm::createInstance() const
75{
76 return new QgsImportPhotosAlgorithm();
77}
78
79QVariant QgsImportPhotosAlgorithm::parseMetadataValue( const QString &value )
80{
81 const thread_local 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
90bool 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
144QVariant 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" ) ) && ( ( 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 ) )
151 alt = -alt;
152 altitude = alt;
153 }
154 return altitude;
155}
156
157QVariant QgsImportPhotosAlgorithm::extractDirectionFromMetadata( const QVariantMap &metadata )
158{
159 QVariant direction;
160 if ( metadata.contains( QStringLiteral( "EXIF_GPSImgDirection" ) ) )
161 {
162 direction = metadata.value( QStringLiteral( "EXIF_GPSImgDirection" ) ).toDouble();
163 }
164 return direction;
165}
166
167QVariant QgsImportPhotosAlgorithm::extractOrientationFromMetadata( const QVariantMap &metadata )
168{
169 QVariant orientation;
170 if ( metadata.contains( QStringLiteral( "EXIF_Orientation" ) ) )
171 {
172 switch ( metadata.value( QStringLiteral( "EXIF_Orientation" ) ).toInt() )
173 {
174 case 1:
175 orientation = 0;
176 break;
177 case 2:
178 orientation = 0;
179 break;
180 case 3:
181 orientation = 180;
182 break;
183 case 4:
184 orientation = 180;
185 break;
186 case 5:
187 orientation = 90;
188 break;
189 case 6:
190 orientation = 90;
191 break;
192 case 7:
193 orientation = 270;
194 break;
195 case 8:
196 orientation = 270;
197 break;
198 }
199 }
200 return orientation;
201}
202
203QVariant QgsImportPhotosAlgorithm::extractTimestampFromMetadata( const QVariantMap &metadata )
204{
205 QVariant ts;
206 if ( metadata.contains( QStringLiteral( "EXIF_DateTimeOriginal" ) ) )
207 {
208 ts = metadata.value( QStringLiteral( "EXIF_DateTimeOriginal" ) );
209 }
210 else if ( metadata.contains( QStringLiteral( "EXIF_DateTimeDigitized" ) ) )
211 {
212 ts = metadata.value( QStringLiteral( "EXIF_DateTimeDigitized" ) );
213 }
214 else if ( metadata.contains( QStringLiteral( "EXIF_DateTime" ) ) )
215 {
216 ts = metadata.value( QStringLiteral( "EXIF_DateTime" ) );
217 }
218
219 if ( !ts.isValid() )
220 return ts;
221
222 const thread_local QRegularExpression dsRegEx( QStringLiteral( "(\\d+):(\\d+):(\\d+)\\s+(\\d+):(\\d+):(\\d+)" ) );
223 const QRegularExpressionMatch dsMatch = dsRegEx.match( ts.toString() );
224 if ( dsMatch.hasMatch() )
225 {
226 const int year = dsMatch.captured( 1 ).toInt();
227 const int month = dsMatch.captured( 2 ).toInt();
228 const int day = dsMatch.captured( 3 ).toInt();
229 const int hour = dsMatch.captured( 4 ).toInt();
230 const int min = dsMatch.captured( 5 ).toInt();
231 const int sec = dsMatch.captured( 6 ).toInt();
232 return QDateTime( QDate( year, month, day ), QTime( hour, min, sec ) );
233 }
234 else
235 {
236 return QVariant();
237 }
238}
239
240QVariant QgsImportPhotosAlgorithm::parseCoord( const QString &string )
241{
242 const thread_local QRegularExpression coordRx( QStringLiteral( "^\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*$" ) );
243 const QRegularExpressionMatch coordMatch = coordRx.match( string );
244 if ( coordMatch.hasMatch() )
245 {
246 const double hours = coordMatch.captured( 1 ).toDouble();
247 const double minutes = coordMatch.captured( 2 ).toDouble();
248 const double seconds = coordMatch.captured( 3 ).toDouble();
249 return hours + minutes / 60.0 + seconds / 3600.0;
250 }
251 else
252 {
253 return QVariant();
254 }
255}
256
257QVariantMap QgsImportPhotosAlgorithm::parseMetadataList( const QStringList &input )
258{
259 QVariantMap results;
260 const thread_local QRegularExpression splitRx( QStringLiteral( "(.*?)=(.*)" ) );
261 for ( const QString &item : input )
262 {
263 const QRegularExpressionMatch match = splitRx.match( item );
264 if ( !match.hasMatch() )
265 continue;
266
267 const QString tag = match.captured( 1 );
268 QVariant value = parseMetadataValue( match.captured( 2 ) );
269
270 if ( tag == QLatin1String( "EXIF_GPSLatitude" ) || tag == QLatin1String( "EXIF_GPSLongitude" ) )
271 value = parseCoord( value.toString() );
272 results.insert( tag, value );
273 }
274 return results;
275}
276
277
278class SetEditorWidgetForPhotoAttributePostProcessor : public QgsProcessingLayerPostProcessorInterface
279{
280 public:
282 {
283 if ( QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( layer ) )
284 {
285 QVariantMap config;
286 // photo field shows picture viewer
287 config.insert( QStringLiteral( "DocumentViewer" ), 1 );
288 config.insert( QStringLiteral( "FileWidget" ), true );
289 config.insert( QStringLiteral( "UseLink" ), true );
290 config.insert( QStringLiteral( "FullUrl" ), true );
291 vl->setEditorWidgetSetup( vl->fields().lookupField( QStringLiteral( "photo" ) ), QgsEditorWidgetSetup( QStringLiteral( "ExternalResource" ), config ) );
292
293 config.clear();
294 // path field is a directory link
295 config.insert( QStringLiteral( "FileWidgetButton" ), true );
296 config.insert( QStringLiteral( "StorageMode" ), 1 );
297 config.insert( QStringLiteral( "UseLink" ), true );
298 config.insert( QStringLiteral( "FullUrl" ), true );
299 vl->setEditorWidgetSetup( vl->fields().lookupField( QStringLiteral( "directory" ) ), QgsEditorWidgetSetup( QStringLiteral( "ExternalResource" ), config ) );
300 }
301 }
302};
303
304QVariantMap QgsImportPhotosAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
305{
306 const QString folder = parameterAsFile( parameters, QStringLiteral( "FOLDER" ), context );
307
308 const QDir importDir( folder );
309 if ( !importDir.exists() )
310 {
311 throw QgsProcessingException( QObject::tr( "Directory %1 does not exist!" ).arg( folder ) );
312 }
313
314 const bool recurse = parameterAsBoolean( parameters, QStringLiteral( "RECURSIVE" ), context );
315
316 QgsFields outFields;
317 outFields.append( QgsField( QStringLiteral( "photo" ), QMetaType::Type::QString ) );
318 outFields.append( QgsField( QStringLiteral( "filename" ), QMetaType::Type::QString ) );
319 outFields.append( QgsField( QStringLiteral( "directory" ), QMetaType::Type::QString ) );
320 outFields.append( QgsField( QStringLiteral( "altitude" ), QMetaType::Type::Double ) );
321 outFields.append( QgsField( QStringLiteral( "direction" ), QMetaType::Type::Double ) );
322 outFields.append( QgsField( QStringLiteral( "rotation" ), QMetaType::Type::Int ) );
323 outFields.append( QgsField( QStringLiteral( "longitude" ), QMetaType::Type::QString ) );
324 outFields.append( QgsField( QStringLiteral( "latitude" ), QMetaType::Type::QString ) );
325 outFields.append( QgsField( QStringLiteral( "timestamp" ), QMetaType::Type::QDateTime ) );
326 QString outputDest;
327 std::unique_ptr<QgsFeatureSink> outputSink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, outputDest, outFields, Qgis::WkbType::PointZ, QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) ) );
328
329 QgsFields invalidFields;
330 invalidFields.append( QgsField( QStringLiteral( "photo" ), QMetaType::Type::QString ) );
331 invalidFields.append( QgsField( QStringLiteral( "filename" ), QMetaType::Type::QString ) );
332 invalidFields.append( QgsField( QStringLiteral( "directory" ), QMetaType::Type::QString ) );
333 invalidFields.append( QgsField( QStringLiteral( "readable" ), QMetaType::Type::Bool ) );
334 QString invalidDest;
335 std::unique_ptr<QgsFeatureSink> invalidSink( parameterAsSink( parameters, QStringLiteral( "INVALID" ), context, invalidDest, invalidFields ) );
336
337 const QStringList nameFilters { "*.jpeg", "*.jpg", "*.heic" };
338 QStringList files;
339
340 if ( !recurse )
341 {
342 const QFileInfoList fileInfoList = importDir.entryInfoList( nameFilters, QDir::NoDotAndDotDot | QDir::Files );
343 for ( auto infoIt = fileInfoList.constBegin(); infoIt != fileInfoList.constEnd(); ++infoIt )
344 {
345 files.append( infoIt->absoluteFilePath() );
346 }
347 }
348 else
349 {
350 QDirIterator it( folder, nameFilters, QDir::NoDotAndDotDot | QDir::Files, QDirIterator::Subdirectories );
351 while ( it.hasNext() )
352 {
353 it.next();
354 files.append( it.filePath() );
355 }
356 }
357
358 auto saveInvalidFile = [&invalidSink, &parameters]( QgsAttributes &attributes, bool readable ) {
359 if ( invalidSink )
360 {
361 QgsFeature f;
362 attributes.append( readable );
363 f.setAttributes( attributes );
364 if ( !invalidSink->addFeature( f, QgsFeatureSink::FastInsert ) )
365 throw QgsProcessingException( writeFeatureError( invalidSink.get(), parameters, QStringLiteral( "INVALID" ) ) );
366 }
367 };
368
369 const double step = files.count() > 0 ? 100.0 / files.count() : 1;
370 int i = 0;
371 for ( const QString &file : files )
372 {
373 i++;
374 if ( feedback->isCanceled() )
375 {
376 break;
377 }
378
379 feedback->setProgress( i * step );
380
381 const QFileInfo fi( file );
382 QgsAttributes attributes;
383 attributes << QDir::toNativeSeparators( file )
384 << fi.completeBaseName()
385 << QDir::toNativeSeparators( fi.absolutePath() );
386
387 const gdal::dataset_unique_ptr hDS( GDALOpen( file.toUtf8().constData(), GA_ReadOnly ) );
388 if ( !hDS )
389 {
390 feedback->reportError( QObject::tr( "Could not open %1" ).arg( QDir::toNativeSeparators( file ) ) );
391 saveInvalidFile( attributes, false );
392 continue;
393 }
394
395 char **GDALmetadata = GDALGetMetadata( hDS.get(), nullptr );
396 if ( !GDALmetadata )
397 {
398 GDALmetadata = GDALGetMetadata( hDS.get(), "EXIF" );
399 }
400 if ( !GDALmetadata )
401 {
402 feedback->reportError( QObject::tr( "No metadata found in %1" ).arg( QDir::toNativeSeparators( file ) ) );
403 saveInvalidFile( attributes, true );
404 }
405 else
406 {
407 if ( !outputSink )
408 continue;
409
410 QgsFeature f;
411 const QVariantMap metadata = parseMetadataList( QgsOgrUtils::cStringListToQStringList( GDALmetadata ) );
412
413 QgsPointXY tag;
414 if ( !extractGeoTagFromMetadata( metadata, tag ) )
415 {
416 // no geotag
417 feedback->reportError( QObject::tr( "Could not retrieve geotag for %1" ).arg( QDir::toNativeSeparators( file ) ) );
418 saveInvalidFile( attributes, true );
419 continue;
420 }
421
422 const QVariant altitude = extractAltitudeFromMetadata( metadata );
423 const QgsGeometry p = QgsGeometry( new QgsPoint( tag.x(), tag.y(), altitude.toDouble(), 0, Qgis::WkbType::PointZ ) );
424 f.setGeometry( p );
425
426 attributes
427 << altitude
428 << extractDirectionFromMetadata( metadata )
429 << extractOrientationFromMetadata( metadata )
430 << tag.x()
431 << tag.y()
432 << extractTimestampFromMetadata( metadata );
433 f.setAttributes( attributes );
434 if ( !outputSink->addFeature( f, QgsFeatureSink::FastInsert ) )
435 throw QgsProcessingException( writeFeatureError( outputSink.get(), parameters, QStringLiteral( "OUTPUT" ) ) );
436 }
437 }
438
439 QVariantMap outputs;
440 if ( outputSink )
441 {
442 outputSink->finalize();
443 outputs.insert( QStringLiteral( "OUTPUT" ), outputDest );
444
445 if ( context.willLoadLayerOnCompletion( outputDest ) )
446 {
447 context.layerToLoadOnCompletionDetails( outputDest ).setPostProcessor( new SetEditorWidgetForPhotoAttributePostProcessor() );
448 }
449 }
450
451 if ( invalidSink )
452 {
453 invalidSink->finalize();
454 outputs.insert( QStringLiteral( "INVALID" ), invalidDest );
455 }
456 return outputs;
457}
458
@ Vector
Tables (i.e. vector layers with or without geometry). When used for a sink this indicates the sink ha...
@ VectorPoint
Vector point layers.
@ Folder
Parameter is a folder.
@ PointZ
PointZ.
A vector of attributes.
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: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:53
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:70
A geometry is the spatial representation of a feature.
Base class for all map layer types.
Definition qgsmaplayer.h:76
static QStringList cStringListToQStringList(char **stringList)
Converts a c string list to a QStringList.
A class to represent 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.
Represents a vector layer which manages a vector based data sets.
CORE_EXPORT const QStringList files(const QString &zip)
Returns the list of files within a zip file.
std::unique_ptr< std::remove_pointer< GDALDatasetH >::type, GDALDatasetCloser > dataset_unique_ptr
Scoped GDAL dataset.