Location tag fixes (#993)
This commit is contained in:
commit
d83607b574
|
@ -60,7 +60,7 @@ const publicLinkDeviceLimits = [50, 25, 10, 5, 2, 1];
|
|||
|
||||
const kilometersPerDegree = 111.16;
|
||||
|
||||
const radiusValues = <int>[2, 10, 20, 40, 80, 200, 400, 1200];
|
||||
const radiusValues = <int>[1, 2, 10, 20, 40, 80, 200, 400, 1200];
|
||||
|
||||
const defaultRadiusValueIndex = 4;
|
||||
|
||||
|
|
|
@ -1375,7 +1375,8 @@ class FilesDB {
|
|||
|
||||
Future<List<File>> getAllFilesFromDB(Set<int> collectionsToIgnore) async {
|
||||
final db = await instance.database;
|
||||
final List<Map<String, dynamic>> result = await db.query(filesTable);
|
||||
final List<Map<String, dynamic>> result =
|
||||
await db.query(filesTable, orderBy: '$columnCreationTime DESC');
|
||||
final List<File> files = convertToFiles(result);
|
||||
final List<File> deduplicatedFiles =
|
||||
_deduplicatedAndFilterIgnoredFiles(files, collectionsToIgnore);
|
||||
|
@ -1463,11 +1464,16 @@ class FilesDB {
|
|||
row[columnMMdVisibility] = file.magicMetadata.visibility;
|
||||
row[columnPubMMdVersion] = file.pubMmdVersion;
|
||||
row[columnPubMMdEncodedJson] = file.pubMmdEncodedJson ?? '{}';
|
||||
if (file.pubMagicMetadata != null &&
|
||||
file.pubMagicMetadata!.editedTime != null) {
|
||||
// override existing creationTime to avoid re-writing all queries related
|
||||
// to loading the gallery
|
||||
row[columnCreationTime] = file.pubMagicMetadata!.editedTime;
|
||||
// override existing fields to avoid re-writing all queries and logic
|
||||
if (file.pubMagicMetadata != null) {
|
||||
if (file.pubMagicMetadata!.editedTime != null) {
|
||||
row[columnCreationTime] = file.pubMagicMetadata!.editedTime;
|
||||
}
|
||||
if (file.pubMagicMetadata!.lat != null &&
|
||||
file.pubMagicMetadata!.long != null) {
|
||||
row[columnLatitude] = file.pubMagicMetadata!.lat;
|
||||
row[columnLongitude] = file.pubMagicMetadata!.long;
|
||||
}
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
|
|
@ -179,12 +179,25 @@ class File extends EnteFile {
|
|||
}
|
||||
}
|
||||
bool hasExifTime = false;
|
||||
if (fileType == FileType.image && mediaUploadData.sourceFile != null) {
|
||||
final exifTime =
|
||||
await getCreationTimeFromEXIF(mediaUploadData.sourceFile!);
|
||||
if (exifTime != null) {
|
||||
hasExifTime = true;
|
||||
creationTime = exifTime.microsecondsSinceEpoch;
|
||||
if ((fileType == FileType.image || fileType == FileType.video) &&
|
||||
mediaUploadData.sourceFile != null) {
|
||||
final exifData = await getExifFromSourceFile(mediaUploadData.sourceFile!);
|
||||
if (exifData != null) {
|
||||
if (fileType == FileType.image) {
|
||||
final exifTime = await getCreationTimeFromEXIF(null, exifData);
|
||||
if (exifTime != null) {
|
||||
hasExifTime = true;
|
||||
creationTime = exifTime.microsecondsSinceEpoch;
|
||||
}
|
||||
}
|
||||
if (Platform.isAndroid) {
|
||||
//Fix for missing location data in lower android versions.
|
||||
final Location? exifLocation = locationFromExif(exifData);
|
||||
if (exifLocation?.latitude != null &&
|
||||
exifLocation?.longitude != null) {
|
||||
location = exifLocation;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Try to get the timestamp from fileName. In case of iOS, file names are
|
||||
|
@ -289,7 +302,7 @@ class File extends EnteFile {
|
|||
|
||||
bool get hasLocation {
|
||||
return location != null &&
|
||||
(location!.longitude != 0 || location!.latitude != 0);
|
||||
((location!.longitude ?? 0) != 0 || (location!.latitude ?? 0) != 0);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -17,6 +17,8 @@ const pubMagicKeyEditedTime = 'editedTime';
|
|||
const pubMagicKeyEditedName = 'editedName';
|
||||
const pubMagicKeyCaption = "caption";
|
||||
const pubMagicKeyUploaderName = "uploaderName";
|
||||
const pubMagicKeyLat = "lat";
|
||||
const pubMagicKeyLong = "long";
|
||||
|
||||
class MagicMetadata {
|
||||
// 0 -> visible
|
||||
|
@ -44,12 +46,16 @@ class PubMagicMetadata {
|
|||
String? editedName;
|
||||
String? caption;
|
||||
String? uploaderName;
|
||||
double? lat;
|
||||
double? long;
|
||||
|
||||
PubMagicMetadata({
|
||||
this.editedTime,
|
||||
this.editedName,
|
||||
this.caption,
|
||||
this.uploaderName,
|
||||
this.lat,
|
||||
this.long,
|
||||
});
|
||||
|
||||
factory PubMagicMetadata.fromEncodedJson(String encodedJson) =>
|
||||
|
@ -65,6 +71,8 @@ class PubMagicMetadata {
|
|||
editedName: map[pubMagicKeyEditedName],
|
||||
caption: map[pubMagicKeyCaption],
|
||||
uploaderName: map[pubMagicKeyUploaderName],
|
||||
lat: map[pubMagicKeyLat],
|
||||
long: map[pubMagicKeyLong],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -198,19 +198,23 @@ class LocationService {
|
|||
}
|
||||
|
||||
class GPSData {
|
||||
final String latRef;
|
||||
final List<double> lat;
|
||||
final String longRef;
|
||||
final List<double> long;
|
||||
final String? latRef;
|
||||
final List<double>? lat;
|
||||
final String? longRef;
|
||||
final List<double>? long;
|
||||
|
||||
GPSData(this.latRef, this.lat, this.longRef, this.long);
|
||||
|
||||
Location toLocationObj() {
|
||||
final latSign = latRef == "N" ? 1 : -1;
|
||||
final longSign = longRef == "E" ? 1 : -1;
|
||||
return Location(
|
||||
latitude: latSign * lat[0] + lat[1] / 60 + lat[2] / 3600,
|
||||
longitude: longSign * long[0] + long[1] / 60 + long[2] / 3600,
|
||||
);
|
||||
Location? toLocationObj() {
|
||||
if (lat == null || long == null || latRef == null || longRef == null) {
|
||||
return null;
|
||||
} else {
|
||||
final latSign = (latRef == "N" ? 1 : -1);
|
||||
final longSign = (longRef == "E" ? 1 : -1);
|
||||
return Location(
|
||||
latitude: latSign * lat![0] + lat![1] / 60 + lat![2] / 3600,
|
||||
longitude: longSign * long![0] + long![1] / 60 + long![2] / 3600,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import "package:exif/exif.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/configuration.dart";
|
||||
import "package:photos/models/file.dart";
|
||||
import "package:photos/models/file_type.dart";
|
||||
import "package:photos/models/magic_metadata.dart";
|
||||
import "package:photos/services/feature_flag_service.dart";
|
||||
import "package:photos/services/file_magic_service.dart";
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
import 'package:photos/ui/components/buttons/icon_button_widget.dart';
|
||||
import "package:photos/ui/components/divider_widget.dart";
|
||||
|
@ -50,20 +52,23 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
|||
bool _isImage = false;
|
||||
late int _currentUserID;
|
||||
bool showExifListTile = false;
|
||||
bool hasLocationData = false;
|
||||
final ValueNotifier<bool> hasLocationData = ValueNotifier(false);
|
||||
final Logger _logger = Logger("_FileDetailsWidgetState");
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
debugPrint('file_details_sheet initState');
|
||||
_currentUserID = Configuration.instance.getUserID()!;
|
||||
hasLocationData.value = widget.file.hasLocation;
|
||||
_isImage = widget.file.fileType == FileType.image ||
|
||||
widget.file.fileType == FileType.livePhoto;
|
||||
|
||||
_exifNotifier.addListener(() {
|
||||
if (_exifNotifier.value != null) {
|
||||
_generateExifForLocation(_exifNotifier.value!);
|
||||
hasLocationData = _hasLocationData();
|
||||
if (_exifNotifier.value != null && !widget.file.hasLocation) {
|
||||
_updateLocationFromExif(_exifNotifier.value!).ignore();
|
||||
}
|
||||
});
|
||||
|
||||
if (_isImage) {
|
||||
_exifNotifier.addListener(() {
|
||||
if (_exifNotifier.value != null) {
|
||||
|
@ -137,25 +142,23 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
|||
},
|
||||
),
|
||||
);
|
||||
if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) {
|
||||
fileDetailsTiles.addAll([
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _exifNotifier,
|
||||
builder: (context, _, __) {
|
||||
return hasLocationData
|
||||
? Column(
|
||||
children: [
|
||||
LocationTagsWidget(
|
||||
widget.file.location!,
|
||||
),
|
||||
const FileDetailsDivider(),
|
||||
],
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
)
|
||||
]);
|
||||
}
|
||||
fileDetailsTiles.addAll([
|
||||
ValueListenableBuilder(
|
||||
valueListenable: hasLocationData,
|
||||
builder: (context, bool value, __) {
|
||||
return value
|
||||
? Column(
|
||||
children: [
|
||||
LocationTagsWidget(
|
||||
widget.file.location!,
|
||||
),
|
||||
const FileDetailsDivider(),
|
||||
],
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
)
|
||||
]);
|
||||
if (_isImage) {
|
||||
fileDetailsTiles.addAll([
|
||||
ValueListenableBuilder(
|
||||
|
@ -231,35 +234,30 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
|
|||
);
|
||||
}
|
||||
|
||||
bool _hasLocationData() {
|
||||
final fileLocation = widget.file.location;
|
||||
final hasLocation = (fileLocation != null &&
|
||||
fileLocation.latitude != null &&
|
||||
fileLocation.longitude != null) &&
|
||||
(fileLocation.latitude != 0 || fileLocation.longitude != 0);
|
||||
return hasLocation;
|
||||
}
|
||||
|
||||
void _generateExifForLocation(Map<String, IfdTag> exif) {
|
||||
if (exif["GPS GPSLatitude"] != null) {
|
||||
_exifData["lat"] = exif["GPS GPSLatitude"]!
|
||||
.values
|
||||
.toList()
|
||||
.map((e) => ((e as Ratio).numerator / e.denominator))
|
||||
.toList();
|
||||
//This code is for updating the location of files in which location data is
|
||||
//missing and the EXIF has location data. This is only happens for a
|
||||
//certain specific minority of devices.
|
||||
Future<void> _updateLocationFromExif(Map<String, IfdTag> exif) async {
|
||||
// If the file is not uploaded or the file is not owned by the current user
|
||||
// then we don't need to update the location.
|
||||
if (!widget.file.isUploaded || widget.file.ownerID! != _currentUserID) {
|
||||
return;
|
||||
}
|
||||
if (exif["GPS GPSLongitude"] != null) {
|
||||
_exifData["long"] = exif["GPS GPSLongitude"]!
|
||||
.values
|
||||
.toList()
|
||||
.map((e) => ((e as Ratio).numerator / e.denominator))
|
||||
.toList();
|
||||
}
|
||||
if (exif["GPS GPSLatitudeRef"] != null) {
|
||||
_exifData["latRef"] = exif["GPS GPSLatitudeRef"].toString();
|
||||
}
|
||||
if (exif["GPS GPSLongitudeRef"] != null) {
|
||||
_exifData["longRef"] = exif["GPS GPSLongitudeRef"].toString();
|
||||
try {
|
||||
final locationDataFromExif = locationFromExif(exif);
|
||||
if (locationDataFromExif?.latitude != null &&
|
||||
locationDataFromExif?.longitude != null) {
|
||||
widget.file.location = locationDataFromExif;
|
||||
await FileMagicService.instance.updatePublicMagicMetadata([
|
||||
widget.file
|
||||
], {
|
||||
pubMagicKeyLat: locationDataFromExif!.latitude,
|
||||
pubMagicKeyLong: locationDataFromExif.longitude
|
||||
});
|
||||
hasLocationData.value = true;
|
||||
}
|
||||
} catch (e, s) {
|
||||
_logger.severe("Error while updating location from EXIF", e, s);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ import 'package:exif/exif.dart';
|
|||
import 'package:intl/intl.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/models/file.dart';
|
||||
import "package:photos/models/location/location.dart";
|
||||
import "package:photos/services/location_service.dart";
|
||||
import 'package:photos/utils/file_util.dart';
|
||||
|
||||
const kDateTimeOriginal = "EXIF DateTimeOriginal";
|
||||
|
@ -30,9 +32,23 @@ Future<Map<String, IfdTag>> getExif(File file) async {
|
|||
}
|
||||
}
|
||||
|
||||
Future<DateTime?> getCreationTimeFromEXIF(io.File file) async {
|
||||
Future<Map<String, IfdTag>?> getExifFromSourceFile(io.File originFile) async {
|
||||
try {
|
||||
final exif = await readExifFromFile(file);
|
||||
final exif = await readExifFromFile(originFile);
|
||||
return exif;
|
||||
} catch (e, s) {
|
||||
_logger.severe("failed to get exif from origin file", e, s);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<DateTime?> getCreationTimeFromEXIF(
|
||||
io.File? file,
|
||||
Map<String, IfdTag>? exifData,
|
||||
) async {
|
||||
try {
|
||||
assert(file != null || exifData != null);
|
||||
final exif = exifData ?? await readExifFromFile(file!);
|
||||
final exifTime = exif.containsKey(kDateTimeOriginal)
|
||||
? exif[kDateTimeOriginal]!.printable
|
||||
: exif.containsKey(kImageDateTime)
|
||||
|
@ -46,3 +62,47 @@ Future<DateTime?> getCreationTimeFromEXIF(io.File file) async {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Location? locationFromExif(Map<String, IfdTag> exif) {
|
||||
try {
|
||||
return _gpsDataFromExif(exif).toLocationObj();
|
||||
} catch (e, s) {
|
||||
_logger.severe("failed to get location from exif", e, s);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
GPSData _gpsDataFromExif(Map<String, IfdTag> exif) {
|
||||
final Map<String, dynamic> exifLocationData = {
|
||||
"lat": null,
|
||||
"long": null,
|
||||
"latRef": null,
|
||||
"longRef": null,
|
||||
};
|
||||
if (exif["GPS GPSLatitude"] != null) {
|
||||
exifLocationData["lat"] = exif["GPS GPSLatitude"]!
|
||||
.values
|
||||
.toList()
|
||||
.map((e) => ((e as Ratio).numerator / e.denominator))
|
||||
.toList();
|
||||
}
|
||||
if (exif["GPS GPSLongitude"] != null) {
|
||||
exifLocationData["long"] = exif["GPS GPSLongitude"]!
|
||||
.values
|
||||
.toList()
|
||||
.map((e) => ((e as Ratio).numerator / e.denominator))
|
||||
.toList();
|
||||
}
|
||||
if (exif["GPS GPSLatitudeRef"] != null) {
|
||||
exifLocationData["latRef"] = exif["GPS GPSLatitudeRef"].toString();
|
||||
}
|
||||
if (exif["GPS GPSLongitudeRef"] != null) {
|
||||
exifLocationData["longRef"] = exif["GPS GPSLongitudeRef"].toString();
|
||||
}
|
||||
return GPSData(
|
||||
exifLocationData["latRef"],
|
||||
exifLocationData["lat"],
|
||||
exifLocationData["longRef"],
|
||||
exifLocationData["long"],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -114,7 +114,7 @@ Future<List<File>> convertIncomingSharedMediaToFile(
|
|||
enteFile.fileType =
|
||||
media.type == SharedMediaType.IMAGE ? FileType.image : FileType.video;
|
||||
if (enteFile.fileType == FileType.image) {
|
||||
final exifTime = await getCreationTimeFromEXIF(ioFile);
|
||||
final exifTime = await getCreationTimeFromEXIF(ioFile, null);
|
||||
if (exifTime != null) {
|
||||
enteFile.creationTime = exifTime.microsecondsSinceEpoch;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue