Files
maps-saas/packages/tactical_app/lib/screens/tactical_map_screen.dart
T

1556 lines
57 KiB
Dart

import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.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/military_grid_utils.dart';
import '../services/offline_los_engine.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/interactive_map_picker_hud.dart';
import '../widgets/place_search_sheet.dart';
import '../widgets/tactical_drawer.dart';
import '../widgets/tactical_los_sheet.dart';
import '../widgets/tactical_viewshed_sheet.dart';
import '../widgets/tactical_route_planner_sheet.dart';
import '../widgets/tactical_route_preview_card.dart';
import '../services/viewshed_360_engine.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', 'viewshed_360', 'isochrone'
AngleUnit _angleUnit = AngleUnit.dual;
TacticalVehicleProfile _selectedProfile = TacticalVehicleProfile.convoy;
// Live GPS Location
LatLng? _currentGpsPosition;
// Camera Center for Interactive Map Pin Picker
LatLng _currentCameraCenter = const LatLng(31.9539, 35.9106);
MapPickerTarget? _activePickerTarget;
// 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 _isLosVisible = true;
OfflineLosReport? _activeLosReport;
// Tactical 360 Viewshed State
Viewshed360Report? _activeViewshedReport;
// Topographic Contours / Style Layer State
bool _showContours = true;
@override
void initState() {
super.initState();
_initGps();
}
Future<void> _initGps() async {
try {
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) return;
LocationPermission perm = await Geolocator.checkPermission();
if (perm == LocationPermission.denied) {
perm = await Geolocator.requestPermission();
}
if (perm == LocationPermission.whileInUse ||
perm == LocationPermission.always) {
final pos = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high, timeLimit: Duration(seconds: 5)),
);
if (mounted) {
setState(() {
_currentGpsPosition = LatLng(pos.latitude, pos.longitude);
_currentCameraCenter = _currentGpsPosition!;
});
_mapController?.animateCamera(
CameraUpdate.newLatLngZoom(_currentGpsPosition!, 14.0),
);
}
}
} catch (e) {
debugPrint('GPS init error: $e');
}
}
@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);
Future.delayed(const Duration(milliseconds: 300), () {
if (mounted && _mapController != null) {
_mapController!.animateCamera(
CameraUpdate.newLatLngZoom(targetPos, 15.0),
);
}
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: const Color(0xFF22C55E),
duration: const Duration(seconds: 4),
content: Row(
children: [
const Icon(Icons.gps_fixed, color: Colors.white, size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(
'تم استخراج موقعك بنجاح: ${result.lat.toStringAsFixed(5)}, ${result.lng.toStringAsFixed(5)} • دقة ±${result.estimatedAccuracyMeters.round()}م',
style:
const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
),
),
],
),
),
);
}
void _openRoutePlanner({LatLng? initialOrigin, LatLng? initialDestination}) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) => TacticalRoutePlannerSheet(
currentGps: _currentGpsPosition ?? const LatLng(31.9539, 35.9106),
resectionFix: _resectionResult != null
? LatLng(_resectionResult!.lat, _resectionResult!.lng)
: null,
initialOrigin: initialOrigin,
initialDestination: initialDestination,
onRouteConfirmed: (origin, destination, profile, autoStartNav) {
_calculateHybridRoute(
customOrigin: origin,
customDestination: destination,
selectedProfile: profile,
autoStartNav: autoStartNav,
);
},
onPickOriginOnMap: () {
setState(() => _activePickerTarget = MapPickerTarget.routeOrigin);
},
onPickDestinationOnMap: () {
setState(
() => _activePickerTarget = MapPickerTarget.routeDestination);
},
onClose: () => Navigator.pop(ctx),
),
);
}
void _onConfirmPicker(MapPickerTarget target, LatLng selectedPos) {
setState(() {
_activePickerTarget = null;
if (target == MapPickerTarget.losObserver) {
_losObserver = selectedPos;
} else if (target == MapPickerTarget.losTarget) {
_losTarget = selectedPos;
}
});
if (target == MapPickerTarget.losObserver ||
target == MapPickerTarget.losTarget) {
_openLosSheet();
} else if (target == MapPickerTarget.routeOrigin ||
target == MapPickerTarget.routeDestination) {
_openRoutePlanner();
}
}
void _onCancelPicker() {
final target = _activePickerTarget;
setState(() => _activePickerTarget = null);
if (target == MapPickerTarget.losObserver ||
target == MapPickerTarget.losTarget) {
_openLosSheet();
} else if (target == MapPickerTarget.routeOrigin ||
target == MapPickerTarget.routeDestination) {
_openRoutePlanner();
}
}
void _resetResection() {
TurnByTurnNavigationEngine.stopNavigation();
setState(() {
_observations.clear();
_resectionResult = null;
_activeRoute = null;
_losObserver = null;
_losTarget = null;
_activeLosReport = null;
_currentTacticalMode = 'nav';
});
}
void _startLosMode() {
setState(() {
_currentTacticalMode = 'los';
});
if (_losObserver != null && _losTarget != null) {
_openLosSheet();
} else {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
duration: Duration(seconds: 4),
backgroundColor: Color(0xFF0F172A),
content: Row(
children: [
Icon(Icons.touch_app, size: 18, color: Color(0xFF38BDF8)),
SizedBox(width: 10),
Expanded(
child: Text(
'انقر على الخريطة لتحديد موقع الراصد 👁️ ثم انقر لتحديد الهدف 🎯',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold),
),
),
],
),
),
);
}
}
void _openLosSheet() {
if (_losObserver == null || _losTarget == null) return;
final initialLos = OfflineLosEngine.calculate(
observer: _losObserver!,
target: _losTarget!,
);
setState(() {
_isLosVisible = initialLos.isDirectlyVisible;
_activeLosReport = initialLos;
});
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) => TacticalLosSheet(
observer: _losObserver!,
target: _losTarget!,
currentGps: _currentGpsPosition,
angleUnit: _angleUnit,
onUpdateLocations: (newObs, newTgt) {
setState(() {
_losObserver = newObs;
_losTarget = newTgt;
});
final updatedLos = OfflineLosEngine.calculate(
observer: newObs,
target: newTgt,
);
setState(() {
_isLosVisible = updatedLos.isDirectlyVisible;
_activeLosReport = updatedLos;
});
_fitRouteBounds([newObs, newTgt]);
},
onVisibilityChanged: (vis) {
if (mounted) setState(() => _isLosVisible = vis);
},
onReportGenerated: (report) {
if (mounted) {
setState(() {
_activeLosReport = report;
_isLosVisible = report.isDirectlyVisible;
});
}
},
onPickObserverOnMap: () {
Navigator.pop(context);
setState(() => _activePickerTarget = MapPickerTarget.losObserver);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
duration: Duration(seconds: 3),
backgroundColor: Color(0xFF0F172A),
content: Text('انقر على الخريطة لتحديد موقع الراصد الجديد 👁️',
style: TextStyle(color: Colors.white)),
),
);
},
onPickTargetOnMap: () {
Navigator.pop(context);
setState(() => _activePickerTarget = MapPickerTarget.losTarget);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
duration: Duration(seconds: 3),
backgroundColor: Color(0xFF0F172A),
content: Text('انقر على الخريطة لتحديد موقع الهدف الجديد 🎯',
style: TextStyle(color: Colors.white)),
),
);
},
onClose: () {
Navigator.pop(context);
setState(() => _currentTacticalMode = 'nav');
},
),
);
}
void _startViewshed360Mode() {
setState(() {
_currentTacticalMode = 'viewshed_360';
});
if (_losObserver != null) {
_openViewshed360Sheet();
} else if (_currentGpsPosition != null) {
setState(() => _losObserver = _currentGpsPosition);
_openViewshed360Sheet();
} else {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
duration: Duration(seconds: 4),
backgroundColor: Color(0xFF0F172A),
content: Row(
children: [
Icon(Icons.radar, size: 18, color: Color(0xFF38BDF8)),
SizedBox(width: 10),
Expanded(
child: Text(
'انقر على الخريطة لتحديد مركز الرصد والرادار 360° 🌐',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold),
),
),
],
),
),
);
}
}
void _openViewshed360Sheet() {
if (_losObserver == null) return;
final initialReport = Viewshed360Engine.calculate(
observer: _losObserver!,
radiusMeters: 5000.0,
observerHeightM: 2.0,
rayCount: 360,
);
setState(() {
_activeViewshedReport = initialReport;
});
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) => TacticalViewshedSheet(
observer: _losObserver!,
currentGps: _currentGpsPosition,
onUpdateObserver: (newObs) {
setState(() => _losObserver = newObs);
},
onReportUpdated: (rep) {
if (mounted) {
setState(() => _activeViewshedReport = rep);
}
},
onPickObserverOnMap: () {
Navigator.pop(context);
setState(() => _activePickerTarget = MapPickerTarget.losObserver);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
duration: Duration(seconds: 3),
backgroundColor: Color(0xFF0F172A),
content: Text('انقر على الخريطة لتحديد مركز الرصد الجديد 👁️',
style: TextStyle(color: Colors.white)),
),
);
},
onClose: () {
Navigator.pop(context);
},
),
);
}
void _handleMapTap(LatLng point) {
if (_activePickerTarget != null) {
if (_activePickerTarget == MapPickerTarget.losObserver) {
setState(() {
_losObserver = point;
_activePickerTarget = null;
});
if (_currentTacticalMode == 'viewshed_360') {
_openViewshed360Sheet();
} else if (_losTarget != null) {
_openLosSheet();
}
} else if (_activePickerTarget == MapPickerTarget.losTarget) {
setState(() {
_losTarget = point;
_activePickerTarget = null;
});
if (_losObserver != null) {
_openLosSheet();
}
} else if (_activePickerTarget == MapPickerTarget.routeOrigin) {
setState(() => _activePickerTarget = null);
_openRoutePlanner(initialOrigin: point);
} else if (_activePickerTarget == MapPickerTarget.routeDestination) {
setState(() => _activePickerTarget = null);
_openRoutePlanner(initialDestination: point);
}
return;
}
if (_currentTacticalMode == 'viewshed_360') {
setState(() => _losObserver = point);
_openViewshed360Sheet();
return;
}
if (_currentTacticalMode == 'los') {
if (_losObserver == null) {
setState(() => _losObserver = point);
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
duration: Duration(seconds: 3),
backgroundColor: Color(0xFF0F172A),
content: Row(
children: [
Icon(Icons.check_circle, size: 16, color: Color(0xFF22C55E)),
SizedBox(width: 8),
Text(
'تم تحديد الراصد 👁️ • انقر لتحديد موقع الهدف 🎯',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold),
),
],
),
),
);
} else if (_losTarget == null) {
setState(() => _losTarget = point);
_openLosSheet();
} else {
setState(() => _losTarget = point);
_openLosSheet();
}
return;
}
final mil = MilitaryGridUtils.fromLatLng(point);
final elev = JordanDemSurface.elevationAt(point.latitude, point.longitude);
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
duration: const Duration(seconds: 3),
backgroundColor: const Color(0xFF0F172A),
content: Row(
children: [
const Icon(Icons.location_searching,
size: 16, color: Color(0xFF38BDF8)),
const SizedBox(width: 8),
Expanded(
child: Text(
'${mil.arabicFullFormat} • ارتفاع: ${elev.round()}م',
style: const TextStyle(
color: Colors.white,
fontSize: 11.5,
fontWeight: FontWeight.bold),
),
),
],
),
),
);
}
void _fitRouteBounds(List<LatLng> points) {
if (points.isEmpty || _mapController == null) return;
double minLat = points.first.latitude;
double maxLat = points.first.latitude;
double minLng = points.first.longitude;
double maxLng = points.first.longitude;
for (final p in points) {
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;
}
final bounds = LatLngBounds(
southwest: LatLng(minLat, minLng),
northeast: LatLng(maxLat, maxLng),
);
_mapController?.animateCamera(
CameraUpdate.newLatLngBounds(
bounds,
left: 40.0,
top: 60.0,
right: 40.0,
bottom: 120.0,
),
);
}
Future<void> _calculateHybridRoute({
LatLng? customOrigin,
LatLng? customDestination,
TacticalVehicleProfile? selectedProfile,
bool autoStartNav = false,
}) async {
final profile = selectedProfile ?? _selectedProfile;
setState(() => _selectedProfile = profile);
final startPoint = customOrigin ??
_currentGpsPosition ??
(_resectionResult != null
? LatLng(_resectionResult!.lat, _resectionResult!.lng)
: const LatLng(31.9539, 35.9106));
final targetPoint = customDestination ??
(_observations.isNotEmpty
? LatLng(_observations.first.landmark.lat,
_observations.first.landmark.lng)
: LatLng(startPoint.latitude + 0.05, startPoint.longitude + 0.05));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
duration: Duration(seconds: 1),
backgroundColor: Color(0xFF0071E3),
content: Text('جاري احتساب مسار القوافل التكتيكي...'),
),
);
OfflineRoutePlan? plan;
// 1. Try Online NestJS Server Routing API
try {
final serverRoute = await TacticalApiService.calculateTacticalRoute(
origin: startPoint,
destination: targetPoint,
profile: profile.name,
);
if (serverRoute != null && serverRoute.points.isNotEmpty) {
plan = OfflineRoutePlan(
polylinePoints: serverRoute.points,
totalDistanceKm: serverRoute.distanceKm,
estimatedDurationMinutes: serverRoute.durationMinutes,
profile: profile,
tacticalWaypoints: const ['موقع الانطلاق', 'الهدف التكتيكي المحدد'],
isOffline: false,
);
}
} catch (e) {
debugPrint(
'Online server route failed, falling back to sovereign offline engine: $e');
}
// 2. Fallback to 100% Sovereign On-Device Offline Routing Engine
plan ??= OfflineRoutingEngine.calculateOnDeviceRoute(
start: startPoint,
destination: targetPoint,
profile: profile,
);
setState(() {
_activeRoute = plan;
_currentTacticalMode = 'routing';
});
_fitRouteBounds(plan.polylinePoints);
if (autoStartNav) {
TurnByTurnNavigationEngine.startNavigation(
plan: plan,
simulate: false,
controller: _mapController,
);
}
}
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()}°',
),
),
);
}
// Tactical Line of Sight (LOS) Markers with Military Grid Coordinates
if (_currentTacticalMode == 'los') {
if (_losObserver != null) {
final obsMil = MilitaryGridUtils.fromLatLng(_losObserver!);
final obsElev = JordanDemSurface.elevationAt(
_losObserver!.latitude, _losObserver!.longitude);
markers.add(
Marker(
markerId: const MarkerId('los_observer_marker'),
position: _losObserver!,
icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueCyan),
infoWindow: InfoWindow(
title: '🔴 موقع الراصد (Observer)',
snippet: '${obsMil.arabicFullFormat} • ${obsElev.round()}م',
),
),
);
}
if (_losTarget != null) {
final tgtMil = MilitaryGridUtils.fromLatLng(_losTarget!);
final tgtElev = JordanDemSurface.elevationAt(
_losTarget!.latitude, _losTarget!.longitude);
markers.add(
Marker(
markerId: const MarkerId('los_target_marker'),
position: _losTarget!,
icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueRed),
infoWindow: InfoWindow(
title: '🎯 موقع الهدف (Target)',
snippet: '${tgtMil.arabicFullFormat} • ${tgtElev.round()}م',
),
),
);
}
// Critical Obstacle Marker (نقطة الحجب التضاريسي الأعلى)
if (_activeLosReport?.highestObstacle != null && !_isLosVisible) {
final obs = _activeLosReport!.highestObstacle!;
final obsMil = MilitaryGridUtils.fromLatLng(LatLng(obs.lat, obs.lng));
markers.add(
Marker(
markerId: const MarkerId('los_obstacle_marker'),
position: LatLng(obs.lat, obs.lng),
icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueViolet),
infoWindow: InfoWindow(
title: '⚠️ عائق جبلي حاجب للرؤية: ${obs.elevationM.round()}م',
snippet:
'اختراق خط الرؤية: +${obs.excessM.toStringAsFixed(1)}م • ${obsMil.arabicFullFormat}',
),
),
);
}
}
// Tactical 360 Viewshed Observer Marker
if (_currentTacticalMode == 'viewshed_360' && _losObserver != null) {
final obsMil = MilitaryGridUtils.fromLatLng(_losObserver!);
final obsElev = JordanDemSurface.elevationAt(
_losObserver!.latitude, _losObserver!.longitude);
markers.add(
Marker(
markerId: const MarkerId('viewshed_360_observer'),
position: _losObserver!,
icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueCyan),
infoWindow: InfoWindow(
title: '🌐 مركز الرصد والرادار 360°',
snippet: '${obsMil.arabicFullFormat} • ${obsElev.round()}م',
),
),
);
}
return markers;
}
Set<Polyline> _buildPolylines(ActiveNavigationState navState) {
final polylines = <Polyline>{};
// 1. Tactical Line of Sight (LOS) Ray (Segmented or Direct)
if (_currentTacticalMode == 'los' &&
_losObserver != null &&
_losTarget != null) {
if (_activeLosReport != null && _activeLosReport!.profile.length > 1) {
final visSegments = _activeLosReport!.visibleSegments;
for (int i = 0; i < visSegments.length; i++) {
polylines.add(
Polyline(
polylineId: PolylineId('tactical_los_vis_$i'),
points: visSegments[i],
color: const Color(0xFF22C55E),
width: 5.0,
),
);
}
final blkSegments = _activeLosReport!.blockedSegments;
for (int i = 0; i < blkSegments.length; i++) {
polylines.add(
Polyline(
polylineId: PolylineId('tactical_los_blk_$i'),
points: blkSegments[i],
color: const Color(0xFFEF4444),
width: 5.0,
),
);
}
} else {
polylines.add(
Polyline(
polylineId: const PolylineId('tactical_los_ray'),
points: [_losObserver!, _losTarget!],
color: _isLosVisible
? const Color(0xFF22C55E)
: const Color(0xFFEF4444),
width: 4.5,
),
);
}
}
// 2. Tactical 360 Viewshed Perimeter Polygon Border
if (_currentTacticalMode == 'viewshed_360' &&
_activeViewshedReport != null) {
polylines.add(
Polyline(
polylineId: const PolylineId('viewshed_360_perimeter'),
points: _activeViewshedReport!.polygonVertices,
color: const Color(0xFF22C55E),
width: 4.0,
),
);
}
// 3. 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,
),
);
}
}
// 4. 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,
),
);
}
// 360 Viewshed Radar Max Range Ring
if (_currentTacticalMode == 'viewshed_360' &&
_activeViewshedReport != null) {
circles.add(
Circle(
circleId: const CircleId('viewshed_360_range_ring'),
center: _activeViewshedReport!.center,
radius: _activeViewshedReport!.radiusMeters,
fillColor: const Color(0x1538BDF8),
strokeColor: const Color(0x6638BDF8),
strokeWidth: 1.5,
),
);
}
return circles;
}
Set<Polygon> _buildPolygons() {
final polygons = <Polygon>{};
// 360 Viewshed Radar Visible Polygon Fill
if (_currentTacticalMode == 'viewshed_360' &&
_activeViewshedReport != null &&
_activeViewshedReport!.polygonVertices.isNotEmpty) {
polygons.add(
Polygon(
polygonId: const PolygonId('viewshed_360_fill'),
points: _activeViewshedReport!.polygonVertices,
fillColor: const Color(0x3322C55E),
strokeColor: const Color(0xFF22C55E),
strokeWidth: 2,
),
);
}
return polygons;
}
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: TurnByTurnNavigationEngine.navigationState,
builder: (context, _) {
final navState = TurnByTurnNavigationEngine.navigationState.value;
return Scaffold(
key: _scaffoldKey,
drawer: navState.isNavigating
? null
: TacticalDrawer(
currentAngleUnit: _angleUnit,
onAngleUnitChanged: (unit) =>
setState(() => _angleUnit = unit),
onOpenResectionHud: () =>
setState(() => _currentTacticalMode = 'resection_cam'),
onStartRoutingMode: _openRoutePlanner,
onStartLosMode: _startLosMode,
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
? 'تم تفعيل طبقات التضاريس والكنتور'
: 'تم إخفاء طبقات التضاريس والكنتور',
),
),
);
},
),
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(
'الملاحة البصرية وتبادل الرؤية والتضاريس',
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,
styleUrl: 'asset://assets/style_offline.json',
markers: _buildMarkers(navState),
polylines: _buildPolylines(navState),
circles: _buildCircles(),
polygons: _buildPolygons(),
onMapCreated: (ctrl) async {
_mapController = ctrl;
TurnByTurnNavigationEngine.setMapController(ctrl);
// الانتقال الفوري والمباشر إلى موقع المستخدم الحالي بمجرد إنشاء الخريطة
if (_currentGpsPosition != null) {
ctrl.animateCamera(
CameraUpdate.newLatLngZoom(_currentGpsPosition!, 15.0),
);
} else {
await _initGps();
}
},
onCameraMove: (CameraPosition pos) {
_currentCameraCenter = pos.target;
},
onTap: _handleMapTap,
onLongPress: (point) {
_calculateHybridRoute(
customDestination: point,
autoStartNav: true,
);
},
),
// ── 0. Interactive Map Center Pin Picker HUD ──
if (_activePickerTarget != null)
InteractiveMapPickerHud(
target: _activePickerTarget!,
centerPosition: _currentCameraCenter,
onConfirm: () => _onConfirmPicker(
_activePickerTarget!, _currentCameraCenter),
onCancel: _onCancelPicker,
),
// ── 1. Active Navigation Mode Top Banner ──
if (navState.isNavigating)
ActiveNavigationTopBanner(navState: navState),
// ── 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 && _activePickerTarget == null)
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,
_openRoutePlanner,
),
_buildModeButton(
'los',
'تبادل الرؤية',
Icons.visibility,
_startLosMode,
),
_buildModeButton(
'viewshed_360',
'رصد 360°',
Icons.radar,
_startViewshed360Mode,
),
],
),
),
),
// ── 3.1 Line of Sight (LOS) Step-by-Step Flow Banner ──────────
if (!navState.isNavigating &&
_currentTacticalMode == 'los' &&
_activePickerTarget == null)
Positioned(
top: 66,
right: 12,
left: 12,
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xF20F172A),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFF38BDF8), width: 1.2),
boxShadow: const [
BoxShadow(
color: Colors.black54,
blurRadius: 8,
offset: Offset(0, 3))
],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _losObserver == null
? const Color(0xFF0284C7)
: (_losTarget == null
? const Color(0xFFF59E0B)
: const Color(0xFF22C55E)),
borderRadius: BorderRadius.circular(6),
),
child: Text(
_losObserver == null
? 'خطوة 1/2'
: (_losTarget == null ? 'خطوة 2/2' : 'جاهز'),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
_losObserver == null
? 'انقر على الخريطة لتحديد موقع الراصد 👁️'
: (_losTarget == null
? 'انقر على الخريطة لتحديد الهدف 🎯'
: 'تم حساب خط الرؤية بالكامل'),
style: const TextStyle(
color: Colors.white,
fontSize: 11.5,
fontWeight: FontWeight.w600),
overflow: TextOverflow.ellipsis,
),
),
if (_losObserver == null && _currentGpsPosition != null)
TextButton.icon(
onPressed: () {
setState(
() => _losObserver = _currentGpsPosition);
},
icon: const Icon(Icons.my_location,
size: 14, color: Color(0xFF38BDF8)),
label: const Text('موقعي',
style: TextStyle(
color: Color(0xFF38BDF8), fontSize: 11)),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2)),
)
else if (_losObserver != null && _losTarget == null)
TextButton(
onPressed: () =>
setState(() => _losObserver = null),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2)),
child: const Text('تغيير الراصد',
style: TextStyle(
color: Color(0xFFF59E0B), fontSize: 11)),
)
else if (_losObserver != null &&
_losTarget != null) ...[
TextButton(
onPressed: _openLosSheet,
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2)),
child: const Text('عرض التقرير 📊',
style: TextStyle(
color: Color(0xFF38BDF8),
fontSize: 11,
fontWeight: FontWeight.bold)),
),
TextButton(
onPressed: () {
setState(() {
_losObserver = null;
_losTarget = null;
_activeLosReport = null;
});
},
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 4, vertical: 2)),
child: const Text('إعادة',
style: TextStyle(
color: Color(0xFFEF4444), fontSize: 11)),
),
],
],
),
),
),
// ── 3.2 Tactical 360 Viewshed Step Flow Banner ────────────────
if (!navState.isNavigating &&
_currentTacticalMode == 'viewshed_360' &&
_activePickerTarget == null)
Positioned(
top: 66,
right: 12,
left: 12,
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xF20F172A),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFF4ADE80), width: 1.2),
boxShadow: const [
BoxShadow(
color: Colors.black54,
blurRadius: 8,
offset: Offset(0, 3))
],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF16A34A),
borderRadius: BorderRadius.circular(6),
),
child: const Text(
'رصد 360°',
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
_losObserver == null
? 'انقر على الخريطة لتحديد مركز الرصد 🌐'
: 'تم مسح محيط الرصد ${_activeViewshedReport != null ? "(${_activeViewshedReport!.visibilityPercentage}%)" : ""}',
style: const TextStyle(
color: Colors.white,
fontSize: 11.5,
fontWeight: FontWeight.w600),
overflow: TextOverflow.ellipsis,
),
),
if (_losObserver == null && _currentGpsPosition != null)
TextButton.icon(
onPressed: () {
setState(
() => _losObserver = _currentGpsPosition);
_openViewshed360Sheet();
},
icon: const Icon(Icons.my_location,
size: 14, color: Color(0xFF4ADE80)),
label: const Text('موقعي',
style: TextStyle(
color: Color(0xFF4ADE80), fontSize: 11)),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2)),
)
else if (_losObserver != null)
TextButton.icon(
onPressed: _openViewshed360Sheet,
icon: const Icon(Icons.tune,
size: 14, color: Color(0xFF38BDF8)),
label: const Text('الخصائص ⚙️',
style: TextStyle(
color: Color(0xFF38BDF8),
fontSize: 11,
fontWeight: FontWeight.bold)),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2)),
),
],
),
),
),
// ── 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)),
),
),
],
),
),
// ── 4.1. Floating "My Location" Button ─────────────────────────
if (!navState.isNavigating && _activePickerTarget == null)
Positioned(
bottom: _activeRoute != null ? 220 : 90,
right: 16,
child: FloatingActionButton.small(
heroTag: 'fab_my_location',
backgroundColor: const Color(0xFF0F172A),
foregroundColor: const Color(0xFF38BDF8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(
color: Color(0xFF0071E3), width: 1.5),
),
onPressed: () {
if (_currentGpsPosition != null) {
_mapController?.animateCamera(
CameraUpdate.newLatLngZoom(
_currentGpsPosition!, 15.0),
);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
duration: Duration(seconds: 2),
backgroundColor: Color(0xFF0071E3),
content: Text('تم التمركز على موقعك الحالي GPS 📍'),
),
);
} else {
_initGps();
}
},
child: const Icon(Icons.my_location, size: 20),
),
),
// ── 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,
),
),
],
),
),
),
);
}
}