24 QString QgsImportPhotosAlgorithm::name()
const
26 return QStringLiteral(
"importphotos" );
29 QString QgsImportPhotosAlgorithm::displayName()
const
31 return QObject::tr(
"Import geotagged photos" );
34 QStringList QgsImportPhotosAlgorithm::tags()
const
36 return QObject::tr(
"exif,metadata,gps,jpeg,jpg" ).split(
',' );
39 QString QgsImportPhotosAlgorithm::group()
const
41 return QObject::tr(
"Vector creation" );
44 QString QgsImportPhotosAlgorithm::groupId()
const
46 return QStringLiteral(
"vectorcreation" );
49 void QgsImportPhotosAlgorithm::initAlgorithm(
const QVariantMap & )
54 std::unique_ptr< QgsProcessingParameterFeatureSink > output = qgis::make_unique< QgsProcessingParameterFeatureSink >( QStringLiteral(
"OUTPUT" ), QObject::tr(
"Photos" ),
QgsProcessing::TypeVectorPoint, QVariant(),
true );
55 output->setCreateByDefault(
true );
56 addParameter( output.release() );
58 std::unique_ptr< QgsProcessingParameterFeatureSink > invalid = qgis::make_unique< QgsProcessingParameterFeatureSink >( QStringLiteral(
"INVALID" ), QObject::tr(
"Invalid photos table" ),
QgsProcessing::TypeVector, QVariant(),
true );
59 invalid->setCreateByDefault(
false );
60 addParameter( invalid.release() );
63 QString QgsImportPhotosAlgorithm::shortHelpString()
const
65 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"
66 "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 "
67 "to set the point's Z value.\n\n"
68 "Optionally, a table of unreadable or non-geotagged photos can also be created." );
71 QgsImportPhotosAlgorithm *QgsImportPhotosAlgorithm::createInstance()
const
73 return new QgsImportPhotosAlgorithm();
76 QVariant QgsImportPhotosAlgorithm::parseMetadataValue(
const QString &value )
78 QRegularExpression numRx( QStringLiteral(
"^\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*$" ) );
79 QRegularExpressionMatch numMatch = numRx.match( value );
80 if ( numMatch.hasMatch() )
82 return numMatch.captured( 1 ).toDouble();
87 bool QgsImportPhotosAlgorithm::extractGeoTagFromMetadata(
const QVariantMap &metadata,
QgsPointXY &tag )
90 if ( metadata.contains( QStringLiteral(
"EXIF_GPSLongitude" ) ) )
93 x = metadata.value( QStringLiteral(
"EXIF_GPSLongitude" ) ).toDouble( &ok );
97 if ( metadata.value( QStringLiteral(
"EXIF_GPSLongitudeRef" ) ).toString().rightRef( 1 ).compare( QLatin1String(
"W" ), Qt::CaseInsensitive ) == 0
98 || metadata.value( QStringLiteral(
"EXIF_GPSLongitudeRef" ) ).toDouble() < 0 )
107 if ( metadata.contains( QStringLiteral(
"EXIF_GPSLatitude" ) ) )
110 y = metadata.value( QStringLiteral(
"EXIF_GPSLatitude" ) ).toDouble( &ok );
114 if ( metadata.value( QStringLiteral(
"EXIF_GPSLatitudeRef" ) ).toString().rightRef( 1 ).compare( QLatin1String(
"S" ), Qt::CaseInsensitive ) == 0
115 || metadata.value( QStringLiteral(
"EXIF_GPSLatitudeRef" ) ).toDouble() < 0 )
127 QVariant QgsImportPhotosAlgorithm::extractAltitudeFromMetadata(
const QVariantMap &metadata )
130 if ( metadata.contains( QStringLiteral(
"EXIF_GPSAltitude" ) ) )
132 double alt = metadata.value( QStringLiteral(
"EXIF_GPSAltitude" ) ).toDouble();
133 if ( metadata.contains( QStringLiteral(
"EXIF_GPSAltitudeRef" ) ) &&
134 ( ( metadata.value( QStringLiteral(
"EXIF_GPSAltitudeRef" ) ).type() == QVariant::String && metadata.value( QStringLiteral(
"EXIF_GPSAltitudeRef" ) ).toString().right( 1 ) == QLatin1String(
"1" ) )
135 || metadata.value( QStringLiteral(
"EXIF_GPSAltitudeRef" ) ).toDouble() < 0 ) )
142 QVariant QgsImportPhotosAlgorithm::extractDirectionFromMetadata(
const QVariantMap &metadata )
145 if ( metadata.contains( QStringLiteral(
"EXIF_GPSImgDirection" ) ) )
147 direction = metadata.value( QStringLiteral(
"EXIF_GPSImgDirection" ) ).toDouble();
152 QVariant QgsImportPhotosAlgorithm::extractTimestampFromMetadata(
const QVariantMap &metadata )
155 if ( metadata.contains( QStringLiteral(
"EXIF_DateTimeOriginal" ) ) )
157 ts = metadata.value( QStringLiteral(
"EXIF_DateTimeOriginal" ) );
159 else if ( metadata.contains( QStringLiteral(
"EXIF_DateTimeDigitized" ) ) )
161 ts = metadata.value( QStringLiteral(
"EXIF_DateTimeDigitized" ) );
163 else if ( metadata.contains( QStringLiteral(
"EXIF_DateTime" ) ) )
165 ts = metadata.value( QStringLiteral(
"EXIF_DateTime" ) );
171 QRegularExpression dsRegEx( QStringLiteral(
"(\\d+):(\\d+):(\\d+)\\s+(\\d+):(\\d+):(\\d+)" ) );
172 QRegularExpressionMatch dsMatch = dsRegEx.match( ts.toString() );
173 if ( dsMatch.hasMatch() )
175 int year = dsMatch.captured( 1 ).toInt();
176 int month = dsMatch.captured( 2 ).toInt();
177 int day = dsMatch.captured( 3 ).toInt();
178 int hour = dsMatch.captured( 4 ).toInt();
179 int min = dsMatch.captured( 5 ).toInt();
180 int sec = dsMatch.captured( 6 ).toInt();
181 return QDateTime( QDate( year, month, day ), QTime( hour, min, sec ) );
189 QVariant QgsImportPhotosAlgorithm::parseCoord(
const QString &
string )
191 QRegularExpression coordRx( QStringLiteral(
"^\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*\\(\\s*([-\\.\\d]+)\\s*\\)\\s*$" ) );
192 QRegularExpressionMatch coordMatch = coordRx.match(
string );
193 if ( coordMatch.hasMatch() )
195 double hours = coordMatch.captured( 1 ).toDouble();
196 double minutes = coordMatch.captured( 2 ).toDouble();
197 double seconds = coordMatch.captured( 3 ).toDouble();
198 return hours + minutes / 60.0 + seconds / 3600.0;
206 QVariantMap QgsImportPhotosAlgorithm::parseMetadataList(
const QStringList &input )
209 QRegularExpression splitRx( QStringLiteral(
"(.*?)=(.*)" ) );
210 for (
const QString &item : input )
212 QRegularExpressionMatch match = splitRx.match( item );
213 if ( !match.hasMatch() )
216 QString tag = match.captured( 1 );
217 QVariant value = parseMetadataValue( match.captured( 2 ) );
219 if ( tag == QLatin1String(
"EXIF_GPSLatitude" ) || tag == QLatin1String(
"EXIF_GPSLongitude" ) )
220 value = parseCoord( value.toString() );
221 results.insert( tag, value );
233 if (
QgsVectorLayer *vl = qobject_cast< QgsVectorLayer * >( layer ) )
237 config.insert( QStringLiteral(
"DocumentViewer" ), 1 );
238 config.insert( QStringLiteral(
"FileWidget" ),
true );
239 vl->setEditorWidgetSetup( 0,
QgsEditorWidgetSetup( QStringLiteral(
"ExternalResource" ), config ) );
243 config.insert( QStringLiteral(
"FileWidgetButton" ),
true );
244 config.insert( QStringLiteral(
"StorageMode" ), 1 );
245 vl->setEditorWidgetSetup( 2,
QgsEditorWidgetSetup( QStringLiteral(
"ExternalResource" ), config ) );
252 QString folder = parameterAsFile( parameters, QStringLiteral(
"FOLDER" ), context );
254 QDir importDir( folder );
255 if ( !importDir.exists() )
260 bool recurse = parameterAsBoolean( parameters, QStringLiteral(
"RECURSIVE" ), context );
263 outFields.
append(
QgsField( QStringLiteral(
"photo" ), QVariant::String ) );
264 outFields.
append(
QgsField( QStringLiteral(
"filename" ), QVariant::String ) );
265 outFields.
append(
QgsField( QStringLiteral(
"directory" ), QVariant::String ) );
266 outFields.
append(
QgsField( QStringLiteral(
"altitude" ), QVariant::Double ) );
267 outFields.
append(
QgsField( QStringLiteral(
"direction" ), QVariant::Double ) );
268 outFields.
append(
QgsField( QStringLiteral(
"longitude" ), QVariant::String ) );
269 outFields.
append(
QgsField( QStringLiteral(
"latitude" ), QVariant::String ) );
270 outFields.
append(
QgsField( QStringLiteral(
"timestamp" ), QVariant::DateTime ) );
272 std::unique_ptr< QgsFeatureSink > outputSink( parameterAsSink( parameters, QStringLiteral(
"OUTPUT" ), context, outputDest, outFields,
276 invalidFields.
append(
QgsField( QStringLiteral(
"photo" ), QVariant::String ) );
277 invalidFields.
append(
QgsField( QStringLiteral(
"filename" ), QVariant::String ) );
278 invalidFields.
append(
QgsField( QStringLiteral(
"directory" ), QVariant::String ) );
279 invalidFields.
append(
QgsField( QStringLiteral(
"readable" ), QVariant::Bool ) );
281 std::unique_ptr< QgsFeatureSink > invalidSink( parameterAsSink( parameters, QStringLiteral(
"INVALID" ), context, invalidDest, invalidFields ) );
283 QStringList nameFilters {
"*.jpeg",
"*.jpg" };
288 QFileInfoList fileInfoList = importDir.entryInfoList( nameFilters, QDir::NoDotAndDotDot | QDir::Files );
289 for (
auto infoIt = fileInfoList.constBegin(); infoIt != fileInfoList.constEnd(); ++infoIt )
291 files.append( infoIt->absoluteFilePath() );
296 QDirIterator it( folder, nameFilters, QDir::NoDotAndDotDot | QDir::Files, QDirIterator::Subdirectories );
297 while ( it.hasNext() )
300 files.append( it.filePath() );
304 auto saveInvalidFile = [&invalidSink](
QgsAttributes & attributes,
bool readable )
309 attributes.append( readable );
315 double step = files.count() > 0 ? 100.0 / files.count() : 1;
317 for (
const QString &file : files )
327 QFileInfo fi( file );
329 attributes << QDir::toNativeSeparators( file )
330 << fi.completeBaseName()
331 << QDir::toNativeSeparators( fi.absolutePath() );
336 feedback->
reportError( QObject::tr(
"Could not open %1" ).arg( QDir::toNativeSeparators( file ) ) );
337 saveInvalidFile( attributes,
false );
341 if (
char **GDALmetadata = GDALGetMetadata( hDS.get(),
nullptr ) )
350 if ( !extractGeoTagFromMetadata( metadata, tag ) )
353 feedback->
reportError( QObject::tr(
"Could not retrieve geotag for %1" ).arg( QDir::toNativeSeparators( file ) ) );
354 saveInvalidFile( attributes,
true );
358 QVariant altitude = extractAltitudeFromMetadata( metadata );
364 << extractDirectionFromMetadata( metadata )
367 << extractTimestampFromMetadata( metadata );
373 feedback->
reportError( QObject::tr(
"No metadata found in %1" ).arg( QDir::toNativeSeparators( file ) ) );
374 saveInvalidFile( attributes,
true );
381 outputs.insert( QStringLiteral(
"OUTPUT" ), outputDest );
390 outputs.insert( QStringLiteral(
"INVALID" ), invalidDest );