new changes maps

This commit is contained in:
2026-08-12 10:50:45 +05:30
parent df4e044d74
commit c65d78878a
25 changed files with 6120 additions and 3814 deletions

View File

@@ -5,12 +5,13 @@ import 'dart:async';
import 'dart:convert';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter_polyline_points/flutter_polyline_points.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:lottie/lottie.dart' hide Marker;
import 'package:nearle/services/maps/map_marker_icons.dart';
import 'package:nearle/services/maps/nearle_map.dart';
import 'package:nearle/services/maps/routing_service.dart';
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
import 'package:nearle/views/helpers/constants/Font_constant.dart';
import 'package:slider_button_lite/feature/presentation/slider_button/slider.dart';

View File

@@ -16,24 +16,18 @@ class _DeliveryMapScreen extends StatefulWidget {
}
class _DeliveryMapScreenState extends State<_DeliveryMapScreen> {
GoogleMapController? _mapController;
NearleMapController? _mapController;
late final LatLng _pickupLocation;
late final LatLng _dropLocation;
final Set<Marker> _markers = {};
final Set<Polyline> _polylines = {};
late final PolylinePoints _polylinePoints;
bool _isLoadingRoute = true;
List<NearleMapMarker> _markers = const [];
List<NearleMapPolyline> _polylines = const [];
bool _isNavigating = false; // Prevent multiple clicks
static const String _googleApiKey = 'AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q';
@override
void initState() {
super.initState();
_polylinePoints = PolylinePoints(apiKey: _googleApiKey);
_resolveLocationsFromDelivery();
_setMarkers();
_isLoadingRoute = false;
_createRealRoute();
}
@@ -73,113 +67,70 @@ class _DeliveryMapScreenState extends State<_DeliveryMapScreen> {
_dropLocation = drop;
}
void _setMarkers() {
_markers.addAll([
Marker(
markerId: const MarkerId('pickup'),
position: _pickupLocation,
infoWindow: const InfoWindow(title: 'Pickup Location'),
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed),
),
Marker(
markerId: const MarkerId('drop'),
position: _dropLocation,
infoWindow: const InfoWindow(title: 'Drop Location'),
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
),
]);
Future<void> _setMarkers() async {
final pickupIcon = await MapMarkerIcons.pin(Colors.red);
final dropIcon = await MapMarkerIcons.pin(Colors.green);
if (!mounted) return;
setState(() {
_markers = [
NearleMapMarker(
id: 'pickup',
position: _pickupLocation,
icon: pickupIcon,
),
NearleMapMarker(id: 'drop', position: _dropLocation, icon: dropIcon),
];
});
}
Future<void> _createRealRoute() async {
try {
final request = PolylineRequest(
origin: PointLatLng(
_pickupLocation.latitude,
_pickupLocation.longitude,
),
destination: PointLatLng(
_dropLocation.latitude,
_dropLocation.longitude,
),
mode: TravelMode.driving,
);
final route = await RoutingService.getRoute(
origin: _pickupLocation,
destination: _dropLocation,
);
final result = await _polylinePoints.getRouteBetweenCoordinates(
request: request,
);
if (result.points.isNotEmpty) {
final routePoints = result.points
.map((e) => LatLng(e.latitude, e.longitude))
.toList();
if (mounted) {
setState(() {
_polylines.add(
Polyline(
polylineId: const PolylineId('real_route'),
color: ColorConstants.primaryColor,
width: 6,
points: routePoints,
),
);
_isLoadingRoute = false;
});
_fitMapToRoute();
}
} else {
debugPrint('[MAP_PREVIEW] No route found');
if (mounted) {
setState(() => _isLoadingRoute = false);
}
}
} catch (e) {
debugPrint('[MAP_PREVIEW] Error creating route: $e');
if (mounted) {
setState(() => _isLoadingRoute = false);
}
}
}
Future<void> _fitMapToRoute() async {
if (!mounted) return;
if (_mapController == null) return;
// Check if controller is still alive (important!)
try {
await _mapController!.getVisibleRegion();
} catch (e) {
debugPrint("❌ Map controller is dead. Skip animateCamera.");
return;
}
final bounds = LatLngBounds(
southwest: LatLng(
math.min(_pickupLocation.latitude, _dropLocation.latitude),
math.min(_pickupLocation.longitude, _dropLocation.longitude),
),
northeast: LatLng(
math.max(_pickupLocation.latitude, _dropLocation.latitude),
math.max(_pickupLocation.longitude, _dropLocation.longitude),
),
);
// Try animate safely
for (int i = 0; i < 10; i++) {
if (!mounted) return;
try {
await _mapController!.animateCamera(
CameraUpdate.newLatLngBounds(bounds, 80),
);
return;
} catch (e) {
await Future.delayed(const Duration(milliseconds: 150));
}
// Fall back to a straight line so the screen still shows the connection
// when routing is unavailable.
final points = (route != null && route.points.isNotEmpty)
? route.points
: <LatLng>[_pickupLocation, _dropLocation];
if (route == null) debugPrint('[MAP_PREVIEW] No route found');
setState(() {
_polylines = [
NearleMapPolyline(
id: 'real_route',
points: points,
color: ColorConstants.primaryColor,
width: 6,
),
];
});
_fitMapToRoute();
}
debugPrint("❌ animateCamera failed after retries (map probably disposed)");
}
Future<void> _fitMapToRoute() async {
final controller = _mapController;
if (!mounted || controller == null) return;
await controller.fitBounds(
LatLngBounds(
southwest: LatLng(
math.min(_pickupLocation.latitude, _dropLocation.latitude),
math.min(_pickupLocation.longitude, _dropLocation.longitude),
),
northeast: LatLng(
math.max(_pickupLocation.latitude, _dropLocation.latitude),
math.max(_pickupLocation.longitude, _dropLocation.longitude),
),
),
padding: 80,
);
}
@@ -188,27 +139,20 @@ Future<void> _fitMapToRoute() async {
return Scaffold(
body: Stack(
children: [
SizedBox.expand(
child: GoogleMap(
initialCameraPosition: CameraPosition(
target: _pickupLocation,
zoom: 14.5,
),
onMapCreated: (controller) {
_mapController = controller;
Future.delayed(const Duration(milliseconds: 500), () {
if (!_isLoadingRoute && _polylines.isNotEmpty) {
_fitMapToRoute();
}
});
},
markers: _markers,
polylines: _polylines,
zoomControlsEnabled: false,
myLocationButtonEnabled: false,
),
),
SizedBox.expand(
child: NearleMap(
initialCameraPosition: CameraPosition(
target: _pickupLocation,
zoom: 14.5,
),
markers: _markers,
polylines: _polylines,
onMapReady: (controller) {
_mapController = controller;
_fitMapToRoute();
},
),
),
Positioned(
top: 50,
left: 16,

View File

@@ -24,15 +24,11 @@ class MultiCustomerMapScreen extends StatefulWidget {
}
class _MultiCustomerMapScreenState extends State<MultiCustomerMapScreen> {
GoogleMapController? mapController;
Set<Marker> markers = {};
Set<Polyline> polylines = {};
NearleMapController? mapController;
List<NearleMapMarker> markers = const [];
List<NearleMapPolyline> polylines = const [];
LatLng? currentLocation;
bool _isLoading = true;
bool _mapReady = false;
final PolylinePoints _polylinePoints = PolylinePoints(
apiKey: 'AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q',
);
final Map<String, Map<String, dynamic>> _deliveryMap = {};
@override
@@ -199,7 +195,7 @@ class _MultiCustomerMapScreenState extends State<MultiCustomerMapScreen> {
}
Future<void> _createDeliveryMarkers() async {
Set<Marker> tempMarkers = {};
final List<NearleMapMarker> tempMarkers = [];
List<_StepPoint> stepPoints = [];
_deliveryMap.clear();
@@ -231,8 +227,6 @@ class _MultiCustomerMapScreenState extends State<MultiCustomerMapScreen> {
);
if (lat == 0 || lon == 0) continue;
final customerName = (delivery['deliverycustomer'] ?? 'Customer ${i + 1}')
.toString();
final orderId = (delivery['orderid'] ?? '').toString();
final status = (delivery['orderstatus']?.toString().toLowerCase() ?? '')
.trim();
@@ -242,23 +236,19 @@ class _MultiCustomerMapScreenState extends State<MultiCustomerMapScreen> {
// Store delivery data for dialog
_deliveryMap['delivery_$orderId'] = delivery;
final icon = await _createCircularMarkerBitmap(
final icon = await MapMarkerIcons.numberedCircle(
displayStep,
primaryColor: ColorConstants.primaryColor,
isSkipped: isSkipped,
isNext: isNext,
);
tempMarkers.add(
Marker(
markerId: MarkerId('delivery_$orderId'),
NearleMapMarker(
id: 'delivery_$orderId',
position: LatLng(lat, lon),
icon: icon,
infoWindow: InfoWindow(
title: isSkipped
? 'Step $displayStep: $customerName (SKIPPED)'
: 'Step $displayStep: $customerName',
snippet: 'Order #$orderId',
),
anchor: MarkerAnchor.center,
onTap: () {
_showCustomerDetailsSheet(delivery, displayStep);
},
@@ -270,13 +260,11 @@ class _MultiCustomerMapScreenState extends State<MultiCustomerMapScreen> {
if (currentLocation != null) {
tempMarkers.add(
Marker(
markerId: const MarkerId('current_location'),
NearleMapMarker(
id: 'current_location',
position: currentLocation!,
icon: BitmapDescriptor.defaultMarkerWithHue(
BitmapDescriptor.hueAzure,
),
infoWindow: const InfoWindow(title: 'You are here'),
icon: await MapMarkerIcons.dot(Colors.blueAccent),
anchor: MarkerAnchor.center,
),
);
stepPoints.insert(0, _StepPoint(0, currentLocation!));
@@ -284,67 +272,7 @@ class _MultiCustomerMapScreenState extends State<MultiCustomerMapScreen> {
stepPoints.sort((a, b) => a.step.compareTo(b.step));
Set<Polyline> newPolylines = {};
int segmentIndex = 0;
for (int i = 0; i < stepPoints.length - 1; i++) {
final start = stepPoints[i].position;
final end = stepPoints[i + 1].position;
try {
// ignore: deprecated_member_use
final request = PolylineRequest(
origin: PointLatLng(start.latitude, start.longitude),
destination: PointLatLng(end.latitude, end.longitude),
mode: TravelMode.driving,
);
final result = await _polylinePoints.getRouteBetweenCoordinates(
request: request,
);
if (result.points.isNotEmpty) {
final routePoints = result.points
.map((p) => LatLng(p.latitude, p.longitude))
.toList();
newPolylines.add(
Polyline(
polylineId: PolylineId('segment_$segmentIndex'),
points: routePoints,
width: 6,
color: Colors.blue,
startCap: Cap.roundCap,
endCap: Cap.roundCap,
jointType: JointType.round,
geodesic: true,
),
);
segmentIndex++;
} else {
newPolylines.add(
Polyline(
polylineId: PolylineId('segment_fallback_$segmentIndex'),
points: [start, end],
width: 4,
color: Colors.blue.shade200,
),
);
segmentIndex++;
}
} catch (e) {
debugPrint('[CUSTOMER_MAP] Directions error for segment $i: $e');
newPolylines.add(
Polyline(
polylineId: PolylineId('segment_error_$segmentIndex'),
points: [start, end],
width: 4,
color: Colors.blue.shade200,
),
);
segmentIndex++;
}
}
final newPolylines = await _buildRoutePolylines(stepPoints);
if (mounted) {
setState(() {
@@ -357,8 +285,50 @@ class _MultiCustomerMapScreenState extends State<MultiCustomerMapScreen> {
_fitBoundsToMarkersAndPolylines();
}
/// Builds the route through every stop in one Directions call.
///
/// This used to issue one request per consecutive pair of stops, so opening
/// this screen with N stops cost N billable requests. OSRM takes every stop
/// as a coordinate in one request, so the whole route is a single call.
Future<List<NearleMapPolyline>> _buildRoutePolylines(
List<_StepPoint> stepPoints,
) async {
if (stepPoints.length < 2) return const [];
final route = await RoutingService.getRoute(
origin: stepPoints.first.position,
destination: stepPoints.last.position,
waypoints: stepPoints
.sublist(1, stepPoints.length - 1)
.map((s) => s.position)
.toList(),
);
if (route != null && route.points.isNotEmpty) {
return [
NearleMapPolyline(
id: 'route',
points: route.points,
width: 6,
color: Colors.blue,
),
];
}
debugPrint('[CUSTOMER_MAP] Routing unavailable, drawing direct segments');
return [
NearleMapPolyline(
id: 'route_fallback',
points: stepPoints.map((s) => s.position).toList(),
width: 4,
color: Colors.blue.shade200,
),
];
}
Future<void> _fitBoundsToMarkersAndPolylines() async {
if (mapController == null) return;
final controller = mapController;
if (controller == null) return;
double minLat = double.infinity;
double maxLat = -double.infinity;
@@ -367,8 +337,7 @@ class _MultiCustomerMapScreenState extends State<MultiCustomerMapScreen> {
bool hasPoint = false;
for (final m in markers) {
final pos = m.position;
void include(LatLng pos) {
minLat = math.min(minLat, pos.latitude);
maxLat = math.max(maxLat, pos.latitude);
minLng = math.min(minLng, pos.longitude);
@@ -376,137 +345,24 @@ class _MultiCustomerMapScreenState extends State<MultiCustomerMapScreen> {
hasPoint = true;
}
for (final m in markers) {
include(m.position);
}
for (final poly in polylines) {
for (final pos in poly.points) {
minLat = math.min(minLat, pos.latitude);
maxLat = math.max(maxLat, pos.latitude);
minLng = math.min(minLng, pos.longitude);
maxLng = math.max(maxLng, pos.longitude);
hasPoint = true;
include(pos);
}
}
if (!hasPoint) return;
final bounds = LatLngBounds(
southwest: LatLng(minLat, minLng),
northeast: LatLng(maxLat, maxLng),
);
WidgetsBinding.instance.addPostFrameCallback((_) async {
try {
await mapController!.animateCamera(
CameraUpdate.newLatLngBounds(bounds, 80),
);
} catch (e) {
Future.delayed(const Duration(milliseconds: 300), () async {
try {
await mapController!.animateCamera(
CameraUpdate.newLatLngBounds(bounds, 80),
);
} catch (e) {
debugPrint('[CUSTOMER_MAP] Retry failed: $e');
}
});
}
});
}
Future<BitmapDescriptor> _createCircularMarkerBitmap(
int number, {
bool isSkipped = false,
bool isNext = false,
}) async {
const double size = 70;
final pictureRecorder = ui.PictureRecorder();
final canvas = Canvas(pictureRecorder);
final center = Offset(size / 2, size / 2);
final paint = Paint()
..color = isSkipped ? Colors.orange : ColorConstants.primaryColor;
canvas.drawCircle(center, 15, paint);
final border = Paint()
..color = isSkipped ? Colors.orange.shade900 : Colors.white
..style = PaintingStyle.stroke
..strokeWidth = isSkipped ? 4 : 3;
canvas.drawCircle(center, 15, border);
final textPainter = TextPainter(
text: TextSpan(
text: number.toString(),
style: const TextStyle(
fontSize: 19,
color: Colors.white,
fontWeight: FontWeight.bold,
),
await controller.fitBounds(
LatLngBounds(
southwest: LatLng(minLat, minLng),
northeast: LatLng(maxLat, maxLng),
),
textDirection: TextDirection.ltr,
padding: 80,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(
center.dx - textPainter.width / 2,
center.dy - textPainter.height / 2,
),
);
// Draw "NEXT" indicator (road sign with down arrow) on top
if (isNext) {
// Draw road sign background (rectangle)
final signPaint = Paint()
..color = Colors.green
..style = PaintingStyle.fill;
final signRect = RRect.fromRectAndRadius(
Rect.fromCenter(center: Offset(size / 2, 8), width: 45, height: 30),
const Radius.circular(4),
);
canvas.drawRRect(signRect, signPaint);
// Draw border
final signBorder = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 1.5;
canvas.drawRRect(signRect, signBorder);
// Draw "NEXT" text
final nextTextPainter = TextPainter(
text: const TextSpan(
text: 'NEXT',
style: TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
textDirection: TextDirection.ltr,
);
nextTextPainter.layout();
nextTextPainter.paint(
canvas,
Offset(size / 2 - nextTextPainter.width / 2, 4),
);
// Draw down arrow
final arrowPath = Path();
arrowPath.moveTo(size / 2, 18);
arrowPath.lineTo(size / 2 - 4, 24);
arrowPath.lineTo(size / 2 + 4, 24);
arrowPath.close();
final arrowPaint = Paint()
..color = Colors.white
..style = PaintingStyle.fill;
canvas.drawPath(arrowPath, arrowPaint);
}
final img = await pictureRecorder.endRecording().toImage(
size.toInt(),
size.toInt(),
);
final data = await img.toByteData(format: ui.ImageByteFormat.png);
return BitmapDescriptor.bytes(data!.buffer.asUint8List());
}
void _showCustomerDetailsSheet(
@@ -872,21 +728,14 @@ class _MultiCustomerMapScreenState extends State<MultiCustomerMapScreen> {
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: GoogleMap(
: NearleMap(
initialCameraPosition: initialCameraPosition,
markers: markers,
polylines: polylines,
myLocationEnabled: true,
myLocationButtonEnabled: true,
onMapCreated: (controller) {
onMapReady: (controller) {
mapController = controller;
_mapReady = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(const Duration(milliseconds: 150), () {
if (_mapReady) _fitBoundsToMarkersAndPolylines();
});
});
_fitBoundsToMarkersAndPolylines();
},
),
);

View File

@@ -19,12 +19,15 @@ class _RiderNavigationScreen extends StatefulWidget {
class _RiderNavigationScreenState extends State<_RiderNavigationScreen>
with WidgetsBindingObserver {
GoogleMapController? mapController;
NearleMapController? mapController;
bool _mapReady = false;
bool _isDisposed = false;
Position? _currentPosition;
StreamSubscription<Position>? _positionStream;
final Set<Marker> _markers = {};
List<NearleMapMarker> _markers = const [];
MarkerIcon? _riderIcon;
MarkerIcon? _dropIcon;
LatLng? _lastRiderPosition;
late final LatLng dropLocation;
bool _isInitializing = true;
bool _hasOpenedGoogleMaps = false;
@@ -46,6 +49,7 @@ class _RiderNavigationScreenState extends State<_RiderNavigationScreen>
super.initState();
WidgetsBinding.instance.addObserver(this);
_resolveDropFromDelivery();
_loadMarkerIcons();
_initializeMap();
_initEtaForNav();
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -214,8 +218,8 @@ class _RiderNavigationScreenState extends State<_RiderNavigationScreen>
if (lastPos != null && mounted) {
setState(() {
_currentPosition = lastPos;
_addMarkers(LatLng(lastPos.latitude, lastPos.longitude));
});
_addMarkers(LatLng(lastPos.latitude, lastPos.longitude));
_startLiveTracking();
}
});
@@ -338,8 +342,8 @@ class _RiderNavigationScreenState extends State<_RiderNavigationScreen>
if (mounted) {
setState(() {
_currentPosition = position;
_addMarkers(LatLng(position.latitude, position.longitude));
});
_addMarkers(LatLng(position.latitude, position.longitude));
final distance = Geolocator.distanceBetween(
position.latitude,
@@ -362,27 +366,36 @@ class _RiderNavigationScreenState extends State<_RiderNavigationScreen>
});
}
/// Marker bitmaps are drawn on a canvas, so they arrive asynchronously.
/// Once they land, rebuild whatever markers are already pending.
Future<void> _loadMarkerIcons() async {
final rider = await MapMarkerIcons.dot(Colors.blueAccent);
final drop = await MapMarkerIcons.pin(Colors.red);
if (!mounted || _isDisposed) return;
setState(() {
_riderIcon = rider;
_dropIcon = drop;
_addMarkers(_lastRiderPosition ?? dropLocation);
});
}
/// Mutates [_markers] only — callers own the [setState] so this can be
/// invoked from inside an existing one without nesting.
void _addMarkers(LatLng riderPos) {
_markers
..clear()
..add(
Marker(
markerId: const MarkerId('rider'),
position: riderPos,
icon: BitmapDescriptor.defaultMarkerWithHue(
BitmapDescriptor.hueAzure,
),
infoWindow: const InfoWindow(title: 'You (Rider)'),
),
)
..add(
Marker(
markerId: const MarkerId('drop'),
position: dropLocation,
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed),
infoWindow: const InfoWindow(title: 'Drop Location'),
),
);
_lastRiderPosition = riderPos;
final riderIcon = _riderIcon;
final dropIcon = _dropIcon;
if (riderIcon == null || dropIcon == null) return;
_markers = [
NearleMapMarker(
id: 'rider',
position: riderPos,
icon: riderIcon,
anchor: MarkerAnchor.center,
),
NearleMapMarker(id: 'drop', position: dropLocation, icon: dropIcon),
];
}
Future<void> _attemptAutoOpenNavigation({bool fromTimer = false}) async {
@@ -511,9 +524,8 @@ class _RiderNavigationScreenState extends State<_RiderNavigationScreen>
return Scaffold(
body: Stack(
children: [
GoogleMap(
mapType: MapType.normal,
onMapCreated: (controller) {
NearleMap(
onMapReady: (controller) {
mapController = controller;
_mapReady = true;
},
@@ -527,10 +539,7 @@ class _RiderNavigationScreenState extends State<_RiderNavigationScreen>
),
markers: _markers,
myLocationEnabled: true,
myLocationButtonEnabled: false,
trafficEnabled: true,
compassEnabled: true,
buildingsEnabled: true,
tiltGesturesEnabled: true,
),
if (!_hasOpenedGoogleMaps)
@@ -994,11 +1003,7 @@ class _RiderNavigationScreenState extends State<_RiderNavigationScreen>
}
void _safeAnimateCamera(CameraUpdate update) {
if (!_mapReady || _isDisposed || mapController == null) return;
try {
mapController?.animateCamera(update);
} catch (e) {
debugPrint('[NAVIGATION] animateCamera skipped: $e');
}
if (!_mapReady || _isDisposed) return;
mapController?.animateCamera(update);
}
}