// transit_map_page.dart — خريطة الباص الحي (مواصلاتي) // خريطة مستقلة داخل مجلد transit — لا تمس home_captain ولا passenger map import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intaleq_maps/intaleq_maps.dart'; import '../../constant/colors.dart'; import '../../constant/style.dart'; import '../../env/env.dart'; import '../../controller/transit/transit_controller.dart'; import 'transit_qr_code_page.dart'; import '../widgets/elevated_btn.dart'; // ── ألوان الصفحة ────────────────────────────────────────────────────────────── Color get _bg => Get.isDarkMode ? const Color(0xFF0A0F1E) : const Color(0xFFF0F4FF); Color get _card => Get.isDarkMode ? const Color(0xFF151C2E) : Colors.white; Color get _text => Get.isDarkMode ? const Color(0xFFEEF2FF) : const Color(0xFF1E293B); Color get _sub => Get.isDarkMode ? const Color(0xFF94A3B8) : const Color(0xFF64748B); const Color _accent = Color(0xFF2563EB); class TransitMapPage extends StatefulWidget { final int routeId; final String routeName; const TransitMapPage({super.key, required this.routeId, required this.routeName}); @override State createState() => _TransitMapPageState(); } class _TransitMapPageState extends State { IntaleqMapController? _mapCtrl; InlqBitmap _busIcon = InlqBitmap.defaultMarkerWithHue(120); bool _busIconLoaded = false; bool _cameraLocked = true; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) async { // تحميل أيقونة الباص من الأصول (bus.png: 499×499، size يضبط الحجم المرئي ~80dp) try { _busIcon = InlqBitmap.fromAsset( 'assets/images/bus.png', size: 0.40, ); _busIconLoaded = true; } catch (_) { _busIcon = InlqBitmap.defaultMarkerWithHue(120); } // تحميل بيانات الرحلة + اشتراك السوكيت await Get.find().openLiveRoute(widget.routeId); // اشتراك FCM لتنبيهات الخط Get.find().subscribeToRouteTopic(widget.routeId); }); } @override void dispose() { Get.find().closeLiveRoute(); Get.find().unsubscribeFromRouteTopic(widget.routeId); super.dispose(); } @override Widget build(BuildContext context) { final bottomPad = MediaQuery.of(context).padding.bottom; return Scaffold( backgroundColor: _bg, body: GetBuilder( builder: (c) { // ── بناء ماركرات المحطات والباص ───────────────────────── final stops = (c.liveTripData?['stops'] is List) ? List.from(c.liveTripData!['stops']) : []; final markers = {}; // ماركر المحطات for (final s in stops) { final lat = double.tryParse(s['latitude']?.toString() ?? '') ?? 0; final lng = double.tryParse(s['longitude']?.toString() ?? '') ?? 0; if (lat == 0 && lng == 0) continue; final isMajor = (s['is_major']?.toString() ?? '0') == '1'; markers.add(Marker( markerId: MarkerId('stop_${s['id']}'), position: LatLng(lat, lng), infoWindow: InfoWindow(title: s['name_ar']?.toString() ?? ''), icon: InlqBitmap.defaultMarkerWithHue(isMajor ? 30 : 200), zIndex: 1, )); } // ماركر الباص — أيقونة مخصصة 80×80 بدون خلفية LatLng? busPos; if (c.liveBusPosition != null) { busPos = LatLng(c.liveBusPosition!.lat, c.liveBusPosition!.lng); markers.add(Marker( markerId: const MarkerId('transit_bus'), position: busPos, rotation: c.liveBusPosition!.heading, anchor: const Offset(0.5, 0.5), flat: true, zIndex: 3, icon: _busIconLoaded ? _busIcon : InlqBitmap.defaultMarkerWithHue(120), )); // تتبع الكاميرا للباص if (_cameraLocked && _mapCtrl != null) { _mapCtrl!.animateCamera(CameraUpdate.newLatLng(busPos)); } } // ── polyline الخط ────────────────────────────────────── final polylines = {}; final encodedPolyline = c.liveTripData?['trip']?['route_polyline']?.toString() ?? ''; if (encodedPolyline.isNotEmpty) { final coords = PolylineUtils.decode(encodedPolyline); if (coords.isNotEmpty) { polylines.add(Polyline( polylineId: const PolylineId('transit_route'), points: coords, color: _accent.withOpacity(0.7), width: 4, )); } } final initialTarget = busPos ?? (markers.isNotEmpty ? markers.first.position : const LatLng(31.95, 35.93)); // ── معلومات البطاقة السفلية ──────────────────────────── final trip = c.liveTripData?['trip']; final driverName = trip?['driver_name']?.toString() ?? ''; final departure = trip?['departure_time']?.toString() ?? ''; final delayMin = int.tryParse(trip?['delay_minutes']?.toString() ?? '0') ?? 0; final currentStopSeq = c.liveBusPosition?.currentStopSeq; final currentStopName = currentStopSeq != null && stops.isNotEmpty ? stops .where((s) => int.tryParse(s['sequence']?.toString() ?? '') == currentStopSeq) .map((s) => s['name_ar']?.toString() ?? '') .firstOrNull : null; return Stack( children: [ // ── 1. الخريطة ───────────────────────────────────── Listener( onPointerDown: (_) { if (_cameraLocked) setState(() => _cameraLocked = false); }, child: IntaleqMap( apiKey: Env.mapSaasKey, initialCameraPosition: CameraPosition(target: initialTarget, zoom: 14.5), markers: markers, polylines: polylines, onMapCreated: (ctrl) => _mapCtrl = ctrl, mapType: Get.isDarkMode ? IntaleqMapType.normal : IntaleqMapType.light, myLocationEnabled: true, ), ), // ── 2. شريط العنوان ──────────────────────────────── Positioned( top: MediaQuery.of(context).padding.top + 8, left: 12, right: 12, child: Row( children: [ // زر الرجوع GestureDetector( onTap: () => Get.back(), child: Container( width: 42, height: 42, decoration: BoxDecoration( color: _card, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.12), blurRadius: 10, offset: const Offset(0, 3)) ], ), child: Icon(Icons.arrow_back_ios_rounded, color: _text, size: 18), ), ), const SizedBox(width: 10), // اسم الخط Expanded( child: Container( padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 10), decoration: BoxDecoration( color: _card, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.08), blurRadius: 10, offset: const Offset(0, 3)) ], ), child: Row( children: [ Container( width: 8, height: 8, decoration: BoxDecoration( shape: BoxShape.circle, color: c.liveBusPosition != null ? Colors.greenAccent.shade400 : Colors.grey, ), ), const SizedBox(width: 8), Expanded( child: Text( widget.routeName, style: AppStyle.title.copyWith( fontSize: 14, color: _text, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), ), ), const SizedBox(width: 10), // زر إعادة تثبيت الكاميرا if (!_cameraLocked) GestureDetector( onTap: () { setState(() => _cameraLocked = true); if (busPos != null && _mapCtrl != null) { _mapCtrl!.animateCamera( CameraUpdate.newLatLngZoom(busPos!, 15)); } }, child: Container( width: 42, height: 42, decoration: BoxDecoration( color: _accent, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: _accent.withValues(alpha: 0.4), blurRadius: 10, offset: const Offset(0, 3)) ], ), child: const Icon(Icons.my_location_rounded, color: Colors.white, size: 20), ), ), ], ), ), // ── 3. بانر التأخير ──────────────────────────────── if (delayMin > 0) Positioned( top: MediaQuery.of(context).padding.top + 66, left: 12, right: 12, child: _statusBanner( '🕐 ${'Bus is delayed'.tr} $delayMin ${'min'.tr}', const Color(0xFFF59E0B), ), ), // ── 4. حالة التحميل أو الخطأ ───────────────────── if (c.isLoadingLiveTrip) Positioned.fill( child: Container( color: Colors.black26, child: const Center(child: CircularProgressIndicator()), ), ), if (!c.isLoadingLiveTrip && c.liveError.isNotEmpty) Positioned( top: MediaQuery.of(context).padding.top + 70, left: 16, right: 16, child: _statusBanner(c.liveError, Colors.redAccent), ), if (!c.isLoadingLiveTrip && c.liveError.isEmpty && c.liveBusPosition == null && !c.isLoadingLiveTrip) Positioned( top: MediaQuery.of(context).padding.top + 70, left: 16, right: 16, child: _statusBanner( 'No bus running on this route right now'.tr, const Color(0xFFF59E0B), ), ), // ── 5. بطاقة المعلومات السفلية ───────────────────── if (!c.isLoadingLiveTrip && trip != null) Positioned( bottom: bottomPad + 12, left: 12, right: 12, child: _InfoCard( driverName: driverName, departure: departure, currentStopName: currentStopName, totalStops: stops.length, currentStopSeq: currentStopSeq, onMissedBus: () { // CTA: فاتك الباص؟ اطلب رحلة Get.back(); Get.toNamed('/home'); }, ), ), ], ); }, ), ); } Widget _statusBanner(String text, Color color) { return Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: _card, borderRadius: BorderRadius.circular(14), border: Border(right: BorderSide(color: color, width: 4)), boxShadow: [ BoxShadow(color: Colors.black.withValues(alpha: 0.08), blurRadius: 8) ], ), child: Text(text, style: AppStyle.subtitle.copyWith( color: color, fontWeight: FontWeight.bold, fontSize: 13)), ); } } // ── بطاقة معلومات الرحلة ──────────────────────────────────────────────────── class _InfoCard extends StatelessWidget { final String driverName; final String departure; final String? currentStopName; final int totalStops; final int? currentStopSeq; final VoidCallback onMissedBus; const _InfoCard({ required this.driverName, required this.departure, required this.currentStopName, required this.totalStops, required this.currentStopSeq, required this.onMissedBus, }); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: _card, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.12), blurRadius: 20, offset: const Offset(0, -4)) ], ), padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, children: [ // ── صف المعلومات ───────────────────────────── Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( color: _accent.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(12), ), child: const Icon(Icons.directions_bus_rounded, color: _accent, size: 26), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (driverName.isNotEmpty) Text(driverName, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14, color: _text)), const SizedBox(height: 2), Row( children: [ if (departure.isNotEmpty) ...[ Icon(Icons.schedule_rounded, size: 13, color: _sub), const SizedBox(width: 3), Text(departure, style: TextStyle(fontSize: 12, color: _sub)), const SizedBox(width: 12), ], if (currentStopSeq != null) ...[ Icon(Icons.location_on_rounded, size: 13, color: Colors.greenAccent.shade400), const SizedBox(width: 3), Text( currentStopName ?? '${'Stop'.tr} $currentStopSeq', style: TextStyle(fontSize: 12, color: _sub), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ], ), ], ), ), // عداد المحطات if (totalStops > 0 && currentStopSeq != null) Column( children: [ Text( '$currentStopSeq/$totalStops', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, color: _accent), ), Text('Stops'.tr, style: TextStyle(fontSize: 10, color: _sub)), ], ), ], ), // ── شريط التقدم بين المحطات ───────────────── if (totalStops > 0 && currentStopSeq != null) ...[ const SizedBox(height: 12), ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: currentStopSeq! / totalStops, backgroundColor: _accent.withValues(alpha: 0.12), valueColor: const AlwaysStoppedAnimation(_accent), minHeight: 6, ), ), ], const SizedBox(height: 12), // ── زر "بطاقة الصعود" (Boarding Pass) ───────────────── SizedBox( width: double.infinity, height: 55, child: MyElevatedButton( onPressed: () { Get.to(() => const TransitQRCodePage()); }, kolor: _accent, title: 'بطاقة الصعود (Boarding Pass)'.tr, ), ), const SizedBox(height: 8), // ── زر "فاتك الباص؟" ───────────────────────── SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: onMissedBus, style: OutlinedButton.styleFrom( side: BorderSide(color: _accent.withValues(alpha: 0.5)), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(vertical: 10), ), icon: const Icon(Icons.local_taxi_rounded, size: 18), label: Text( 'Missed the bus? Book a ride now'.tr, style: const TextStyle(fontSize: 13), ), ), ), ], ), ); } }