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.