23#include <QDirIterator>
25#include <QRegularExpression>
29QString QgsImportPhotosAlgorithm::name()
const
31 return QStringLiteral(
"importphotos" );
34QString QgsImportPhotosAlgorithm::displayName()
const
36 return QObject::tr(
"Import geotagged photos" );
39QStringList QgsImportPhotosAlgorithm::tags()
const
41 return QObject::tr(
"exif,metadata,gps,jpeg,jpg" ).split(
',' );
44QString QgsImportPhotosAlgorithm::group()
const
46 return QObject::tr(
"Vector creation" );
49QString QgsImportPhotosAlgorithm::groupId()
const
51 return QStringLiteral(
"vectorcreation" );
54void QgsImportPhotosAlgorithm::initAlgorithm(
const QVariantMap & )
60 output->setCreateByDefault(
true );
61 addParameter( output.release() );
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() );
68QString QgsImportPhotosAlgorithm::shortHelpString()
const
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." );
76QString QgsImportPhotosAlgorithm::shortDescription()
const
78 return QObject::tr(
"Creates a point layer corresponding to the geotagged locations from JPEG or HEIF/HEIC images from a source folder." );
81QgsImportPhotosAlgorithm *QgsImportPhotosAlgorithm::createInstance()
const
83 return new QgsImportPhotosAlgorithm();
86QVariant QgsImportPhotosAlgorithm::parseMetadataValue(
const QString &value )
88 const thread_local QRegularExpression numRx( QStringLiteral(
"^\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*$" ) );
89 const QRegularExpressionMatch numMatch = numRx.match( value );
90 if ( numMatch.hasMatch() )
92 return numMatch.captured( 1 ).toDouble();
97bool QgsImportPhotosAlgorithm::extractGeoTagFromMetadata(
const QVariantMap &metadata,
QgsPointXY &tag )
100 if ( metadata.contains( QStringLiteral(
"EXIF_GPSLongitude" ) ) )
103 x = metadata.value( QStringLiteral(
"EXIF_GPSLongitude" ) ).toDouble( &ok );
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 )
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 )
124 if ( metadata.contains( QStringLiteral(
"EXIF_GPSLatitude" ) ) )
127 y = metadata.value( QStringLiteral(
"EXIF_GPSLatitude" ) ).toDouble( &ok );
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 )
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 )
151QVariant QgsImportPhotosAlgorithm::extractAltitudeFromMetadata(
const QVariantMap &metadata )
154 if ( metadata.contains( QStringLiteral(
"EXIF_GPSAltitude" ) ) )
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 ) )
164QVariant QgsImportPhotosAlgorithm::extractDirectionFromMetadata(
const QVariantMap &metadata )
167 if ( metadata.contains( QStringLiteral(
"EXIF_GPSImgDirection" ) ) )
169 direction = metadata.value( QStringLiteral(
"EXIF_GPSImgDirection" ) ).toDouble();
174QVariant QgsImportPhotosAlgorithm::extractOrientationFromMetadata(
const QVariantMap &metadata )
176 QVariant orientation;
177 if ( metadata.contains( QStringLiteral(
"EXIF_Orientation" ) ) )
179 switch ( metadata.value( QStringLiteral(
"EXIF_Orientation" ) ).toInt() )
210QVariant QgsImportPhotosAlgorithm::extractTimestampFromMetadata(
const QVariantMap &metadata )
213 if ( metadata.contains( QStringLiteral(
"EXIF_DateTimeOriginal" ) ) )
215 ts = metadata.value( QStringLiteral(
"EXIF_DateTimeOriginal" ) );
217 else if ( metadata.contains( QStringLiteral(
"EXIF_DateTimeDigitized" ) ) )
219 ts = metadata.value( QStringLiteral(
"EXIF_DateTimeDigitized" ) );
221 else if ( metadata.contains( QStringLiteral(
"EXIF_DateTime" ) ) )
223 ts = metadata.value( QStringLiteral(
"EXIF_DateTime" ) );
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() )
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 ) );
247QVariant QgsImportPhotosAlgorithm::parseCoord(
const QString &
string )
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() )
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;
264QVariantMap QgsImportPhotosAlgorithm::parseMetadataList(
const QStringList &input )
267 const thread_local QRegularExpression splitRx( QStringLiteral(
"(.*?)=(.*)" ) );
268 for (
const QString &item : input )
270 const QRegularExpressionMatch match = splitRx.match( item );
271 if ( !match.hasMatch() )
274 const QString tag = match.captured( 1 );
275 QVariant value = parseMetadataValue( match.captured( 2 ) );
277 if ( tag == QLatin1String(
"EXIF_GPSLatitude" ) || tag == QLatin1String(
"EXIF_GPSLongitude" ) )
278 value = parseCoord( value.toString() );
279 results.insert( tag, value );
288 void postProcessLayer( QgsMapLayer *layer, QgsProcessingContext &, QgsProcessingFeedback * )
override
290 if ( QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( layer ) )
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 ) );
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 ) );
313 const QString folder = parameterAsFile( parameters, QStringLiteral(
"FOLDER" ), context );
315 const QDir importDir( folder );
316 if ( !importDir.exists() )
321 const bool recurse = parameterAsBoolean( parameters, QStringLiteral(
"RECURSIVE" ), context );
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 ) );
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 ) );
342 std::unique_ptr<QgsFeatureSink> invalidSink( parameterAsSink( parameters, QStringLiteral(
"INVALID" ), context, invalidDest, invalidFields ) );
344 const QStringList nameFilters {
"*.jpeg",
"*.jpg",
"*.heic" };
349 const QFileInfoList fileInfoList = importDir.entryInfoList( nameFilters, QDir::NoDotAndDotDot | QDir::Files );
350 for (
auto infoIt = fileInfoList.constBegin(); infoIt != fileInfoList.constEnd(); ++infoIt )
352 files.append( infoIt->absoluteFilePath() );
357 QDirIterator it( folder, nameFilters, QDir::NoDotAndDotDot | QDir::Files, QDirIterator::Subdirectories );
358 while ( it.hasNext() )
361 files.append( it.filePath() );
365 auto saveInvalidFile = [&invalidSink, ¶meters](
QgsAttributes &attributes,
bool readable ) {
369 attributes.append( readable );
372 throw QgsProcessingException( writeFeatureError( invalidSink.get(), parameters, QStringLiteral(
"INVALID" ) ) );
376 const double step = files.count() > 0 ? 100.0 / files.count() : 1;
378 for (
const QString &file : files )
388 const QFileInfo fi( file );
390 attributes << QDir::toNativeSeparators( file )
391 << fi.completeBaseName()
392 << QDir::toNativeSeparators( fi.absolutePath() );
397 feedback->
reportError( QObject::tr(
"Could not open %1" ).arg( QDir::toNativeSeparators( file ) ) );
398 saveInvalidFile( attributes,
false );
402 char **GDALmetadata = GDALGetMetadata( hDS.get(),
nullptr );
405 GDALmetadata = GDALGetMetadata( hDS.get(),
"EXIF" );
409 feedback->
reportError( QObject::tr(
"No metadata found in %1" ).arg( QDir::toNativeSeparators( file ) ) );
410 saveInvalidFile( attributes,
true );
421 if ( !extractGeoTagFromMetadata( metadata, tag ) )
424 feedback->
reportError( QObject::tr(
"Could not retrieve geotag for %1" ).arg( QDir::toNativeSeparators( file ) ) );
425 saveInvalidFile( attributes,
true );
429 const QVariant altitude = extractAltitudeFromMetadata( metadata );
435 << extractDirectionFromMetadata( metadata )
436 << extractOrientationFromMetadata( metadata )
439 << extractTimestampFromMetadata( metadata );
442 throw QgsProcessingException( writeFeatureError( outputSink.get(), parameters, QStringLiteral(
"OUTPUT" ) ) );
449 outputSink->finalize();
450 outputs.insert( QStringLiteral(
"OUTPUT" ), outputDest );
460 invalidSink->finalize();
461 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.
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.
std::unique_ptr< std::remove_pointer< GDALDatasetH >::type, GDALDatasetCloser > dataset_unique_ptr
Scoped GDAL dataset.