import 'dart:async'; import 'dart:math' show cos, max, min, pi, pow, sqrt; import 'dart:typed_data'; import 'package:siro_rider/controller/home/map/ride_lifecycle_controller.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:intaleq_maps/intaleq_maps.dart'; import 'package:image/image.dart' as img; import '../../../constant/colors.dart'; // contains global 'box' import '../../../print.dart'; import '../../../views/home/map_widget.dart/cancel_raide_page.dart'; import 'location_search_controller.dart'; import 'nearby_drivers_controller.dart'; import 'ride_lifecycle_controller.dart'; import '../points_for_rider_controller.dart'; import '../../../constant/univeries_polygon.dart'; class MapEngineController extends GetxController { IntaleqMapController? mapController; bool isStyleLoaded = false; bool isIconsLoaded = false; Set markers = {}; Set polyLines = {}; List polylineCoordinates = []; Set polygons = {}; Set circles = {}; LatLngBounds? lastComputedBounds; bool mapType = false; bool mapTrafficON = false; bool isMarkersShown = false; String markerIcon = "marker_icon"; String tripIcon = "trip_icon"; String startIcon = "start_icon"; String endIcon = "end_icon"; String carIcon = "car_icon"; String motoIcon = "moto_icon"; String ladyIcon = "lady_icon"; double height = 150; double heightMenu = 0; double widthMenu = 0; double heightPickerContainer = 90; double heightPointsPageForRider = 0; double mainBottomMenuMapHeight = Get.height * .2; double wayPointSheetHeight = 0; bool heightMenuBool = false; bool isPickerShown = false; bool isPointsPageForRider = false; bool isBottomSheetShown = false; bool reloadStartApp = false; bool isCancelRidePageShown = false; bool isCashConfirmPageShown = false; bool isPaymentMethodPageShown = false; bool isRideFinished = false; bool rideConfirm = false; bool isMainBottomMenuMap = true; bool isWayPointSheet = false; bool isWayPointStopsSheet = false; bool isWayPointStopsSheetUtilGetMap = false; double heightBottomSheetShown = 0; double cashConfirmPageShown = 250; double widthMapTypeAndTraffic = 50; double paymentPageShown = Get.height * .6; bool isAnotherOreder = false; bool isWhatsAppOrder = false; Map _animationTimers = {}; final int updateIntervalMs = 100; final double minMovementThreshold = 1.0; void onMapCreated(IntaleqMapController controller) { mapController = controller; update(); } void onStyleLoaded() async { Log.print('🗺️ Intaleq Map Style Loaded. Initializing...'); isStyleLoaded = true; await _loadMapIcons(); final locationSearch = Get.find(); Get.find().reinit(); if (mapController != null) { if (markers.isNotEmpty && lastComputedBounds != null) { await _safeAnimateCameraBounds(lastComputedBounds); } else { mapController!.animateCamera( CameraUpdate.newLatLng(locationSearch.passengerLocation), ); } } update(); } Future _safeAnimateCameraBounds(LatLngBounds? bounds, {double left = 60, double top = 60, double right = 60, double bottom = 60}) async { if (bounds == null || mapController == null) return; try { if (bounds.northeast.latitude == bounds.southwest.latitude && bounds.northeast.longitude == bounds.southwest.longitude) { Log.print( '⚠️ _safeAnimateCameraBounds: Bounds are a single point, zooming to point instead.'); await mapController ?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 15)); return; } await Future.delayed(const Duration(milliseconds: 200)); await mapController?.animateCamera( CameraUpdate.newLatLngBounds( bounds, left: left, top: top, right: right, bottom: bottom, ), ); } catch (e) { Log.print('❌ _safeAnimateCameraBounds CRASH PREVENTED: $e'); try { await mapController ?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 14)); } catch (_) {} } } Future _loadMapIcons() async { isIconsLoaded = false; for (int i = 0; i < 15; i++) { if (mapController != null && isStyleLoaded) break; await Future.delayed(const Duration(milliseconds: 200)); } if (mapController == null || !isStyleLoaded) { Log.print( '⚠️ _loadMapIcons: mapController or style not ready. Icons may not load.'); } await _addMapImage(startIcon, 'assets/images/A.png'); await _addMapImage(endIcon, 'assets/images/b.png'); await _addMapImage(carIcon, 'assets/images/car.png'); await _addMapImage(motoIcon, 'assets/images/moto.png'); await _addMapImage(ladyIcon, 'assets/images/lady.png'); await _addMapImage('picker_icon', 'assets/images/picker.png'); await _addMapImage('orange_marker', 'assets/images/moto1.png'); await _addMapImage('violet_marker', 'assets/images/lady1.png'); isIconsLoaded = true; markers = markers.map((m) => m.copyWith()).toSet(); update(); if (Get.isRegistered()) { Get.find() .getCarsLocationByPassengerAndReloadMarker(); } } Future _addMapImage(String id, String path) async { try { final ByteData bytes = await rootBundle.load(path); final size = _getImageSize(id); if (size != null && (id == carIcon || id == motoIcon || id == ladyIcon)) { final resized = await _resizeImage(bytes.buffer.asUint8List(), size); await mapController?.addImage(id, resized); Log.print( 'delimited: successfully added resized map image: $id (${size}x${size})'); } else { await mapController?.addImage(id, bytes.buffer.asUint8List()); Log.print('delimited: successfully added map image: $id'); } } catch (e) { Log.print('❌ Error loading map icon $id: $e'); } } int? _getImageSize(String id) { if (id == carIcon || id == motoIcon || id == ladyIcon) return 120; return null; } Future _resizeImage(Uint8List bytes, int size) async { return await compute((Uint8List data) { final image = img.decodeImage(data); if (image == null) return data; final resized = img.copyResize(image, width: size, height: size); return Uint8List.fromList(img.encodePng(resized)); }, bytes); } void clearPolyline() { polyLines.clear(); update(); } LatLngBounds calculateBounds(double lat, double lng, double radiusInMeters) { const double earthRadius = 6378137.0; double latDelta = (radiusInMeters / earthRadius) * (180 / pi); double lngDelta = (radiusInMeters / (earthRadius * cos(pi * lat / 180))) * (180 / pi); double minLat = lat - latDelta; double maxLat = lat + latDelta; double minLng = lng - lngDelta; double maxLng = lng + lngDelta; minLat = max(-90.0, minLat); maxLat = min(90.0, maxLat); minLng = (minLng + 180) % 360 - 180; maxLng = (maxLng + 180) % 360 - 180; if (minLng > maxLng) { double temp = minLng; minLng = maxLng; maxLng = temp; } return LatLngBounds( southwest: LatLng(minLat, minLng), northeast: LatLng(maxLat, maxLng), ); } Future playRouteAnimation( List coords, LatLngBounds? bounds) async { const List segmentColors = [ Color(0xFF109642), // Green Color(0xFFF59E0B), // Amber Color(0xFF7C3AED), // Purple Color(0xFFEF4444), // Red ]; Set newPolylines = {}; final locationSearch = Get.find(); if (locationSearch.activeMenuWaypointCount > 0) { List splitIndices = []; for (int w = 0; w < locationSearch.activeMenuWaypointCount; w++) { final wp = locationSearch.menuWaypoints[w]; if (wp == null) continue; int bestIdx = 0; double bestDist = double.infinity; for (int j = 0; j < coords.length; j++) { final dx = coords[j].latitude - wp.latitude; final dy = coords[j].longitude - wp.longitude; final d = dx * dx + dy * dy; if (d < bestDist) { bestDist = d; bestIdx = j; } } splitIndices.add(bestIdx); } splitIndices.sort(); List boundaries = [0, ...splitIndices, coords.length - 1]; for (int s = 0; s < boundaries.length - 1; s++) { int from = boundaries[s]; int to = boundaries[s + 1] + 1; if (to > coords.length) to = coords.length; if (from >= to - 1) continue; final segCoords = coords.sublist(from, to); if (segCoords.length < 2) continue; final color = segmentColors[s % segmentColors.length]; newPolylines.add(Polyline( polylineId: PolylineId('segment_$s'), points: segCoords, color: color, width: 6, )); } } else { newPolylines.add(Polyline( polylineId: const PolylineId('route_primary'), points: coords, color: AppColor.primaryColor, width: 6, )); } polyLines = newPolylines; update(); Log.print( '🗺️ Drawing ${markers.length} markers + ${polyLines.length} polylines on map'); if (bounds != null) { await _safeAnimateCameraBounds(bounds); } } void _fitCameraToPoints(LatLng p1, LatLng p2) async { if (mapController == null) return; if (p1.latitude == p2.latitude && p1.longitude == p2.longitude) { try { mapController?.animateCamera(CameraUpdate.newLatLngZoom(p1, 17)); } catch (e) { Log.print("Error animating to single point: $e"); } return; } double minLat = min(p1.latitude, p2.latitude); double maxLat = max(p1.latitude, p2.latitude); double minLng = min(p1.longitude, p2.longitude); double maxLng = max(p1.longitude, p2.longitude); if ((maxLat - minLat).abs() < 0.002 && (maxLng - minLng).abs() < 0.002) { try { mapController?.animateCamera(CameraUpdate.newLatLngZoom(p1, 16)); } catch (e) { Log.print("Error animating to single point: $e"); } return; } double padding = 50.0; try { await mapController?.animateCamera( CameraUpdate.newLatLngBounds( LatLngBounds( southwest: LatLng(minLat, minLng), northeast: LatLng(maxLat, maxLng), ), left: padding, top: padding, right: padding, bottom: padding, ), ); } catch (e) { Log.print("Error animating bounds: $e"); try { LatLng center = LatLng((minLat + maxLat) / 2, (minLng + maxLng) / 2); mapController?.animateCamera(CameraUpdate.newLatLngZoom(center, 14)); } catch (_) {} } } void fitCameraToPoints(LatLng p1, LatLng p2) { _fitCameraToPoints(p1, p2); } void clearMarkersExceptStartEndAndDriver() { const String currentDriverMarkerId = 'assigned_driver_marker'; markers.removeWhere((marker) { String id = marker.markerId.value; if (id == 'start') return false; if (id == 'end') return false; if (id == currentDriverMarkerId) return false; return true; }); update(); } void clearMarkersExceptStartEnd() { markers.removeWhere((marker) { String id = marker.markerId.value; return id != 'start' && id != 'end'; }); update(); } void _updateMarkerPosition( LatLng newPosition, double newHeading, String icon) { const String markerId = 'driverToPassengers'; final mId = MarkerId(markerId); final existingMarker = markers.cast().firstWhere( (m) => m?.markerId == mId, orElse: () => null, ); if (existingMarker != null) { _smoothlyUpdateMarker(existingMarker, newPosition, newHeading, icon); } else { markers = { ...markers, Marker( markerId: mId, position: newPosition, rotation: newHeading, icon: InlqBitmap.fromStyleImage(icon), anchor: const Offset(0.5, 0.5), ), }; update(); } mapController?.animateCamera(CameraUpdate.newLatLng(newPosition)); } void updateMarkerPosition( LatLng newPosition, double newHeading, String icon) { _updateMarkerPosition(newPosition, newHeading, icon); } void _smoothlyUpdateMarker( Marker oldMarker, LatLng newPosition, double newHeading, String icon) { double distance = Geolocator.distanceBetween( oldMarker.position.latitude, oldMarker.position.longitude, newPosition.latitude, newPosition.longitude); if (distance < 2.0) return; final MarkerId markerIdKey = oldMarker.markerId; _animationTimers[markerIdKey.value]?.cancel(); int ticks = 0; const int totalSteps = 20; const int stepDuration = 50; double latStep = (newPosition.latitude - oldMarker.position.latitude) / totalSteps; double lngStep = (newPosition.longitude - oldMarker.position.longitude) / totalSteps; double headingStep = (newHeading - oldMarker.rotation) / totalSteps; LatLng currentPos = oldMarker.position; double currentHeading = oldMarker.rotation; _animationTimers[markerIdKey.value] = Timer.periodic(const Duration(milliseconds: stepDuration), (timer) { ticks++; currentPos = LatLng(currentPos.latitude + latStep, currentPos.longitude + lngStep); currentHeading += headingStep; final updatedMarker = oldMarker.copyWith( position: currentPos, rotation: currentHeading, icon: InlqBitmap.fromStyleImage(icon), ); markers = { ...markers.where((m) => m.markerId != markerIdKey), updatedMarker, }; if (mapController != null) { mapController!.animateCamera(CameraUpdate.newLatLng(currentPos)); } update(); if (ticks >= totalSteps) { timer.cancel(); _animationTimers.remove(markerIdKey.value); } }); } // تحديث موقع العلامة (Marker) واتجاهها بسلاسة على الخريطة. // تحسب الدالة المسافة بين الموقع الحالي والجديد؛ وإذا كانت أكبر من مترين، // تقوم بتقسيم الحركة والدوران إلى 20 خطوة متباعدة بـ 50 مللي ثانية (إجمالي ثانية واحدة). // يتم تحديث موضع العلامة وتحريك الكاميرا تدريجياً لتبدو حركة السيارة انسيابية. void smoothlyUpdateMarker( Marker oldMarker, LatLng newPosition, double newHeading, String icon) { _smoothlyUpdateMarker(oldMarker, newPosition, newHeading, icon); } void changeBottomSheetShown({bool? forceValue}) { if (forceValue != null) { isBottomSheetShown = forceValue; } else { isBottomSheetShown = !isBottomSheetShown; } heightBottomSheetShown = isBottomSheetShown == true ? 250 : 0; update(); } void changeCashConfirmPageShown() { isCashConfirmPageShown = !isCashConfirmPageShown; final rideLife = Get.find(); rideLife.isCashSelectedBeforeConfirmRide = true; cashConfirmPageShown = isCashConfirmPageShown == true ? 250 : 0; update(); rideLife.update(); } void changePaymentMethodPageShown() { isPaymentMethodPageShown = !isPaymentMethodPageShown; paymentPageShown = isPaymentMethodPageShown == true ? Get.height * .6 : 0; update(); } void changeMapType() { mapType = !mapType; update(); } void changeMapTraffic() { mapTrafficON = !mapTrafficON; update(); } void changeisAnotherOreder(bool val) { isAnotherOreder = val; update(); } void changeIsWhatsAppOrder(bool val) { isWhatsAppOrder = val; update(); } void changeCancelRidePageShow() { showCancelRideBottomSheet(); isCancelRidePageShown = !isCancelRidePageShown; update(); if (Get.isRegistered()) { Get.find().update(); } } void getDrawerMenu() { heightMenuBool = !heightMenuBool; widthMapTypeAndTraffic = heightMenuBool == true ? 0 : 50; heightMenu = heightMenuBool == true ? 80 : 0; widthMenu = heightMenuBool == true ? 110 : 0; update(); } void changeMainBottomMenuMap() { if (isWayPointStopsSheetUtilGetMap == true) { changeWayPointSheet(); } else { isMainBottomMenuMap = !isMainBottomMenuMap; mainBottomMenuMapHeight = isMainBottomMenuMap == true ? Get.height * .22 : Get.height * .6; isWayPointSheet = false; if (heightMenuBool == true) { getDrawerMenu(); } Get.find().initilizeGetStorage(); update(); } } void downPoints() { if (Get.find().wayPoints.length < 2) { isWayPointStopsSheetUtilGetMap = false; isWayPointSheet = false; wayPointSheetHeight = isWayPointStopsSheet ? Get.height * .45 : 0; update(); } update(); } void changeWayPointSheet() { isWayPointSheet = !isWayPointSheet; wayPointSheetHeight = isWayPointSheet == false ? 0 : Get.height * .45; update(); } void changeWayPointStopsSheet() { final locationSearch = Get.find(); if (locationSearch.wayPointIndex > -1) { isWayPointStopsSheet = true; isWayPointStopsSheetUtilGetMap = true; } isWayPointStopsSheet = !isWayPointStopsSheet; wayPointSheetHeight = isWayPointStopsSheet ? Get.height * .45 : 0; update(); } void changeHeightPlaces() { final locationSearch = Get.find(); if (locationSearch.placesDestination.isEmpty) { height = 0; update(); } else { height = 150; update(); } } void changeHeightStartPlaces() { final locationSearch = Get.find(); if (locationSearch.placesStart.isEmpty) { height = 0; update(); } else { height = 150; update(); } } void changeHeightPlacesAll(int index) { final locationSearch = Get.find(); if (locationSearch.placeListResponseAll[index].isEmpty) { height = 0; update(); } else { height = 150; update(); } } void changeHeightPlaces1() { final locationSearch = Get.find(); if (locationSearch.wayPoint1.isEmpty) { height = 0; update(); } else { height = 150; update(); } } void changeHeightPlaces2() { final locationSearch = Get.find(); if (locationSearch.wayPoint2.isEmpty) { height = 0; update(); } else { height = 150; update(); } } void changeHeightPlaces3() { final locationSearch = Get.find(); if (locationSearch.wayPoint3.isEmpty) { height = 0; update(); } else { height = 150; update(); } } void changeHeightPlaces4() { final locationSearch = Get.find(); if (locationSearch.wayPoint4.isEmpty) { height = 0; update(); } else { height = 150; update(); } } void hidePlaces() { height = 0; update(); } void changePickerShown() { isPickerShown = !isPickerShown; heightPickerContainer = isPickerShown == true ? 150 : 90; update(); } void _initializePolygons() { List> universityPolygons = UniversitiesPolygons.universityPolygons; for (int i = 0; i < universityPolygons.length; i++) { Polygon polygon = Polygon( polygonId: PolygonId('univ_$i'), points: universityPolygons[i], fillColor: Colors.blueAccent.withOpacity(0.2), strokeColor: Colors.blueAccent, strokeWidth: 2, ); polygons.add(polygon); } update(); } void _applyLowEndModeIfNeeded() { // Placeholder comment from original } Future _initMinimalIcons() async { // Icons are loaded dynamically } Future _playRouteAnimation( List coords, LatLngBounds? bounds) async { const List segmentColors = [ Color(0xFF109642), // Green Color(0xFFF59E0B), // Amber Color(0xFF7C3AED), // Purple Color(0xFFEF4444), // Red ]; Set newPolylines = {}; final locSearch = Get.find(); if (locSearch.activeMenuWaypointCount > 0) { List splitIndices = []; for (int w = 0; w < locSearch.activeMenuWaypointCount; w++) { final wp = locSearch.menuWaypoints[w]; if (wp == null) continue; int bestIdx = 0; double bestDist = double.infinity; for (int j = 0; j < coords.length; j++) { final dx = coords[j].latitude - wp.latitude; final dy = coords[j].longitude - wp.longitude; final d = dx * dx + dy * dy; if (d < bestDist) { bestDist = d; bestIdx = j; } } splitIndices.add(bestIdx); } splitIndices.sort(); List boundaries = [0, ...splitIndices, coords.length - 1]; for (int s = 0; s < boundaries.length - 1; s++) { int from = boundaries[s]; int to = boundaries[s + 1] + 1; if (to > coords.length) to = coords.length; if (from >= to - 1) continue; final segCoords = coords.sublist(from, to); if (segCoords.length < 2) continue; final color = segmentColors[s % segmentColors.length]; newPolylines.add(Polyline( polylineId: PolylineId('segment_$s'), points: segCoords, color: color, width: 6, )); } } else { newPolylines.add(Polyline( polylineId: const PolylineId('route_primary'), points: coords, color: AppColor.primaryColor, width: 6, )); } polyLines = newPolylines; update(); Log.print( '🗺️ Drawing ${markers.length} markers + ${polyLines.length} polylines on map'); update(); if (bounds != null) { await _safeAnimateCameraBounds(bounds); } } void reset() { isPickerShown = false; isPointsPageForRider = false; isBottomSheetShown = false; isCancelRidePageShown = false; isCashConfirmPageShown = false; isPaymentMethodPageShown = false; isRideFinished = false; rideConfirm = false; isMainBottomMenuMap = true; isWayPointSheet = false; isWayPointStopsSheet = false; isWayPointStopsSheetUtilGetMap = false; heightBottomSheetShown = 0; mainBottomMenuMapHeight = Get.height * 0.22; wayPointSheetHeight = 0; markers.clear(); polyLines.clear(); polylineCoordinates.clear(); _animationTimers.forEach((key, timer) => timer.cancel()); _animationTimers.clear(); update(); } @override void onClose() { _animationTimers.forEach((key, timer) => timer.cancel()); _animationTimers.clear(); mapController = null; super.onClose(); } }