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
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)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|