import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intaleq_maps/intaleq_maps.dart'; import '../../core/constants/api_constants.dart'; import '../../core/constants/app_colors.dart'; import '../../core/services/deep_link_service.dart'; import '../../core/services/pip_service.dart'; import '../../logic/cubits/navigation/navigation_cubit.dart'; import '../../logic/cubits/navigation/navigation_state.dart'; import '../../data/models/place_model.dart'; import 'widgets/search_bar_widget.dart'; import 'widgets/explore_panel_widget.dart'; import 'widgets/active_nav_hud_widget.dart'; import 'widgets/pip_nav_hud_widget.dart'; import 'widgets/place_gates_sheet.dart'; import 'widgets/layer_selector_sheet.dart'; import 'widgets/report_hazard_sheet.dart'; import 'widgets/add_place_sheet.dart'; import 'widgets/vehicle_customizer_sheet.dart'; import 'widgets/about_awards_sheet.dart'; class MapView extends StatefulWidget { const MapView({super.key}); @override State createState() => _MapViewState(); } class _MapViewState extends State { final TextEditingController _searchController = TextEditingController(); final FocusNode _searchFocusNode = FocusNode(); bool _isSearchFocused = false; StreamSubscription? _deepLinkSub; @override void initState() { super.initState(); print("🚀 [MapView] initState: MapView mounted."); _searchFocusNode.addListener(() { if (mounted) setState(() => _isSearchFocused = _searchFocusNode.hasFocus); }); // Listen to deep links from external apps (geo:, siromaps://, google.navigation, etc.) _deepLinkSub = DeepLinkService.instance.targetStream.listen((target) { if (mounted) _handleDeepLinkTarget(target); }); // Check if a deep link arrived while the app was cold starting final initial = DeepLinkService.instance.initialTarget; if (initial != null) { DeepLinkService.instance.clearInitialTarget(); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _handleDeepLinkTarget(initial); }); } WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { print("📌 [MapView] PostFrameCallback: Triggering relockCameraToUser"); final cubit = context.read(); cubit.relockCameraToUser(); } }); } Future _handleDeepLinkTarget(DeepLinkTarget target) async { print("🔗 [MapView] Handling DeepLinkTarget: $target"); final cubit = context.read(); int retries = 0; while (cubit.state.myLocation == null && retries < 15 && mounted) { await Future.delayed(const Duration(milliseconds: 200)); retries++; } if (!mounted) return; if (cubit.state.myLocation != null) { await cubit.calculateRouteTo(target.destination, title: target.title); if (target.autoStartNavigation && mounted) { cubit.startNavigation(); } } } @override void dispose() { _deepLinkSub?.cancel(); _searchController.dispose(); _searchFocusNode.dispose(); super.dispose(); } void _recenterOnUser(NavigationCubit cubit, NavigationState state) { cubit.relockCameraToUser(); } void _showVehicleCustomizer(BuildContext context, NavigationCubit cubit, NavigationState state) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => VehicleCustomizerSheet( cubit: cubit, state: cubit.state, ), ); } void _showAboutAwardsSheet(BuildContext context) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => const AboutAwardsSheet(), ); } void _showLayerSelector(BuildContext context, NavigationCubit cubit, MapThemeType current) { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, builder: (_) => LayerSelectorSheet( currentTheme: current, onThemeChanged: (theme) { cubit.setMapTheme(theme); Navigator.of(context).pop(); }, onOpenVehicleCustomizer: () { Navigator.of(context).pop(); _showVehicleCustomizer(context, cubit, cubit.state); }, onOpenAboutAwards: () { Navigator.of(context).pop(); _showAboutAwardsSheet(context); }, ), ); } void _showHazardSheet(BuildContext context, NavigationCubit cubit, {LatLng? initialLocation}) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => ReportHazardSheet( onReport: (type, title, desc) async { final messenger = ScaffoldMessenger.of(context); final ok = await cubit.reportHazard( type: type, title: title, description: desc, position: initialLocation, ); if (mounted && ok) { messenger.showSnackBar( const SnackBar( content: Text( 'تم إرسال البلاغ بنجاح وتحديث الشبكة التشاركية.', style: TextStyle(fontSize: 12), ), backgroundColor: AppColors.tacticalEmerald, ), ); } }, ), ).whenComplete(() { if (cubit.state.isSelectingLocationOnMap) { cubit.cancelLocationPicking(); } }); } void _showAddPlaceSheet(BuildContext context, NavigationCubit cubit, {LatLng? initialLocation}) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => AddPlaceSheet( onSubmit: (name, cat) async { final messenger = ScaffoldMessenger.of(context); final ok = await cubit.submitPlace( name, cat, position: initialLocation, ); if (mounted) { messenger.showSnackBar( SnackBar( content: Text( ok ? 'تمت إضافة المكان بنجاح! شكراً لمساهمتك.' : 'تعذر حفظ المكان، يرجى المحاولة لاحقاً.', style: const TextStyle(fontSize: 12), ), backgroundColor: ok ? AppColors.tacticalEmerald : AppColors.coralDanger, ), ); } }, ), ).whenComplete(() { if (cubit.state.isSelectingLocationOnMap) { cubit.cancelLocationPicking(); } }); } void _showPlaceGatesSheet(BuildContext context, NavigationCubit cubit, PlaceModel place) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => PlaceGatesSheet( place: place, userLocation: cubit.state.myLocation, onSelectGate: (gate) { Navigator.of(context).pop(); cubit.calculateRouteTo( LatLng(gate.latitude, gate.longitude), title: '${place.name} - ${gate.nameAr}', ); }, onSelectMainPlace: () { Navigator.of(context).pop(); cubit.calculateRouteTo( LatLng(place.latitude, place.longitude), title: place.name, ); }, ), ); } @override Widget build(BuildContext context) { final cubit = context.read(); return BlocConsumer( listener: (context, state) { if (state.errorMessage != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( state.errorMessage!, style: const TextStyle(fontSize: 12), ), backgroundColor: AppColors.coralDanger, ), ); } // Auto-center camera only when idle and locked (NEVER during active navigation or route preview, as navigation cubit controls the 3D tilted camera) if (state.isCameraLocked && !state.isNavigating && state.status != NavigationStatus.routePreview && state.myLocation != null && cubit.mapController != null && cubit.isMapStyleLoaded) { cubit.mapController!.animateCamera( CameraUpdate.newLatLngZoom(state.myLocation!, 16.5), ); } // Fit route bounds when calculated if (state.status == NavigationStatus.routePreview && state.currentRoute != null && state.currentRoute!.coordinates.isNotEmpty && cubit.mapController != null && cubit.isMapStyleLoaded) { final pts = state.currentRoute!.coordinates .map((c) => LatLng(c.latitude, c.longitude)) .toList(); if (pts.isNotEmpty) { double minLat = pts.first.latitude; double maxLat = pts.first.latitude; double minLng = pts.first.longitude; double maxLng = pts.first.longitude; for (final p in pts) { if (p.latitude < minLat) minLat = p.latitude; if (p.latitude > maxLat) maxLat = p.latitude; if (p.longitude < minLng) minLng = p.longitude; if (p.longitude > maxLng) maxLng = p.longitude; } cubit.mapController!.animateCamera( CameraUpdate.newLatLngBounds( LatLngBounds( southwest: LatLng(minLat, minLng), northeast: LatLng(maxLat, maxLng), ), left: 40, right: 40, top: 130, bottom: 230, ), ); } } }, builder: (context, state) { return ValueListenableBuilder( valueListenable: PipService.instance.isInPipMode, builder: (context, isInPip, _) { return Scaffold( resizeToAvoidBottomInset: false, backgroundColor: AppColors.canvasLight, body: SizedBox.expand( child: Stack( fit: StackFit.expand, children: [ // ── 1. REAL INTERACTIVE MAP ENGINE (Siro Real Tiles) ── Positioned.fill( child: _buildRealMapEngine(context, cubit, state), ), // ── 2. TOP SEARCH BAR, OFFLINE BANNER & EXPLORE CHIPS ── if (!state.isNavigating && !isInPip) Positioned( top: 0, left: 0, right: 0, child: SafeArea( bottom: false, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Connectivity Status Banner if (!state.isOnline) Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), decoration: BoxDecoration( color: AppColors.sovereignGold.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(12), border: Border.all( color: AppColors.sovereignGold.withValues(alpha: 0.4), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.wifi_off_rounded, size: 14, color: AppColors.sovereignGold), const SizedBox(width: 8), const Text( 'أنت غير متصل بالإنترنت • تم تفعيل وضع التوجيه السيادي دون اتصال', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: AppColors.sovereignGold, ), ), ], ), ), SearchBarWidget( controller: _searchController, focusNode: _searchFocusNode, isSearching: state.isSearching, onChanged: cubit.onSearchChanged, onSubmitted: (q) => cubit.searchImmediately(q), onClear: () { _searchController.clear(); cubit.clearSearch(); }, onMenuTap: () => _showLayerSelector(context, cubit, state.mapTheme), ), const SizedBox(height: 10), ExplorePanelWidget( onCategorySelected: (q) { _searchController.text = q; cubit.searchImmediately(q); }, ), ], ), ), ), ), // ── 3. SEARCH RESULTS & RECENT HISTORY DROPDOWN ── if (!state.isNavigating && (state.searchResults.isNotEmpty || (_isSearchFocused && _searchController.text.trim().isEmpty && state.recentSearches.isNotEmpty) || (_isSearchFocused && _searchController.text.trim().length >= 2 && !state.isSearching && state.searchResults.isEmpty))) Positioned( top: 130, left: 16, right: 16, child: Container( constraints: const BoxConstraints(maxHeight: 280), decoration: BoxDecoration( color: AppColors.pureWhite, borderRadius: BorderRadius.circular(20), boxShadow: const [ BoxShadow( color: Color(0x1F000000), blurRadius: 20, offset: Offset(0, 6), ), ], border: Border.all(color: AppColors.borderSubtle), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ if (state.searchResults.isNotEmpty) ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'الوجهات المطابقة', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700, color: AppColors.textPrimary, ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: AppColors.appleBlue.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Text( '${state.searchResults.length} نتائج', style: const TextStyle( fontSize: 10, fontWeight: FontWeight.w600, color: AppColors.appleBlue, ), ), ), ], ), ), const Divider(height: 1, color: AppColors.borderSubtle), Flexible( child: ListView.separated( shrinkWrap: true, padding: const EdgeInsets.symmetric(vertical: 4), itemCount: state.searchResults.length, separatorBuilder: (_, __) => const Divider(height: 1, color: AppColors.borderSubtle), itemBuilder: (context, index) { final place = state.searchResults[index]; final iconData = _getCategoryIcon(place.category); final iconColor = _getCategoryColor(place.category); final categoryAr = _getCategoryArabicName(place.category); double? distM; if (state.myLocation != null) { distM = cubit.locationService.calculateDistance( state.myLocation!, LatLng(place.latitude, place.longitude), ); } final distStr = distM != null ? (distM > 1000 ? '${(distM / 1000).toStringAsFixed(1)} كم' : '${distM.round()} م') : null; return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2), leading: Container( width: 38, height: 38, decoration: BoxDecoration( color: iconColor.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(12), ), child: Icon(iconData, color: iconColor, size: 20), ), title: Text( place.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), subtitle: Row( children: [ Text( categoryAr, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w500, color: iconColor, ), ), if (place.address != null && place.address!.isNotEmpty) ...[ const Text(' • ', style: TextStyle(color: AppColors.textMuted)), Expanded( child: Text( place.address!, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 11, color: AppColors.textSecondary, ), ), ), ], ], ), trailing: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ if (place.hasGates) ...[ Container( margin: const EdgeInsets.only(bottom: 4), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: AppColors.appleBlue.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(4), border: Border.all( color: AppColors.appleBlue.withValues(alpha: 0.3), width: 0.5, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.meeting_room_rounded, size: 10, color: AppColors.appleBlue), const SizedBox(width: 3), Text( '${place.gates.length} بوابات', style: const TextStyle( fontSize: 9, fontWeight: FontWeight.w700, color: AppColors.appleBlue, ), ), ], ), ), ], if (distStr != null) Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: AppColors.surfaceMuted, borderRadius: BorderRadius.circular(6), ), child: Text( distStr, style: const TextStyle( fontSize: 10, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), ), if (place.elevationMeters > 0) Padding( padding: const EdgeInsets.only(top: 2), child: Text( '${place.elevationMeters.toInt()} م', style: const TextStyle( fontSize: 9, color: AppColors.textMuted, ), ), ), ], ), onTap: () { cubit.saveRecentSearch(place.name); _searchController.clear(); cubit.clearSearch(); _searchFocusNode.unfocus(); if (place.hasGates) { _showPlaceGatesSheet(context, cubit, place); } else { cubit.calculateRouteTo( LatLng(place.latitude, place.longitude), title: place.name, ); } }, ); }, ), ), ] else if (_isSearchFocused && _searchController.text.trim().isEmpty && state.recentSearches.isNotEmpty) ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Row( children: [ Icon(Icons.history_rounded, size: 16, color: AppColors.appleBlue), SizedBox(width: 6), Text( 'عمليات البحث الأخيرة', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700, color: AppColors.textPrimary, ), ), ], ), GestureDetector( onTap: cubit.clearRecentSearches, child: const Text( 'مسح السجل', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: AppColors.coralDanger, ), ), ), ], ), ), const Divider(height: 1, color: AppColors.borderSubtle), Flexible( child: ListView.separated( shrinkWrap: true, padding: const EdgeInsets.symmetric(vertical: 4), itemCount: state.recentSearches.length, separatorBuilder: (_, __) => const Divider(height: 1, color: AppColors.borderSubtle), itemBuilder: (context, index) { final query = state.recentSearches[index]; return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0), leading: const Icon(Icons.access_time_rounded, size: 18, color: AppColors.textMuted), title: Text( query, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: AppColors.textPrimary, ), ), trailing: const Icon(Icons.north_west_rounded, size: 14, color: AppColors.textMuted), onTap: () { _searchController.text = query; cubit.searchImmediately(query); }, ); }, ), ), ] else if (_isSearchFocused && _searchController.text.trim().length >= 2 && !state.isSearching) ...[ Padding( padding: const EdgeInsets.all(20), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.search_off_rounded, size: 36, color: AppColors.textMuted), const SizedBox(height: 8), Text( 'لا توجد نتائج مطابقة لـ "${_searchController.text.trim()}"', style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), const SizedBox(height: 4), const Text( 'تأكد من كتابة الاسم بدقة أو جرب فئات مثل: مطعم، صيدلية، وقود', textAlign: TextAlign.center, style: TextStyle( fontSize: 11, color: AppColors.textMuted, ), ), ], ), ), ], ], ), ), ), // ── 4. MULTI-ROUTE PREVIEW & NAVIGATION LAUNCHER ── if (state.status == NavigationStatus.routePreview && state.currentRoute != null) Positioned( bottom: 24, left: 16, right: 16, child: Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: AppColors.pureWhite, borderRadius: BorderRadius.circular(28), boxShadow: const [ BoxShadow( color: Color(0x1F000000), blurRadius: 28, offset: Offset(0, 8), ), ], border: Border.all(color: AppColors.borderSubtle), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Dual Origin (A) & Destination (B) Header with close button Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Visual Pins (Green A -> Red B with connector) Column( children: [ Container( width: 22, height: 22, decoration: const BoxDecoration( color: AppColors.tacticalEmerald, shape: BoxShape.circle, ), child: const Center( child: Text( 'أ', style: TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold, ), ), ), ), Container( width: 2, height: 18, margin: const EdgeInsets.symmetric(vertical: 2), color: AppColors.borderSubtle, ), Container( width: 22, height: 22, decoration: const BoxDecoration( color: AppColors.coralDanger, shape: BoxShape.circle, ), child: const Center( child: Text( 'ب', style: TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold, ), ), ), ), ], ), const SizedBox(width: 12), // Location Titles & Metrics Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( state.originTitle.isNotEmpty ? state.originTitle : 'موقعي الحالي', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.textSecondary, ), ), const SizedBox(height: 14), Text( state.destinationTitle.isNotEmpty ? state.destinationTitle : 'الوجهة المحددة', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w800, color: AppColors.textPrimary, ), ), ], ), ), IconButton( icon: const Icon(Icons.close_rounded, color: AppColors.textMuted), onPressed: cubit.stopNavigation, ), ], ), const SizedBox(height: 10), // ETA, Distance & Timing Badge Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: AppColors.tacticalEmerald.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( state.currentRoute!.formattedDuration, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w800, color: AppColors.tacticalEmerald, ), ), const SizedBox(width: 8), Text( '• ${state.currentRoute!.formattedDistance} • وصول ${state.arrivalTime}', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), ], ), ), // Multi-Route Options Selector (Route 1 vs Route 2) if (state.routes.length > 1) ...[ const SizedBox(height: 12), SizedBox( height: 96, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: state.routes.length, separatorBuilder: (_, __) => const SizedBox(width: 10), itemBuilder: (context, index) { final r = state.routes[index]; final isSelected = state.selectedRouteIndex == index; return GestureDetector( onTap: () => cubit.selectRoute(index), child: Container( width: 160, padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: isSelected ? AppColors.appleBlue.withValues(alpha: 0.08) : AppColors.surfaceMuted, borderRadius: BorderRadius.circular(16), border: Border.all( color: isSelected ? AppColors.appleBlue : AppColors.borderSubtle, width: isSelected ? 2.0 : 1.0, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Icon( Icons.route_rounded, size: 16, color: isSelected ? AppColors.appleBlue : AppColors.textMuted, ), const SizedBox(width: 6), Expanded( child: Text( r.routeName, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 12, fontWeight: isSelected ? FontWeight.w700 : FontWeight.w600, color: isSelected ? AppColors.appleBlue : AppColors.textPrimary, ), ), ), ], ), Text( '${r.formattedDuration} • ${r.formattedDistance}', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700, color: isSelected ? AppColors.textPrimary : AppColors.textSecondary, ), ), // Route Badges (Fastest, Eco, Steep Slope) SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ if (r.isFastest) Container( margin: const EdgeInsets.only(left: 4), padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), decoration: BoxDecoration( color: AppColors.appleBlue.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(6), ), child: const Text( 'الأسرع', style: TextStyle(fontSize: 9, fontWeight: FontWeight.w700, color: AppColors.appleBlue), ), ), if (r.isEcoFriendly) Container( margin: const EdgeInsets.only(left: 4), padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), decoration: BoxDecoration( color: const Color(0xFF2E7D32).withValues(alpha: 0.15), borderRadius: BorderRadius.circular(6), ), child: const Text( 'موفر للوقود', style: TextStyle(fontSize: 9, fontWeight: FontWeight.w700, color: Color(0xFF2E7D32)), ), ), if (r.hasSteepSlope) Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), decoration: BoxDecoration( color: const Color(0xFFE65100).withValues(alpha: 0.15), borderRadius: BorderRadius.circular(6), ), child: const Text( 'منحدر شديد', style: TextStyle(fontSize: 9, fontWeight: FontWeight.w700, color: Color(0xFFE65100)), ), ), ], ), ), ], ), ), ); }, ), ), ], // Slope Safety Advisory (Real SRTM Satellite Elevation Warning) if (state.currentRoute!.hasSteepSlope || state.currentRoute!.slopeWarning != null) Container( margin: const EdgeInsets.only(top: 12), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: const Color(0xFFFFF3E0), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFFFCC80)), ), child: Row( children: [ const Icon(Icons.warning_amber_rounded, color: Color(0xFFE65100), size: 18), const SizedBox(width: 8), Expanded( child: Text( state.currentRoute!.slopeWarning ?? 'طريق شديدة الانحدار، يرجى استخدام الغيارات العكسية وتفقد الفرامل.', style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xFFBF360C), ), ), ), ], ), ), // Eco-friendly Fuel Savings Advisory if (state.currentRoute!.isEcoFriendly && state.currentRoute!.fuelSavingsPercent > 0) Container( margin: const EdgeInsets.only(top: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), decoration: BoxDecoration( color: const Color(0xFFE8F5E9), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFA5D6A7)), ), child: Row( children: [ const Icon(Icons.eco_rounded, color: Color(0xFF2E7D32), size: 16), const SizedBox(width: 8), Expanded( child: Text( 'مسار اقتصادي موفر للوقود بنسبة ${state.currentRoute!.fuelSavingsPercent.round()}% بفضل تضاريس الطريق المناسبة.', style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xFF1B5E20), ), ), ), ], ), ), const SizedBox(height: 16), // Start Navigation Action Button SizedBox( width: double.infinity, height: 52, child: ElevatedButton.icon( onPressed: () { HapticFeedback.mediumImpact(); cubit.startNavigation(); }, icon: const Icon(Icons.navigation_rounded, color: Colors.white, size: 20), label: const Text( 'ابدأ الملاحة', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700), ), style: ElevatedButton.styleFrom( backgroundColor: AppColors.appleBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), elevation: 0, ), ), ), ], ), ), ), // ── 5. ACTIVE TURN-BY-TURN HUD & BANNER (OR PIP HUD) ── if (state.isNavigating && isInPip) Positioned( top: 0, left: 0, right: 0, child: PipNavHudWidget( state: state, onExpand: () => PipService.instance.exitPictureInPicture(), onStopNavigation: cubit.stopNavigation, ), ), if (state.isNavigating && !isInPip) Positioned.fill( child: SafeArea( child: ActiveNavHudWidget( state: state, onStopNavigation: cubit.stopNavigation, onToggleMute: cubit.toggleMute, onRecenter: () => _recenterOnUser(cubit, state), onOverviewStep: (idx) => cubit.overviewStepBounding(idx), onOpenVehicleCustomizer: () => _showVehicleCustomizer(context, cubit, state), ), ), ), // ── 5b. INTERACTIVE LOCATION PIN PICKER HUD (Add Place & Hazard) ── if (!isInPip && state.isSelectingLocationOnMap) ...[ // Centered Floating Target Pin IgnorePointer( child: Center( child: Padding( padding: const EdgeInsets.only(bottom: 38), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: state.activePickerMode == 'place' ? AppColors.appleBlue : AppColors.coralDanger, shape: BoxShape.circle, boxShadow: const [ BoxShadow( color: Color(0x33000000), blurRadius: 16, offset: Offset(0, 6), ), ], ), child: Icon( state.activePickerMode == 'place' ? Icons.add_location_alt_rounded : Icons.warning_amber_rounded, size: 28, color: Colors.white, ), ), Container( width: 3, height: 14, color: state.activePickerMode == 'place' ? AppColors.appleBlue : AppColors.coralDanger, ), Container( width: 8, height: 8, decoration: const BoxDecoration( color: Colors.black54, shape: BoxShape.circle, ), ), ], ), ), ), ), // Bottom Confirmation Card Positioned( bottom: 24, left: 16, right: 16, child: SafeArea( top: false, child: Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: AppColors.pureWhite, borderRadius: BorderRadius.circular(24), boxShadow: const [ BoxShadow( color: Color(0x29000000), blurRadius: 24, offset: Offset(0, 8), ), ], border: Border.all(color: AppColors.borderSubtle), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: (state.activePickerMode == 'place' ? AppColors.appleBlue : AppColors.coralDanger) .withValues(alpha: 0.12), borderRadius: BorderRadius.circular(12), ), child: Icon( state.activePickerMode == 'place' ? Icons.place_rounded : Icons.report_problem_rounded, size: 20, color: state.activePickerMode == 'place' ? AppColors.appleBlue : AppColors.coralDanger, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( state.activePickerMode == 'place' ? 'تحديد موقع المنشأة على الخريطة' : 'تحديد موقع البلاغ أو الخطر على الخريطة', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.textPrimary, ), ), const SizedBox(height: 2), Text( 'إحداثيات: ${state.pickedLocation?.latitude.toStringAsFixed(5) ?? '--'} ، ${state.pickedLocation?.longitude.toStringAsFixed(5) ?? '--'}', style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w500, color: AppColors.textSecondary, ), ), ], ), ), ], ), const SizedBox(height: 16), Row( children: [ Expanded( child: SizedBox( height: 48, child: ElevatedButton( onPressed: () { final loc = state.pickedLocation; if (state.activePickerMode == 'place') { _showAddPlaceSheet(context, cubit, initialLocation: loc); } else { _showHazardSheet(context, cubit, initialLocation: loc); } }, style: ElevatedButton.styleFrom( backgroundColor: state.activePickerMode == 'place' ? AppColors.appleBlue : AppColors.coralDanger, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), elevation: 0, ), child: const Text( 'تأكيد الموقع ومتابعة التفاصيل', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w700, fontSize: 13, ), ), ), ), ), const SizedBox(width: 10), SizedBox( height: 48, child: TextButton( onPressed: cubit.cancelLocationPicking, style: TextButton.styleFrom( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), backgroundColor: AppColors.surfaceMuted, ), child: const Text( 'إلغاء', style: TextStyle( color: AppColors.textSecondary, fontWeight: FontWeight.w600, fontSize: 13, ), ), ), ), ], ), ], ), ), ), ), ], // ── 6. FLOATING ACTION BUTTONS (Right side, anchored to bottom) ── if (!isInPip && !state.isNavigating && state.status != NavigationStatus.routePreview && !state.isSelectingLocationOnMap) Positioned( right: 16, bottom: 28, child: SafeArea( top: false, child: Column( mainAxisSize: MainAxisSize.min, children: [ // Layer Selector Button _buildFloatingCircle( icon: Icons.layers_rounded, color: AppColors.textPrimary, tooltip: 'طبقات الخريطة', onTap: () => _showLayerSelector(context, cubit, state.mapTheme), ), const SizedBox(height: 12), // Uruk International Prize & Platform Architecture Button _buildFloatingCircle( icon: Icons.workspace_premium_rounded, color: AppColors.urukGoldDark, tooltip: 'عن جائزة أوروك والمنظومة التقنية', onTap: () => _showAboutAwardsSheet(context), ), const SizedBox(height: 12), // Add Place Button _buildFloatingCircle( icon: Icons.add_location_alt_rounded, color: AppColors.appleBlue, tooltip: 'إضافة مكان', onTap: () => cubit.startLocationPicking('place'), ), const SizedBox(height: 12), // Report Hazard Button _buildFloatingCircle( icon: Icons.warning_amber_rounded, color: AppColors.sovereignGold, tooltip: 'إبلاغ عن حالة طريق', onTap: () => cubit.startLocationPicking('hazard'), ), const SizedBox(height: 12), // Recenter GPS Button _buildFloatingCircle( icon: Icons.my_location_rounded, color: AppColors.appleBlue, tooltip: 'موقعي', onTap: () => _recenterOnUser(cubit, state), ), ], ), ), ), // ── 6b. LIVE FLOATING SPEEDOMETER (Bottom left, when driving) ── if (!isInPip && !state.isNavigating && state.status != NavigationStatus.routePreview && state.speed > 3.0) Positioned( left: 16, bottom: 32, child: SafeArea( top: false, child: Container( width: 58, height: 58, decoration: BoxDecoration( color: AppColors.pureWhite, shape: BoxShape.circle, border: Border.all( color: AppColors.appleBlue.withValues(alpha: 0.3), width: 2.5, ), boxShadow: const [ BoxShadow( color: Color(0x1F000000), blurRadius: 14, offset: Offset(0, 4), ), ], ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '${state.speed.round()}', style: const TextStyle( fontFamily: '.SF Pro Text', fontSize: 18, fontWeight: FontWeight.w800, height: 1.0, color: AppColors.textPrimary, ), ), const SizedBox(height: 1), const Text( 'كم/س', style: TextStyle( fontFamily: '.SF Pro Text', fontSize: 8, fontWeight: FontWeight.w700, color: AppColors.textMuted, ), ), ], ), ), ), ), // ── 7. LOADING OVERLAY ── if (state.status == NavigationStatus.loading) Container( color: Colors.black.withValues(alpha: 0.15), child: const Center( child: CircularProgressIndicator(color: AppColors.appleBlue), ), ), ], ), ), ); }, ); }, ); } Widget _buildFloatingCircle({ required IconData icon, required Color color, required String tooltip, required VoidCallback onTap, }) { return Tooltip( message: tooltip, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(25), child: Container( width: 48, height: 48, decoration: BoxDecoration( color: AppColors.pureWhite, shape: BoxShape.circle, boxShadow: const [ BoxShadow( color: Color(0x1F000000), blurRadius: 14, offset: Offset(0, 4), ), ], border: Border.all(color: AppColors.borderSubtle), ), child: Center(child: Icon(icon, color: color, size: 22)), ), ), ); } IconData _getCategoryIcon(String category) { switch (category.toLowerCase()) { case 'restaurant': case 'cafe': case 'food': return Icons.restaurant_rounded; case 'fuel': case 'gas_station': return Icons.local_gas_station_rounded; case 'hospital': case 'clinic': case 'pharmacy': return Icons.local_hospital_rounded; case 'school': case 'university': case 'education': return Icons.school_rounded; case 'bank': case 'atm': return Icons.account_balance_rounded; case 'mosque': case 'place_of_worship': return Icons.mosque_rounded; case 'supermarket': case 'mall': case 'shop': return Icons.shopping_bag_rounded; case 'hotel': return Icons.hotel_rounded; default: return Icons.place_rounded; } } Color _getCategoryColor(String category) { switch (category.toLowerCase()) { case 'restaurant': case 'cafe': case 'food': return const Color(0xFFE67E22); case 'fuel': case 'gas_station': return const Color(0xFFD97706); case 'hospital': case 'clinic': case 'pharmacy': return AppColors.coralDanger; case 'school': case 'university': case 'education': return const Color(0xFF8B5CF6); case 'bank': case 'atm': return AppColors.tacticalEmerald; case 'mosque': case 'place_of_worship': return const Color(0xFF059669); case 'supermarket': case 'mall': case 'shop': return const Color(0xFFEC4899); case 'hotel': return const Color(0xFF0284C7); default: return AppColors.appleBlue; } } String _getCategoryArabicName(String category) { switch (category.toLowerCase()) { case 'restaurant': case 'food': return 'مطعم'; case 'cafe': return 'مقهى'; case 'fuel': case 'gas_station': return 'محطة وقود'; case 'hospital': case 'clinic': return 'مستشفى'; case 'pharmacy': return 'صيدلية'; case 'school': return 'مدرسة'; case 'university': return 'جامعة'; case 'education': return 'تعليم'; case 'bank': return 'بنك'; case 'atm': return 'صراف آلي'; case 'mosque': case 'place_of_worship': return 'مسجد'; case 'supermarket': case 'mall': case 'shop': return 'تسوق'; case 'hotel': return 'فندق'; default: return 'موقع'; } } void _showDestinationDialog(BuildContext context, NavigationCubit cubit, LatLng pos) { showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: AppColors.pureWhite, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), title: const Text( 'بدء الملاحة إلى هذا الموقع؟', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700), ), content: Text( 'الإحداثيات: ${pos.latitude.toStringAsFixed(4)}, ${pos.longitude.toStringAsFixed(4)}', style: const TextStyle(fontSize: 12, color: AppColors.textSecondary), ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text( 'إلغاء', style: TextStyle(fontSize: 13, color: AppColors.textMuted), ), ), ElevatedButton( onPressed: () { Navigator.of(ctx).pop(); cubit.calculateRouteTo(pos, title: 'الموقع المحدد على الخريطة'); }, style: ElevatedButton.styleFrom( backgroundColor: AppColors.appleBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(999)), elevation: 0, ), child: const Text( 'احسب المسار', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w700), ), ), ], ), ); } Widget _buildRealMapEngine( BuildContext context, NavigationCubit cubit, NavigationState state, ) { final resolvedTarget = state.myLocation ?? const LatLng(ApiConstants.defaultLat, ApiConstants.defaultLng); final mapType = switch (state.mapTheme) { MapThemeType.vectorDark => IntaleqMapType.normal, MapThemeType.vectorLight => IntaleqMapType.light, MapThemeType.satellite => IntaleqMapType.satellite, }; print("🗺️ [MapView] _buildRealMapEngine: theme=${state.mapTheme}, mapType=$mapType, initialPos=(${resolvedTarget.latitude.toStringAsFixed(4)}, ${resolvedTarget.longitude.toStringAsFixed(4)}), markersCount=${state.markers.length}, polylinesCount=${state.polylines.length}"); return IntaleqMap( apiKey: ApiConstants.mapSaasKey, initialCameraPosition: CameraPosition( target: resolvedTarget, zoom: 16.0, ), mapType: mapType, markers: state.markers, polylines: state.polylines, onMapCreated: cubit.onMapCreated, onStyleLoaded: cubit.onStyleLoaded, myLocationEnabled: false, autoCache: true, rotateGesturesEnabled: true, scrollGesturesEnabled: true, tiltGesturesEnabled: true, zoomControlsEnabled: false, compassEnabled: false, onCameraMove: (pos) { if (state.isSelectingLocationOnMap) { cubit.updatePickedLocation(pos.target); } }, onTap: (latLng) { if (state.isSelectingLocationOnMap) { cubit.updatePickedLocation(latLng); } else { cubit.clearSearch(); FocusScope.of(context).unfocus(); } }, onLongPress: (latLng) { _showDestinationDialog(context, cubit, latLng); }, ); } }