21#include <QDirIterator>
23#include <QRegularExpression>
27QString QgsImportPhotosAlgorithm::name()
const
29 return QStringLiteral(
"importphotos" );
32QString QgsImportPhotosAlgorithm::displayName()
const
34 return QObject::tr(
"Import geotagged photos" );
37QStringList QgsImportPhotosAlgorithm::tags()
const
39 return QObject::tr(
"exif,metadata,gps,jpeg,jpg" ).split(
',' );
42QString QgsImportPhotosAlgorithm::group()
const
44 return QObject::tr(
"Vector creation" );
47QString QgsImportPhotosAlgorithm::groupId()
const
49 return QStringLiteral(
"vectorcreation" );
52void QgsImportPhotosAlgorithm::initAlgorithm(
const QVariantMap & )
58 output->setCreateByDefault(
true );
59 addParameter( output.release() );
61 auto 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() );
66QString QgsImportPhotosAlgorithm::shortHelpString()
const
68 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"
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." );
74QString QgsImportPhotosAlgorithm::shortDescription()
const
76 return QObject::tr(
"Creates a point layer corresponding to the geotagged locations from JPEG or HEIF/HEIC images from a source folder." );
79QgsImportPhotosAlgorithm *QgsImportPhotosAlgorithm::createInstance()
const
81 return new QgsImportPhotosAlgorithm();
84QVariant QgsImportPhotosAlgorithm::parseMetadataValue(
const QString &value )
86 const thread_local QRegularExpression numRx( QStringLiteral(
"^\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*$" ) );
87 const QRegularExpressionMatch numMatch = numRx.match( value );
88 if ( numMatch.hasMatch() )
90 return numMatch.captured( 1 ).toDouble();
95bool QgsImportPhotosAlgorithm::extractGeoTagFromMetadata(
const QVariantMap &metadata,
QgsPointXY &tag )
98 if ( metadata.contains( QStringLiteral(
"EXIF_GPSLongitude" ) ) )
101 x = metadata.value( QStringLiteral(
"EXIF_GPSLongitude" ) ).toDouble( &ok );
105#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
106 if ( metadata.value( QStringLiteral(
"EXIF_GPSLongitudeRef" ) ).toString().rightRef( 1 ).compare( QLatin1String(
"W" ), Qt::CaseInsensitive ) == 0
107 || metadata.value( QStringLiteral(
"EXIF_GPSLongitudeRef" ) ).toDouble() < 0 )
109 if ( QStringView { metadata.value( QStringLiteral(
"EXIF_GPSLongitudeRef" ) ).toString() }.right( 1 ).compare( QLatin1String(
"W" ), Qt::CaseInsensitive ) == 0
110 || metadata.value( QStringLiteral(
"EXIF_GPSLongitudeRef" ) ).toDouble() < 0 )
122 if ( metadata.contains( QStringLiteral(
"EXIF_GPSLatitude" ) ) )
125 y = metadata.value( QStringLiteral(
"EXIF_GPSLatitude" ) ).toDouble( &ok );
129#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
130 if ( metadata.value( QStringLiteral(
"EXIF_GPSLatitudeRef" ) ).toString().rightRef( 1 ).compare( QLatin1String(
"S" ), Qt::CaseInsensitive ) == 0
131 || metadata.value( QStringLiteral(
"EXIF_GPSLatitudeRef" ) ).toDouble() < 0 )
133 if ( QStringView { metadata.value( QStringLiteral(
"EXIF_GPSLatitudeRef" ) ).toString() }.right( 1 ).compare( QLatin1String(
"S" ), Qt::CaseInsensitive ) == 0
134 || metadata.value( QStringLiteral(
"EXIF_GPSLatitudeRef" ) ).toDouble() < 0 )
149QVariant QgsImportPhotosAlgorithm::extractAltitudeFromMetadata(
const QVariantMap &metadata )
152 if ( metadata.contains( QStringLiteral(
"EXIF_GPSAltitude" ) ) )
154 double alt = metadata.value( QStringLiteral(
"EXIF_GPSAltitude" ) ).toDouble();
155 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 ) )
162QVariant QgsImportPhotosAlgorithm::extractDirectionFromMetadata(
const QVariantMap &metadata )
165 if ( metadata.contains( QStringLiteral(
"EXIF_GPSImgDirection" ) ) )
167 direction = metadata.value( QStringLiteral(
"EXIF_GPSImgDirection" ) ).toDouble();
172QVariant QgsImportPhotosAlgorithm::extractOrientationFromMetadata(
const QVariantMap &metadata )
174 QVariant orientation;
175 if ( metadata.contains( QStringLiteral(
"EXIF_Orientation" ) ) )
177 switch ( metadata.value( QStringLiteral(
"EXIF_Orientation" ) ).toInt() )
208QVariant QgsImportPhotosAlgorithm::extractTimestampFromMetadata(
const QVariantMap &metadata )
211 if ( metadata.contains( QStringLiteral(
"EXIF_DateTimeOriginal" ) ) )
213 ts = metadata.value( QStringLiteral(
"EXIF_DateTimeOriginal" ) );
215 else if ( metadata.contains( QStringLiteral(
"EXIF_DateTimeDigitized" ) ) )
217 ts = metadata.value( QStringLiteral(
"EXIF_DateTimeDigitized" ) );
219 else if ( metadata.contains( QStringLiteral(
"EXIF_DateTime" ) ) )
221 ts = metadata.value( QStringLiteral(
"EXIF_DateTime" ) );
227 const thread_local QRegularExpression dsRegEx( QStringLiteral(
"(\\d+):(\\d+):(\\d+)\\s+(\\d+):(\\d+):(\\d+)" ) );
228 const QRegularExpressionMatch dsMatch = dsRegEx.match( ts.toString() );
229 if ( dsMatch.hasMatch() )
231 const int year = dsMatch.captured( 1 ).toInt();
232 const int month = dsMatch.captured( 2 ).toInt();
233 const int day = dsMatch.captured( 3 ).toInt();
234 const int hour = dsMatch.captured( 4 ).toInt();
235 const int min = dsMatch.captured( 5 ).toInt();
236 const int sec = dsMatch.captured( 6 ).toInt();
237 return QDateTime( QDate( year, month, day ), QTime( hour, min, sec ) );
245QVariant QgsImportPhotosAlgorithm::parseCoord(
const QString &
string )
247 const thread_local QRegularExpression coordRx( QStringLiteral(
"^\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*$" ) );
248 const QRegularExpressionMatch coordMatch = coordRx.match(
string );
249 if ( coordMatch.hasMatch() )
251 const double hours = coordMatch.captured( 1 ).toDouble();
252 const double minutes = coordMatch.captured( 2 ).toDouble();
253 const double seconds = coordMatch.captured( 3 ).toDouble();
254 return hours + minutes / 60.0 + seconds / 3600.0;
262QVariantMap QgsImportPhotosAlgorithm::parseMetadataList(
const QStringList &input )
265 const thread_local QRegularExpression splitRx( QStringLiteral(
"(.*?)=(.*)" ) );
266 for (
const QString &item : input )
268 const QRegularExpressionMatch match = splitRx.match( item );
269 if ( !match.hasMatch() )
272 const QString tag = match.captured( 1 );
273 QVariant value = parseMetadataValue( match.captured( 2 ) );
275 if ( tag == QLatin1String(
"EXIF_GPSLatitude" ) || tag == QLatin1String(
"EXIF_GPSLongitude" ) )
276 value = parseCoord( value.toString() );
277 results.insert( tag, value );
288 if (
QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( layer ) )
292 config.insert( QStringLiteral(
"DocumentViewer" ), 1 );
293 config.insert( QStringLiteral(
"FileWidget" ),
true );
294 config.insert( QStringLiteral(
"UseLink" ),
true );
295 config.insert( QStringLiteral(
"FullUrl" ),
true );
296 vl->setEditorWidgetSetup( vl->fields().lookupField( QStringLiteral(
"photo" ) ),
QgsEditorWidgetSetup( QStringLiteral(
"ExternalResource" ), config ) );
300 config.insert( QStringLiteral(
"FileWidgetButton" ),
true );
301 config.insert( QStringLiteral(
"StorageMode" ), 1 );
302 config.insert( QStringLiteral(
"UseLink" ),
true );
303 config.insert( QStringLiteral(
"FullUrl" ),
true );
304 vl->setEditorWidgetSetup( vl->fields().lookupField( QStringLiteral(
"directory" ) ),
QgsEditorWidgetSetup( QStringLiteral(
"ExternalResource" ), config ) );
311 const QString folder = parameterAsFile( parameters, QStringLiteral(
"FOLDER" ), context );
313 const QDir importDir( folder );
314 if ( !importDir.exists() )
319 const bool recurse = parameterAsBoolean( parameters, QStringLiteral(
"RECURSIVE" ), context );
322 outFields.
append(
QgsField( QStringLiteral(
"photo" ), QMetaType::Type::QString ) );
323 outFields.
append(
QgsField( QStringLiteral(
"filename" ), QMetaType::Type::QString ) );
324 outFields.
append(
QgsField( QStringLiteral(
"directory" ), QMetaType::Type::QString ) );
325 outFields.
append(
QgsField( QStringLiteral(
"altitude" ), QMetaType::Type::Double ) );
326 outFields.
append(
QgsField( QStringLiteral(
"direction" ), QMetaType::Type::Double ) );
327 outFields.
append(
QgsField( QStringLiteral(
"rotation" ), QMetaType::Type::Int ) );
328 outFields.
append(
QgsField( QStringLiteral(
"longitude" ), QMetaType::Type::QString ) );
329 outFields.
append(
QgsField( QStringLiteral(
"latitude" ), QMetaType::Type::QString ) );
330 outFields.
append(
QgsField( QStringLiteral(
"timestamp" ), QMetaType::Type::QDateTime ) );
335 invalidFields.
append(
QgsField( QStringLiteral(
"photo" ), QMetaType::Type::QString ) );
336 invalidFields.
append(
QgsField( QStringLiteral(
"filename" ), QMetaType::Type::QString ) );
337 invalidFields.
append(
QgsField( QStringLiteral(
"directory" ), QMetaType::Type::QString ) );
338 invalidFields.
append(
QgsField( QStringLiteral(
"readable" ), QMetaType::Type::Bool ) );
340 std::unique_ptr<QgsFeatureSink> invalidSink( parameterAsSink( parameters, QStringLiteral(
"INVALID" ), context, invalidDest, invalidFields ) );
342 const QStringList nameFilters {
"*.jpeg",
"*.jpg",
"*.heic" };
347 const QFileInfoList fileInfoList = importDir.entryInfoList( nameFilters, QDir::NoDotAndDotDot | QDir::Files );
348 for (
auto infoIt = fileInfoList.constBegin(); infoIt != fileInfoList.constEnd(); ++infoIt )
350 files.append( infoIt->absoluteFilePath() );
355 QDirIterator it( folder, nameFilters, QDir::NoDotAndDotDot | QDir::Files, QDirIterator::Subdirectories );
356 while ( it.hasNext() )
359 files.append( it.filePath() );
363 auto saveInvalidFile = [&invalidSink, ¶meters](
QgsAttributes &attributes,
bool readable ) {
367 attributes.append( readable );
370 throw QgsProcessingException( writeFeatureError( invalidSink.get(), parameters, QStringLiteral(
"INVALID" ) ) );
374 const double step = files.count() > 0 ? 100.0 / files.count() : 1;
376 for (
const QString &file : files )
386 const QFileInfo fi( file );
388 attributes << QDir::toNativeSeparators( file )
389 << fi.completeBaseName()
390 << QDir::toNativeSeparators( fi.absolutePath() );
395 feedback->
reportError( QObject::tr(
"Could not open %1" ).arg( QDir::toNativeSeparators( file ) ) );
396 saveInvalidFile( attributes,
false );
400 char **GDALmetadata = GDALGetMetadata( hDS.get(),
nullptr );
403 GDALmetadata = GDALGetMetadata( hDS.get(),
"EXIF" );
407 feedback->
reportError( QObject::tr(
"No metadata found in %1" ).arg( QDir::toNativeSeparators( file ) ) );
408 saveInvalidFile( attributes,
true );
419 if ( !extractGeoTagFromMetadata( metadata, tag ) )
422 feedback->
reportError( QObject::tr(
"Could not retrieve geotag for %1" ).arg( QDir::toNativeSeparators( file ) ) );
423 saveInvalidFile( attributes,
true );
427 const QVariant altitude = extractAltitudeFromMetadata( metadata );
433 << extractDirectionFromMetadata( metadata )
434 << extractOrientationFromMetadata( metadata )
437 << extractTimestampFromMetadata( metadata );
440 throw QgsProcessingException( writeFeatureError( outputSink.get(), parameters, QStringLiteral(
"OUTPUT" ) ) );
447 outputSink->finalize();
448 outputs.insert( QStringLiteral(
"OUTPUT" ), outputDest );
458 invalidSink->finalize();
459 outputs.insert( QStringLiteral(
"INVALID" ), invalidDest );
@ 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.
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...
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.
void setProgress(double progress)
Sets the current progress for the feedback object.
Encapsulate a field in an attribute table or data source.
Container of fields for a vector layer.
bool append(const QgsField &field, Qgis::FieldOrigin origin=Qgis::FieldOrigin::Provider, int originIndex=-1)
Appends a field.
A geometry is the spatial representation of a feature.
Base class for all map layer types.
static QStringList cStringListToQStringList(char **stringList)
Converts a c string list to a QStringList.
Point geometry type, with support for z-dimension and m-values.
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 dataset.
std::unique_ptr< std::remove_pointer< GDALDatasetH >::type, GDALDatasetCloser > dataset_unique_ptr
Scoped GDAL dataset.