Files
Siro/siro_rider/lib/views/transit/transit_map_page.dart
T

528 lines
21 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<TransitMapPage> createState() => _TransitMapPageState();
}
class _TransitMapPageState extends State<TransitMapPage> {
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<TransitController>().openLiveRoute(widget.routeId);
// اشتراك FCM لتنبيهات الخط
Get.find<TransitController>().subscribeToRouteTopic(widget.routeId);
});
}
@override
void dispose() {
Get.find<TransitController>().closeLiveRoute();
Get.find<TransitController>().unsubscribeFromRouteTopic(widget.routeId);
super.dispose();
}
@override
Widget build(BuildContext context) {
final bottomPad = MediaQuery.of(context).padding.bottom;
return Scaffold(
backgroundColor: _bg,
body: GetBuilder<TransitController>(
builder: (c) {
// ── بناء ماركرات المحطات والباص ─────────────────────────
final stops = (c.liveTripData?['stops'] is List)
? List<Map>.from(c.liveTripData!['stops'])
: <Map>[];
final markers = <Marker>{};
// ماركر المحطات
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 = <Polyline>{};
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<Color>(_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),
),
),
),
],
),
);
}
}