1466 lines
69 KiB
Dart
1466 lines
69 KiB
Dart
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 '../../logic/cubits/navigation/navigation_cubit.dart';
|
|
import '../../logic/cubits/navigation/navigation_state.dart';
|
|
import 'widgets/search_bar_widget.dart';
|
|
import 'widgets/explore_panel_widget.dart';
|
|
import 'widgets/active_nav_hud_widget.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';
|
|
|
|
class MapView extends StatefulWidget {
|
|
const MapView({super.key});
|
|
|
|
@override
|
|
State<MapView> createState() => _MapViewState();
|
|
}
|
|
|
|
class _MapViewState extends State<MapView> {
|
|
final TextEditingController _searchController = TextEditingController();
|
|
final FocusNode _searchFocusNode = FocusNode();
|
|
bool _isSearchFocused = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
print("🚀 [MapView] initState: MapView mounted.");
|
|
_searchFocusNode.addListener(() {
|
|
if (mounted) setState(() => _isSearchFocused = _searchFocusNode.hasFocus);
|
|
});
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (mounted) {
|
|
print("📌 [MapView] PostFrameCallback: Triggering relockCameraToUser");
|
|
final cubit = context.read<NavigationCubit>();
|
|
cubit.relockCameraToUser();
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_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 _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);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
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();
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final cubit = context.read<NavigationCubit>();
|
|
|
|
return BlocConsumer<NavigationCubit, NavigationState>(
|
|
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 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)
|
|
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 (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();
|
|
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 ──
|
|
if (state.isNavigating)
|
|
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 (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 (!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),
|
|
// 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 (!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);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
|