chore: add build artifacts, map assets, and routing data

This commit is contained in:
Hamza-Ayed
2026-09-19 12:34:24 +03:00
parent aa2b9f131f
commit 43ad2e0ad4
327 changed files with 52421 additions and 1810 deletions
@@ -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++) {