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

609 lines
28 KiB
Dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import '../config/app_config.dart';
import '../controllers/tactical_map_controller.dart';
import '../models/navigation_state.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/optical_rangefinder_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_route_preview_card.dart';
import '../widgets/tactical_symbols_sheet.dart';
import '../widgets/tactical_viewshed_sheet.dart';
import '../models/tactical_sheet_type.dart';
import '../widgets/persistent_tactical_sheet_wrapper.dart';
/// ============================================================================
/// [TacticalMapScreen] - الشاشة التكتيكية الرئيسية (GetView Architecture)
/// ============================================================================
/// English:
/// Master map screen implemented as a reactive GetView listening to [TacticalMapController].
/// Decoupled from direct state mutations, using Obx for declarative UI rendering.
///
/// العربية:
/// شاشة العمليات والخريطة الرئيسية بنمط GetView التفاعلي، ترتبط بمتحكم [TacticalMapController]
/// وتستخدم Obx لتحديث طبقات الخريطة واللوحات المنبثقة فور تغير الحالة الميدانية.
/// ============================================================================
class TacticalMapScreen extends GetView<TacticalMapController> {
const TacticalMapScreen({super.key});
@override
Widget build(BuildContext context) {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
// Check Initial Provisioning on startup
_checkProvisioning(context);
return ValueListenableBuilder<ActiveNavigationState>(
valueListenable: TurnByTurnNavigationEngine.navigationState,
builder: (context, navState, _) {
return Scaffold(
key: scaffoldKey,
drawer: Obx(
() => TacticalDrawer(
currentAngleUnit: controller.angleUnit.value,
onAngleUnitChanged: controller.setAngleUnit,
onOpenResectionHud: () => controller.switchMode('resection_cam'),
onStartRangefinderMode: () => controller.switchMode('rangefinder'),
onStartRoutingMode: () => _openRoutePlanner(context),
onStartLosMode: () {
controller.switchMode('los');
_openLosSheet(context);
},
onStartViewshedMode: () {
controller.switchMode('viewshed_360');
_openViewshed360Sheet(context);
},
onStartArtilleryMode: () {
controller.switchMode('artillery');
_openArtillerySheet(context);
},
onStartHlzMode: () {
controller.switchMode('hlz');
_openHlzSheet(context);
},
onStartMinefieldMode: () {
controller.switchMode('minefield');
_openMinefieldSheet(context);
},
onStartIsochroneMode: () {
controller.switchMode('isochrone');
_openIsochroneSheet(context);
},
onStartSymbolsMode: () {
controller.switchMode('symbols');
_openSymbolsSheet(context);
},
onStartOverlaysMode: () {
controller.switchMode('overlays');
_openOverlaysSheet(context);
},
onLandmarksSynced: () => controller.update(),
onClearMap: () {
controller.clearAllTacticalLayers();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
backgroundColor: Color(0xFF0F172A),
content: Text('🧹 تم مسح كافة الرسوم والأدوات وتصفير الخريطة بنجاح.'),
duration: Duration(seconds: 2),
),
);
},
showContours: controller.showContours.value,
onToggleContours: controller.toggleContours,
),
),
body: Stack(
children: [
// ── 1. Map Canvas with Declarative Layer Overlays ──────
Obx(() {
final markers = controller.buildMarkers(navState);
final polylines = controller.buildPolylines(navState);
final polygons = controller.buildPolygons();
return IntaleqMap(
apiKey: AppConfig.apiKey,
initialCameraPosition: CameraPosition(
target: controller.currentGpsPosition.value ?? const LatLng(31.9539, 35.9106),
zoom: 14.0,
),
styleUrl: controller.showContours.value
? 'assets/tactical-style-contours.json'
: 'assets/tactical-style.json',
onMapCreated: (ctrl) {
controller.mapController = ctrl;
},
onStyleLoaded: () {
if (controller.currentGpsPosition.value != null) {
controller.mapController?.animateCamera(
CameraUpdate.newLatLngZoom(controller.currentGpsPosition.value!, 14.0),
);
}
},
onCameraMove: (pos) {
controller.currentCameraCenter.value = pos.target;
},
onTap: controller.handleMapTap,
markers: markers,
polylines: polylines,
polygons: polygons,
);
}),
// ── 2. Top Operations Floating Bar ────────────────────
if (!navState.isNavigating)
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),
Expanded(
child: Obx(
() => Text(
controller.getModeTitle(),
style: const TextStyle(
color: Colors.white,
fontSize: 12.5,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
),
],
),
),
),
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.cleaning_services_rounded,
color: Color(0xFFF43F5E), size: 20),
tooltip: 'مسح وتنظيف الخريطة',
onPressed: () {
controller.clearAllTacticalLayers();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
backgroundColor: Color(0xFF0F172A),
content: Text('🧹 تم مسح كافة الرسوم والأدوات وتصفير الخريطة بنجاح.'),
duration: Duration(seconds: 2),
),
);
},
),
),
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(context),
),
),
],
),
),
// ── 3. Interactive Crosshair Target Picker HUD ─────────
Obx(() {
if (controller.activePickerTarget.value == null) {
return const SizedBox.shrink();
}
return InteractiveMapPickerHud(
target: controller.activePickerTarget.value!,
centerPosition: controller.currentCameraCenter.value,
onConfirm: () => controller.confirmPicker(
controller.activePickerTarget.value!,
controller.currentCameraCenter.value,
),
onCancel: controller.cancelPicker,
);
}),
// ── 4. Optical Resection Camera HUD (GPS-Denied) ───────
Obx(() {
if (controller.currentTacticalMode.value != 'resection_cam') {
return const SizedBox.shrink();
}
return CameraResectionView(
observations: controller.resection.observations,
angleUnit: controller.angleUnit.value,
initialSelectedLandmark: controller.resection.selectedMapLandmark.value,
onObservationAdded: controller.resection.addObservation,
onPickFromMap: () {
controller.switchMode('nav');
controller.setPickerTarget(MapPickerTarget.resectionLandmark);
},
onCalculatePressed: () {
final res = controller.resection.computePosition();
if (res != null) {
controller.switchMode('nav');
controller.mapController?.animateCamera(
CameraUpdate.newLatLngZoom(
LatLng(res.lat, res.lng), 15.0),
);
}
},
onResetPressed: controller.resection.reset,
onClose: () => controller.switchMode('nav'),
);
}),
// ── 4b. Optical Rangefinder Camera HUD (قياس المدى البصري) ───
Obx(() {
if (controller.currentTacticalMode.value != 'rangefinder') {
return const SizedBox.shrink();
}
return OpticalRangefinderHud(
angleUnit: controller.angleUnit.value,
onClose: () => controller.switchMode('nav'),
onRangeLocked: (dist, heading, pitch) {
controller.switchMode('nav');
},
);
}),
// ── 4c. Persistent Tactical Sheet ──────────────────────────
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Obx(() {
final activeSheet = controller.activeSheet.value;
if (activeSheet == null || navState.isNavigating) {
return const SizedBox.shrink();
}
Widget sheetContent = const SizedBox.shrink();
switch (activeSheet.type) {
case TacticalSheetType.route:
sheetContent = TacticalRoutePlannerSheet(
initialOrigin: controller.pickedRouteOrigin.value,
initialDestination: controller.pickedRouteDestination.value,
currentGps: controller.currentGpsPosition.value ?? const LatLng(31.9539, 35.9106),
resectionFix: controller.resection.resectionResult.value != null
? LatLng(controller.resection.resectionResult.value!.lat, controller.resection.resectionResult.value!.lng)
: null,
onRouteConfirmed: (origin, destination, profile, autoStartNav) async {
final plan = await controller.navigation.calculateRoute(
origin: origin,
destination: destination,
profile: profile,
autoStart: autoStartNav,
mapController: controller.mapController,
);
if (plan != null && plan.polylinePoints.isNotEmpty) {
controller.fitBounds(plan.polylinePoints);
}
},
onPickOriginOnMap: () {
controller.isSheetMinimized.value = true;
controller.setPickerTarget(MapPickerTarget.routeOrigin);
},
onPickDestinationOnMap: () {
controller.isSheetMinimized.value = true;
controller.setPickerTarget(MapPickerTarget.routeDestination);
},
onClose: controller.closeSheet,
);
break;
case TacticalSheetType.los:
sheetContent = TacticalLosSheet(
observer: controller.los.observerPosition.value ?? const LatLng(31.9539, 35.9106),
target: controller.los.targetPosition.value ?? const LatLng(31.9800, 35.9500),
currentGps: controller.currentGpsPosition.value,
angleUnit: controller.angleUnit.value,
onUpdateLocations: (newObs, newTgt) {
controller.los.setObserver(newObs);
controller.los.setTarget(newTgt);
controller.fitBounds([newObs, newTgt]);
},
onVisibilityChanged: (_) {},
onReportGenerated: (_) {},
onPickObserverOnMap: () {
controller.isSheetMinimized.value = true;
controller.setPickerTarget(MapPickerTarget.losObserver);
},
onPickTargetOnMap: () {
controller.isSheetMinimized.value = true;
controller.setPickerTarget(MapPickerTarget.losTarget);
},
onClose: () {
controller.closeSheet();
controller.switchMode('nav');
},
);
break;
case TacticalSheetType.viewshed:
sheetContent = TacticalViewshedSheet(
observer: controller.viewshed.observerPosition.value ?? controller.currentGpsPosition.value ?? const LatLng(31.9539, 35.9106),
currentGps: controller.currentGpsPosition.value,
onUpdateObserver: controller.viewshed.setObserver,
onReportUpdated: (_) {},
onPickObserverOnMap: () {
controller.isSheetMinimized.value = true;
controller.setPickerTarget(MapPickerTarget.viewshedCenter);
},
onClose: controller.closeSheet,
);
break;
case TacticalSheetType.artillery:
sheetContent = TacticalArtillerySheet(
gunPosition: controller.artillery.gunPosition.value ?? controller.currentGpsPosition.value ?? const LatLng(31.9539, 35.9106),
targetPosition: controller.artillery.targetPosition.value,
angleUnit: controller.angleUnit.value,
onPickGun: () {
controller.isSheetMinimized.value = true;
controller.setPickerTarget(MapPickerTarget.artilleryGun);
},
onPickTarget: () {
controller.isSheetMinimized.value = true;
controller.setPickerTarget(MapPickerTarget.artilleryTarget);
},
onSwap: controller.artillery.swapPositions,
onSolutionCalculated: (sol) {
controller.fitBounds([sol.gunPosition, sol.targetPosition]);
},
onClose: controller.closeSheet,
);
break;
case TacticalSheetType.hlz:
sheetContent = TacticalHlzSheet(
selectedPosition: controller.hlz.selectedPosition.value ?? controller.currentGpsPosition.value ?? const LatLng(31.9539, 35.9106),
onPickLocation: () {
controller.isSheetMinimized.value = true;
controller.setPickerTarget(MapPickerTarget.hlzCenter);
},
onAssessmentCompleted: (_) {},
onClose: controller.closeSheet,
);
break;
case TacticalSheetType.minefield:
sheetContent = TacticalMinefieldSheet(
startPoint: controller.minefield.startPoint.value ?? controller.currentGpsPosition.value ?? const LatLng(31.9539, 35.9106),
endPoint: controller.minefield.endPoint.value,
onPickStart: () {
controller.isSheetMinimized.value = true;
controller.setPickerTarget(MapPickerTarget.minefieldStart);
},
onPickEnd: () {
controller.isSheetMinimized.value = true;
controller.setPickerTarget(MapPickerTarget.minefieldEnd);
},
onSwap: controller.minefield.swapPoints,
onZoneCalculated: (res) {
controller.fitBounds([res.startPoint, res.endPoint]);
},
onClose: controller.closeSheet,
);
break;
case TacticalSheetType.isochrone:
sheetContent = TacticalIsochroneSheet(
center: controller.isochrone.center.value ?? controller.currentGpsPosition.value ?? const LatLng(31.9539, 35.9106),
onPickCenter: () {
controller.isSheetMinimized.value = true;
controller.setPickerTarget(MapPickerTarget.isochroneCenter);
},
onIsochronesCalculated: (_) {},
onClose: controller.closeSheet,
);
break;
case TacticalSheetType.symbols:
sheetContent = TacticalSymbolsSheet(
activePlacementType: controller.symbols.activePlacementType.value,
placedSymbols: controller.symbols.placedSymbols,
onSelectSymbolType: (type) {
controller.symbols.selectSymbolForPlacement(type);
controller.isSheetMinimized.value = true;
Get.snackbar(
'الرموز العسكرية',
'انقر على الخريطة لتثبيت الرمز في الميدان 📍',
backgroundColor: const Color(0xFF0071E3),
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
},
onDeleteSymbol: (s) => controller.symbols.removeSymbol(s.id),
onClose: controller.closeSheet,
);
break;
case TacticalSheetType.overlays:
sheetContent = TacticalOverlaysSheet(
layers: controller.overlays.overlayLayers,
onToggleLayer: controller.overlays.toggleLayer,
onClose: controller.closeSheet,
);
break;
}
return PersistentTacticalSheetWrapper(
title: activeSheet.title,
icon: activeSheet.icon,
isMinimized: controller.isSheetMinimized.value,
onToggleMinimize: () => controller.isSheetMinimized.toggle(),
onClose: controller.closeSheet,
child: sheetContent,
);
}),
),
// ── 5. Active Turn-by-Turn Navigation HUD ──────────────
if (navState.isNavigating) ...[
Positioned(
top: 48,
left: 16,
right: 16,
child: ActiveNavigationTopBanner(navState: navState),
),
Positioned(
bottom: 20,
left: 16,
right: 16,
child: ActiveNavigationBottomHUD(
navState: navState,
onStopNavigation: controller.navigation.stopNavigation,
),
),
],
// ── 6. Tactical Route Preview Card ────────────────────
// Positioned خارج Obx إلزامياً: Positioned يجب أن يكون ابناً
// مباشراً لـ Stack وإلا فشل التخطيط وhit-test.
Positioned(
bottom: 16,
left: 16,
right: 16,
child: Obx(() {
final route = controller.navigation.activeRoute.value;
if (route == null || navState.isNavigating) {
return const SizedBox.shrink();
}
return TacticalRoutePreviewCard(
activeRoute: route,
onClose: () => controller.navigation.reset(),
onStartNavigation: () {
controller.navigation.startNavigation(
mapController: controller.mapController,
);
},
);
}),
),
],
),
);
},
);
}
// ── Operations Modal Launchers ─────────────────────────────
Future<void> _checkProvisioning(BuildContext context) async {
final needed =
await TacticalInitialProvisioningDialog.isProvisioningNeeded();
if (needed) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog(
context: context,
barrierDismissible: false,
builder: (ctx) => TacticalInitialProvisioningDialog(
onCompleted: () => Navigator.pop(ctx),
),
);
});
}
}
void _openRoutePlanner(BuildContext context) {
controller.openSheet(ActiveTacticalSheet(TacticalSheetType.route, 'مخطط مسار القوافل', Icons.route));
}
void _openLosSheet(BuildContext context) {
controller.openSheet(ActiveTacticalSheet(TacticalSheetType.los, 'تحليل خط الرؤية (LOS)', Icons.visibility));
}
void _openViewshed360Sheet(BuildContext context) {
controller.openSheet(ActiveTacticalSheet(TacticalSheetType.viewshed, 'رادار الرؤية 360 درجة', Icons.radar));
}
void _openArtillerySheet(BuildContext context) {
controller.openSheet(ActiveTacticalSheet(TacticalSheetType.artillery, 'حاسبة المدفعية الميدانية', Icons.track_changes));
}
void _openHlzSheet(BuildContext context) {
controller.openSheet(ActiveTacticalSheet(TacticalSheetType.hlz, 'استطلاع المهابط (HLZ)', Icons.local_airport));
}
void _openMinefieldSheet(BuildContext context) {
controller.openSheet(ActiveTacticalSheet(TacticalSheetType.minefield, 'تأمين الممرات والألغام', Icons.warning_amber_rounded));
}
void _openIsochroneSheet(BuildContext context) {
controller.openSheet(ActiveTacticalSheet(TacticalSheetType.isochrone, 'نطاقات الحركة والإمداد', Icons.speed));
}
void _openSymbolsSheet(BuildContext context) {
controller.openSheet(ActiveTacticalSheet(TacticalSheetType.symbols, 'الرموز العسكرية (MIL-STD)', Icons.category));
}
void _openOverlaysSheet(BuildContext context) {
controller.openSheet(ActiveTacticalSheet(TacticalSheetType.overlays, 'شفافات العمليات (IPB)', Icons.layers));
}
void _openPlaceSearch(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) => PlaceSearchSheet(
onDestinationSelected: (landmark, startNav) {
final dest = LatLng(landmark.lat, landmark.lng);
final origin = controller.currentGpsPosition.value ??
const LatLng(31.9539, 35.9106);
controller.navigation.calculateRoute(
origin: origin,
destination: dest,
autoStart: startNav,
mapController: controller.mapController,
);
},
),
);
}
}