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

1399 lines
46 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/military_operations_models.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/tactical_isochrone_engine.dart';
import '../services/turn_by_turn_navigation_engine.dart';
import '../services/viewshed_360_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_artillery_sheet.dart';
import '../widgets/tactical_drawer.dart';
import '../widgets/tactical_hlz_sheet.dart';
import '../widgets/tactical_initial_provisioning_dialog.dart';
import '../widgets/tactical_isochrone_sheet.dart';
import '../widgets/tactical_los_sheet.dart';
import '../widgets/tactical_minefield_sheet.dart';
import '../widgets/tactical_overlays_sheet.dart';
import '../widgets/tactical_route_planner_sheet.dart';
import '../widgets/tactical_symbols_sheet.dart';
import '../widgets/tactical_viewshed_sheet.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', 'artillery', 'hlz', 'minefield', 'isochrone', 'symbols', 'overlays'
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;
OfflineLosReport? _activeLosReport;
// Tactical 360 Viewshed State
Viewshed360Report? _activeViewshedReport;
// Artillery Fire Mission State
LatLng? _artilleryGun;
LatLng? _artilleryTarget;
ArtilleryFiringSolution? _activeArtillerySolution;
// Helicopter Landing Zone State
LatLng? _hlzCenter;
HlzAssessmentResult? _activeHlzResult;
// Minefield & Breaching State
LatLng? _minefieldStart;
LatLng? _minefieldEnd;
MinefieldZoneResult? _activeMinefieldResult;
// Isochrone QRF Reachability State
LatLng? _isochroneCenter;
List<IsochroneRing>? _activeIsochroneRings;
// Tactical Placed Symbols
TacticalSymbolType? _activePlacementSymbolType;
final List<TacticalSymbolItem> _placedSymbols = [
TacticalSymbolItem(
id: 'sym-default-1',
name: 'كتيبة المشاة الآلية 4',
type: TacticalSymbolType.friendlyInfantry,
position: const LatLng(31.9800, 35.8800),
echelon: MilitaryEchelon.battalion,
),
TacticalSymbolItem(
id: 'sym-default-2',
name: 'مربض مدفعية M109',
type: TacticalSymbolType.friendlyArtillery,
position: const LatLng(31.9300, 35.9100),
echelon: MilitaryEchelon.battalion,
),
TacticalSymbolItem(
id: 'sym-default-3',
name: 'رادار كشف تكتيكي',
type: TacticalSymbolType.friendlyRadar,
position: const LatLng(32.0200, 35.8400),
echelon: MilitaryEchelon.company,
),
];
// Tactical Overlays (IPB Layers)
List<TacticalOverlayLayer> _overlayLayers = [
TacticalOverlayLayer(
id: 'layer_feba',
nameAr: 'الخط الأمامي لمنطقة القتال (FEBA / FLOT)',
color: const Color(0xFF0071E3),
isVisible: true,
lines: [
[
const LatLng(32.1000, 35.7500),
const LatLng(32.0200, 35.8500),
const LatLng(31.9500, 35.9000),
const LatLng(31.8500, 35.9800),
]
],
),
TacticalOverlayLayer(
id: 'layer_mobility',
nameAr: 'ممرات الحركة ومحاور التقدم (Mobility Corridors)',
color: const Color(0xFF10B981),
isVisible: true,
lines: [
[
const LatLng(31.9539, 35.9106),
const LatLng(32.0000, 35.9500),
const LatLng(32.0500, 36.0200),
]
],
),
TacticalOverlayLayer(
id: 'layer_no_fire',
nameAr: 'مناطق الحظر وعدم الرماية (No-Fire Areas - NFA)',
color: const Color(0xFFEF4444),
isVisible: false,
polygons: [
[
const LatLng(31.9800, 35.9300),
const LatLng(32.0000, 35.9300),
const LatLng(32.0000, 35.9600),
const LatLng(31.9800, 35.9600),
const LatLng(31.9800, 35.9300),
]
],
),
];
// Topographic Contours / Style Layer State
bool _showContours = true;
@override
void initState() {
super.initState();
_initGps();
_checkInitialProvisioning();
}
Future<void> _checkInitialProvisioning() async {
final needed = await TacticalInitialProvisioningDialog.isProvisioningNeeded();
if (needed && mounted) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog(
context: context,
barrierDismissible: false,
builder: (ctx) => TacticalInitialProvisioningDialog(
onCompleted: () {
Navigator.pop(ctx);
setState(() {});
},
),
);
});
}
}
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;
} else if (target == MapPickerTarget.artilleryGun) {
_artilleryGun = selectedPos;
} else if (target == MapPickerTarget.artilleryTarget) {
_artilleryTarget = selectedPos;
} else if (target == MapPickerTarget.hlzCenter) {
_hlzCenter = selectedPos;
} else if (target == MapPickerTarget.minefieldStart) {
_minefieldStart = selectedPos;
} else if (target == MapPickerTarget.minefieldEnd) {
_minefieldEnd = selectedPos;
} else if (target == MapPickerTarget.isochroneCenter) {
_isochroneCenter = selectedPos;
} else if (target == MapPickerTarget.viewshedCenter) {
_losObserver = selectedPos;
}
});
if (target == MapPickerTarget.losObserver ||
target == MapPickerTarget.losTarget) {
_openLosSheet();
} else if (target == MapPickerTarget.viewshedCenter) {
_openViewshed360Sheet();
} else if (target == MapPickerTarget.routeOrigin ||
target == MapPickerTarget.routeDestination) {
_openRoutePlanner();
} else if (target == MapPickerTarget.artilleryGun ||
target == MapPickerTarget.artilleryTarget) {
_openArtillerySheet();
} else if (target == MapPickerTarget.hlzCenter) {
_openHlzSheet();
} else if (target == MapPickerTarget.minefieldStart ||
target == MapPickerTarget.minefieldEnd) {
_openMinefieldSheet();
} else if (target == MapPickerTarget.isochroneCenter) {
_openIsochroneSheet();
}
}
void _onCancelPicker() {
final target = _activePickerTarget;
setState(() => _activePickerTarget = null);
if (target == MapPickerTarget.losObserver ||
target == MapPickerTarget.losTarget) {
_openLosSheet();
} else if (target == MapPickerTarget.viewshedCenter) {
_openViewshed360Sheet();
} else if (target == MapPickerTarget.routeOrigin ||
target == MapPickerTarget.routeDestination) {
_openRoutePlanner();
} else if (target == MapPickerTarget.artilleryGun ||
target == MapPickerTarget.artilleryTarget) {
_openArtillerySheet();
} else if (target == MapPickerTarget.hlzCenter) {
_openHlzSheet();
} else if (target == MapPickerTarget.minefieldStart ||
target == MapPickerTarget.minefieldEnd) {
_openMinefieldSheet();
} else if (target == MapPickerTarget.isochroneCenter) {
_openIsochroneSheet();
}
}
void _resetResection() {
TurnByTurnNavigationEngine.stopNavigation();
setState(() {
_observations.clear();
_resectionResult = null;
_activeRoute = null;
_losObserver = null;
_losTarget = null;
_activeLosReport = null;
_activeArtillerySolution = null;
_activeHlzResult = null;
_activeMinefieldResult = null;
_activeIsochroneRings = null;
_currentTacticalMode = 'nav';
});
}
// ── Operations Modal Launchers ─────────────────────────────
void _openLosSheet() {
if (_losObserver == null || _losTarget == null) return;
final initialLos = OfflineLosEngine.calculate(
observer: _losObserver!,
target: _losTarget!,
);
setState(() {
_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(() {
_activeLosReport = updatedLos;
});
_fitRouteBounds([newObs, newTgt]);
},
onVisibilityChanged: (_) {},
onReportGenerated: (report) {
if (mounted) {
setState(() {
_activeLosReport = report;
});
}
},
onPickObserverOnMap: () {
Navigator.pop(context);
setState(() => _activePickerTarget = MapPickerTarget.losObserver);
},
onPickTargetOnMap: () {
Navigator.pop(context);
setState(() => _activePickerTarget = MapPickerTarget.losTarget);
},
onClose: () {
Navigator.pop(context);
setState(() => _currentTacticalMode = 'nav');
},
),
);
}
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.viewshedCenter);
},
onClose: () => Navigator.pop(context),
),
);
}
void _openArtillerySheet() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) => TacticalArtillerySheet(
gunPosition: _artilleryGun ?? _currentGpsPosition ?? const LatLng(31.9539, 35.9106),
targetPosition: _artilleryTarget,
angleUnit: _angleUnit,
onPickGun: () {
Navigator.pop(context);
setState(() => _activePickerTarget = MapPickerTarget.artilleryGun);
},
onPickTarget: () {
Navigator.pop(context);
setState(() => _activePickerTarget = MapPickerTarget.artilleryTarget);
},
onSwap: () {
setState(() {
final temp = _artilleryGun;
_artilleryGun = _artilleryTarget;
_artilleryTarget = temp;
});
},
onSolutionCalculated: (sol) {
setState(() => _activeArtillerySolution = sol);
_fitRouteBounds([sol.gunPosition, sol.targetPosition]);
},
onClose: () => Navigator.pop(context),
),
);
}
void _openHlzSheet() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) => TacticalHlzSheet(
selectedPosition: _hlzCenter ?? _currentGpsPosition ?? const LatLng(31.9539, 35.9106),
onPickLocation: () {
Navigator.pop(context);
setState(() => _activePickerTarget = MapPickerTarget.hlzCenter);
},
onAssessmentCompleted: (res) {
setState(() => _activeHlzResult = res);
},
onClose: () => Navigator.pop(context),
),
);
}
void _openMinefieldSheet() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) => TacticalMinefieldSheet(
startPoint: _minefieldStart ?? _currentGpsPosition ?? const LatLng(31.9539, 35.9106),
endPoint: _minefieldEnd,
onPickStart: () {
Navigator.pop(context);
setState(() => _activePickerTarget = MapPickerTarget.minefieldStart);
},
onPickEnd: () {
Navigator.pop(context);
setState(() => _activePickerTarget = MapPickerTarget.minefieldEnd);
},
onSwap: () {
setState(() {
final temp = _minefieldStart;
_minefieldStart = _minefieldEnd;
_minefieldEnd = temp;
});
},
onZoneCalculated: (res) {
setState(() => _activeMinefieldResult = res);
_fitRouteBounds([res.startPoint, res.endPoint]);
},
onClose: () => Navigator.pop(context),
),
);
}
void _openIsochroneSheet() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) => TacticalIsochroneSheet(
center: _isochroneCenter ?? _currentGpsPosition ?? const LatLng(31.9539, 35.9106),
onPickCenter: () {
Navigator.pop(context);
setState(() => _activePickerTarget = MapPickerTarget.isochroneCenter);
},
onIsochronesCalculated: (rings) {
setState(() => _activeIsochroneRings = rings);
},
onClose: () => Navigator.pop(context),
),
);
}
void _openSymbolsSheet() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) => TacticalSymbolsSheet(
activePlacementType: _activePlacementSymbolType,
placedSymbols: _placedSymbols,
onSelectSymbolType: (type) {
setState(() => _activePlacementSymbolType = type);
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: const Color(0xFF0071E3),
duration: const Duration(seconds: 4),
content: const Row(
children: [
Icon(Icons.touch_app, color: Colors.white, size: 18),
SizedBox(width: 10),
Expanded(
child: Text(
'انقر على الخريطة لتثبيت الرمز العسكري في الميدان 📍',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
),
);
},
onDeleteSymbol: (s) {
setState(() => _placedSymbols.removeWhere((item) => item.id == s.id));
},
onClose: () => Navigator.pop(context),
),
);
}
void _openOverlaysSheet() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) => TacticalOverlaysSheet(
layers: _overlayLayers,
onToggleLayer: (layerId, isVisible) {
setState(() {
_overlayLayers = _overlayLayers.map((l) {
if (l.id == layerId) return l.copyWith(isVisible: isVisible);
return l;
}).toList();
});
},
onClose: () => Navigator.pop(context),
),
);
}
void _handleMapTap(LatLng point) {
// 1. Check Active Placement of Tactical Symbol
if (_activePlacementSymbolType != null) {
final type = _activePlacementSymbolType!;
setState(() {
_placedSymbols.add(TacticalSymbolItem(
id: 'sym_${DateTime.now().millisecondsSinceEpoch}',
name: 'وحدة ميدانية ${_placedSymbols.length + 1}',
type: type,
position: point,
));
_activePlacementSymbolType = null;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
backgroundColor: Color(0xFF22C55E),
content: Text('تم تثبيت الرمز العسكري على الخريطة بنجاح ✅'),
),
);
return;
}
// 2. Check Active Picker
if (_activePickerTarget != null) {
_onConfirmPicker(_activePickerTarget!, point);
return;
}
// 3. Mode handlers
if (_currentTacticalMode == 'viewshed_360') {
setState(() => _losObserver = point);
_openViewshed360Sheet();
return;
}
if (_currentTacticalMode == 'artillery') {
if (_artilleryGun == null) {
setState(() => _artilleryGun = point);
} else {
setState(() => _artilleryTarget = point);
}
_openArtillerySheet();
return;
}
if (_currentTacticalMode == 'hlz') {
setState(() => _hlzCenter = point);
_openHlzSheet();
return;
}
if (_currentTacticalMode == 'minefield') {
if (_minefieldStart == null) {
setState(() => _minefieldStart = point);
} else {
setState(() => _minefieldEnd = point);
}
_openMinefieldSheet();
return;
}
if (_currentTacticalMode == 'isochrone') {
setState(() => _isochroneCenter = point);
_openIsochroneSheet();
return;
}
if (_currentTacticalMode == 'los') {
if (_losObserver == null) {
setState(() => _losObserver = point);
} else {
setState(() => _losTarget = point);
_openLosSheet();
}
return;
}
// Default Tap Info
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));
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 fallback: $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 Map ─────────────────────
Set<Marker> _buildMarkers(ActiveNavigationState navState) {
final markers = <Marker>{};
// 1. 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),
),
),
);
}
// 2. Resection 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: 'تم استخراج الموقع بالتقاطع البصري العكسي',
),
),
);
}
// 3. 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: 'نقطة الوصول المحددة',
),
),
);
}
// 4. Moving Vehicle Marker
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()}°',
),
),
);
}
// 5. Tactical Placed Symbols
for (final sym in _placedSymbols) {
markers.add(
Marker(
markerId: MarkerId(sym.id),
position: sym.position,
icon: InlqBitmap.defaultMarkerWithHue(
sym.isEnemy ? InlqBitmap.hueRed : InlqBitmap.hueBlue,
),
infoWindow: InfoWindow(
title: sym.name,
snippet: MilitaryGridUtils.latLngToMgrs(sym.position.latitude, sym.position.longitude),
),
),
);
}
// 6. Artillery Gun & Target Markers
if (_currentTacticalMode == 'artillery') {
if (_artilleryGun != null) {
markers.add(
Marker(
markerId: const MarkerId('artillery_gun_marker'),
position: _artilleryGun!,
icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueBlue),
infoWindow: const InfoWindow(title: '🎯 مربض المدفعية'),
),
);
}
if (_artilleryTarget != null) {
markers.add(
Marker(
markerId: const MarkerId('artillery_target_marker'),
position: _artilleryTarget!,
icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueRed),
infoWindow: const InfoWindow(title: '💥 الهدف المعادي'),
),
);
}
}
// 7. HLZ Center Marker
if (_currentTacticalMode == 'hlz' && _hlzCenter != null) {
markers.add(
Marker(
markerId: const MarkerId('hlz_center_marker'),
position: _hlzCenter!,
icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueGreen),
infoWindow: const InfoWindow(title: '🚁 مهبط الطيران العامودي (HLZ)'),
),
);
}
return markers;
}
Set<Polyline> _buildPolylines(ActiveNavigationState navState) {
final polylines = <Polyline>{};
// 1. Tactical Line of Sight (LOS)
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,
),
);
}
}
}
// 2. Artillery Ballistic Arc Ground Track
if (_currentTacticalMode == 'artillery' && _activeArtillerySolution != null) {
final points = _activeArtillerySolution!.trajectoryProfile.map((p) => p.coordinate).toList();
polylines.add(
Polyline(
polylineId: const PolylineId('artillery_trajectory_track'),
points: points,
color: _activeArtillerySolution!.isCrestClear
? const Color(0xFF00F0FF)
: const Color(0xFFEF4444),
width: 4.5,
),
);
}
// 3. Minefield Safe Breaching Lane Centerline
if (_currentTacticalMode == 'minefield' && _activeMinefieldResult != null) {
polylines.add(
Polyline(
polylineId: const PolylineId('minefield_breach_centerline'),
points: _activeMinefieldResult!.breachLaneCenterline,
color: const Color(0xFF10B981),
width: 4.0,
),
);
}
// 4. Tactical Overlays Lines
for (final layer in _overlayLayers) {
if (layer.isVisible) {
for (int i = 0; i < layer.lines.length; i++) {
polylines.add(
Polyline(
polylineId: PolylineId('overlay_${layer.id}_line_$i'),
points: layer.lines[i],
color: layer.color,
width: 4.0,
),
);
}
}
}
// 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<Polygon> _buildPolygons() {
final polygons = <Polygon>{};
// 1. 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(0x4422C55E),
strokeColor: const Color(0xFF22C55E),
strokeWidth: 2,
),
);
}
// 2. HLZ Pad Boundary & Approach Corridor
if (_currentTacticalMode == 'hlz' && _activeHlzResult != null) {
polygons.add(
Polygon(
polygonId: const PolygonId('hlz_pad_boundary'),
points: _activeHlzResult!.padBoundary,
fillColor: _activeHlzResult!.gradeColor.withAlpha(50),
strokeColor: _activeHlzResult!.gradeColor,
strokeWidth: 2,
),
);
polygons.add(
Polygon(
polygonId: const PolygonId('hlz_approach_corridor'),
points: _activeHlzResult!.approachFunnel,
fillColor: const Color(0x3338BDF8),
strokeColor: const Color(0xFF38BDF8),
strokeWidth: 1.5,
),
);
}
// 3. Minefield Boundary & Safe Breaching Lane
if (_currentTacticalMode == 'minefield' && _activeMinefieldResult != null) {
polygons.add(
Polygon(
polygonId: const PolygonId('minefield_threat_boundary'),
points: _activeMinefieldResult!.boundaryPolygon,
fillColor: const Color(0x44EF4444),
strokeColor: const Color(0xFFEF4444),
strokeWidth: 2,
),
);
polygons.add(
Polygon(
polygonId: const PolygonId('minefield_safe_breach_polygon'),
points: _activeMinefieldResult!.breachLanePolygon,
fillColor: const Color(0x6610B981),
strokeColor: const Color(0xFF10B981),
strokeWidth: 2,
),
);
}
// 4. Isochrone Response Time Rings
if (_currentTacticalMode == 'isochrone' && _activeIsochroneRings != null) {
for (int i = 0; i < _activeIsochroneRings!.length; i++) {
final ring = _activeIsochroneRings![i];
polygons.add(
Polygon(
polygonId: PolygonId('isochrone_ring_$i'),
points: ring.polygonCoordinates,
fillColor: ring.ringColor.withAlpha(30),
strokeColor: ring.ringColor,
strokeWidth: 2,
),
);
}
}
// 5. Tactical Overlays Polygons
for (final layer in _overlayLayers) {
if (layer.isVisible) {
for (int i = 0; i < layer.polygons.length; i++) {
polygons.add(
Polygon(
polygonId: PolygonId('overlay_${layer.id}_poly_$i'),
points: layer.polygons[i],
fillColor: layer.color.withAlpha(40),
strokeColor: layer.color,
strokeWidth: 2,
),
);
}
}
}
return polygons;
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<ActiveNavigationState>(
valueListenable: TurnByTurnNavigationEngine.navigationState,
builder: (context, navState, _) {
final markers = _buildMarkers(navState);
final polylines = _buildPolylines(navState);
final polygons = _buildPolygons();
return Scaffold(
key: _scaffoldKey,
drawer: TacticalDrawer(
currentAngleUnit: _angleUnit,
onAngleUnitChanged: (unit) => setState(() => _angleUnit = unit),
onOpenResectionHud: () => setState(() => _currentTacticalMode = 'resection_cam'),
onStartRoutingMode: () => _openRoutePlanner(),
onStartLosMode: () {
setState(() => _currentTacticalMode = 'los');
_openLosSheet();
},
onStartViewshedMode: () {
setState(() => _currentTacticalMode = 'viewshed_360');
_openViewshed360Sheet();
},
onStartArtilleryMode: () {
setState(() => _currentTacticalMode = 'artillery');
_openArtillerySheet();
},
onStartHlzMode: () {
setState(() => _currentTacticalMode = 'hlz');
_openHlzSheet();
},
onStartMinefieldMode: () {
setState(() => _currentTacticalMode = 'minefield');
_openMinefieldSheet();
},
onStartIsochroneMode: () {
setState(() => _currentTacticalMode = 'isochrone');
_openIsochroneSheet();
},
onStartSymbolsMode: () {
setState(() => _currentTacticalMode = 'symbols');
_openSymbolsSheet();
},
onStartOverlaysMode: () {
setState(() => _currentTacticalMode = 'overlays');
_openOverlaysSheet();
},
onLandmarksSynced: () => setState(() {}),
showContours: _showContours,
onToggleContours: (v) => setState(() => _showContours = v),
),
body: Stack(
children: [
// ── Map Canvas ─────────────────────────────────────────
IntaleqMap(
apiKey: AppConfig.apiKey,
initialCameraPosition: CameraPosition(
target: _currentCameraCenter,
zoom: 13.0,
),
styleUrl: _showContours
? 'assets/style_dark.json'
: 'assets/style.json',
onMapCreated: (ctrl) {
_mapController = ctrl;
},
onCameraMove: (pos) {
_currentCameraCenter = pos.target;
},
onTap: _handleMapTap,
markers: markers,
polylines: polylines,
polygons: polygons,
),
// ── Top Tactical Operations Bar ────────────────────────
Positioned(
top: 48,
left: 16,
right: 16,
child: Row(
children: [
Container(
decoration: BoxDecoration(
color: const Color(0xFF090E17).withAlpha(220),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF0071E3).withAlpha(120)),
),
child: IconButton(
icon: const Icon(Icons.menu, color: Color(0xFF38BDF8)),
onPressed: () => _scaffoldKey.currentState?.openDrawer(),
),
),
const SizedBox(width: 8),
Expanded(
child: Container(
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 14),
decoration: BoxDecoration(
color: const Color(0xFF090E17).withAlpha(220),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white12),
),
child: Row(
children: [
const Icon(Icons.shield, color: Color(0xFF0071E3), size: 20),
const SizedBox(width: 8),
Text(
_getModeTitle(),
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
const SizedBox(width: 8),
Container(
decoration: BoxDecoration(
color: const Color(0xFF090E17).withAlpha(220),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white12),
),
child: IconButton(
icon: const Icon(Icons.search, color: Color(0xFF38BDF8)),
onPressed: _openPlaceSearch,
),
),
],
),
),
// ── Interactive Pin Picker HUD ────────────────────────
if (_activePickerTarget != null)
InteractiveMapPickerHud(
target: _activePickerTarget!,
centerPosition: _currentCameraCenter,
onConfirm: () => _onConfirmPicker(_activePickerTarget!, _currentCameraCenter),
onCancel: _onCancelPicker,
),
// ── Resection Camera Overlay ───────────────────────────
if (_currentTacticalMode == 'resection_cam')
CameraResectionView(
observations: _observations,
angleUnit: _angleUnit,
onObservationAdded: _onObservationAdded,
onCalculatePressed: _executeResectionCalculation,
onResetPressed: _resetResection,
onClose: () => setState(() => _currentTacticalMode = 'nav'),
),
// ── Active Turn-by-Turn Navigation HUD ─────────────────
if (navState.isNavigating)
Positioned(
top: 48,
left: 16,
right: 16,
child: ActiveNavigationHud(
state: navState,
angleUnit: _angleUnit,
onStopNavigation: () {
TurnByTurnNavigationEngine.stopNavigation();
},
),
),
],
),
);
},
);
}
String _getModeTitle() {
switch (_currentTacticalMode) {
case 'los':
return 'تبادل الرؤية والمراقبة (LOS)';
case 'viewshed_360':
return 'رادار الرصد الدائري 360°';
case 'artillery':
return 'رماية المدفعية وقوس القذيفة';
case 'hlz':
return 'مهابط المروحيات (HLZ)';
case 'minefield':
return 'حقول الألغام وممرات العبور';
case 'isochrone':
return 'نطاق التدخل السريع (QRF)';
case 'symbols':
return 'الرموز والتشكيلات العسكرية';
case 'overlays':
return 'منظومة الشفافات (IPB)';
case 'routing':
return 'توجيه القوافل التكتيكي';
default:
return 'منظومة العمليات الميدانية (Off-Grid)';
}
}
}