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.