feat: implement MapCameraLocation widget for capturing camera previews with integrated map and location overlays

This commit is contained in:
2026-04-02 10:25:38 +07:00
parent 03e33ff749
commit f446b26196

View File

@@ -1,53 +1,20 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:geocoding/geocoding.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:latlong2/latlong.dart' as lat; import 'package:latlong2/latlong.dart' as lat;
import 'package:map_camera_flutter/map_camera_flutter.dart'; import 'image_and_location_data.dart'; // Standard local import
import 'components/location_details_widget.dart'; // Standard local import
///import 'package:your_app/map_camera_flutter.dart'; // Import the file where the MapCameraLocation widget is defined import 'package:path_provider/path_provider.dart';
/// ```
/// void main() {
/// final cameras = await availableCameras();
/// final firstCamera = cameras.first;
/// runApp(MyApp(camera: firstCamera));
/// }
///
/// class MyApp extends StatelessWidget {
/// final CameraDescription camera;
/// const MyApp({super.key, required this.camera});
///
/// @override
/// Widget build(BuildContext context) {
/// return MaterialApp(
/// home: CameraLocationScreen(camera: firstCamera),
/// );
/// }
/// }
///
/// class CameraLocationScreen extends StatelessWidget {
/// final CameraDescription camera;
/// const MyApp({super.key, required this.camera});
// // Callback function to handle the captured image and location data
/// void handleImageAndLocationData(ImageAndLocationData data) {
// // You can use the data here as needed
/// print('Image Path: ${data.imagePath}');
/// print('Latitude: ${data.latitude}');
/// print('Longitude: ${data.longitude}');
/// print('Location Name: ${data.locationName}');
/// print('SubLocation: ${data.subLocation}');
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// // Provide the CameraDescription and the handleImageAndLocationData callback function to the MapCameraLocation widget
/// return MapCameraLocation(
/// camera: camera, // YOUR_CAMERA_DESCRIPTION_OBJECT, // Replace YOUR_CAMERA_DESCRIPTION_OBJECT with your actual CameraDescription
/// onImageCaptured: handleImageAndLocationData,
/// );
/// }
/// }
/// ```
// Callback function type for capturing image and location data // Callback function type for capturing image and location data
typedef ImageAndLocationCallback = void Function(ImageAndLocationData data); typedef ImageAndLocationCallback = void Function(ImageAndLocationData data);
@@ -57,18 +24,17 @@ class MapCameraLocation extends StatefulWidget {
final ImageAndLocationCallback? onImageCaptured; final ImageAndLocationCallback? onImageCaptured;
final String userAgent; final String userAgent;
final String packageName; final String packageName;
final double? latitude;
final double? longitude;
/// Constructs a MapCameraLocation widget.
///
/// The [camera] parameter is required and represents the camera to be used for capturing images.
/// The [onImageCaptured] parameter is an optional callback function that will be triggered when an image and location data are captured.
/// The [userAgent], and [packageName] parameters are required for open street map policies https://operations.osmfoundation.org/policies/tiles.
const MapCameraLocation({ const MapCameraLocation({
super.key, super.key,
required this.camera, required this.camera,
this.onImageCaptured, this.onImageCaptured,
required this.userAgent, required this.userAgent,
required this.packageName, required this.packageName,
this.latitude,
this.longitude,
}); });
@override @override
@@ -77,46 +43,18 @@ class MapCameraLocation extends StatefulWidget {
class _MapCameraLocationState extends State<MapCameraLocation> { class _MapCameraLocationState extends State<MapCameraLocation> {
late CameraController _controller; late CameraController _controller;
/// Represents a controller for the camera, used to control camera-related operations.
late Future<void> _initializeControllerFuture; late Future<void> _initializeControllerFuture;
/// Represents a future that resolves when the camera controller has finished initializing.
late AlignOnUpdate _followOnLocationUpdate; late AlignOnUpdate _followOnLocationUpdate;
/// Enum value indicating when to follow location updates.
late StreamController<double?> _followCurrentLocationStreamController; late StreamController<double?> _followCurrentLocationStreamController;
/// Stream controller used to track the current location.
File? cameraImagePath; File? cameraImagePath;
/// File path of the captured camera image.
File? ssImage; File? ssImage;
/// File path of the captured screen shot image.
String? dateTime; String? dateTime;
/// A formatted string representing the current date and time.
final globalKey = GlobalKey(); final globalKey = GlobalKey();
/// Key used to uniquely identify and control a widget.
Placemark? placeMark; Placemark? placeMark;
/// Represents geocoded location information.
LocationData? locationData; LocationData? locationData;
/// SubLocation of the current location as a string.
/// Callback function to retrieve the image and location data.
ImageAndLocationData getImageAndLocationData() { ImageAndLocationData getImageAndLocationData() {
return ImageAndLocationData( return ImageAndLocationData(
imagePath: cameraImagePath?.path, imagePath: cameraImagePath?.path,
@@ -125,16 +63,22 @@ class _MapCameraLocationState extends State<MapCameraLocation> {
} }
Timer? _positionTimer; Timer? _positionTimer;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_positionTimer = Timer.periodic(const Duration(seconds: 1), (timer) async {
if (mounted) {
await updatePosition(context);
}
});
// Initialize the camera controller // Support manual location injection for "Source of Truth" from Home screen
if (widget.latitude != null && widget.longitude != null) {
locationData = LocationData(
latitude: widget.latitude.toString(),
longitude: widget.longitude.toString(),
locationName: 'Fetching address...',
subLocation: '',
);
}
// Initialize camera
_controller = CameraController( _controller = CameraController(
widget.camera, widget.camera,
ResolutionPreset.medium, ResolutionPreset.medium,
@@ -143,7 +87,15 @@ class _MapCameraLocationState extends State<MapCameraLocation> {
_followOnLocationUpdate = AlignOnUpdate.always; _followOnLocationUpdate = AlignOnUpdate.always;
_followCurrentLocationStreamController = StreamController<double?>(); _followCurrentLocationStreamController = StreamController<double?>();
// Get the current date and time in a formatted string // Initial fetch and start periodic updates
// Reduce frequency to 5 seconds to be more battery/CPU efficient
Future.microtask(() => updatePosition(context));
_positionTimer = Timer.periodic(const Duration(seconds: 5), (timer) {
if (mounted) {
updatePosition(context);
}
});
dateTime = DateFormat.yMd().add_jm().format(DateTime.now()); dateTime = DateFormat.yMd().add_jm().format(DateTime.now());
} }
@@ -160,7 +112,7 @@ class _MapCameraLocationState extends State<MapCameraLocation> {
if (mounted) { if (mounted) {
super.setState(fn); super.setState(fn);
} else { } else {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) { if (mounted) {
super.setState(fn); super.setState(fn);
} }
@@ -176,14 +128,15 @@ class _MapCameraLocationState extends State<MapCameraLocation> {
future: _initializeControllerFuture, future: _initializeControllerFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
final currentLat = double.tryParse(locationData?.latitude ?? '0') ?? 0;
final currentLng = double.tryParse(locationData?.longitude ?? '0') ?? 0;
return Center( return Center(
child: RepaintBoundary( child: RepaintBoundary(
key: globalKey, key: globalKey,
child: Stack( child: Stack(
children: [ children: [
CameraPreview( CameraPreview(_controller),
_controller,
),
Positioned( Positioned(
left: 0, left: 0,
right: 0, right: 0,
@@ -191,84 +144,64 @@ class _MapCameraLocationState extends State<MapCameraLocation> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
SizedBox( if (locationData != null)
height: 160, SizedBox(
child: Row( height: 160,
crossAxisAlignment: CrossAxisAlignment.start, child: Row(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Padding( children: [
padding: const EdgeInsets.symmetric( Padding(
horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Card( child: Card(
elevation: 3, elevation: 3,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: borderRadius: BorderRadius.circular(8.0),
BorderRadius.circular(8.0)), ),
child: SizedBox( child: SizedBox(
// height: 130, width: 120,
width: 120, child: Padding(
child: Padding( padding: const EdgeInsets.all(5.0),
padding: const EdgeInsets.all(5.0), child: FlutterMap(
child: locationData == null options: MapOptions(
? const Center( initialCenter: lat.LatLng(currentLat, currentLng),
child: initialZoom: 14.0,
CircularProgressIndicator()) onPositionChanged: (pos, hasGesture) {
: FlutterMap( if (hasGesture) {
options: MapOptions( setState(() => _followOnLocationUpdate = AlignOnUpdate.never);
initialCenter: }
const lat.LatLng(0, 0), },
initialZoom: 13.0, ),
onPositionChanged: (position, children: [
bool hasGesture) { TileLayer(
if (hasGesture) { urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
setState( tileProvider: NetworkTileProvider(
() => headers: {'User-Agent': widget.userAgent},
_followOnLocationUpdate =
AlignOnUpdate
.never,
);
}
},
), ),
children: [ userAgentPackageName: widget.packageName,
TileLayer( minZoom: 12,
urlTemplate:
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
tileProvider:
NetworkTileProvider(
headers: {
'User-Agent':
widget.userAgent,
},
),
userAgentPackageName:
widget.packageName,
minZoom: 12,
),
CurrentLocationLayer(
alignPositionStream:
_followCurrentLocationStreamController
.stream,
alignPositionOnUpdate:
_followOnLocationUpdate,
),
],
), ),
CurrentLocationLayer(
alignPositionStream: _followCurrentLocationStreamController.stream,
alignPositionOnUpdate: _followOnLocationUpdate,
),
],
),
),
), ),
), ),
), ),
), Expanded(
Expanded( child: LocationDetailsWidget(
child: LocationDetailsWidget(
locationData: locationData, locationData: locationData,
dateTime: dateTime), dateTime: dateTime,
), ),
const SizedBox( ),
width: 10, const SizedBox(width: 10)
) ],
], ),
), )
), else
const Center(child: CircularProgressIndicator(color: Colors.white)),
], ],
), ),
), ),
@@ -277,7 +210,7 @@ class _MapCameraLocationState extends State<MapCameraLocation> {
), ),
); );
} else { } else {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator(color: Colors.white));
} }
}, },
), ),
@@ -285,11 +218,9 @@ class _MapCameraLocationState extends State<MapCameraLocation> {
onPressed: () async { onPressed: () async {
try { try {
await _initializeControllerFuture; await _initializeControllerFuture;
takeScreenshot(); await takeScreenshot();
} catch (e) { } catch (e) {
if (kDebugMode) { if (kDebugMode) debugPrint('Capture Error: $e');
print(e);
}
} }
}, },
child: const Icon(Icons.camera_alt), child: const Icon(Icons.camera_alt),
@@ -297,139 +228,112 @@ class _MapCameraLocationState extends State<MapCameraLocation> {
); );
} }
/// Takes a screenshot of the current screen and saves it as an image file.
/// Returns the file path of the captured image and triggers the [onImageCaptured]
/// callback if provided.
/// Throws an exception if there is an error capturing the screenshot.
Future<void> takeScreenshot() async { Future<void> takeScreenshot() async {
var rng = Random(); final rng = Random();
final boundary = globalKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
// Get the render boundary of the widget final image = await boundary.toImage(pixelRatio: 2.0);
final RenderRepaintBoundary boundary =
globalKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
// Capture the screen as an image
ui.Image image = await boundary.toImage();
final directory = (await getApplicationDocumentsDirectory()).path; final directory = (await getApplicationDocumentsDirectory()).path;
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
final pngBytes = byteData!.buffer.asUint8List();
final imgFile = File('$directory/screenshot${rng.nextInt(10000)}.png');
// Convert the image to bytes in PNG format
ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
Uint8List pngBytes = byteData!.buffer.asUint8List();
// Generate a random file name for the screenshot
File imgFile = File('$directory/screenshot${rng.nextInt(200)}.png');
// Write the bytes to the file
await imgFile.writeAsBytes(pngBytes); await imgFile.writeAsBytes(pngBytes);
// Check if the file exists if (imgFile.existsSync()) {
bool isExists = imgFile.existsSync(); setState(() => cameraImagePath = imgFile);
if (isExists) {
// Set the file path of the captured image
setState(() {
cameraImagePath = imgFile;
});
// Trigger the image captured callback
if (widget.onImageCaptured != null) { if (widget.onImageCaptured != null) {
ImageAndLocationData data = ImageAndLocationData( widget.onImageCaptured!(ImageAndLocationData(
imagePath: imgFile.path, imagePath: imgFile.path,
locationData: locationData, locationData: locationData,
); ));
widget.onImageCaptured!(data);
} }
} else {
debugPrint('File does not exist');
} }
} }
/// Updates the current position by retrieving the latitude, longitude, location name,
/// and subLocation based on the user's device location. Updates the corresponding
/// state variables with the retrieved data.
/// Throws an exception if there is an error retrieving the location information.
Future<void> updatePosition(BuildContext context) async { Future<void> updatePosition(BuildContext context) async {
try { try {
// Determine the current position double latVal;
final position = await _determinePosition(); double lngVal;
// Retrieve the placeMarks for the current position if (widget.latitude != null && widget.longitude != null) {
final placeMarks = latVal = widget.latitude!;
await placemarkFromCoordinates(position.latitude, position.longitude); lngVal = widget.longitude!;
LocationData locationData;
if (placeMarks.isNotEmpty) {
final placeMark = placeMarks.first;
locationData = LocationData(
latitude: position.latitude.toString(),
longitude: position.longitude.toString(),
locationName:
"${placeMark.locality ?? ""}, ${placeMark.administrativeArea ?? ""}, ${placeMark.country ?? ""}",
subLocation:
"${placeMark.street ?? ""}, ${placeMark.thoroughfare ?? ""} ${placeMark.administrativeArea ?? ""}");
} else { } else {
locationData = LocationData( final pos = await _determinePosition();
longitude: null, latVal = pos.latitude;
latitude: null, lngVal = pos.longitude;
locationName: 'No Location Data',
subLocation: "");
} }
if (locationData != this.locationData) {
// Update the state variables with the retrieved location data final placeMarks = await placemarkFromCoordinates(latVal, lngVal);
if (mounted) {
setState(() { setState(() {
this.locationData = locationData; if (placeMarks.isNotEmpty) {
final p = placeMarks.first;
locationData = LocationData(
latitude: latVal.toString(),
longitude: lngVal.toString(),
locationName: "${p.locality ?? ""}, ${p.administrativeArea ?? ""}, ${p.country ?? ""}",
subLocation: "${p.street ?? ""}, ${p.thoroughfare ?? ""} ${p.administrativeArea ?? ""}",
);
} else {
locationData = LocationData(
latitude: latVal.toString(),
longitude: lngVal.toString(),
locationName: 'Coordinate Found',
subLocation: "No address details",
);
}
}); });
} }
if (kDebugMode) {
print(
"Latitude: ${locationData.latitude}, Longitude: ${locationData.longitude}, Location: ${locationData.locationName}");
}
} catch (e) { } catch (e) {
// Handle any errors that occurred during location retrieval if (kDebugMode) debugPrint("Location Error: $e");
setState(() { if (locationData == null) {
locationData = LocationData( setState(() {
longitude: null, locationData = LocationData(
latitude: null, latitude: null,
locationName: 'Error Retrieving Location', longitude: null,
subLocation: ""); locationName: 'Error Loading Location',
}); subLocation: e.toString(),
);
});
}
} }
} }
/// Determines the current position using the GeoLocator package.
/// Returns the current position as a [Position] object.
/// Throws an exception if there is an error determining the position or if the necessary permissions are not granted.
Future<Position> _determinePosition() async { Future<Position> _determinePosition() async {
bool serviceEnabled; final serviceEnabled = await Geolocator.isLocationServiceEnabled();
LocationPermission permission;
// Check if location services are enabled
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) { if (!serviceEnabled) {
// If location services are disabled, throw an exception final lastKnown = await Geolocator.getLastKnownPosition();
throw Exception('Location services are disabled.'); if (lastKnown != null) return lastKnown;
throw Exception('Location service disabled');
} }
// Check location permission
permission = await Geolocator.checkPermission(); var permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) { if (permission == LocationPermission.denied) {
// If location permission is denied, request it
permission = await Geolocator.requestPermission(); permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) { if (permission == LocationPermission.denied) {
// If location permission is still denied, throw an exception final lastKnown = await Geolocator.getLastKnownPosition();
throw Exception('Location permissions are denied'); if (lastKnown != null) return lastKnown;
throw Exception('Location permission denied');
} }
} }
// Check if location permission is permanently denied
if (permission == LocationPermission.deniedForever) { if (permission == LocationPermission.deniedForever) {
// Throw an exception if location permission is permanently denied final lastKnown = await Geolocator.getLastKnownPosition();
throw Exception( if (lastKnown != null) return lastKnown;
'Location permissions are permanently denied, we cannot request permissions.'); throw Exception('Location permission permanently denied');
} }
// Get the current position try {
return await Geolocator.getCurrentPosition(); return await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.medium,
timeLimit: const Duration(seconds: 8),
);
} catch (e) {
final lastKnown = await Geolocator.getLastKnownPosition();
if (lastKnown != null) return lastKnown;
rethrow;
}
} }
} }