2023-06-04 18:29:03 +00:00
|
|
|
import "dart:async";
|
2023-06-05 03:12:43 +00:00
|
|
|
import "dart:math";
|
2023-06-04 18:29:03 +00:00
|
|
|
|
2023-06-05 03:12:43 +00:00
|
|
|
import "package:flutter/foundation.dart";
|
2023-06-04 18:29:03 +00:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter_map/flutter_map.dart';
|
|
|
|
import "package:latlong2/latlong.dart";
|
2023-06-04 23:22:21 +00:00
|
|
|
import "package:logging/logging.dart";
|
2023-06-04 18:29:03 +00:00
|
|
|
import "package:photos/models/file.dart";
|
|
|
|
import "package:photos/models/file_load_result.dart";
|
|
|
|
import "package:photos/ui/map/image_marker.dart";
|
|
|
|
import "package:photos/ui/map/map_credits.dart";
|
|
|
|
import "package:photos/ui/map/map_view.dart";
|
|
|
|
import "package:photos/ui/viewer/file/detail_page.dart";
|
|
|
|
import "package:photos/ui/viewer/file/thumbnail_widget.dart";
|
|
|
|
import "package:photos/utils/navigation_util.dart";
|
|
|
|
|
|
|
|
class MapScreen extends StatefulWidget {
|
2023-06-05 03:40:25 +00:00
|
|
|
// Add a function parameter where the function returns a Future<List<File>>
|
|
|
|
|
|
|
|
final Future<List<File>> Function() filesFutureFn;
|
|
|
|
|
|
|
|
const MapScreen({
|
|
|
|
super.key,
|
|
|
|
required this.filesFutureFn,
|
|
|
|
});
|
2023-06-04 18:29:03 +00:00
|
|
|
|
|
|
|
@override
|
|
|
|
State<StatefulWidget> createState() {
|
|
|
|
return _MapScreenState();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _MapScreenState extends State<MapScreen> {
|
|
|
|
List<ImageMarker> imageMarkers = [];
|
|
|
|
List<File> allImages = [];
|
|
|
|
List<File> visibleImages = [];
|
|
|
|
MapController mapController = MapController();
|
|
|
|
bool isLoading = true;
|
2023-06-05 03:40:25 +00:00
|
|
|
double initialZoom = 4.0;
|
2023-06-05 03:12:43 +00:00
|
|
|
LatLng center = LatLng(10.732951, 78.405635);
|
2023-06-04 23:22:21 +00:00
|
|
|
final Logger _logger = Logger("_MapScreenState");
|
2023-06-04 18:29:03 +00:00
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
super.initState();
|
|
|
|
initialize();
|
|
|
|
}
|
|
|
|
|
2023-06-04 23:22:21 +00:00
|
|
|
Future<void> initialize() async {
|
|
|
|
try {
|
2023-06-05 03:40:25 +00:00
|
|
|
allImages = await widget.filesFutureFn();
|
2023-06-04 23:22:21 +00:00
|
|
|
processFiles(allImages);
|
|
|
|
} catch (e, s) {
|
|
|
|
_logger.severe("Error initializing map screen", e, s);
|
|
|
|
}
|
2023-06-04 18:29:03 +00:00
|
|
|
}
|
|
|
|
|
2023-06-05 03:12:43 +00:00
|
|
|
// Simple function to estimate zoom level
|
|
|
|
double estimateZoomLevel(
|
|
|
|
double range,
|
|
|
|
double maxRange,
|
|
|
|
double minZoom,
|
|
|
|
double maxZoom,
|
|
|
|
) {
|
|
|
|
if (range >= maxRange) return minZoom;
|
|
|
|
return maxZoom - ((range / maxRange) * (maxZoom - minZoom));
|
|
|
|
}
|
|
|
|
|
2023-06-04 18:29:03 +00:00
|
|
|
void processFiles(List<File> files) {
|
2023-06-05 03:12:43 +00:00
|
|
|
late double minLat, maxLat, minLon, maxLon;
|
2023-06-04 18:29:03 +00:00
|
|
|
final List<ImageMarker> tempMarkers = [];
|
2023-06-05 03:12:43 +00:00
|
|
|
bool hasAnyLocation = false;
|
2023-06-04 18:29:03 +00:00
|
|
|
for (var file in files) {
|
2023-06-04 23:21:51 +00:00
|
|
|
if (file.hasLocation) {
|
2023-06-05 03:12:43 +00:00
|
|
|
if (!hasAnyLocation) {
|
|
|
|
minLat = file.location!.latitude!;
|
|
|
|
minLon = file.location!.longitude!;
|
|
|
|
maxLat = file.location!.latitude!;
|
|
|
|
maxLon = file.location!.longitude!;
|
|
|
|
hasAnyLocation = true;
|
|
|
|
} else {
|
|
|
|
minLat = min(minLat, file.location!.latitude!);
|
|
|
|
minLon = min(minLon, file.location!.longitude!);
|
|
|
|
maxLat = max(maxLat, file.location!.latitude!);
|
|
|
|
maxLon = max(maxLon, file.location!.longitude!);
|
|
|
|
}
|
2023-06-04 23:21:51 +00:00
|
|
|
tempMarkers.add(
|
|
|
|
ImageMarker(
|
|
|
|
latitude: file.location!.latitude!,
|
|
|
|
longitude: file.location!.longitude!,
|
|
|
|
imageFile: file,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2023-06-04 18:29:03 +00:00
|
|
|
}
|
2023-06-05 03:12:43 +00:00
|
|
|
if (hasAnyLocation) {
|
|
|
|
center = LatLng(
|
|
|
|
minLat + (maxLat - minLat) / 2,
|
|
|
|
minLon + (maxLon - minLon) / 2,
|
|
|
|
);
|
|
|
|
final double latZoom = estimateZoomLevel(maxLat - minLat, 90, 0, 19);
|
|
|
|
final double lonZoom = estimateZoomLevel(maxLon - minLon, 180, 0, 19);
|
|
|
|
initialZoom = min(latZoom, lonZoom);
|
|
|
|
if (kDebugMode) {
|
|
|
|
debugPrint("Info for map: center $center, initialZoom $initialZoom");
|
|
|
|
debugPrint("Info for map: minLat $minLat, maxLat $maxLat");
|
|
|
|
debugPrint("Info for map: minLon $minLon, maxLon $maxLon");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-04 18:29:03 +00:00
|
|
|
setState(() {
|
|
|
|
imageMarkers = tempMarkers;
|
|
|
|
isLoading = false;
|
|
|
|
});
|
|
|
|
updateVisibleImages(mapController.bounds!);
|
|
|
|
}
|
|
|
|
|
|
|
|
void updateVisibleImages(LatLngBounds bounds) async {
|
|
|
|
final images = imageMarkers
|
|
|
|
.where((imageMarker) {
|
|
|
|
final point = LatLng(imageMarker.latitude, imageMarker.longitude);
|
|
|
|
return bounds.contains(point);
|
|
|
|
})
|
|
|
|
.map((imageMarker) => imageMarker.imageFile)
|
|
|
|
.toList();
|
|
|
|
|
|
|
|
setState(() {
|
|
|
|
visibleImages = images;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
String formatNumber(int number) {
|
|
|
|
if (number <= 99) {
|
|
|
|
return number.toString();
|
|
|
|
} else if (number <= 999) {
|
|
|
|
return '${(number / 100).toStringAsFixed(0)}00+';
|
|
|
|
} else if (number >= 1000 && number < 2000) {
|
|
|
|
return '1K+';
|
|
|
|
} else {
|
|
|
|
final int thousands = ((number - 1) ~/ 1000);
|
|
|
|
return '${thousands}K+';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void onTap(File image, int index) {
|
|
|
|
final page = DetailPage(
|
|
|
|
DetailPageConfiguration(
|
|
|
|
List.unmodifiable(visibleImages),
|
|
|
|
(
|
|
|
|
creationStartTime,
|
|
|
|
creationEndTime, {
|
|
|
|
limit,
|
|
|
|
asc,
|
|
|
|
}) async {
|
|
|
|
final result = FileLoadResult(allImages, false);
|
|
|
|
return result;
|
|
|
|
},
|
|
|
|
index,
|
|
|
|
'Map',
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
|
|
|
routeToPage(
|
|
|
|
context,
|
|
|
|
page,
|
|
|
|
forceCustomPageRoute: true,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2023-06-05 03:12:43 +00:00
|
|
|
_logger.info('Building with Zoom $initialZoom');
|
2023-06-04 18:29:03 +00:00
|
|
|
return SafeArea(
|
|
|
|
child: Scaffold(
|
|
|
|
body: Stack(
|
|
|
|
children: [
|
|
|
|
Column(
|
|
|
|
children: [
|
|
|
|
Expanded(
|
|
|
|
child: MapView(
|
|
|
|
updateVisibleImages: updateVisibleImages,
|
|
|
|
controller: mapController,
|
|
|
|
imageMarkers: imageMarkers,
|
2023-06-05 03:12:43 +00:00
|
|
|
initialZoom: initialZoom,
|
|
|
|
center: center,
|
2023-06-04 18:29:03 +00:00
|
|
|
),
|
|
|
|
),
|
|
|
|
const SizedBox(
|
|
|
|
child: MapCredits(),
|
|
|
|
),
|
|
|
|
SizedBox(
|
|
|
|
height: 120,
|
|
|
|
child: Center(
|
|
|
|
child: ListView.builder(
|
|
|
|
itemCount: visibleImages.length,
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
itemBuilder: (context, index) {
|
|
|
|
final image = visibleImages[index];
|
|
|
|
return InkWell(
|
|
|
|
onTap: () => onTap(image, index),
|
|
|
|
child: Container(
|
|
|
|
margin: const EdgeInsets.symmetric(
|
|
|
|
horizontal: 6,
|
|
|
|
vertical: 10,
|
|
|
|
),
|
|
|
|
width: 100,
|
|
|
|
height: 100,
|
|
|
|
child: ClipRRect(
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
child: ThumbnailWidget(image),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
],
|
|
|
|
),
|
|
|
|
isLoading
|
|
|
|
? Container(
|
|
|
|
color: Colors.black87,
|
|
|
|
child: const Center(
|
|
|
|
child: CircularProgressIndicator(color: Colors.green),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
: const SizedBox.shrink(),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|