import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_polyline_points/flutter_polyline_points.dart'; import 'package:get/get.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:location/location.dart'; import 'package:http/http.dart' as http; import '../common/settings.dart'; import 'dart:ui' as ui; import 'package:flutter_compass/flutter_compass.dart'; import 'order_arrived.dart'; class OrderTrackingPage extends StatefulWidget { var orderDetails; final double lat; final double lng; final double d_lat; final double d_lng; final String u_address; OrderTrackingPage({ required this.lat, required this.lng, required this.d_lat, required this.d_lng, required this.u_address, this.orderDetails }); @override State createState() => _OrderTrackingPageState(); } class _OrderTrackingPageState extends State { final Completer _mapController = Completer(); final Location _location = Location(); late StreamSubscription _locationSubscription; StreamSubscription? _compassSubscription; LocationData? _currentLocation; bool _showOrderSummary = false; BitmapDescriptor? truckIcon; BitmapDescriptor? destinationIcon; Set _markers = {}; Map _polylines = {}; List _routeCoords = []; String _eta = ''; double _distance = 0.0; double _truckRotation = 0.0; final String _googleApiKey = 'AIzaSyDJpK9RVhlBejtJu9xSGfneuTN6HOfJgSM'; late LatLng userLatLng; late LatLng driverLatLng; // ----------------------- NEWLY ADDED ----------------------- bool _arrivalTriggered = false; Timer? _arrivalTimer; // ------------------------------------------------------------ Timer? _autoNavigateTimer; @override void initState() { super.initState(); userLatLng = LatLng(widget.lat, widget.lng); driverLatLng = LatLng(widget.d_lat, widget.d_lng); _loadIcons().then((_) => _initLocationTracking()); _initCompass(); // 👉 Auto navigate after 20 seconds _autoNavigateTimer = Timer(Duration(seconds: 5), () { if (mounted) { Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => ArrivalScreen( orderDetails: widget.orderDetails, ), ), ); } }); } void _initCompass() { _compassSubscription = FlutterCompass.events?.listen((event) { if (!mounted) return; // 🛡 lifecycle guard final heading = event.heading; if (heading == null) return; setState(() { _truckRotation = heading; }); }); } @override void dispose() { try { _locationSubscription.cancel(); } catch (_) {} _compassSubscription?.cancel(); _arrivalTimer?.cancel(); _autoNavigateTimer?.cancel(); super.dispose(); } Future _loadIcons() async { truckIcon = await getResizedMarker('images/tanker_map_horizontal.png', 90, null); destinationIcon = await getResizedMarker( 'images/location_supplier_landing.png', 90, Color(0XFFE76960)); } Future _initLocationTracking() async { bool serviceEnabled = await _location.serviceEnabled(); if (!serviceEnabled) serviceEnabled = await _location.requestService(); if (!serviceEnabled) return; PermissionStatus permissionGranted = await _location.hasPermission(); if (permissionGranted == PermissionStatus.denied) { permissionGranted = await _location.requestPermission(); if (permissionGranted != PermissionStatus.granted) return; } _currentLocation = await _location.getLocation(); _updateRouteAndMarkers(); _locationSubscription = _location.onLocationChanged.listen((newLoc) { if (!mounted) return; // 🛡 REQUIRED _currentLocation = newLoc; _updateRouteAndMarkers(); }); } Future getResizedMarker( String imagePath, int width, Color? tintColor) async { final ByteData data = await rootBundle.load(imagePath); final Uint8List imageData = data.buffer.asUint8List(); final ui.Codec codec = await ui.instantiateImageCodec( imageData, targetWidth: width, ); final ui.FrameInfo fi = await codec.getNextFrame(); final ui.PictureRecorder recorder = ui.PictureRecorder(); final Canvas canvas = Canvas(recorder); final Paint paint = Paint(); if (tintColor != null) { paint.colorFilter = ui.ColorFilter.mode(tintColor, BlendMode.srcIn); } canvas.drawImage(fi.image, Offset.zero, paint); final ui.Image finalImage = await recorder.endRecording().toImage(fi.image.width, fi.image.height); final ByteData? byteData = await finalImage.toByteData(format: ui.ImageByteFormat.png); return BitmapDescriptor.fromBytes(byteData!.buffer.asUint8List()); } // ----------------------- NEWLY ADDED METHOD ----------------------- void _checkDriverArrival() { if (_arrivalTriggered) return; double dist = _calculateDistance( driverLatLng.latitude, driverLatLng.longitude, userLatLng.latitude, userLatLng.longitude, ); if (dist <= 0.05) { _arrivalTriggered = true; _arrivalTimer = Timer(Duration(minutes: 1), () { if (mounted) { Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => ArrivalScreen(orderDetails: widget.orderDetails,)), ); } }); } } // ------------------------------------------------------------ Future _updateRouteAndMarkers() async { if (_currentLocation == null) return; await _fetchPolyline(driverLatLng, userLatLng); await _fetchETA(driverLatLng, userLatLng); setState(() { _markers = { Marker( markerId: MarkerId('truck'), position: driverLatLng, icon: truckIcon ?? BitmapDescriptor.defaultMarker, rotation: _truckRotation, anchor: Offset(0.5, 0.5), flat: true, ), Marker( markerId: MarkerId('destination'), position: userLatLng, icon: destinationIcon ?? BitmapDescriptor.defaultMarker, infoWindow: InfoWindow(title: widget.u_address), ), }; }); final controller = await _mapController.future; LatLngBounds bounds = LatLngBounds( southwest: LatLng( min(driverLatLng.latitude, userLatLng.latitude), min(driverLatLng.longitude, userLatLng.longitude), ), northeast: LatLng( max(driverLatLng.latitude, userLatLng.latitude), max(driverLatLng.longitude, userLatLng.longitude), ), ); controller.animateCamera(CameraUpdate.newLatLngBounds(bounds, 80)); // ----------------------- NEWLY ADDED ----------------------- _checkDriverArrival(); // ------------------------------------------------------------ } Future _fetchPolyline(LatLng from, LatLng to) async { PolylinePoints polylinePoints = PolylinePoints(); PolylineResult result = await polylinePoints.getRouteBetweenCoordinates( _googleApiKey, PointLatLng(from.latitude, from.longitude), PointLatLng(to.latitude, to.longitude), travelMode: TravelMode.driving, ); if (result.points.isNotEmpty) { _routeCoords = result.points.map((p) => LatLng(p.latitude, p.longitude)).toList(); double totalDistance = 0; for (int i = 0; i < _routeCoords.length - 1; i++) { totalDistance += _calculateDistance( _routeCoords[i].latitude, _routeCoords[i].longitude, _routeCoords[i + 1].latitude, _routeCoords[i + 1].longitude, ); } _distance = totalDistance; setState(() { _polylines = { PolylineId('route'): Polyline( polylineId: PolylineId('route'), points: _routeCoords, color: primaryColor, width: 4, patterns: [ PatternItem.dash(20), PatternItem.gap(0), ], jointType: JointType.round, ) }; }); } } Future _fetchETA(LatLng from, LatLng to) async { final url = Uri.parse( 'https://maps.googleapis.com/maps/api/directions/json?origin=${from.latitude},${from.longitude}&destination=${to.latitude},${to.longitude}&key=$_googleApiKey', ); final response = await http.get(url); if (response.statusCode == 200) { final data = json.decode(response.body); String duration = data['routes'][0]['legs'][0]['duration']['text']; setState(() { _eta = duration; }); } } double _calculateDistance(lat1, lon1, lat2, lon2) { var p = 0.017453292519943295; var a = 0.5 - cos((lat2 - lat1) * p) / 2 + cos(lat1 * p) * cos(lat2 * p) * (1 - cos((lon2 - lon1) * p)) / 2; return 12742 * asin(sqrt(a)); } Widget _priceRow(String label, String amount, {bool isBold = false}) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: fontTextStyle( 12, Color(0xFF444444), isBold ? FontWeight.w600 : FontWeight.w400, ), ), Text( amount, style: fontTextStyle( 12, Color(0xFF444444), isBold ? FontWeight.w600 : FontWeight.w400, ), ), ], ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppSettings.supplierAppBarWithoutActions( 'Order#${widget.orderDetails.bookingid}', context), body: Stack( children: [ Positioned( top: 0, left: 0, right: 0, height: MediaQuery.of(context).size.height * 0.6, child: GoogleMap( initialCameraPosition: CameraPosition( target: driverLatLng, zoom: 14, ), myLocationEnabled: true, zoomControlsEnabled: false, markers: _markers, polylines: Set.of(_polylines.values), onMapCreated: (controller) => _mapController.complete(controller), ), ), DraggableScrollableSheet( initialChildSize: 0.38, minChildSize: 0.38, maxChildSize: 0.70, builder: (context, scrollController) { return Container( padding: EdgeInsets.only(left: 16, right: 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), child: ListView( controller: scrollController, padding: EdgeInsets.only(top: 10), children: [ Center( child: Container( width: 53, height: 2, margin: EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: Color(0XFF757575), borderRadius: BorderRadius.circular(10), ), ), ), Center( child: Column( children: [ Text( "Arriving in $_eta", style: fontTextStyle( 16, Color(0xFF232527), FontWeight.w800, ), ), SizedBox( height: MediaQuery.of(context).size.height * .004, ), RichText( textAlign: TextAlign.center, text: TextSpan( children: [ TextSpan( text: "HOME", style: fontTextStyle( 11, Color(0xFF343637), FontWeight.w500), ), TextSpan( text: " - ${AppSettings.userAddress}", style: fontTextStyle( 11, Color(0xFF343637), FontWeight.w400), ), ], ), ), ], ), ), SizedBox( height: MediaQuery.of(context).size.height * .012, ), Divider(color: Color(0xFFC3C4C4), thickness: 1), SizedBox( height: MediaQuery.of(context).size.height * .012, ), Row( children: [ CircleAvatar( radius: 20, backgroundColor: Color(0XFFE8F2FF), child: Image.asset( 'images/profile_user.png', fit: BoxFit.cover, width: 50, height: 50, ), ), SizedBox( width: MediaQuery.of(context).size.width * .012, ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Prashanth", style: fontTextStyle( 12, Color(0xFF343637), FontWeight.w500, ), ), SizedBox( height: MediaQuery.of(context).size.height * .004, ), Text( "TS J8 8905", style: fontTextStyle( 12, Color(0xFF646566), FontWeight.w500, ), ), ], ), Spacer(), Padding( padding: EdgeInsets.fromLTRB(8, 0, 8, 0), child: Row( children: [ Image.asset( 'images/message.png', width: 24, height: 24, ), SizedBox( width: MediaQuery.of(context).size.width * .020, ), Image.asset( 'images/phone.png', width: 24, height: 24, ), ], ), ) ], ), SizedBox( height: MediaQuery.of(context).size.height * .024, ), InkWell( onTap: () { setState(() { _showOrderSummary = !_showOrderSummary; }); }, child: Row( children: [ Text( "View Order Summary", style: fontTextStyle( 12, Color(0xFF444444), FontWeight.w500, ), ), Spacer(), Image.asset( _showOrderSummary ? 'images/arrow-up.png' : 'images/arrow-down.png', width: 16, height: 16, ), ], ), ), SizedBox( height: MediaQuery.of(context).size.height * .010, ), AnimatedContainer( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, height: _showOrderSummary ? null : 0, padding: _showOrderSummary ? EdgeInsets.all(0) : EdgeInsets.zero, child: _showOrderSummary ? Card( color: Color(0XFFFFFFFF), child: Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "ITEMS", style: fontTextStyle( 12, Color(0xFF444444), FontWeight.w700, ), ), SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "10,000 L Drinking water x 1", style: fontTextStyle( 12, Color(0xFF515253), FontWeight.w400, ), ), Text( "₹2,500", style: fontTextStyle( 12, Color(0xFF515253), FontWeight.w400, ), ), ], ), Divider( thickness: 1, color: Color(0xFFC3C4C4)), _priceRow( "Item Total", "₹2,346.00"), _priceRow( "Delivery Charges", "₹150.00"), _priceRow( "Platform Fee", "₹6.00"), _priceRow("Taxes", "₹12.49"), Divider( thickness: 1, color: Color(0xFFC3C4C4)), _priceRow("Total Bill", "₹2,514", isBold: true), Divider( thickness: 1, color: Color(0xFFC3C4C4)), SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Mode of Payment", style: fontTextStyle( 12, Color(0xFF444444), FontWeight.w500, ), ), Row( children: [ Image.asset( 'images/success-toast.png', width: 12, height: 12, ), SizedBox(width: 4), Text( "Cash on delivery", style: fontTextStyle( 12, Color(0xFF444444), FontWeight.w500, ), ), ], ) ], ), ], ), ), ) : null, ), SizedBox( height: MediaQuery.of(context).size.height * .012, ), Padding( padding: EdgeInsets.fromLTRB(0, 0, 0, 12), child: Container( width: double.infinity, height: 80, decoration: BoxDecoration( color: Color(0xFFE8F2FF), borderRadius: BorderRadius.circular(12), ), ), ), ], ), ); }, ), ], ), ); } Widget _itemRow(String title, String price, {bool bold = false}) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(title, style: fontTextStyle( 12, Color(0XFF646566), FontWeight.w400)), Text(price, style: fontTextStyle( 12, Color(0XFF646566), FontWeight.w400)), ], ), ); } }