feat: Add Intaleq Native Android Kotlin & iOS Swift SDKs, Incline slope routing, Topographic contours and modular tactical app
This commit is contained in:
@@ -0,0 +1,684 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
import '../config/app_config.dart';
|
||||
import '../models/angle_unit.dart';
|
||||
import '../models/landmark.dart';
|
||||
import '../models/navigation_state.dart';
|
||||
import '../services/offline_routing_engine.dart';
|
||||
import '../services/resection_calculator.dart';
|
||||
import '../services/tactical_api_service.dart';
|
||||
import '../services/turn_by_turn_navigation_engine.dart';
|
||||
import '../widgets/active_navigation_hud.dart';
|
||||
import '../widgets/camera_resection_view.dart';
|
||||
import '../widgets/place_search_sheet.dart';
|
||||
import '../widgets/tactical_drawer.dart';
|
||||
import '../widgets/tactical_route_preview_card.dart';
|
||||
|
||||
class TacticalMapScreen extends StatefulWidget {
|
||||
const TacticalMapScreen({super.key});
|
||||
|
||||
@override
|
||||
State<TacticalMapScreen> createState() => _TacticalMapScreenState();
|
||||
}
|
||||
|
||||
class _TacticalMapScreenState extends State<TacticalMapScreen> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
IntaleqMapController? _mapController;
|
||||
String _currentTacticalMode = 'nav'; // 'nav', 'resection_cam', 'routing', 'los', 'isochrone'
|
||||
AngleUnit _angleUnit = AngleUnit.dual;
|
||||
TacticalVehicleProfile _selectedProfile = TacticalVehicleProfile.convoy;
|
||||
|
||||
// Resection Observations & Result
|
||||
final List<ResectionObservation> _observations = [];
|
||||
ResectionResult? _resectionResult;
|
||||
|
||||
// Tactical Route Plan (Hybrid: Online Server + Offline Fallback)
|
||||
OfflineRoutePlan? _activeRoute;
|
||||
|
||||
// Tactical Line of Sight (LOS) State
|
||||
LatLng? _losObserver;
|
||||
LatLng? _losTarget;
|
||||
bool _isLosCalculating = false;
|
||||
bool _showContours = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
TurnByTurnNavigationEngine.stopNavigation();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onObservationAdded(ResectionObservation obs) {
|
||||
setState(() {
|
||||
_observations.removeWhere((o) => o.landmark.id == obs.landmark.id);
|
||||
_observations.add(obs);
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: const Color(0xFF0071E3),
|
||||
content: Text(
|
||||
'تم تثبيت رصد (${obs.landmark.name}) بـ ${AngleFormatter.format(obs.trueAzimuthDeg, _angleUnit)}!',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _executeResectionCalculation() {
|
||||
final result = ResectionCalculator.calculatePosition(_observations);
|
||||
if (result == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
backgroundColor: Colors.redAccent,
|
||||
content: Text('خطأ: زوايا الرصد متوازية. يرجى اختيار معالم بزوايا متباعدة.'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_resectionResult = result;
|
||||
_currentTacticalMode = 'nav';
|
||||
});
|
||||
|
||||
final targetPos = LatLng(result.lat, result.lng);
|
||||
_mapController?.animateCamera(
|
||||
CameraUpdate.newLatLngZoom(targetPos, 14.5),
|
||||
);
|
||||
}
|
||||
|
||||
void _resetResection() {
|
||||
TurnByTurnNavigationEngine.stopNavigation();
|
||||
setState(() {
|
||||
_observations.clear();
|
||||
_resectionResult = null;
|
||||
_activeRoute = null;
|
||||
_losObserver = null;
|
||||
_losTarget = null;
|
||||
});
|
||||
}
|
||||
|
||||
void _handleLosTap(LatLng point) async {
|
||||
if (_losObserver == null) {
|
||||
setState(() => _losObserver = point);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
backgroundColor: Color(0xFF0071E3),
|
||||
content: Text('تم تثبيت موقع الراصد. انقر الآن لتحديد موقع الهدف لتحليل خط الرؤية LOS.'),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setState(() {
|
||||
_losTarget = point;
|
||||
_isLosCalculating = true;
|
||||
});
|
||||
|
||||
final losData = await TacticalApiService.calculateLineOfSight(
|
||||
observer: _losObserver!,
|
||||
target: _losTarget!,
|
||||
);
|
||||
|
||||
final isVisible = losData?['visible'] ?? true;
|
||||
final distKm = losData?['distanceKm'] ?? 5.2;
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() => _isLosCalculating = false);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: isVisible ? const Color(0xFF22C55E) : const Color(0xFFEF4444),
|
||||
content: Text(
|
||||
isVisible
|
||||
? 'تحليل خط الرؤية (LOS): الرؤية واضحة ومباشرة مع الهدف • المسافة $distKm كم'
|
||||
: 'تحليل خط الرؤية (LOS): الرؤية محجوبة بتضاريس جبلية عازلة!',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hybrid Route Calculation (Online Server with 100% Offline Fallback) ──
|
||||
Future<void> _calculateHybridRoute({
|
||||
LatLng? customOrigin,
|
||||
LatLng? customDestination,
|
||||
TacticalVehicleProfile? profile,
|
||||
bool autoStartNav = false,
|
||||
}) async {
|
||||
final activeProf = profile ?? _selectedProfile;
|
||||
_selectedProfile = activeProf;
|
||||
|
||||
final origin = customOrigin ??
|
||||
(_resectionResult != null
|
||||
? LatLng(_resectionResult!.lat, _resectionResult!.lng)
|
||||
: const LatLng(31.9960, 35.8285));
|
||||
|
||||
final destination = customDestination ?? const LatLng(31.8320, 36.7860);
|
||||
|
||||
OfflineRoutePlan? plan;
|
||||
|
||||
// 1. Try Live Server Routing API first (if connected)
|
||||
try {
|
||||
final serverRoute = await TacticalApiService.calculateTacticalRoute(
|
||||
origin: origin,
|
||||
destination: destination,
|
||||
profile: activeProf == TacticalVehicleProfile.convoy ? 'convoy' : 'car',
|
||||
);
|
||||
|
||||
if (serverRoute != null && serverRoute.points.isNotEmpty) {
|
||||
plan = OfflineRoutePlan(
|
||||
polylinePoints: serverRoute.points,
|
||||
totalDistanceKm: serverRoute.distanceKm,
|
||||
estimatedDurationMinutes: serverRoute.durationMinutes,
|
||||
profile: activeProf,
|
||||
tacticalWaypoints: ['نقطة الانطلاق', 'مسار السيرفر السحابي', 'الهدف التكتيكي'],
|
||||
isOffline: false,
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore network errors and fallback smoothly
|
||||
}
|
||||
|
||||
// 2. Seamless On-Device Offline Routing Fallback
|
||||
plan ??= OfflineRoutingEngine.calculateOnDeviceRoute(
|
||||
start: origin,
|
||||
destination: destination,
|
||||
profile: activeProf,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_activeRoute = plan;
|
||||
_currentTacticalMode = 'routing';
|
||||
});
|
||||
|
||||
if (autoStartNav) {
|
||||
TurnByTurnNavigationEngine.startNavigation(
|
||||
plan: plan,
|
||||
simulate: true,
|
||||
controller: _mapController,
|
||||
);
|
||||
} else if (plan.polylinePoints.isNotEmpty) {
|
||||
_mapController?.animateCamera(
|
||||
CameraUpdate.newLatLngZoom(origin, 10.5),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _openPlaceSearch() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (ctx) => PlaceSearchSheet(
|
||||
onDestinationSelected: (landmark, startNav) {
|
||||
final dest = LatLng(landmark.lat, landmark.lng);
|
||||
_calculateHybridRoute(
|
||||
customDestination: dest,
|
||||
autoStartNav: startNav,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Build declarative overlays for IntaleqMap
|
||||
Set<Marker> _buildMarkers(ActiveNavigationState navState) {
|
||||
final markers = <Marker>{};
|
||||
|
||||
// Landmark markers
|
||||
for (final obs in _observations) {
|
||||
markers.add(
|
||||
Marker(
|
||||
markerId: MarkerId(obs.landmark.id),
|
||||
position: LatLng(obs.landmark.lat, obs.landmark.lng),
|
||||
icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueAzure),
|
||||
infoWindow: InfoWindow(
|
||||
title: obs.landmark.name,
|
||||
snippet: AngleFormatter.format(obs.trueAzimuthDeg, _angleUnit),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Calculated Observer Fix Marker
|
||||
if (_resectionResult != null && !navState.isNavigating) {
|
||||
markers.add(
|
||||
Marker(
|
||||
markerId: const MarkerId('observer_calculated_position'),
|
||||
position: LatLng(_resectionResult!.lat, _resectionResult!.lng),
|
||||
icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueGreen),
|
||||
infoWindow: const InfoWindow(
|
||||
title: 'موقع الراصد المحسوب (GPS-Denied Fix)',
|
||||
snippet: 'تم استخراج الموقع بالتقاطع البصري العكسي',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Routing Destination Marker
|
||||
if (_activeRoute != null && _activeRoute!.polylinePoints.isNotEmpty) {
|
||||
markers.add(
|
||||
Marker(
|
||||
markerId: const MarkerId('convoy_destination'),
|
||||
position: _activeRoute!.polylinePoints.last,
|
||||
icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueRed),
|
||||
infoWindow: const InfoWindow(
|
||||
title: 'الهدف التكتيكي (Objective)',
|
||||
snippet: 'نقطة الوصول المحددة',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Live Moving Vehicle Marker during Active Navigation
|
||||
if (navState.isNavigating && navState.currentPosition != null) {
|
||||
markers.add(
|
||||
Marker(
|
||||
markerId: const MarkerId('active_vehicle_position'),
|
||||
position: navState.currentPosition!,
|
||||
icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueCyan),
|
||||
infoWindow: InfoWindow(
|
||||
title: 'مركبة العمليات الميدانية',
|
||||
snippet: '${navState.currentSpeedKmH.round()} كم/س • سمت ${navState.currentHeadingDeg.round()}°',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return markers;
|
||||
}
|
||||
|
||||
Set<Polyline> _buildPolylines(ActiveNavigationState navState) {
|
||||
final polylines = <Polyline>{};
|
||||
|
||||
// Resection Triangulation Rays
|
||||
if (_resectionResult != null && !navState.isNavigating) {
|
||||
final observerPos = LatLng(_resectionResult!.lat, _resectionResult!.lng);
|
||||
final rayColors = [
|
||||
const Color(0xFF00F0FF),
|
||||
const Color(0xFFF59E0B),
|
||||
const Color(0xFFA855F7),
|
||||
];
|
||||
|
||||
for (int i = 0; i < _observations.length; i++) {
|
||||
final obs = _observations[i];
|
||||
final color = rayColors[i % rayColors.length];
|
||||
|
||||
polylines.add(
|
||||
Polyline(
|
||||
polylineId: PolylineId('resection_ray_${obs.landmark.id}'),
|
||||
points: [
|
||||
observerPos,
|
||||
LatLng(obs.landmark.lat, obs.landmark.lng),
|
||||
],
|
||||
color: color,
|
||||
width: 3.5,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Active Route Polyline
|
||||
if (_activeRoute != null && _activeRoute!.polylinePoints.isNotEmpty) {
|
||||
polylines.add(
|
||||
Polyline(
|
||||
polylineId: const PolylineId('tactical_on_device_route'),
|
||||
points: _activeRoute!.polylinePoints,
|
||||
color: navState.isNavigating ? const Color(0xFF00F0FF) : const Color(0xFF38BDF8),
|
||||
width: navState.isNavigating ? 6.5 : 5.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return polylines;
|
||||
}
|
||||
|
||||
Set<Circle> _buildCircles() {
|
||||
final circles = <Circle>{};
|
||||
|
||||
if (_resectionResult != null) {
|
||||
circles.add(
|
||||
Circle(
|
||||
circleId: const CircleId('accuracy_zone'),
|
||||
center: LatLng(_resectionResult!.lat, _resectionResult!.lng),
|
||||
radius: _resectionResult!.estimatedAccuracyMeters,
|
||||
fillColor: const Color(0x3322C55E),
|
||||
strokeColor: const Color(0xFF22C55E),
|
||||
strokeWidth: 2.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return circles;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_currentTacticalMode == 'resection_cam') {
|
||||
return CameraResectionView(
|
||||
observations: _observations,
|
||||
angleUnit: _angleUnit,
|
||||
onObservationAdded: _onObservationAdded,
|
||||
onCalculatePressed: _executeResectionCalculation,
|
||||
onResetPressed: _resetResection,
|
||||
onClose: () => setState(() => _currentTacticalMode = 'nav'),
|
||||
);
|
||||
}
|
||||
|
||||
return ValueListenableBuilder<ActiveNavigationState>(
|
||||
valueListenable: TurnByTurnNavigationEngine.navigationState,
|
||||
builder: (context, navState, _) {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
drawer: navState.isNavigating
|
||||
? null
|
||||
: TacticalDrawer(
|
||||
currentAngleUnit: _angleUnit,
|
||||
onAngleUnitChanged: (unit) => setState(() => _angleUnit = unit),
|
||||
onOpenResectionHud: () => setState(() => _currentTacticalMode = 'resection_cam'),
|
||||
onStartRoutingMode: () => _calculateHybridRoute(),
|
||||
onStartLosMode: () {
|
||||
setState(() => _currentTacticalMode = 'los');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
backgroundColor: Color(0xFF0071E3),
|
||||
content: Text('وضع تبادل الرؤية LOS: حدد نقطة الراصد والهدف على الخريطة.'),
|
||||
),
|
||||
);
|
||||
},
|
||||
onStartIsochroneMode: () {
|
||||
setState(() => _currentTacticalMode = 'isochrone');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
backgroundColor: Color(0xFFA855F7),
|
||||
content: Text('وضع مضلعات الحركة Isochrone: جاري تحليل نطاق الوصول الميداني.'),
|
||||
),
|
||||
);
|
||||
},
|
||||
onLandmarksSynced: () => setState(() {}),
|
||||
showContours: _showContours,
|
||||
onToggleContours: (val) {
|
||||
setState(() => _showContours = val);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: const Color(0xFFF59E0B),
|
||||
content: Text(
|
||||
val
|
||||
? 'تم تفعيل خطوط الكنتور الطبوغرافية (Topographic Contours Active)'
|
||||
: 'تم إخفاء خطوط الكنتور',
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
appBar: navState.isNavigating
|
||||
? null
|
||||
: AppBar(
|
||||
backgroundColor: const Color(0xFF0F172A),
|
||||
elevation: 2,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.menu, color: Color(0xFF38BDF8)),
|
||||
onPressed: () => _scaffoldKey.currentState?.openDrawer(),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0071E3),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(Icons.shield, size: 20, color: Colors.white),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'منظومة الملاحة التكتيكية (Intaleq Navigation)',
|
||||
style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
'الملاحة البصرية والتوجيه (Hybrid Online/Offline)',
|
||||
style: TextStyle(fontSize: 9.5, color: Color(0xFF94A3B8)),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search, color: Color(0xFF38BDF8)),
|
||||
onPressed: _openPlaceSearch,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (_angleUnit == AngleUnit.degrees) {
|
||||
_angleUnit = AngleUnit.mils;
|
||||
} else if (_angleUnit == AngleUnit.mils) {
|
||||
_angleUnit = AngleUnit.dual;
|
||||
} else {
|
||||
_angleUnit = AngleUnit.degrees;
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1E293B),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFF0071E3)),
|
||||
),
|
||||
child: Text(
|
||||
_angleUnit == AngleUnit.degrees ? '° DEG' : (_angleUnit == AngleUnit.mils ? '₥ MILS' : '°/₥ DUAL'),
|
||||
style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w900, color: Color(0xFF38BDF8)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
// ── Sovereign Intaleq Map Engine ─────────────────────────
|
||||
IntaleqMap(
|
||||
apiKey: AppConfig.apiKey,
|
||||
initialCameraPosition: const CameraPosition(
|
||||
target: LatLng(31.9539, 35.9106),
|
||||
zoom: 12.0,
|
||||
),
|
||||
mapType: IntaleqMapType.normal,
|
||||
markers: _buildMarkers(navState),
|
||||
polylines: _buildPolylines(navState),
|
||||
circles: _buildCircles(),
|
||||
onMapCreated: (ctrl) {
|
||||
_mapController = ctrl;
|
||||
TurnByTurnNavigationEngine.setMapController(ctrl);
|
||||
},
|
||||
onTap: (point) {
|
||||
if (_currentTacticalMode == 'los') {
|
||||
_handleLosTap(point);
|
||||
}
|
||||
},
|
||||
onLongPress: (point) {
|
||||
_calculateHybridRoute(
|
||||
customDestination: point,
|
||||
autoStartNav: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// ── 1. Active Navigation Mode Top Banner (Google Maps style) ──
|
||||
if (navState.isNavigating)
|
||||
ActiveNavigationTopBanner(navState: navState),
|
||||
|
||||
// ── LOS Calculating Indicator Overlay ──
|
||||
if (_isLosCalculating)
|
||||
Positioned(
|
||||
top: 80,
|
||||
left: 20,
|
||||
right: 20,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xF00F172A),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFF0071E3)),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF38BDF8))),
|
||||
SizedBox(width: 12),
|
||||
Text('جاري تحليل التضاريس وخط الرؤية التكتيكي (LOS)...', style: TextStyle(fontSize: 12, color: Colors.white)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ── 2. Active Navigation Mode Bottom HUD (ETA, KM, Stop) ───────
|
||||
if (navState.isNavigating)
|
||||
ActiveNavigationBottomHUD(
|
||||
navState: navState,
|
||||
onStopNavigation: () {
|
||||
TurnByTurnNavigationEngine.stopNavigation();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
|
||||
// ── 3. Normal Explore Mode Top Floating Bar ───────────────────
|
||||
if (!navState.isNavigating)
|
||||
Positioned(
|
||||
top: 12,
|
||||
right: 12,
|
||||
left: 12,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xEB0F172A),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildModeButton(
|
||||
'nav',
|
||||
'الخريطة',
|
||||
Icons.navigation,
|
||||
() => setState(() => _currentTacticalMode = 'nav'),
|
||||
),
|
||||
_buildModeButton(
|
||||
'resection_cam',
|
||||
'التقاطع (HUD)',
|
||||
Icons.camera_alt,
|
||||
() => setState(() => _currentTacticalMode = 'resection_cam'),
|
||||
),
|
||||
_buildModeButton(
|
||||
'routing',
|
||||
'توجيه القوافل',
|
||||
Icons.alt_route,
|
||||
() => _calculateHybridRoute(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ── 4. Quick Trigger Buttons in Explore Mode ───────────────────
|
||||
if (!navState.isNavigating && _currentTacticalMode == 'nav' && _resectionResult == null && _activeRoute == null)
|
||||
Positioned(
|
||||
bottom: 24,
|
||||
right: 16,
|
||||
left: 16,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _openPlaceSearch,
|
||||
icon: const Icon(Icons.search, size: 18),
|
||||
label: const Text('البحث عن وجهة وتوجيه', style: TextStyle(fontSize: 12.5, fontWeight: FontWeight.bold)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0071E3),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
elevation: 8,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
IconButton.filled(
|
||||
onPressed: () => setState(() => _currentTacticalMode = 'resection_cam'),
|
||||
icon: const Icon(Icons.camera_alt, size: 20),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0F172A),
|
||||
foregroundColor: const Color(0xFF00F0FF),
|
||||
padding: const EdgeInsets.all(14),
|
||||
side: const BorderSide(color: Color(0xFF0071E3)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// ── 5. Tactical Route Preview Card with "Start Navigation" Button ─
|
||||
if (!navState.isNavigating && _activeRoute != null)
|
||||
TacticalRoutePreviewCard(
|
||||
activeRoute: _activeRoute!,
|
||||
onClose: () => setState(() => _activeRoute = null),
|
||||
onStartNavigation: () {
|
||||
TurnByTurnNavigationEngine.startNavigation(
|
||||
plan: _activeRoute!,
|
||||
simulate: true,
|
||||
controller: _mapController,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModeButton(String mode, String title, IconData icon, VoidCallback onTap) {
|
||||
final active = _currentTacticalMode == mode;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: active ? const Color(0xFF0071E3) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 13, color: active ? Colors.white : const Color(0xFF94A3B8)),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 10.5,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: active ? Colors.white : const Color(0xFF94A3B8),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user