You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

673 lines
23 KiB

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<OrderTrackingPage> createState() => _OrderTrackingPageState();
}
class _OrderTrackingPageState extends State<OrderTrackingPage> {
final Completer<GoogleMapController> _mapController = Completer();
final Location _location = Location();
late StreamSubscription<LocationData> _locationSubscription;
StreamSubscription<CompassEvent>? _compassSubscription;
LocationData? _currentLocation;
bool _showOrderSummary = false;
BitmapDescriptor? truckIcon;
BitmapDescriptor? destinationIcon;
Set<Marker> _markers = {};
Map<PolylineId, Polyline> _polylines = {};
List<LatLng> _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<void> _loadIcons() async {
truckIcon =
await getResizedMarker('images/tanker_map_horizontal.png', 90, null);
destinationIcon = await getResizedMarker(
'images/location_supplier_landing.png', 90, Color(0XFFE76960));
}
Future<void> _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<BitmapDescriptor> 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<void> _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<void> _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<void> _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<Polyline>.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)),
],
),
);
}
}