QGIS API Documentation 3.41.0-Master (3440c17df1d)
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" ) ) &&
151 ( ( metadata.value( QStringLiteral( "EXIF_GPSAltitudeRef" ) ).userType() == QMetaType::Type::QString && metadata.value( QStringLiteral( "EXIF_GPSAltitudeRef" ) ).toString().right( 1 ) == QLatin1String( "1" ) )
152 || metadata.value( QStringLiteral( "EXIF_GPSAltitudeRef" ) ).toDouble() < 0 ) )
153 alt = -alt;
154 altitude = alt;
155 }
156 return altitude;
157}
158
159QVariant QgsImportPhotosAlgorithm::extractDirectionFromMetadata( const QVariantMap &metadata )
160{
161 QVariant direction;
162 if ( metadata.contains( QStringLiteral( "EXIF_GPSImgDirection" ) ) )
163 {
164 direction = metadata.value( QStringLiteral( "EXIF_GPSImgDirection" ) ).toDouble();
165 }
166 return direction;
167}
168
169QVariant QgsImportPhotosAlgorithm::extractOrientationFromMetadata( const QVariantMap &metadata )
170{
171 QVariant orientation;
172 if ( metadata.contains( QStringLiteral( "EXIF_Orientation" ) ) )
173 {
174 switch ( metadata.value( QStringLiteral( "EXIF_Orientation" ) ).toInt() )
175 {
176 case 1:
177 orientation = 0;
178 break;
179 case 2:
180 orientation = 0;
181 break;
182 case 3:
183 orientation = 180;
184 break;
185 case 4:
186 orientation = 180;
187 break;
188 case 5:
189 orientation = 90;
190 break;
191 case 6:
192 orientation = 90;
193 break;
194 case 7:
195 orientation = 270;
196 break;
197 case 8:
198 orientation = 270;
199 break;
200 }
201 }
202 return orientation;
203}
204
205QVariant QgsImportPhotosAlgorithm::extractTimestampFromMetadata( const QVariantMap &metadata )
206{
207 QVariant ts;
208 if ( metadata.contains( QStringLiteral( "EXIF_DateTimeOriginal" ) ) )
209 {
210 ts = metadata.value( QStringLiteral( "EXIF_DateTimeOriginal" ) );
211 }
212 else if ( metadata.contains( QStringLiteral( "EXIF_DateTimeDigitized" ) ) )
213 {
214 ts = metadata.value( QStringLiteral( "EXIF_DateTimeDigitized" ) );
215 }
216 else if ( metadata.contains( QStringLiteral( "EXIF_DateTime" ) ) )
217 {
218 ts = metadata.value( QStringLiteral( "EXIF_DateTime" ) );
219 }
220
221 if ( !ts.isValid() )
222 return ts;
223
224 const thread_local QRegularExpression dsRegEx( QStringLiteral( "(\\d+):(\\d+):(\\d+)\\s+(\\d+):(\\d+):(\\d+)" ) );
225 const QRegularExpressionMatch dsMatch = dsRegEx.match( ts.toString() );
226 if ( dsMatch.hasMatch() )
227 {
228 const int year = dsMatch.captured( 1 ).toInt();
229 const int month = dsMatch.captured( 2 ).toInt();
230 const int day = dsMatch.captured( 3 ).toInt();
231 const int hour = dsMatch.captured( 4 ).toInt();
232 const int min = dsMatch.captured( 5 ).toInt();
233 const int sec = dsMatch.captured( 6 ).toInt();
234 return QDateTime( QDate( year, month, day ), QTime( hour, min, sec ) );
235 }
236 else
237 {
238 return QVariant();
239 }
240}
241
242QVariant QgsImportPhotosAlgorithm::parseCoord( const QString &string )
243{
244 const thread_local QRegularExpression coordRx( QStringLiteral( "^\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*$" ) );
245 const QRegularExpressionMatch coordMatch = coordRx.match( string );
246 if ( coordMatch.hasMatch() )
247 {
248 const double hours = coordMatch.captured( 1 ).toDouble();
249 const double minutes = coordMatch.captured( 2 ).toDouble();
250 const double seconds = coordMatch.captured( 3 ).toDouble();
251 return hours + minutes / 60.0 + seconds / 3600.0;
252 }
253 else
254 {
255 return QVariant();
256 }
257}
258
259QVariantMap QgsImportPhotosAlgorithm::parseMetadataList( const QStringList &input )
260{
261 QVariantMap results;
262 const thread_local QRegularExpression splitRx( QStringLiteral( "(.*?)=(.*)" ) );
263 for ( const QString &item : input )
264 {
265 const QRegularExpressionMatch match = splitRx.match( item );
266 if ( !match.hasMatch() )
267 continue;
268
269 const QString tag = match.captured( 1 );
270 QVariant value = parseMetadataValue( match.captured( 2 ) );
271
272 if ( tag == QLatin1String( "EXIF_GPSLatitude" ) || tag == QLatin1String( "EXIF_GPSLongitude" ) )
273 value = parseCoord( value.toString() );
274 results.insert( tag, value );
275 }
276 return results;
277}
278
279
280class SetEditorWidgetForPhotoAttributePostProcessor : public QgsProcessingLayerPostProcessorInterface
281{
282 public:
283
285 {
286 if ( QgsVectorLayer *vl = qobject_cast< QgsVectorLayer * >( layer ) )
287 {
288 QVariantMap config;
289 // photo field shows picture viewer
290 config.insert( QStringLiteral( "DocumentViewer" ), 1 );
291 config.insert( QStringLiteral( "FileWidget" ), true );
292 config.insert( QStringLiteral( "UseLink" ), true );
293 config.insert( QStringLiteral( "FullUrl" ), true );
294 vl->setEditorWidgetSetup( vl->fields().lookupField( QStringLiteral( "photo" ) ), QgsEditorWidgetSetup( QStringLiteral( "ExternalResource" ), config ) );
295
296 config.clear();
297 // path field is a directory link
298 config.insert( QStringLiteral( "FileWidgetButton" ), true );
299 config.insert( QStringLiteral( "StorageMode" ), 1 );
300 config.insert( QStringLiteral( "UseLink" ), true );
301 config.insert( QStringLiteral( "FullUrl" ), true );
302 vl->setEditorWidgetSetup( vl->fields().lookupField( QStringLiteral( "directory" ) ), QgsEditorWidgetSetup( QStringLiteral( "ExternalResource" ), config ) );
303 }
304 }
305};
306
307QVariantMap QgsImportPhotosAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
308{
309 const QString folder = parameterAsFile( parameters, QStringLiteral( "FOLDER" ), context );
310
311 const QDir importDir( folder );
312 if ( !importDir.exists() )
313 {
314 throw QgsProcessingException( QObject::tr( "Directory %1 does not exist!" ).arg( folder ) );
315 }
316
317 const bool recurse = parameterAsBoolean( parameters, QStringLiteral( "RECURSIVE" ), context );
318
319 QgsFields outFields;
320 outFields.append( QgsField( QStringLiteral( "photo" ), QMetaType::Type::QString ) );
321 outFields.append( QgsField( QStringLiteral( "filename" ), QMetaType::Type::QString ) );
322 outFields.append( QgsField( QStringLiteral( "directory" ), QMetaType::Type::QString ) );
323 outFields.append( QgsField( QStringLiteral( "altitude" ), QMetaType::Type::Double ) );
324 outFields.append( QgsField( QStringLiteral( "direction" ), QMetaType::Type::Double ) );
325 outFields.append( QgsField( QStringLiteral( "rotation" ), QMetaType::Type::Int ) );
326 outFields.append( QgsField( QStringLiteral( "longitude" ), QMetaType::Type::QString ) );
327 outFields.append( QgsField( QStringLiteral( "latitude" ), QMetaType::Type::QString ) );
328 outFields.append( QgsField( QStringLiteral( "timestamp" ), QMetaType::Type::QDateTime ) );
329 QString outputDest;
330 std::unique_ptr< QgsFeatureSink > outputSink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, outputDest, outFields,
331 Qgis::WkbType::PointZ, QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) ) );
332
333 QgsFields invalidFields;
334 invalidFields.append( QgsField( QStringLiteral( "photo" ), QMetaType::Type::QString ) );
335 invalidFields.append( QgsField( QStringLiteral( "filename" ), QMetaType::Type::QString ) );
336 invalidFields.append( QgsField( QStringLiteral( "directory" ), QMetaType::Type::QString ) );
337 invalidFields.append( QgsField( QStringLiteral( "readable" ), QMetaType::Type::Bool ) );
338 QString invalidDest;
339 std::unique_ptr< QgsFeatureSink > invalidSink( parameterAsSink( parameters, QStringLiteral( "INVALID" ), context, invalidDest, invalidFields ) );
340
341 const QStringList nameFilters { "*.jpeg", "*.jpg", "*.heic" };
342 QStringList files;
343
344 if ( !recurse )
345 {
346 const QFileInfoList fileInfoList = importDir.entryInfoList( nameFilters, QDir::NoDotAndDotDot | QDir::Files );
347 for ( auto infoIt = fileInfoList.constBegin(); infoIt != fileInfoList.constEnd(); ++infoIt )
348 {
349 files.append( infoIt->absoluteFilePath() );
350 }
351 }
352 else
353 {
354 QDirIterator it( folder, nameFilters, QDir::NoDotAndDotDot | QDir::Files, QDirIterator::Subdirectories );
355 while ( it.hasNext() )
356 {
357 it.next();
358 files.append( it.filePath() );
359 }
360 }
361
362 auto saveInvalidFile = [&invalidSink, &parameters]( QgsAttributes & attributes, bool readable )
363 {
364 if ( invalidSink )
365 {
366 QgsFeature f;
367 attributes.append( readable );
368 f.setAttributes( attributes );
369 if ( !invalidSink->addFeature( f, QgsFeatureSink::FastInsert ) )
370 throw QgsProcessingException( writeFeatureError( invalidSink.get(), parameters, QStringLiteral( "INVALID" ) ) );
371 }
372 };
373
374 const double step = files.count() > 0 ? 100.0 / files.count() : 1;
375 int i = 0;
376 for ( const QString &file : files )
377 {
378 i++;
379 if ( feedback->isCanceled() )
380 {
381 break;
382 }
383
384 feedback->setProgress( i * step );
385
386 const QFileInfo fi( file );
387 QgsAttributes attributes;
388 attributes << QDir::toNativeSeparators( file )
389 << fi.completeBaseName()
390 << QDir::toNativeSeparators( fi.absolutePath() );
391
392 const gdal::dataset_unique_ptr hDS( GDALOpen( file.toUtf8().constData(), GA_ReadOnly ) );
393 if ( !hDS )
394 {
395 feedback->reportError( QObject::tr( "Could not open %1" ).arg( QDir::toNativeSeparators( file ) ) );
396 saveInvalidFile( attributes, false );
397 continue;
398 }
399
400 char **GDALmetadata = GDALGetMetadata( hDS.get(), nullptr );
401 if ( ! GDALmetadata )
402 {
403 GDALmetadata = GDALGetMetadata( hDS.get(), "EXIF" );
404 }
405 if ( ! GDALmetadata )
406 {
407 feedback->reportError( QObject::tr( "No metadata found in %1" ).arg( QDir::toNativeSeparators( file ) ) );
408 saveInvalidFile( attributes, true );
409 }
410 else
411 {
412 if ( !outputSink )
413 continue;
414
415 QgsFeature f;
416 const QVariantMap metadata = parseMetadataList( QgsOgrUtils::cStringListToQStringList( GDALmetadata ) );
417
418 QgsPointXY tag;
419 if ( !extractGeoTagFromMetadata( metadata, tag ) )
420 {
421 // no geotag
422 feedback->reportError( QObject::tr( "Could not retrieve geotag for %1" ).arg( QDir::toNativeSeparators( file ) ) );
423 saveInvalidFile( attributes, true );
424 continue;
425 }
426
427 const QVariant altitude = extractAltitudeFromMetadata( metadata );
428 const QgsGeometry p = QgsGeometry( new QgsPoint( tag.x(), tag.y(), altitude.toDouble(), 0, Qgis::WkbType::PointZ ) );
429 f.setGeometry( p );
430
431 attributes
432 << altitude
433 << extractDirectionFromMetadata( metadata )
434 << extractOrientationFromMetadata( metadata )
435 << tag.x()
436 << tag.y()
437 << extractTimestampFromMetadata( metadata );
438 f.setAttributes( attributes );
439 if ( !outputSink->addFeature( f, QgsFeatureSink::FastInsert ) )
440 throw QgsProcessingException( writeFeatureError( outputSink.get(), parameters, QStringLiteral( "OUTPUT" ) ) );
441 }
442 }
443
444 QVariantMap outputs;
445 if ( outputSink )
446 {
447 outputSink->finalize();
448 outputs.insert( QStringLiteral( "OUTPUT" ), outputDest );
449
450 if ( context.willLoadLayerOnCompletion( outputDest ) )
451 {
452 context.layerToLoadOnCompletionDetails( outputDest ).setPostProcessor( new SetEditorWidgetForPhotoAttributePostProcessor() );
453 }
454 }
455
456 if ( invalidSink )
457 {
458 invalidSink->finalize();
459 outputs.insert( QStringLiteral( "INVALID" ), invalidDest );
460 }
461 return outputs;
462}
463
@ 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.