From f446b26196aa8ec6c27217fbb7867c07d8e08cdc Mon Sep 17 00:00:00 2001 From: kiki Date: Thu, 2 Apr 2026 10:25:38 +0700 Subject: [PATCH] feat: implement MapCameraLocation widget for capturing camera previews with integrated map and location overlays --- lib/src/map_camera.dart | 432 ++++++++++++++++------------------------ 1 file changed, 168 insertions(+), 264 deletions(-) diff --git a/lib/src/map_camera.dart b/lib/src/map_camera.dart index 4872161..7482df3 100644 --- a/lib/src/map_camera.dart +++ b/lib/src/map_camera.dart @@ -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:intl/intl.dart'; import 'dart:ui' as ui; import 'package:latlong2/latlong.dart' as lat; -import 'package:map_camera_flutter/map_camera_flutter.dart'; - -///import 'package:your_app/map_camera_flutter.dart'; // Import the file where the MapCameraLocation widget is defined - -/// ``` -/// 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, -/// ); -/// } -/// } -/// ``` +import 'image_and_location_data.dart'; // Standard local import +import 'components/location_details_widget.dart'; // Standard local import +import 'package:path_provider/path_provider.dart'; // Callback function type for capturing image and location data typedef ImageAndLocationCallback = void Function(ImageAndLocationData data); @@ -57,18 +24,17 @@ class MapCameraLocation extends StatefulWidget { final ImageAndLocationCallback? onImageCaptured; final String userAgent; 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({ super.key, required this.camera, this.onImageCaptured, required this.userAgent, required this.packageName, + this.latitude, + this.longitude, }); @override @@ -77,46 +43,18 @@ class MapCameraLocation extends StatefulWidget { class _MapCameraLocationState extends State { late CameraController _controller; - - /// Represents a controller for the camera, used to control camera-related operations. - late Future _initializeControllerFuture; - - /// Represents a future that resolves when the camera controller has finished initializing. - late AlignOnUpdate _followOnLocationUpdate; - - /// Enum value indicating when to follow location updates. - late StreamController _followCurrentLocationStreamController; - /// Stream controller used to track the current location. - File? cameraImagePath; - - /// File path of the captured camera image. - File? ssImage; - - /// File path of the captured screen shot image. - String? dateTime; - - /// A formatted string representing the current date and time. - final globalKey = GlobalKey(); - /// Key used to uniquely identify and control a widget. - Placemark? placeMark; - - /// Represents geocoded location information. - LocationData? locationData; - /// SubLocation of the current location as a string. - - /// Callback function to retrieve the image and location data. ImageAndLocationData getImageAndLocationData() { return ImageAndLocationData( imagePath: cameraImagePath?.path, @@ -125,16 +63,22 @@ class _MapCameraLocationState extends State { } Timer? _positionTimer; + @override void 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( widget.camera, ResolutionPreset.medium, @@ -143,7 +87,15 @@ class _MapCameraLocationState extends State { _followOnLocationUpdate = AlignOnUpdate.always; _followCurrentLocationStreamController = StreamController(); - // 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()); } @@ -160,7 +112,7 @@ class _MapCameraLocationState extends State { if (mounted) { super.setState(fn); } else { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { super.setState(fn); } @@ -176,14 +128,15 @@ class _MapCameraLocationState extends State { future: _initializeControllerFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { + final currentLat = double.tryParse(locationData?.latitude ?? '0') ?? 0; + final currentLng = double.tryParse(locationData?.longitude ?? '0') ?? 0; + return Center( child: RepaintBoundary( key: globalKey, child: Stack( children: [ - CameraPreview( - _controller, - ), + CameraPreview(_controller), Positioned( left: 0, right: 0, @@ -191,84 +144,64 @@ class _MapCameraLocationState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - SizedBox( - height: 160, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8.0), - child: Card( - elevation: 3, - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(8.0)), - child: SizedBox( - // height: 130, - width: 120, - child: Padding( - padding: const EdgeInsets.all(5.0), - child: locationData == null - ? const Center( - child: - CircularProgressIndicator()) - : FlutterMap( - options: MapOptions( - initialCenter: - const lat.LatLng(0, 0), - initialZoom: 13.0, - onPositionChanged: (position, - bool hasGesture) { - if (hasGesture) { - setState( - () => - _followOnLocationUpdate = - AlignOnUpdate - .never, - ); - } - }, + if (locationData != null) + SizedBox( + height: 160, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + child: SizedBox( + width: 120, + child: Padding( + padding: const EdgeInsets.all(5.0), + child: FlutterMap( + options: MapOptions( + initialCenter: lat.LatLng(currentLat, currentLng), + initialZoom: 14.0, + onPositionChanged: (pos, hasGesture) { + if (hasGesture) { + setState(() => _followOnLocationUpdate = AlignOnUpdate.never); + } + }, + ), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + tileProvider: NetworkTileProvider( + headers: {'User-Agent': widget.userAgent}, ), - children: [ - TileLayer( - 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, - ), - ], + userAgentPackageName: widget.packageName, + minZoom: 12, ), + CurrentLocationLayer( + alignPositionStream: _followCurrentLocationStreamController.stream, + alignPositionOnUpdate: _followOnLocationUpdate, + ), + ], + ), + ), ), ), ), - ), - Expanded( - child: LocationDetailsWidget( + Expanded( + child: LocationDetailsWidget( locationData: locationData, - dateTime: dateTime), - ), - const SizedBox( - width: 10, - ) - ], - ), - ), + dateTime: dateTime, + ), + ), + const SizedBox(width: 10) + ], + ), + ) + else + const Center(child: CircularProgressIndicator(color: Colors.white)), ], ), ), @@ -277,7 +210,7 @@ class _MapCameraLocationState extends State { ), ); } else { - return const Center(child: CircularProgressIndicator()); + return const Center(child: CircularProgressIndicator(color: Colors.white)); } }, ), @@ -285,11 +218,9 @@ class _MapCameraLocationState extends State { onPressed: () async { try { await _initializeControllerFuture; - takeScreenshot(); + await takeScreenshot(); } catch (e) { - if (kDebugMode) { - print(e); - } + if (kDebugMode) debugPrint('Capture Error: $e'); } }, child: const Icon(Icons.camera_alt), @@ -297,139 +228,112 @@ class _MapCameraLocationState extends State { ); } - /// 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 takeScreenshot() async { - var rng = Random(); - - // Get the render boundary of the widget - final RenderRepaintBoundary boundary = - globalKey.currentContext!.findRenderObject()! as RenderRepaintBoundary; - - // Capture the screen as an image - ui.Image image = await boundary.toImage(); + final rng = Random(); + final boundary = globalKey.currentContext!.findRenderObject()! as RenderRepaintBoundary; + final image = await boundary.toImage(pixelRatio: 2.0); 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); - // Check if the file exists - bool isExists = imgFile.existsSync(); - - if (isExists) { - // Set the file path of the captured image - setState(() { - cameraImagePath = imgFile; - }); - - // Trigger the image captured callback + if (imgFile.existsSync()) { + setState(() => cameraImagePath = imgFile); if (widget.onImageCaptured != null) { - ImageAndLocationData data = ImageAndLocationData( + widget.onImageCaptured!(ImageAndLocationData( imagePath: imgFile.path, 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 updatePosition(BuildContext context) async { try { - // Determine the current position - final position = await _determinePosition(); + double latVal; + double lngVal; - // Retrieve the placeMarks for the current position - final placeMarks = - await placemarkFromCoordinates(position.latitude, position.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 ?? ""}"); + if (widget.latitude != null && widget.longitude != null) { + latVal = widget.latitude!; + lngVal = widget.longitude!; } else { - locationData = LocationData( - longitude: null, - latitude: null, - locationName: 'No Location Data', - subLocation: ""); + final pos = await _determinePosition(); + latVal = pos.latitude; + lngVal = pos.longitude; } - if (locationData != this.locationData) { - // Update the state variables with the retrieved location data + + final placeMarks = await placemarkFromCoordinates(latVal, lngVal); + + if (mounted) { 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) { - // Handle any errors that occurred during location retrieval - setState(() { - locationData = LocationData( - longitude: null, + if (kDebugMode) debugPrint("Location Error: $e"); + if (locationData == null) { + setState(() { + locationData = LocationData( latitude: null, - locationName: 'Error Retrieving Location', - subLocation: ""); - }); + longitude: null, + 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 _determinePosition() async { - bool serviceEnabled; - LocationPermission permission; - - // Check if location services are enabled - serviceEnabled = await Geolocator.isLocationServiceEnabled(); + final serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { - // If location services are disabled, throw an exception - throw Exception('Location services are disabled.'); + final lastKnown = await Geolocator.getLastKnownPosition(); + 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 location permission is denied, request it permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { - // If location permission is still denied, throw an exception - throw Exception('Location permissions are denied'); + final lastKnown = await Geolocator.getLastKnownPosition(); + if (lastKnown != null) return lastKnown; + throw Exception('Location permission denied'); } } - // Check if location permission is permanently denied if (permission == LocationPermission.deniedForever) { - // Throw an exception if location permission is permanently denied - throw Exception( - 'Location permissions are permanently denied, we cannot request permissions.'); + final lastKnown = await Geolocator.getLastKnownPosition(); + if (lastKnown != null) return lastKnown; + throw Exception('Location permission permanently denied'); } - // Get the current position - return await Geolocator.getCurrentPosition(); + try { + 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; + } } }