Location info item in file details (#942)

This commit is contained in:
Vishnu Mohandas 2023-03-28 13:37:26 +05:30 committed by GitHub
commit ef4bbaa994
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 204 additions and 21 deletions

View file

@ -57,3 +57,5 @@ const double restrictedMaxWidth = 430;
const double mobileSmallThreshold = 336;
const publicLinkDeviceLimits = [50, 25, 10, 5, 2, 1];
const kilometersPerDegree = 111.16;

View file

@ -1,6 +1,8 @@
import "dart:collection";
import "dart:convert";
import "dart:math";
import "package:photos/core/constants.dart";
import "package:shared_preferences/shared_preferences.dart";
class LocationService {
@ -13,7 +15,7 @@ class LocationService {
prefs ??= await SharedPreferences.getInstance();
}
List<String> getLocations() {
List<String> getAllLocationTags() {
var list = prefs!.getStringList('locations');
list ??= [];
return list;
@ -22,22 +24,55 @@ class LocationService {
Future<void> addLocation(
String location,
double lat,
double lon,
double long,
int radius,
) async {
final list = getLocations();
final list = getAllLocationTags();
//The area enclosed by the location tag will be a circle on a 3D spherical
//globe and an ellipse on a 2D Mercator projection (2D map)
//a & b are the semi-major and semi-minor axes of the ellipse
//Converting the unit from kilometers to degrees for a and b as that is
//the unit on the caritesian plane
final a = (radius * _scaleFactor(lat)) / kilometersPerDegree;
final b = radius / kilometersPerDegree;
final center = [lat, long];
final data = {
"id": list.length,
"name": location,
"lat": lat,
"lon": lon,
"radius": radius,
"aSquare": a * a,
"bSquare": b * b,
"center": center,
};
final encodedMap = json.encode(data);
list.add(encodedMap);
await prefs!.setStringList('locations', list);
}
///The area bounded by the location tag becomes more elliptical with increase
///in the magnitude of the latitude on the caritesian plane. When latitude is
///0 degrees, the ellipse is a circle with a = b = r. When latitude incrases,
///the major axis (a) has to be scaled by the secant of the latitude.
double _scaleFactor(double lat) {
return 1 / cos(lat * (pi / 180));
}
List<String> enclosingLocationTags(List<double> coordinates) {
final result = List<String>.of([]);
final allLocationTags = getAllLocationTags();
for (var locationTag in allLocationTags) {
final locationJson = json.decode(locationTag);
final aSquare = locationJson["aSquare"];
final bSquare = locationJson["bSquare"];
final center = locationJson["center"];
final x = coordinates[0] - center[0];
final y = coordinates[1] - center[1];
if ((x * x) / (aSquare) + (y * y) / (bSquare) <= 1) {
result.add(locationJson["name"]);
}
}
return result;
}
Future<void> addFileToLocation(int locationId, int fileId) async {
final list = getFilesByLocation(locationId.toString());
list.add(fileId.toString());
@ -51,7 +86,7 @@ class LocationService {
}
List<String> getLocationsByFileID(int fileId) {
final locationList = getLocations();
final locationList = getAllLocationTags();
final locations = List<dynamic>.of([]);
for (String locationString in locationList) {
final locationJson = json.decode(locationString);
@ -81,3 +116,21 @@ class LocationService {
return map;
}
}
class GPSData {
final String latRef;
final List<double> lat;
final String longRef;
final List<double> long;
GPSData(this.latRef, this.lat, this.longRef, this.long);
List<double> toSignedDecimalDegreeCoordinates() {
final latSign = latRef == "N" ? 1 : -1;
final longSign = longRef == "E" ? 1 : -1;
return [
latSign * lat[0] + lat[1] / 60 + lat[2] / 3600,
longSign * long[0] + long[1] / 60 + long[2] / 3600
];
}
}

View file

@ -270,7 +270,7 @@ class SearchService {
String query,
) async {
final List<GenericSearchResult> searchResults = [];
final locations = LocationService.instance.getLocations();
final locations = LocationService.instance.getAllLocationTags();
for (String location in locations) {
final locationJson = json.decode(location);
final locationName = locationJson["name"].toString();

View file

@ -37,14 +37,16 @@ class ChipButtonWidget extends StatelessWidget {
size: 17,
)
: const SizedBox.shrink(),
const SizedBox(width: 4),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Text(
label ?? "",
style: getEnteTextTheme(context).smallBold,
),
)
if (label != null && leadingIcon != null)
const SizedBox(width: 4),
if (label != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Text(
label!,
style: getEnteTextTheme(context).smallBold,
),
)
],
),
),

View file

@ -5,6 +5,7 @@ import "package:photos/core/configuration.dart";
import "package:photos/models/file.dart";
import "package:photos/models/file_type.dart";
import "package:photos/services/feature_flag_service.dart";
import "package:photos/services/location_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";
@ -16,6 +17,7 @@ import 'package:photos/ui/viewer/file_details/backed_up_time_item_widget.dart';
import "package:photos/ui/viewer/file_details/creation_time_item_widget.dart";
import 'package:photos/ui/viewer/file_details/exif_item_widgets.dart';
import "package:photos/ui/viewer/file_details/file_properties_item_widget.dart";
import "package:photos/ui/viewer/file_details/location_tags_widget.dart";
import "package:photos/ui/viewer/file_details/objects_item_widget.dart";
import "package:photos/utils/exif_util.dart";
@ -39,12 +41,17 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
"takenOnDevice": null,
"exposureTime": null,
"ISO": null,
"megaPixels": null
"megaPixels": null,
"lat": null,
"long": null,
"latRef": null,
"longRef": null,
};
bool _isImage = false;
late int _currentUserID;
bool showExifListTile = false;
bool hasGPSData = false;
@override
void initState() {
@ -52,6 +59,12 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
_currentUserID = Configuration.instance.getUserID()!;
_isImage = widget.file.fileType == FileType.image ||
widget.file.fileType == FileType.livePhoto;
_exifNotifier.addListener(() {
if (_exifNotifier.value != null) {
_generateExifForLocation(_exifNotifier.value!);
hasGPSData = _haGPSData();
}
});
if (_isImage) {
_exifNotifier.addListener(() {
if (_exifNotifier.value != null) {
@ -63,10 +76,10 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
_exifData["exposureTime"] != null ||
_exifData["ISO"] != null;
});
getExif(widget.file).then((exif) {
_exifNotifier.value = exif;
});
}
getExif(widget.file).then((exif) {
_exifNotifier.value = exif;
});
super.initState();
}
@ -125,6 +138,30 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
},
),
);
if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) {
fileDetailsTiles.addAll([
ValueListenableBuilder(
valueListenable: _exifNotifier,
builder: (context, _, __) {
return hasGPSData
? Column(
children: [
LocationTagsWidget(
GPSData(
_exifData["latRef"],
_exifData["lat"],
_exifData["longRef"],
_exifData["long"],
).toSignedDecimalDegreeCoordinates(),
),
const FileDetailsDivider(),
],
)
: const SizedBox.shrink();
},
)
]);
}
if (_isImage) {
fileDetailsTiles.addAll([
ValueListenableBuilder(
@ -200,6 +237,36 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
);
}
bool _haGPSData() {
return _exifData["lat"] != null &&
_exifData["long"] != null &&
_exifData["latRef"] != null &&
_exifData["longRef"] != null;
}
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();
}
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();
}
}
_generateExifForDetails(Map<String, IfdTag> exif) {
if (exif["EXIF FocalLength"] != null) {
_exifData["focalLength"] =

View file

@ -0,0 +1,59 @@
import "dart:async";
import "package:flutter/material.dart";
import "package:photos/services/location_service.dart";
import "package:photos/ui/components/buttons/chip_button_widget.dart";
import "package:photos/ui/components/buttons/inline_button_widget.dart";
import "package:photos/ui/components/info_item_widget.dart";
class LocationTagsWidget extends StatefulWidget {
final List<double> coordinates;
const LocationTagsWidget(this.coordinates, {super.key});
@override
State<LocationTagsWidget> createState() => _LocationTagsWidgetState();
}
class _LocationTagsWidgetState extends State<LocationTagsWidget> {
String title = "Add location";
IconData leadingIcon = Icons.add_location_alt_outlined;
bool hasChipButtons = false;
late final Future<List<Widget>> locationTagChips;
@override
void initState() {
locationTagChips = _getLocationTags();
super.initState();
}
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
switchInCurve: Curves.easeInOutExpo,
switchOutCurve: Curves.easeInOutExpo,
child: InfoItemWidget(
key: ValueKey(title),
leadingIcon: Icons.add_location_alt_outlined,
title: title,
subtitleSection: locationTagChips,
hasChipButtons: hasChipButtons,
),
);
}
Future<List<Widget>> _getLocationTags() async {
final locationTags =
LocationService.instance.enclosingLocationTags(widget.coordinates);
if (locationTags.isEmpty) {
return [
InlineButtonWidget("Group nearby photos", () {}),
];
}
setState(() {
title = "Location";
leadingIcon = Icons.pin_drop_outlined;
hasChipButtons = true;
});
return locationTags.map((e) => ChipButtonWidget(e)).toList();
}
}

View file

@ -12,7 +12,7 @@ description: ente photos application
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.7.37+437
version: 0.7.38+438
environment:
sdk: '>=2.17.0 <3.0.0'