23#include <QDirIterator>
25#include <QRegularExpression>
28using namespace Qt::StringLiterals;
32QString QgsImportPhotosAlgorithm::name()
const
34 return u
"importphotos"_s;
37QString QgsImportPhotosAlgorithm::displayName()
const
39 return QObject::tr(
"Import geotagged photos" );
42QStringList QgsImportPhotosAlgorithm::tags()
const
44 return QObject::tr(
"exif,metadata,gps,jpeg,jpg" ).split(
',' );
47QString QgsImportPhotosAlgorithm::group()
const
49 return QObject::tr(
"Vector creation" );
52QString QgsImportPhotosAlgorithm::groupId()
const
54 return u
"vectorcreation"_s;
57void QgsImportPhotosAlgorithm::initAlgorithm(
const QVariantMap & )
63 output->setCreateByDefault(
true );
64 addParameter( output.release() );
66 auto invalid = std::make_unique<QgsProcessingParameterFeatureSink>( u
"INVALID"_s, QObject::tr(
"Invalid photos table" ),
Qgis::ProcessingSourceType::Vector, QVariant(),
true );
67 invalid->setCreateByDefault(
false );
68 addParameter( invalid.release() );
71QString QgsImportPhotosAlgorithm::shortHelpString()
const
74 "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"
75 "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 "
76 "to set the point's Z value.\n\n"
77 "Optionally, a table of unreadable or non-geotagged photos can also be created."
81QString QgsImportPhotosAlgorithm::shortDescription()
const
83 return QObject::tr(
"Creates a point layer corresponding to the geotagged locations from JPEG or HEIF/HEIC images from a source folder." );
86QgsImportPhotosAlgorithm *QgsImportPhotosAlgorithm::createInstance()
const
88 return new QgsImportPhotosAlgorithm();
91QVariant QgsImportPhotosAlgorithm::parseMetadataValue(
const QString &value )
93 const thread_local QRegularExpression numRx( u
"^\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*$"_s );
94 const QRegularExpressionMatch numMatch = numRx.match( value );
95 if ( numMatch.hasMatch() )
97 return numMatch.captured( 1 ).toDouble();
102bool QgsImportPhotosAlgorithm::extractGeoTagFromMetadata(
const QVariantMap &metadata,
QgsPointXY &tag )
105 if ( metadata.contains( u
"EXIF_GPSLongitude"_s ) )
108 x = metadata.value( u
"EXIF_GPSLongitude"_s ).toDouble( &ok );
112 if ( QStringView { metadata.value( u
"EXIF_GPSLongitudeRef"_s ).toString() }.right( 1 ).compare(
'W'_L1, Qt::CaseInsensitive ) == 0 || metadata.value( u
"EXIF_GPSLongitudeRef"_s ).toDouble() < 0 )
123 if ( metadata.contains( u
"EXIF_GPSLatitude"_s ) )
126 y = metadata.value( u
"EXIF_GPSLatitude"_s ).toDouble( &ok );
130 if ( QStringView { metadata.value( u
"EXIF_GPSLatitudeRef"_s ).toString() }.right( 1 ).compare(
'S'_L1, Qt::CaseInsensitive ) == 0 || metadata.value( u
"EXIF_GPSLatitudeRef"_s ).toDouble() < 0 )
144QVariant QgsImportPhotosAlgorithm::extractAltitudeFromMetadata(
const QVariantMap &metadata )
147 if ( metadata.contains( u
"EXIF_GPSAltitude"_s ) )
149 double alt = metadata.value( u
"EXIF_GPSAltitude"_s ).toDouble();
150 if ( metadata.contains( u
"EXIF_GPSAltitudeRef"_s ) && ( ( metadata.value( u
"EXIF_GPSAltitudeRef"_s ).userType() == QMetaType::Type::QString && metadata.value( u
"EXIF_GPSAltitudeRef"_s ).toString().right( 1 ) ==
"1"_L1 ) || metadata.value( u
"EXIF_GPSAltitudeRef"_s ).toDouble() < 0 ) )
157QVariant QgsImportPhotosAlgorithm::extractDirectionFromMetadata(
const QVariantMap &metadata )
160 if ( metadata.contains( u
"EXIF_GPSImgDirection"_s ) )
162 direction = metadata.value( u
"EXIF_GPSImgDirection"_s ).toDouble();
167QVariant QgsImportPhotosAlgorithm::extractOrientationFromMetadata(
const QVariantMap &metadata )
169 QVariant orientation;
170 if ( metadata.contains( u
"EXIF_Orientation"_s ) )
172 switch ( metadata.value( u
"EXIF_Orientation"_s ).toInt() )
203QVariant QgsImportPhotosAlgorithm::extractTimestampFromMetadata(
const QVariantMap &metadata )
206 if ( metadata.contains( u
"EXIF_DateTimeOriginal"_s ) )
208 ts = metadata.value( u
"EXIF_DateTimeOriginal"_s );
210 else if ( metadata.contains( u
"EXIF_DateTimeDigitized"_s ) )
212 ts = metadata.value( u
"EXIF_DateTimeDigitized"_s );
214 else if ( metadata.contains( u
"EXIF_DateTime"_s ) )
216 ts = metadata.value( u
"EXIF_DateTime"_s );
222 const thread_local QRegularExpression dsRegEx( u
"(\\d+):(\\d+):(\\d+)\\s+(\\d+):(\\d+):(\\d+)"_s );
223 const QRegularExpressionMatch dsMatch = dsRegEx.match( ts.toString() );
224 if ( dsMatch.hasMatch() )
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 ) );
240QVariant QgsImportPhotosAlgorithm::parseCoord(
const QString &
string )
242 const thread_local QRegularExpression coordRx( u
"^\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*$"_s );
243 const QRegularExpressionMatch coordMatch = coordRx.match(
string );
244 if ( coordMatch.hasMatch() )
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;
257QVariantMap QgsImportPhotosAlgorithm::parseMetadataList(
const QStringList &input )
260 const thread_local QRegularExpression splitRx( u
"(.*?)=(.*)"_s );
261 for (
const QString &item : input )
263 const QRegularExpressionMatch match = splitRx.match( item );
264 if ( !match.hasMatch() )
267 const QString tag = match.captured( 1 );
268 QVariant value = parseMetadataValue( match.captured( 2 ) );
270 if ( tag ==
"EXIF_GPSLatitude"_L1 || tag ==
"EXIF_GPSLongitude"_L1 )
271 value = parseCoord( value.toString() );
272 results.insert( tag, value );
281 void postProcessLayer( QgsMapLayer *layer, QgsProcessingContext &, QgsProcessingFeedback * )
override
283 if ( QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( layer ) )
287 config.insert( u
"DocumentViewer"_s, 1 );
288 config.insert( u
"FileWidget"_s,
true );
289 config.insert( u
"UseLink"_s,
true );
290 config.insert( u
"FullUrl"_s,
true );
291 vl->setEditorWidgetSetup( vl->fields().lookupField( u
"photo"_s ), QgsEditorWidgetSetup( u
"ExternalResource"_s, config ) );
295 config.insert( u
"FileWidgetButton"_s,
true );
296 config.insert( u
"StorageMode"_s, 1 );
297 config.insert( u
"UseLink"_s,
true );
298 config.insert( u
"FullUrl"_s,
true );
299 vl->setEditorWidgetSetup( vl->fields().lookupField( u
"directory"_s ), QgsEditorWidgetSetup( u
"ExternalResource"_s, config ) );
306 QGS_MARK_ALGORITHM_SOURCE
308 const QString folder = parameterAsFile( parameters, u
"FOLDER"_s, context );
310 const QDir importDir( folder );
311 if ( !importDir.exists() )
316 const bool recurse = parameterAsBoolean( parameters, u
"RECURSIVE"_s, context );
319 outFields.
append(
QgsField( u
"photo"_s, QMetaType::Type::QString ) );
320 outFields.
append(
QgsField( u
"filename"_s, QMetaType::Type::QString ) );
321 outFields.
append(
QgsField( u
"directory"_s, QMetaType::Type::QString ) );
322 outFields.
append(
QgsField( u
"altitude"_s, QMetaType::Type::Double ) );
323 outFields.
append(
QgsField( u
"direction"_s, QMetaType::Type::Double ) );
324 outFields.
append(
QgsField( u
"rotation"_s, QMetaType::Type::Int ) );
325 outFields.
append(
QgsField( u
"longitude"_s, QMetaType::Type::QString ) );
326 outFields.
append(
QgsField( u
"latitude"_s, QMetaType::Type::QString ) );
327 outFields.
append(
QgsField( u
"timestamp"_s, QMetaType::Type::QDateTime ) );
332 invalidFields.
append(
QgsField( u
"photo"_s, QMetaType::Type::QString ) );
333 invalidFields.
append(
QgsField( u
"filename"_s, QMetaType::Type::QString ) );
334 invalidFields.
append(
QgsField( u
"directory"_s, QMetaType::Type::QString ) );
335 invalidFields.
append(
QgsField( u
"readable"_s, QMetaType::Type::Bool ) );
337 std::unique_ptr<QgsFeatureSink> invalidSink( parameterAsSink( parameters, u
"INVALID"_s, context, invalidDest, invalidFields ) );
339 const QStringList nameFilters {
"*.jpeg",
"*.jpg",
"*.heic" };
344 const QFileInfoList fileInfoList = importDir.entryInfoList( nameFilters, QDir::NoDotAndDotDot | QDir::Files );
345 for (
auto infoIt = fileInfoList.constBegin(); infoIt != fileInfoList.constEnd(); ++infoIt )
347 files.append( infoIt->absoluteFilePath() );
352 QDirIterator it( folder, nameFilters, QDir::NoDotAndDotDot | QDir::Files, QDirIterator::Subdirectories );
353 while ( it.hasNext() )
356 files.append( it.filePath() );
360 auto saveInvalidFile = [&invalidSink, ¶meters, feedback, invalidDest](
QgsAttributes &attributes,
bool readable ) {
364 attributes.append( readable );
369 feedback->featureAddedToSink( u
"INVALID"_s );
373 const double step = files.count() > 0 ? 100.0 / files.count() : 1;
375 for (
const QString &file : files )
378 if ( feedback->isCanceled() )
383 feedback->setProgress( i * step );
385 const QFileInfo fi( file );
387 attributes << QDir::toNativeSeparators( file ) << fi.completeBaseName() << QDir::toNativeSeparators( fi.absolutePath() );
392 feedback->reportError( QObject::tr(
"Could not open %1" ).arg( QDir::toNativeSeparators( file ) ) );
393 saveInvalidFile( attributes,
false );
397 CSLConstList GDALmetadata = GDALGetMetadata( hDS.get(),
nullptr );
400 GDALmetadata = GDALGetMetadata( hDS.get(),
"EXIF" );
404 feedback->reportError( QObject::tr(
"No metadata found in %1" ).arg( QDir::toNativeSeparators( file ) ) );
405 saveInvalidFile( attributes,
true );
416 if ( !extractGeoTagFromMetadata( metadata, tag ) )
419 feedback->reportError( QObject::tr(
"Could not retrieve geotag for %1" ).arg( QDir::toNativeSeparators( file ) ) );
420 saveInvalidFile( attributes,
true );
424 const QVariant altitude = extractAltitudeFromMetadata( metadata );
428 attributes << altitude << extractDirectionFromMetadata( metadata ) << extractOrientationFromMetadata( metadata ) << tag.
x() << tag.
y() << extractTimestampFromMetadata( metadata );
432 feedback->featureSinkFinalized( u
"OUTPUT"_s );
439 outputSink->finalize();
440 feedback->featureSinkFinalized( u
"OUTPUT"_s );
441 outputs.insert( u
"OUTPUT"_s, outputDest );
451 invalidSink->finalize();
452 feedback->featureSinkFinalized( u
"INVALID"_s );
453 outputs.insert( u
"INVALID"_s, 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.
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(const char *const *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.
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.