21 #include <QDirIterator>
23 #include <QRegularExpression>
27 QString QgsImportPhotosAlgorithm::name()
const
29 return QStringLiteral(
"importphotos" );
32 QString QgsImportPhotosAlgorithm::displayName()
const
34 return QObject::tr(
"Import geotagged photos" );
37 QStringList QgsImportPhotosAlgorithm::tags()
const
39 return QObject::tr(
"exif,metadata,gps,jpeg,jpg" ).split(
',' );
42 QString QgsImportPhotosAlgorithm::group()
const
44 return QObject::tr(
"Vector creation" );
47 QString QgsImportPhotosAlgorithm::groupId()
const
49 return QStringLiteral(
"vectorcreation" );
52 void QgsImportPhotosAlgorithm::initAlgorithm(
const QVariantMap & )
57 std::unique_ptr< QgsProcessingParameterFeatureSink > output = std::make_unique< QgsProcessingParameterFeatureSink >( QStringLiteral(
"OUTPUT" ), QObject::tr(
"Photos" ),
QgsProcessing::TypeVectorPoint, QVariant(),
true );
58 output->setCreateByDefault(
true );
59 addParameter( output.release() );
61 std::unique_ptr< QgsProcessingParameterFeatureSink > invalid = std::make_unique< QgsProcessingParameterFeatureSink >( QStringLiteral(
"INVALID" ), QObject::tr(
"Invalid photos table" ),
QgsProcessing::TypeVector, QVariant(),
true );
62 invalid->setCreateByDefault(
false );
63 addParameter( invalid.release() );
66 QString QgsImportPhotosAlgorithm::shortHelpString()
const
68 return QObject::tr(
"Creates a point layer corresponding to the geotagged locations from JPEG 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." );
74 QgsImportPhotosAlgorithm *QgsImportPhotosAlgorithm::createInstance()
const
76 return new QgsImportPhotosAlgorithm();
79 QVariant QgsImportPhotosAlgorithm::parseMetadataValue(
const QString &value )
81 QRegularExpression numRx( QStringLiteral(
"^\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*$" ) );
82 QRegularExpressionMatch numMatch = numRx.match( value );
83 if ( numMatch.hasMatch() )
85 return numMatch.captured( 1 ).toDouble();
90 bool QgsImportPhotosAlgorithm::extractGeoTagFromMetadata(
const QVariantMap &metadata,
QgsPointXY &tag )
93 if ( metadata.contains( QStringLiteral(
"EXIF_GPSLongitude" ) ) )
96 x = metadata.value( QStringLiteral(
"EXIF_GPSLongitude" ) ).toDouble( &ok );
100 if ( metadata.value( QStringLiteral(
"EXIF_GPSLongitudeRef" ) ).toString().rightRef( 1 ).compare( QLatin1String(
"W" ), Qt::CaseInsensitive ) == 0
101 || metadata.value( QStringLiteral(
"EXIF_GPSLongitudeRef" ) ).toDouble() < 0 )
110 if ( metadata.contains( QStringLiteral(
"EXIF_GPSLatitude" ) ) )
113 y = metadata.value( QStringLiteral(
"EXIF_GPSLatitude" ) ).toDouble( &ok );
117 if ( metadata.value( QStringLiteral(
"EXIF_GPSLatitudeRef" ) ).toString().rightRef( 1 ).compare( QLatin1String(
"S" ), Qt::CaseInsensitive ) == 0
118 || metadata.value( QStringLiteral(
"EXIF_GPSLatitudeRef" ) ).toDouble() < 0 )
130 QVariant QgsImportPhotosAlgorithm::extractAltitudeFromMetadata(
const QVariantMap &metadata )
133 if ( metadata.contains( QStringLiteral(
"EXIF_GPSAltitude" ) ) )
135 double alt = metadata.value( QStringLiteral(
"EXIF_GPSAltitude" ) ).toDouble();
136 if ( metadata.contains( QStringLiteral(
"EXIF_GPSAltitudeRef" ) ) &&
137 ( ( metadata.value( QStringLiteral(
"EXIF_GPSAltitudeRef" ) ).type() == QVariant::String && metadata.value( QStringLiteral(
"EXIF_GPSAltitudeRef" ) ).toString().right( 1 ) == QLatin1String(
"1" ) )
138 || metadata.value( QStringLiteral(
"EXIF_GPSAltitudeRef" ) ).toDouble() < 0 ) )
145 QVariant QgsImportPhotosAlgorithm::extractDirectionFromMetadata(
const QVariantMap &metadata )
148 if ( metadata.contains( QStringLiteral(
"EXIF_GPSImgDirection" ) ) )
150 direction = metadata.value( QStringLiteral(
"EXIF_GPSImgDirection" ) ).toDouble();
155 QVariant QgsImportPhotosAlgorithm::extractOrientationFromMetadata(
const QVariantMap &metadata )
157 QVariant orientation;
158 if ( metadata.contains( QStringLiteral(
"EXIF_Orientation" ) ) )
160 switch ( metadata.value( QStringLiteral(
"EXIF_Orientation" ) ).toInt() )
191 QVariant QgsImportPhotosAlgorithm::extractTimestampFromMetadata(
const QVariantMap &metadata )
194 if ( metadata.contains( QStringLiteral(
"EXIF_DateTimeOriginal" ) ) )
196 ts = metadata.value( QStringLiteral(
"EXIF_DateTimeOriginal" ) );
198 else if ( metadata.contains( QStringLiteral(
"EXIF_DateTimeDigitized" ) ) )
200 ts = metadata.value( QStringLiteral(
"EXIF_DateTimeDigitized" ) );
202 else if ( metadata.contains( QStringLiteral(
"EXIF_DateTime" ) ) )
204 ts = metadata.value( QStringLiteral(
"EXIF_DateTime" ) );
210 QRegularExpression dsRegEx( QStringLiteral(
"(\\d+):(\\d+):(\\d+)\\s+(\\d+):(\\d+):(\\d+)" ) );
211 QRegularExpressionMatch dsMatch = dsRegEx.match( ts.toString() );
212 if ( dsMatch.hasMatch() )
214 int year = dsMatch.captured( 1 ).toInt();
215 int month = dsMatch.captured( 2 ).toInt();
216 int day = dsMatch.captured( 3 ).toInt();
217 int hour = dsMatch.captured( 4 ).toInt();
218 int min = dsMatch.captured( 5 ).toInt();
219 int sec = dsMatch.captured( 6 ).toInt();
220 return QDateTime( QDate( year, month, day ), QTime( hour, min, sec ) );
228 QVariant QgsImportPhotosAlgorithm::parseCoord(
const QString &
string )
230 QRegularExpression coordRx( QStringLiteral(
"^\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*$" ) );
231 QRegularExpressionMatch coordMatch = coordRx.match(
string );
232 if ( coordMatch.hasMatch() )
234 double hours = coordMatch.captured( 1 ).toDouble();
235 double minutes = coordMatch.captured( 2 ).toDouble();
236 double seconds = coordMatch.captured( 3 ).toDouble();
237 return hours + minutes / 60.0 + seconds / 3600.0;
245 QVariantMap QgsImportPhotosAlgorithm::parseMetadataList(
const QStringList &input )
248 QRegularExpression splitRx( QStringLiteral(
"(.*?)=(.*)" ) );
249 for (
const QString &item : input )
251 QRegularExpressionMatch match = splitRx.match( item );
252 if ( !match.hasMatch() )
255 QString tag = match.captured( 1 );
256 QVariant value = parseMetadataValue( match.captured( 2 ) );
258 if ( tag == QLatin1String(
"EXIF_GPSLatitude" ) || tag == QLatin1String(
"EXIF_GPSLongitude" ) )
259 value = parseCoord( value.toString() );
260 results.insert( tag, value );
272 if (
QgsVectorLayer *vl = qobject_cast< QgsVectorLayer * >( layer ) )
276 config.insert( QStringLiteral(
"DocumentViewer" ), 1 );
277 config.insert( QStringLiteral(
"FileWidget" ),
true );
278 config.insert( QStringLiteral(
"UseLink" ),
true );
279 config.insert( QStringLiteral(
"FullUrl" ),
true );
280 vl->setEditorWidgetSetup( vl->fields().lookupField( QStringLiteral(
"photo" ) ),
QgsEditorWidgetSetup( QStringLiteral(
"ExternalResource" ), config ) );
284 config.insert( QStringLiteral(
"FileWidgetButton" ),
true );
285 config.insert( QStringLiteral(
"StorageMode" ), 1 );
286 config.insert( QStringLiteral(
"UseLink" ),
true );
287 config.insert( QStringLiteral(
"FullUrl" ),
true );
288 vl->setEditorWidgetSetup( vl->fields().lookupField( QStringLiteral(
"directory" ) ),
QgsEditorWidgetSetup( QStringLiteral(
"ExternalResource" ), config ) );
295 QString folder = parameterAsFile( parameters, QStringLiteral(
"FOLDER" ), context );
297 QDir importDir( folder );
298 if ( !importDir.exists() )
303 bool recurse = parameterAsBoolean( parameters, QStringLiteral(
"RECURSIVE" ), context );
306 outFields.
append(
QgsField( QStringLiteral(
"photo" ), QVariant::String ) );
307 outFields.
append(
QgsField( QStringLiteral(
"filename" ), QVariant::String ) );
308 outFields.
append(
QgsField( QStringLiteral(
"directory" ), QVariant::String ) );
309 outFields.
append(
QgsField( QStringLiteral(
"altitude" ), QVariant::Double ) );
310 outFields.
append(
QgsField( QStringLiteral(
"direction" ), QVariant::Double ) );
311 outFields.
append(
QgsField( QStringLiteral(
"rotation" ), QVariant::Int ) );
312 outFields.
append(
QgsField( QStringLiteral(
"longitude" ), QVariant::String ) );
313 outFields.
append(
QgsField( QStringLiteral(
"latitude" ), QVariant::String ) );
314 outFields.
append(
QgsField( QStringLiteral(
"timestamp" ), QVariant::DateTime ) );
316 std::unique_ptr< QgsFeatureSink > outputSink( parameterAsSink( parameters, QStringLiteral(
"OUTPUT" ), context, outputDest, outFields,
320 invalidFields.
append(
QgsField( QStringLiteral(
"photo" ), QVariant::String ) );
321 invalidFields.
append(
QgsField( QStringLiteral(
"filename" ), QVariant::String ) );
322 invalidFields.
append(
QgsField( QStringLiteral(
"directory" ), QVariant::String ) );
323 invalidFields.
append(
QgsField( QStringLiteral(
"readable" ), QVariant::Bool ) );
325 std::unique_ptr< QgsFeatureSink > invalidSink( parameterAsSink( parameters, QStringLiteral(
"INVALID" ), context, invalidDest, invalidFields ) );
327 QStringList nameFilters {
"*.jpeg",
"*.jpg" };
332 QFileInfoList fileInfoList = importDir.entryInfoList( nameFilters, QDir::NoDotAndDotDot | QDir::Files );
333 for (
auto infoIt = fileInfoList.constBegin(); infoIt != fileInfoList.constEnd(); ++infoIt )
335 files.append( infoIt->absoluteFilePath() );
340 QDirIterator it( folder, nameFilters, QDir::NoDotAndDotDot | QDir::Files, QDirIterator::Subdirectories );
341 while ( it.hasNext() )
344 files.append( it.filePath() );
348 auto saveInvalidFile = [&invalidSink](
QgsAttributes & attributes,
bool readable )
353 attributes.append( readable );
359 double step = files.count() > 0 ? 100.0 / files.count() : 1;
361 for (
const QString &file : files )
371 QFileInfo fi( file );
373 attributes << QDir::toNativeSeparators( file )
374 << fi.completeBaseName()
375 << QDir::toNativeSeparators( fi.absolutePath() );
380 feedback->
reportError( QObject::tr(
"Could not open %1" ).arg( QDir::toNativeSeparators( file ) ) );
381 saveInvalidFile( attributes,
false );
385 if (
char **GDALmetadata = GDALGetMetadata( hDS.get(),
nullptr ) )
394 if ( !extractGeoTagFromMetadata( metadata, tag ) )
397 feedback->
reportError( QObject::tr(
"Could not retrieve geotag for %1" ).arg( QDir::toNativeSeparators( file ) ) );
398 saveInvalidFile( attributes,
true );
402 QVariant altitude = extractAltitudeFromMetadata( metadata );
408 << extractDirectionFromMetadata( metadata )
409 << extractOrientationFromMetadata( metadata )
412 << extractTimestampFromMetadata( metadata );
418 feedback->
reportError( QObject::tr(
"No metadata found in %1" ).arg( QDir::toNativeSeparators( file ) ) );
419 saveInvalidFile( attributes,
true );
426 outputs.insert( QStringLiteral(
"OUTPUT" ), outputDest );
435 outputs.insert( QStringLiteral(
"INVALID" ), invalidDest );
This class 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 SIP_HOLDGIL
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, FieldOrigin origin=OriginProvider, int originIndex=-1)
Appends a field. The field must have unique name, otherwise it is rejected (returns false)
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.
A class to represent a 2D point.
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.
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...
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...
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.
@ Folder
Parameter is a folder.
@ TypeVector
Tables (i.e. vector layers with or without geometry). When used for a sink this indicates the sink ha...
@ TypeVectorPoint
Vector point layers.
Represents a vector layer which manages a vector based data sets.
std::unique_ptr< std::remove_pointer< GDALDatasetH >::type, GDALDatasetCloser > dataset_unique_ptr
Scoped GDAL dataset.