chore: add build artifacts, map assets, and routing data
This commit is contained in:
@@ -1,21 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import '../services/offline_road_graph_engine.dart';
|
||||
import '../services/offline_routing_engine.dart';
|
||||
import '../services/offline_routing_package_service.dart';
|
||||
import '../services/tactical_api_service.dart';
|
||||
import '../services/turn_by_turn_navigation_engine.dart';
|
||||
import '../services/valhalla_offline_engine.dart';
|
||||
|
||||
/// ============================================================================
|
||||
/// [NavigationController] - وحدة التحكم بالملاحة وتوجيه القوافل التكتيكية
|
||||
/// ============================================================================
|
||||
/// English:
|
||||
/// GetX Controller managing hybrid tactical convoy routing (Online API + Sovereign
|
||||
/// Offline Engine fallback), active turn-by-turn guidance, and navigation metrics.
|
||||
///
|
||||
/// العربية:
|
||||
/// متحكم GetX لإدارة توجيه القوافل والآليات العسكرية (هجين: سيرفر أونلاين + محرك أوفلاين محلي)،
|
||||
/// وتتبع مسار الملاحة خطوة بخطوة وحساب السرعة والوقت المتبقي للهدف.
|
||||
/// ============================================================================
|
||||
class NavigationController extends GetxController {
|
||||
// ── Observables / الحالات التفاعلية ──────────────────────────────────────
|
||||
final Rx<OfflineRoutePlan?> activeRoute = Rx<OfflineRoutePlan?>(null);
|
||||
@@ -23,7 +18,20 @@ class NavigationController extends GetxController {
|
||||
Rx<TacticalVehicleProfile>(TacticalVehicleProfile.convoy);
|
||||
final RxBool isCalculating = false.obs;
|
||||
|
||||
/// Calculate Hybrid Tactical Route / احتساب مسار تكتيكي هجين (سيرفر + محلي)
|
||||
/// هل التوجيه المحلي الحقيقي (شبكة الطرق الكاملة) جاهز على الجهاز؟
|
||||
final RxBool isRealRoadEngineReady = false.obs;
|
||||
|
||||
NavigationController() {
|
||||
_refreshEngineStatus();
|
||||
}
|
||||
|
||||
Future<void> _refreshEngineStatus() async {
|
||||
final pkgInstalled = await OfflineRoutingPackageService.isInstalled();
|
||||
final dbReady = await OfflineRoadGraphEngine.isDatabaseAvailable();
|
||||
isRealRoadEngineReady.value = pkgInstalled || dbReady;
|
||||
}
|
||||
|
||||
/// Calculate Hybrid Tactical Route / احتساب مسار تكتيكي هجين
|
||||
Future<OfflineRoutePlan?> calculateRoute({
|
||||
required LatLng origin,
|
||||
required LatLng destination,
|
||||
@@ -53,13 +61,47 @@ class NavigationController extends GetxController {
|
||||
profile: activeProfile,
|
||||
tacticalWaypoints: const ['موقع الانطلاق', 'الهدف التكتيكي المحدد'],
|
||||
isOffline: false,
|
||||
usesRealRoadNetwork: true,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Online route fallback: $e');
|
||||
}
|
||||
|
||||
// 2. Fallback to 100% Sovereign On-Device Offline Routing Engine
|
||||
// ── Offline chain preparation ──
|
||||
// تأكد من وجود حزمة التوجيه محلياً؛ إن غابت وحُاول الحساب أثناء توفر
|
||||
// الشبكة تُنزَّل مرة واحدة تلقائياً (يفشل بسرعة عند انقطاع الإنترنت).
|
||||
final routingPackageReady = await OfflineRoutingPackageService.ensureInstalled();
|
||||
if (!routingPackageReady) {
|
||||
debugPrint('Navigation: routing package unavailable — offline engines will degrade gracefully');
|
||||
}
|
||||
|
||||
// 2. On-Device SQLite road graph engine (667K real edges, Arabic street names)
|
||||
// الأولوية الأعلى أوفلاين: يحتوي على كامل شبكة طرق الأردن الحقيقية
|
||||
// (1.99 مليون عقدة، 667 ألف حافة) مع أسماء شوارع عربية وانحناءات واقعية.
|
||||
// يعمل بسرعة < 150ms حتى للمسافات الطويلة (عمان ← العقبة).
|
||||
plan ??= await OfflineRoadGraphEngine.calculateRoute(
|
||||
start: origin,
|
||||
destination: destination,
|
||||
profile: activeProfile,
|
||||
);
|
||||
|
||||
// 3. On-Device Valhalla engine (native bridge — may not be available on all platforms)
|
||||
// يحترم الاتجاه الممنوع وقيود الدوران + ارتفاعات SRTM.
|
||||
plan ??= await ValhallaOfflineEngine.calculateOfflineRoute(
|
||||
start: origin,
|
||||
destination: destination,
|
||||
profile: activeProfile,
|
||||
regionDir: await OfflineRoutingPackageService.installDirPath(),
|
||||
);
|
||||
|
||||
if (plan != null) {
|
||||
debugPrint('Navigation: using real road network route '
|
||||
'(${plan.polylinePoints.length} pts, ${plan.totalDistanceKm.toStringAsFixed(1)} km, '
|
||||
'${plan.maneuvers.length} maneuvers, offline=${plan.isOffline})');
|
||||
}
|
||||
|
||||
// 4. Last resort: legacy built-in synthetic graph (36 strategic nodes only)
|
||||
plan ??= OfflineRoutingEngine.calculateOnDeviceRoute(
|
||||
start: origin,
|
||||
destination: destination,
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_compass/flutter_compass.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sensors_plus/sensors_plus.dart';
|
||||
import '../models/angle_unit.dart';
|
||||
import '../services/camera_sensor_calibration_service.dart';
|
||||
|
||||
/// ============================================================================
|
||||
/// [OpticalRangefinderController] - متحكم منظومة قياس المدى البصري وحساسات الكاميرا
|
||||
/// ============================================================================
|
||||
/// English:
|
||||
/// GetX Controller managing live optical stadiametric rangefinding, camera pinch-to-zoom,
|
||||
/// sensor intrinsics calibration, inclination pitch, and directional compass heading (الاتجاه).
|
||||
///
|
||||
/// العربية:
|
||||
/// متحكم GetX لإدارة قياس المسافة البصري عبر الكاميرا والتقريب والتبعيد بدون GPS،
|
||||
/// مع متابعة زوايا الميل والاتجاه المغناطيسي وفحص مستشعر الجهاز.
|
||||
/// ============================================================================
|
||||
class OpticalRangefinderController extends GetxController {
|
||||
CameraController? cameraController;
|
||||
|
||||
// ── Observables / الحالات التفاعلية ──────────────────────────────────────
|
||||
final RxBool isCameraReady = false.obs;
|
||||
final RxDouble currentZoom = 1.0.obs;
|
||||
final RxDouble minZoom = 1.0.obs;
|
||||
final RxDouble maxZoom = 8.0.obs;
|
||||
|
||||
/// زاوية الاتجاه بالدرجات (0 - 360) — مصطلح "الاتجاه"
|
||||
final RxDouble headingDeg = 0.0.obs;
|
||||
|
||||
/// زاوية الميل الرأسي بالدرجات (-90 إلى +90)
|
||||
final RxDouble pitchDeg = 0.0.obs;
|
||||
|
||||
/// زاوية الميل الجانبي (Roll)
|
||||
final RxDouble rollDeg = 0.0.obs;
|
||||
|
||||
/// الهدف التكتيكي المختار
|
||||
final Rx<TargetPreset> selectedTarget = CameraSensorCalibrationService.standardTargets[0].obs;
|
||||
final RxDouble customHeight = 2.0.obs;
|
||||
final RxBool isCustomTarget = false.obs;
|
||||
|
||||
/// ارتفاع شبكة التصويب بالبكسل / نسبة الشاشة (0.05 إلى 0.8)
|
||||
final RxDouble reticuleHeightRatio = 0.15.obs;
|
||||
|
||||
/// المسافة المحسوبة بالأمتار
|
||||
final RxDouble calculatedDistanceMeters = 0.0.obs;
|
||||
final RxDouble errorMarginMeters = 0.0.obs;
|
||||
|
||||
/// معلومات المستشعر الحالية
|
||||
final Rx<CameraSensorProfile> sensorProfile = CameraSensorCalibrationService.currentProfile.obs;
|
||||
|
||||
// Stream Subscriptions
|
||||
StreamSubscription? _compassSub;
|
||||
StreamSubscription? _accelerometerSub;
|
||||
|
||||
double _screenHeight = 800.0;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
initSensors();
|
||||
initCamera();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_compassSub?.cancel();
|
||||
_accelerometerSub?.cancel();
|
||||
cameraController?.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
/// تهيئة مستشعرات الاتجاه والميل
|
||||
void initSensors() {
|
||||
// 1. Compass for Direction / الاتجاه
|
||||
_compassSub = FlutterCompass.events?.listen((event) {
|
||||
if (event.heading != null) {
|
||||
headingDeg.value = (event.heading! + 360.0) % 360.0;
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Accelerometer for Pitch & Roll Inclination
|
||||
_accelerometerSub = accelerometerEventStream().listen((event) {
|
||||
// Calculate Pitch angle from gravity vector
|
||||
final gX = event.x;
|
||||
final gY = event.y;
|
||||
final gZ = event.z;
|
||||
|
||||
final pitch = math.atan2(-gY, math.sqrt(gX * gX + gZ * gZ)) * (180.0 / math.pi);
|
||||
final roll = math.atan2(gX, gZ) * (180.0 / math.pi);
|
||||
|
||||
pitchDeg.value = pitch;
|
||||
rollDeg.value = roll;
|
||||
});
|
||||
}
|
||||
|
||||
/// تهيئة الكاميرا وفحص المستشعر
|
||||
Future<void> initCamera() async {
|
||||
try {
|
||||
final cameras = await availableCameras();
|
||||
if (cameras.isEmpty) return;
|
||||
|
||||
final backCamera = cameras.firstWhere(
|
||||
(c) => c.lensDirection == CameraLensDirection.back,
|
||||
orElse: () => cameras.first,
|
||||
);
|
||||
|
||||
cameraController = CameraController(
|
||||
backCamera,
|
||||
ResolutionPreset.high,
|
||||
enableAudio: false,
|
||||
);
|
||||
|
||||
await cameraController!.initialize();
|
||||
|
||||
// Get Zoom limits
|
||||
minZoom.value = await cameraController!.getMinZoomLevel();
|
||||
maxZoom.value = math.min(10.0, await cameraController!.getMaxZoomLevel());
|
||||
currentZoom.value = minZoom.value;
|
||||
|
||||
// Inspect & Calibrate sensor parameters
|
||||
final previewSize = cameraController!.value.previewSize ?? const Size(1920, 1080);
|
||||
sensorProfile.value = CameraSensorCalibrationService.inspectSensor(
|
||||
backCamera,
|
||||
Size(previewSize.width, previewSize.height),
|
||||
);
|
||||
|
||||
isCameraReady.value = true;
|
||||
recalculateDistance();
|
||||
} catch (e) {
|
||||
debugPrint('Optical Rangefinder Camera Error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// تحديث ارتفاع الشاشة الفعلي عند الرسم
|
||||
void updateScreenDimensions(Size size) {
|
||||
if (size.height > 0 && size.height != _screenHeight) {
|
||||
_screenHeight = size.height;
|
||||
recalculateDistance();
|
||||
}
|
||||
}
|
||||
|
||||
/// تغيير مستوى التقريب (Pinch or Slider)
|
||||
Future<void> setZoom(double newZoom) async {
|
||||
final clamped = newZoom.clamp(minZoom.value, maxZoom.value);
|
||||
currentZoom.value = clamped;
|
||||
try {
|
||||
await cameraController?.setZoomLevel(clamped);
|
||||
} catch (_) {}
|
||||
recalculateDistance();
|
||||
}
|
||||
|
||||
/// تعديل حجم مؤشر التصويب (Pinch / Drag Reticule)
|
||||
void setReticuleHeightRatio(double newRatio) {
|
||||
reticuleHeightRatio.value = newRatio.clamp(0.02, 0.75);
|
||||
recalculateDistance();
|
||||
}
|
||||
|
||||
/// اختيار هدف تكتيكي جاهز
|
||||
void selectTarget(TargetPreset preset) {
|
||||
selectedTarget.value = preset;
|
||||
isCustomTarget.value = false;
|
||||
recalculateDistance();
|
||||
}
|
||||
|
||||
/// تعيين ارتفاع مخصص للهدف
|
||||
void setCustomTargetHeight(double heightMeters) {
|
||||
customHeight.value = heightMeters.clamp(0.2, 500.0);
|
||||
isCustomTarget.value = true;
|
||||
recalculateDistance();
|
||||
}
|
||||
|
||||
/// إعادة احتساب المسافة بناءً على المعادلة البصرية
|
||||
void recalculateDistance() {
|
||||
final targetH = isCustomTarget.value
|
||||
? customHeight.value
|
||||
: selectedTarget.value.heightMeters;
|
||||
|
||||
final reticulePx = reticuleHeightRatio.value * _screenHeight;
|
||||
|
||||
final dist = CameraSensorCalibrationService.calculateStadiametricDistance(
|
||||
targetRealHeightMeters: targetH,
|
||||
reticulePixelHeight: reticulePx,
|
||||
screenHeightPixels: _screenHeight,
|
||||
zoomMultiplier: currentZoom.value,
|
||||
);
|
||||
|
||||
calculatedDistanceMeters.value = dist;
|
||||
errorMarginMeters.value = CameraSensorCalibrationService.estimateErrorMarginMeters(
|
||||
dist,
|
||||
currentZoom.value,
|
||||
);
|
||||
}
|
||||
|
||||
/// تنسيق قيمة الاتجاه بحسب نظام الزوايا المعتمد
|
||||
String formatHeading(AngleUnit unit) {
|
||||
return unit.formatHeading(headingDeg.value);
|
||||
}
|
||||
|
||||
/// تنسيق نص المسافة (متر / كم)
|
||||
String formatDistance() {
|
||||
final dist = calculatedDistanceMeters.value;
|
||||
final err = errorMarginMeters.value;
|
||||
if (dist >= 1000.0) {
|
||||
final km = dist / 1000.0;
|
||||
final errKm = err / 1000.0;
|
||||
return '${km.toStringAsFixed(2)} كم (±${(errKm * 1000).toInt()}م)';
|
||||
}
|
||||
return '${dist.toStringAsFixed(0)} م (±${err.toStringAsFixed(0)}م)';
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import '../models/military_operations_models.dart';
|
||||
|
||||
/// ============================================================================
|
||||
@@ -21,43 +20,22 @@ class OverlaysController extends GetxController {
|
||||
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),
|
||||
]
|
||||
],
|
||||
isVisible: false,
|
||||
lines: [],
|
||||
),
|
||||
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),
|
||||
]
|
||||
],
|
||||
isVisible: false,
|
||||
lines: [],
|
||||
),
|
||||
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),
|
||||
]
|
||||
],
|
||||
polygons: [],
|
||||
),
|
||||
].obs;
|
||||
|
||||
@@ -82,4 +60,7 @@ class OverlaysController extends GetxController {
|
||||
overlayLayers[i] = overlayLayers[i].copyWith(isVisible: false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset / إعادة تعيين
|
||||
void reset() => hideAll();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,21 @@ class ResectionController extends GetxController {
|
||||
final RxList<ResectionObservation> observations = <ResectionObservation>[].obs;
|
||||
final Rx<ResectionResult?> resectionResult = Rx<ResectionResult?>(null);
|
||||
final RxDouble currentHeadingDeg = 0.0.obs;
|
||||
final Rx<TacticalLandmark?> selectedMapLandmark = Rx<TacticalLandmark?>(null);
|
||||
|
||||
/// Set Landmark picked from interactive Map / تعيين المعلم المختار مباشرة من الخريطة
|
||||
void setMapLandmark(double lat, double lng, {String? customName, int? elevationM}) {
|
||||
selectedMapLandmark.value = TacticalLandmark(
|
||||
id: 'picked_${DateTime.now().millisecondsSinceEpoch}',
|
||||
name: customName ?? 'معلم تكتيكي مرصود (${lat.toStringAsFixed(4)}, ${lng.toStringAsFixed(4)})',
|
||||
region: 'قاطع العمليات الميداني',
|
||||
type: LandmarkType.mountain,
|
||||
lat: lat,
|
||||
lng: lng,
|
||||
elevationM: elevationM ?? 850,
|
||||
description: 'تم تحديده بصرياً عبر إشارة المصلب التفاعلية على الخريطة',
|
||||
);
|
||||
}
|
||||
|
||||
/// Add Observation / إضافة رصد اتجاهي لمعلم جغرافي
|
||||
void addObservation(ResectionObservation obs) {
|
||||
@@ -38,6 +53,24 @@ class ResectionController extends GetxController {
|
||||
return res;
|
||||
}
|
||||
|
||||
/// Compute Position from Single Landmark + Baseline step-off
|
||||
/// استخراج الموقع بالرصد على معلم واحد وخط أساس متحرك (10م، 11م، 20م، 50م)
|
||||
ResectionResult? computeSingleLandmarkPosition({
|
||||
required TacticalLandmark landmark,
|
||||
required double azimuth1Deg,
|
||||
required double azimuth2Deg,
|
||||
required double baselineMeters,
|
||||
}) {
|
||||
final res = ResectionCalculator.calculateSingleLandmarkPolar(
|
||||
landmark: landmark,
|
||||
azimuth1Deg: azimuth1Deg,
|
||||
azimuth2Deg: azimuth2Deg,
|
||||
baselineMeters: baselineMeters,
|
||||
);
|
||||
resectionResult.value = res;
|
||||
return res;
|
||||
}
|
||||
|
||||
/// Reset all observations / تصفير وإعادة ضبط الرصد
|
||||
void reset() {
|
||||
observations.clear();
|
||||
|
||||
@@ -77,4 +77,7 @@ class SymbolsController extends GetxController {
|
||||
placedSymbols.clear();
|
||||
activePlacementType.value = null;
|
||||
}
|
||||
|
||||
/// Reset / إعادة تعيين
|
||||
void reset() => clearAll();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -5,8 +6,12 @@ import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
import '../models/angle_unit.dart';
|
||||
import '../models/navigation_state.dart';
|
||||
import '../models/tactical_sheet_type.dart';
|
||||
import '../services/military_grid_utils.dart';
|
||||
import '../services/offline_los_engine.dart';
|
||||
|
||||
import '../services/local_network_tracker.dart';
|
||||
|
||||
import '../widgets/interactive_map_picker_hud.dart';
|
||||
import 'artillery_controller.dart';
|
||||
import 'hlz_controller.dart';
|
||||
@@ -42,17 +47,52 @@ class TacticalMapController extends GetxController {
|
||||
final ViewshedController viewshed = Get.find<ViewshedController>();
|
||||
final ResectionController resection = Get.find<ResectionController>();
|
||||
final NavigationController navigation = Get.find<NavigationController>();
|
||||
final LocalNetworkTrackerService tracker = Get.find<LocalNetworkTrackerService>();
|
||||
|
||||
|
||||
// ── Observables / الحالات التفاعلية ──────────────────────────────────────
|
||||
final RxString currentTacticalMode = 'nav'.obs;
|
||||
final Rx<AngleUnit> angleUnit = AngleUnit.dual.obs;
|
||||
final RxBool showContours = true.obs;
|
||||
final RxBool showContours = false.obs;
|
||||
final Rx<LatLng?> currentGpsPosition = Rx<LatLng?>(null);
|
||||
final Rx<LatLng> currentCameraCenter = const LatLng(31.9539, 35.9106).obs;
|
||||
final Rx<MapPickerTarget?> activePickerTarget = Rx<MapPickerTarget?>(null);
|
||||
final Rx<LatLng?> pickedRouteOrigin = Rx<LatLng?>(null);
|
||||
final Rx<LatLng?> pickedRouteDestination = Rx<LatLng?>(null);
|
||||
|
||||
// ── Persistent Sheets / النوافذ التكتيكية السفلية المستمرة ────────────────
|
||||
final Rx<ActiveTacticalSheet?> activeSheet = Rx<ActiveTacticalSheet?>(null);
|
||||
final RxBool isSheetMinimized = false.obs;
|
||||
|
||||
void openSheet(ActiveTacticalSheet sheet) {
|
||||
activeSheet.value = sheet;
|
||||
isSheetMinimized.value = false;
|
||||
}
|
||||
|
||||
void closeSheet() {
|
||||
activeSheet.value = null;
|
||||
isSheetMinimized.value = false;
|
||||
}
|
||||
|
||||
IntaleqMapController? mapController;
|
||||
|
||||
/// Clear All Tactical Layers & Reset Map / مسح كافة الطبقات والحسابات التكتيكية وتصفير الخريطة
|
||||
void clearAllTacticalLayers() {
|
||||
artillery.reset();
|
||||
hlz.reset();
|
||||
minefield.reset();
|
||||
isochrone.reset();
|
||||
los.reset();
|
||||
viewshed.reset();
|
||||
resection.reset();
|
||||
navigation.reset();
|
||||
symbols.reset();
|
||||
pickedRouteOrigin.value = null;
|
||||
pickedRouteDestination.value = null;
|
||||
activePickerTarget.value = null;
|
||||
currentTacticalMode.value = 'nav';
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
@@ -70,6 +110,7 @@ class TacticalMapController extends GetxController {
|
||||
perm = await Geolocator.requestPermission();
|
||||
}
|
||||
if (perm == LocationPermission.whileInUse || perm == LocationPermission.always) {
|
||||
// 1. Get initial position and jump camera
|
||||
final pos = await Geolocator.getCurrentPosition(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
@@ -78,9 +119,23 @@ class TacticalMapController extends GetxController {
|
||||
);
|
||||
currentGpsPosition.value = LatLng(pos.latitude, pos.longitude);
|
||||
currentCameraCenter.value = currentGpsPosition.value!;
|
||||
tracker.updateMyPosition(currentGpsPosition.value!, pos.heading);
|
||||
|
||||
mapController?.animateCamera(
|
||||
CameraUpdate.newLatLngZoom(currentGpsPosition.value!, 14.0),
|
||||
);
|
||||
|
||||
// 2. Listen to continuous GPS updates for Blue Force Tracking
|
||||
Geolocator.getPositionStream(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
distanceFilter: 2, // update every 2 meters
|
||||
),
|
||||
).listen((Position newPos) {
|
||||
final latLng = LatLng(newPos.latitude, newPos.longitude);
|
||||
currentGpsPosition.value = latLng;
|
||||
tracker.updateMyPosition(latLng, newPos.heading);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('GPS init error: $e');
|
||||
@@ -206,10 +261,31 @@ class TacticalMapController extends GetxController {
|
||||
case MapPickerTarget.isochroneCenter:
|
||||
isochrone.setCenter(pos);
|
||||
break;
|
||||
case MapPickerTarget.resectionLandmark:
|
||||
resection.setMapLandmark(pos.latitude, pos.longitude);
|
||||
switchMode('resection_cam');
|
||||
break;
|
||||
case MapPickerTarget.routeOrigin:
|
||||
pickedRouteOrigin.value = pos;
|
||||
switchMode('routing');
|
||||
break;
|
||||
case MapPickerTarget.routeDestination:
|
||||
pickedRouteDestination.value = pos;
|
||||
switchMode('routing');
|
||||
final origin = pickedRouteOrigin.value ?? currentGpsPosition.value ?? const LatLng(31.9539, 35.9106);
|
||||
navigation.calculateRoute(
|
||||
origin: origin,
|
||||
destination: pos,
|
||||
mapController: mapController,
|
||||
).then((plan) {
|
||||
if (plan != null && plan.polylinePoints.isNotEmpty) {
|
||||
fitBounds(plan.polylinePoints);
|
||||
}
|
||||
|
||||
});
|
||||
break;
|
||||
}
|
||||
if (activeSheet.value != null) { isSheetMinimized.value = false; }
|
||||
}
|
||||
|
||||
/// Cancel Picker / إلغاء وضع المؤشر
|
||||
@@ -270,6 +346,8 @@ class TacticalMapController extends GetxController {
|
||||
return 'منظومة الشفافات (IPB)';
|
||||
case 'routing':
|
||||
return 'توجيه القوافل التكتيكي';
|
||||
case 'rangefinder':
|
||||
return 'قياس المسافة البصري (بدون GPS)';
|
||||
default:
|
||||
return 'منظومة العمليات الميدانية (Off-Grid)';
|
||||
}
|
||||
@@ -294,7 +372,7 @@ class TacticalMapController extends GetxController {
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Resection Fix Marker
|
||||
// 2. Resection Fix Marker (موقع الراصد المحسوب - ماركر بارز باللون الأخضر التكتيكي)
|
||||
if (resection.resectionResult.value != null && !navState.isNavigating) {
|
||||
final res = resection.resectionResult.value!;
|
||||
markers.add(
|
||||
@@ -302,9 +380,9 @@ class TacticalMapController extends GetxController {
|
||||
markerId: const MarkerId('observer_calculated_position'),
|
||||
position: LatLng(res.lat, res.lng),
|
||||
icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueGreen),
|
||||
infoWindow: const InfoWindow(
|
||||
title: 'موقع الراصد المحسوب (GPS-Denied Fix)',
|
||||
snippet: 'تم استخراج الموقع بالتقاطع البصري العكسي',
|
||||
infoWindow: InfoWindow(
|
||||
title: '🎯 موقعك المحسوب (GPS-Denied Fix)',
|
||||
snippet: 'خطأ الرصد: ±${res.estimatedAccuracyMeters.toStringAsFixed(1)}م • دقة تكتيكية عالية',
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -336,7 +414,7 @@ class TacticalMapController extends GetxController {
|
||||
infoWindow: InfoWindow(
|
||||
title: 'مركبة العمليات الميدانية',
|
||||
snippet:
|
||||
'${navState.currentSpeedKmH.round()} كم/س • سمت ${navState.currentHeadingDeg.round()}°',
|
||||
'${navState.currentSpeedKmH.round()} كم/س • اتجاه ${navState.currentHeadingDeg.round()}°',
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -396,6 +474,22 @@ class TacticalMapController extends GetxController {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// --- Blue Force Tracking (Friendly Units) ---
|
||||
for (final unit in tracker.friendlyUnits.values) {
|
||||
markers.add(
|
||||
Marker(
|
||||
markerId: MarkerId('bft_${unit.deviceId}'),
|
||||
position: unit.position,
|
||||
icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueAzure), // Blue for friendly
|
||||
infoWindow: InfoWindow(
|
||||
title: '${unit.callsign} (${unit.role.name})',
|
||||
snippet: 'آخر ظهور: منذ ${DateTime.now().difference(unit.lastSeen).inSeconds} ثوانٍ',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return markers;
|
||||
}
|
||||
|
||||
@@ -496,6 +590,24 @@ class TacticalMapController extends GetxController {
|
||||
);
|
||||
}
|
||||
|
||||
// 6. Visual Resection Sightlines (خطوط الرؤية البصرية للمعالم المرصودة)
|
||||
if (resection.resectionResult.value != null && resection.observations.isNotEmpty) {
|
||||
final res = resection.resectionResult.value!;
|
||||
final observerPos = LatLng(res.lat, res.lng);
|
||||
for (int i = 0; i < resection.observations.length; i++) {
|
||||
final obs = resection.observations[i];
|
||||
final landmarkPos = LatLng(obs.landmark.lat, obs.landmark.lng);
|
||||
polylines.add(
|
||||
Polyline(
|
||||
polylineId: PolylineId('resection_sightline_$i'),
|
||||
points: [observerPos, landmarkPos],
|
||||
color: const Color(0xFF00F0FF),
|
||||
width: 3.5,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return polylines;
|
||||
}
|
||||
|
||||
@@ -559,14 +671,36 @@ class TacticalMapController extends GetxController {
|
||||
Polygon(
|
||||
polygonId: const PolygonId('minefield_safe_breach_polygon'),
|
||||
points: minefield.zoneResult.value!.breachLanePolygon,
|
||||
fillColor: const Color(0x6610B981),
|
||||
fillColor: const Color(0x3310B981),
|
||||
strokeColor: const Color(0xFF10B981),
|
||||
strokeWidth: 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Isochrone Response Time Rings
|
||||
// 4. Resection Position Uncertainty Ring (دائرة دقة الموقع التكتيكي الأخضر)
|
||||
if (resection.resectionResult.value != null) {
|
||||
final res = resection.resectionResult.value!;
|
||||
final radiusMeters = math.max(25.0, res.estimatedAccuracyMeters);
|
||||
final ringPoints = <LatLng>[];
|
||||
for (int a = 0; a <= 360; a += 15) {
|
||||
final rad = a * (math.pi / 180.0);
|
||||
final dLat = (radiusMeters / 6371000.0) * (180.0 / math.pi);
|
||||
final dLng = (radiusMeters / 6371000.0) * (180.0 / math.pi) / math.cos(res.lat * math.pi / 180.0);
|
||||
ringPoints.add(LatLng(res.lat + dLat * math.sin(rad), res.lng + dLng * math.cos(rad)));
|
||||
}
|
||||
polygons.add(
|
||||
Polygon(
|
||||
polygonId: const PolygonId('resection_accuracy_circle'),
|
||||
points: ringPoints,
|
||||
fillColor: const Color(0x3322C55E),
|
||||
strokeColor: const Color(0xFF22C55E),
|
||||
strokeWidth: 2.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Isochrone Response Time Rings
|
||||
if (currentTacticalMode.value == 'isochrone' &&
|
||||
isochrone.isochroneRings.isNotEmpty) {
|
||||
for (int i = 0; i < isochrone.isochroneRings.length; i++) {
|
||||
@@ -583,7 +717,7 @@ class TacticalMapController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Tactical Overlays Polygons
|
||||
// 6. Tactical Overlays Polygons
|
||||
for (final layer in overlays.overlayLayers) {
|
||||
if (layer.isVisible) {
|
||||
for (int i = 0; i < layer.polygons.length; i++) {
|
||||
|
||||
Reference in New Issue
Block a user