feat(tactical-app): full sovereign military suite in Flutter with first-run onboarding, artillery ballistics, HLZ assessment, minefield breach, isochrone, symbols, and overlays

This commit is contained in:
Hamza-Ayed
2026-08-22 15:24:09 +03:00
parent c10fd2396c
commit 5e3be672ee
23 changed files with 4096 additions and 1028 deletions
@@ -0,0 +1,313 @@
import 'package:flutter/material.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
/// Military Echelon Levels (المستوى القيادي / التشكيل)
enum MilitaryEchelon {
team, // طاقم / جماعة (●)
squad, // حظيرة (●●)
platoon, // فصيل (●●●)
company, // سرية ( | )
battalion, // كتيبة ( || )
regiment, // فوج ( ||| )
brigade, // لواء ( X )
division, // فرقة ( XX )
}
/// Tactical Symbol Types (الرموز العسكرية القياسية)
enum TacticalSymbolType {
friendlyInfantry, // مشاة صديقة
friendlyArmor, // دروع / دبابات صديقة
friendlyArtillery,// مدفعية ميدان صديقة
friendlyAirDefense,// دفاع جوي صديق
friendlyRadar, // رادار واستطلاع أرضي
friendlyHq, // مركز قيادة وسيطرة (HQ)
checkpoint, // نقطة غلق وتفتيش أمني
observationPost, // نقطة مراقبة واستطلاع أمامي (OP)
enemyInfantry, // مشاة معادية
enemyArmor, // دروع معادية
enemyArtillery, // مدفعية معادية
minefield, // حقل ألغام
hlz, // مهبط طيران عامودي
}
/// Tactical Unit / Symbol on Map
class TacticalSymbolItem {
final String id;
final String name;
final TacticalSymbolType type;
final MilitaryEchelon echelon;
final LatLng position;
final double azimuthDeg;
final String callsign;
final String notes;
const TacticalSymbolItem({
required this.id,
required this.name,
required this.type,
this.echelon = MilitaryEchelon.battalion,
required this.position,
this.azimuthDeg = 0.0,
this.callsign = '',
this.notes = '',
});
bool get isEnemy =>
type == TacticalSymbolType.enemyInfantry ||
type == TacticalSymbolType.enemyArmor ||
type == TacticalSymbolType.enemyArtillery;
Color get color {
if (isEnemy) return const Color(0xFFEF4444); // Red
if (type == TacticalSymbolType.minefield) return const Color(0xFFF59E0B); // Amber
if (type == TacticalSymbolType.hlz) return const Color(0xFF10B981); // Emerald
if (type == TacticalSymbolType.checkpoint || type == TacticalSymbolType.observationPost) {
return const Color(0xFF38BDF8); // Cyan
}
return const Color(0xFF0071E3); // Friendly Blue
}
IconData get icon {
switch (type) {
case TacticalSymbolType.friendlyInfantry:
case TacticalSymbolType.enemyInfantry:
return Icons.group;
case TacticalSymbolType.friendlyArmor:
case TacticalSymbolType.enemyArmor:
return Icons.shield;
case TacticalSymbolType.friendlyArtillery:
case TacticalSymbolType.enemyArtillery:
return Icons.gps_fixed;
case TacticalSymbolType.friendlyAirDefense:
return Icons.radar;
case TacticalSymbolType.friendlyRadar:
return Icons.track_changes;
case TacticalSymbolType.friendlyHq:
return Icons.flag;
case TacticalSymbolType.checkpoint:
return Icons.gavel;
case TacticalSymbolType.observationPost:
return Icons.visibility;
case TacticalSymbolType.minefield:
return Icons.warning_amber;
case TacticalSymbolType.hlz:
return Icons.flight_land;
}
}
}
/// Artillery Weapon Profile (أنظمة المدفعية الميدانية والراجمات)
class ArtilleryWeaponSystem {
final String id;
final String nameAr;
final String caliber;
final double maxRangeMeters;
final double minRangeMeters;
final double muzzleVelocityMps; // سرعة الفوهة م/ث
final double minElevationDeg;
final double maxElevationDeg;
const ArtilleryWeaponSystem({
required this.id,
required this.nameAr,
required this.caliber,
required this.maxRangeMeters,
required this.minRangeMeters,
required this.muzzleVelocityMps,
required this.minElevationDeg,
required this.maxElevationDeg,
});
static const List<ArtilleryWeaponSystem> standardSystems = [
ArtilleryWeaponSystem(
id: 'm109_155',
nameAr: 'هاوتزر ذاتي الحركة M109A2/A3 (155 ملم)',
caliber: '155mm',
minRangeMeters: 3000,
maxRangeMeters: 24000,
muzzleVelocityMps: 684,
minElevationDeg: 3,
maxElevationDeg: 75,
),
ArtilleryWeaponSystem(
id: 'mortar_120',
nameAr: 'هاون ثقيل M120 (120 ملم)',
caliber: '120mm',
minRangeMeters: 200,
maxRangeMeters: 7200,
muzzleVelocityMps: 318,
minElevationDeg: 45,
maxElevationDeg: 85,
),
ArtilleryWeaponSystem(
id: 'grad_122',
nameAr: 'راجمة صواريخ BM-21 غراد (122 ملم)',
caliber: '122mm Rocket',
minRangeMeters: 5000,
maxRangeMeters: 20400,
muzzleVelocityMps: 690,
minElevationDeg: 0,
maxElevationDeg: 55,
),
ArtilleryWeaponSystem(
id: 'mortar_81',
nameAr: 'هاون متوسط L16 (81 ملم)',
caliber: '81mm',
minRangeMeters: 100,
maxRangeMeters: 5650,
muzzleVelocityMps: 250,
minElevationDeg: 45,
maxElevationDeg: 85,
),
];
}
/// Ballistic Trajectory Point
class BallisticTrajectoryPoint {
final double distanceMeters;
final double altitudeMeters;
final double groundElevationMeters;
final LatLng coordinate;
const BallisticTrajectoryPoint({
required this.distanceMeters,
required this.altitudeMeters,
required this.groundElevationMeters,
required this.coordinate,
});
}
/// Complete Artillery Firing Solution (حل الرماية وقوس القذيفة)
class ArtilleryFiringSolution {
final ArtilleryWeaponSystem weapon;
final LatLng gunPosition;
final LatLng targetPosition;
final double distanceMeters;
final double azimuthDeg;
final double azimuthMilsNato; // 6400 mils
final double quadrantElevationDeg; // زاوية الارتفاع
final double quadrantElevationMilsNato;
final double timeOfFlightSeconds; // زمن الطيران
final double apogeeAltitudeMeters; // ذروة القوس
final bool isCrestClear; // سلامة تجاوز قمم الجبال (Crest Clearance)
final double minCrestClearanceMeters;
final List<BallisticTrajectoryPoint> trajectoryProfile;
const ArtilleryFiringSolution({
required this.weapon,
required this.gunPosition,
required this.targetPosition,
required this.distanceMeters,
required this.azimuthDeg,
required this.azimuthMilsNato,
required this.quadrantElevationDeg,
required this.quadrantElevationMilsNato,
required this.timeOfFlightSeconds,
required this.apogeeAltitudeMeters,
required this.isCrestClear,
required this.minCrestClearanceMeters,
required this.trajectoryProfile,
});
}
/// Helicopter Types for HLZ
enum HelicopterType {
lightUtility, // طوافة استطلاع خفيفة (Little Bird / Bell 407)
mediumLift, // طوافة نقل وتكتيك متوسطة (UH-60 Blackhawk / AH-64 Apache)
heavyTransport // طوافة نقل ثقيل (CH-47 Chinook / Super Stallion)
}
/// Helicopter Landing Zone Assessment Result (تقييم مهبط الطيران العامودي)
class HlzAssessmentResult {
final LatLng center;
final HelicopterType helicopterType;
final double groundElevationM;
final double maxSlopePercent; // نسبة انحدار الأرض
final double avgSlopePercent;
final double recommendedClearanceRadiusM;
final bool isSlopeAcceptable;
final bool isObstacleClear;
final String suitabilityGrade; // "ممتاز (OPTIMAL)", "مقبول بحذر (MARGINAL)", "غير صالح (NO-GO)"
final Color gradeColor;
final double approachAzimuthDeg; // ممر الاقتراب الآمن
final List<LatLng> padBoundary;
final List<LatLng> approachFunnel;
const HlzAssessmentResult({
required this.center,
required this.helicopterType,
required this.groundElevationM,
required this.maxSlopePercent,
required this.avgSlopePercent,
required this.recommendedClearanceRadiusM,
required this.isSlopeAcceptable,
required this.isObstacleClear,
required this.suitabilityGrade,
required this.gradeColor,
required this.approachAzimuthDeg,
required this.padBoundary,
required this.approachFunnel,
});
}
/// Minefield Type
enum MinefieldType {
antiTank, // حقل ألغام ضد الدروع (AT)
antiPersonnel, // حقل ألغام ضد الأفراد (AP)
mixedBarrier, // حقل موانع مركب ومختلط
}
/// Minefield Zone & Breaching Corridor (حقل الألغام وممرات العبور)
class MinefieldZoneResult {
final LatLng startPoint;
final LatLng endPoint;
final MinefieldType type;
final double widthMeters;
final double lengthMeters;
final double estimatedMinesCount;
final List<LatLng> boundaryPolygon;
final List<LatLng> breachLaneCenterline; // ممر الثغرة الآمن
final List<LatLng> breachLanePolygon;
const MinefieldZoneResult({
required this.startPoint,
required this.endPoint,
required this.type,
required this.widthMeters,
required this.lengthMeters,
required this.estimatedMinesCount,
required this.boundaryPolygon,
required this.breachLaneCenterline,
required this.breachLanePolygon,
});
}
/// Tactical Overlays (منظومة الشفافات العسكرية)
class TacticalOverlayLayer {
final String id;
final String nameAr;
final Color color;
final bool isVisible;
final List<List<LatLng>> polygons;
final List<List<LatLng>> lines;
const TacticalOverlayLayer({
required this.id,
required this.nameAr,
required this.color,
this.isVisible = true,
this.polygons = const [],
this.lines = const [],
});
TacticalOverlayLayer copyWith({bool? isVisible}) {
return TacticalOverlayLayer(
id: id,
nameAr: nameAr,
color: color,
isVisible: isVisible ?? this.isVisible,
polygons: polygons,
lines: lines,
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,124 @@
import 'dart:math' as math;
import 'package:intaleq_maps/intaleq_maps.dart';
import '../models/military_operations_models.dart';
import 'dem_tile_elevation_service.dart';
import 'military_grid_utils.dart';
/// Sovereign On-Device Ballistic Trajectory & Artillery Fire Mission Engine
class ArtilleryBallisticsEngine {
ArtilleryBallisticsEngine._();
static const double g = 9.80665; // Earth gravity m/s^2
/// Compute high-precision ballistic firing solution and check terrain crest clearance
static Future<ArtilleryFiringSolution> calculateFireMission({
required ArtilleryWeaponSystem weapon,
required LatLng gunPos,
required LatLng targetPos,
bool highAngle = false,
}) async {
// 1. Calculate Geodesic Range & Azimuth
final distanceMeters = MilitaryGridUtils.haversineDistance(
gunPos.latitude,
gunPos.longitude,
targetPos.latitude,
targetPos.longitude,
);
final azimuthDeg = MilitaryGridUtils.calculateBearing(
gunPos.latitude,
gunPos.longitude,
targetPos.latitude,
targetPos.longitude,
);
final azimuthMilsNato = (azimuthDeg / 360.0) * 6400.0;
// 2. Query Ground Elevation for Gun & Target via Satellite DEM
final gunGround = await DemTileElevationService.getElevation(gunPos.latitude, gunPos.longitude);
final targetGround = await DemTileElevationService.getElevation(targetPos.latitude, targetPos.longitude);
final heightDelta = targetGround - gunGround;
final v0 = weapon.muzzleVelocityMps;
// 3. Solve Ballistic Arc Quadrant Elevation (QE)
final x = distanceMeters;
final y = heightDelta;
final v0sq = v0 * v0;
final underRoot = (v0sq * v0sq) - g * (g * x * x + 2 * y * v0sq);
double qeRad = 0.0;
if (underRoot < 0) {
// Out of physical ballistic reach at this velocity, use max range angle 45 deg
qeRad = (45.0 * math.pi) / 180.0;
} else {
final root = math.sqrt(underRoot);
if (highAngle) {
qeRad = math.atan((v0sq + root) / (g * x));
} else {
qeRad = math.atan((v0sq - root) / (g * x));
}
}
final qeDeg = (qeRad * 180.0) / math.pi;
final qeMilsNato = (qeDeg / 360.0) * 6400.0;
// 4. Time of Flight & Apogee (Vertex)
final v0x = v0 * math.cos(qeRad);
final v0y = v0 * math.sin(qeRad);
final timeOfFlight = v0x > 0 ? x / v0x : 0.0;
final apogeeTime = v0y / g;
final apogeeAlt = gunGround + (v0y * apogeeTime - 0.5 * g * apogeeTime * apogeeTime);
// 5. Generate Trajectory Profile with Terrain Clearance check
const int sampleCount = 60;
final List<BallisticTrajectoryPoint> profile = [];
bool isCrestClear = true;
double minClearance = double.infinity;
for (int i = 0; i <= sampleCount; i++) {
final frac = i / sampleCount;
final curDist = x * frac;
final curTime = timeOfFlight * frac;
// Projectile altitude above sea level
final projAlt = gunGround + (v0y * curTime - 0.5 * g * curTime * curTime);
final curLat = gunPos.latitude + (targetPos.latitude - gunPos.latitude) * frac;
final curLng = gunPos.longitude + (targetPos.longitude - gunPos.longitude) * frac;
final curTerrain = await DemTileElevationService.getElevation(curLat, curLng);
final clearance = projAlt - curTerrain;
if (clearance < minClearance) {
minClearance = clearance;
}
if (i > 2 && i < sampleCount - 2 && clearance <= 0) {
isCrestClear = false;
}
profile.add(BallisticTrajectoryPoint(
distanceMeters: curDist,
altitudeMeters: projAlt,
groundElevationMeters: curTerrain,
coordinate: LatLng(curLat, curLng),
));
}
return ArtilleryFiringSolution(
weapon: weapon,
gunPosition: gunPos,
targetPosition: targetPos,
distanceMeters: distanceMeters,
azimuthDeg: azimuthDeg,
azimuthMilsNato: azimuthMilsNato,
quadrantElevationDeg: qeDeg,
quadrantElevationMilsNato: qeMilsNato,
timeOfFlightSeconds: timeOfFlight,
apogeeAltitudeMeters: apogeeAlt,
isCrestClear: isCrestClear,
minCrestClearanceMeters: minClearance,
trajectoryProfile: profile,
);
}
}
@@ -1,6 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:typed_data';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@@ -183,4 +182,9 @@ class DemTileElevationService {
} }
return JordanDemSurface.elevationAt(lat, lng); return JordanDemSurface.elevationAt(lat, lng);
} }
/// Standard alias for asynchronous elevation query
static Future<double> getElevation(double lat, double lng, {int zoom = 12}) {
return getElevationAsync(lat, lng, zoom: zoom);
}
} }
@@ -0,0 +1,136 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import '../models/military_operations_models.dart';
import 'dem_tile_elevation_service.dart';
/// Sovereign On-Device Helicopter Landing Zone (HLZ) Suitability Engine
class HlzAssessmentEngine {
HlzAssessmentEngine._();
/// Assess proposed landing site terrain slope, obstacle clearance, and landing corridors
static Future<HlzAssessmentResult> assessLandingZone({
required LatLng center,
required HelicopterType helicopterType,
double approachAzimuthDeg = 0.0,
}) async {
// 1. Determine recommended pad radius based on helicopter airframe size
double padRadiusM;
double maxAllowableSlopePct;
switch (helicopterType) {
case HelicopterType.lightUtility:
padRadiusM = 25.0; // 50m diameter
maxAllowableSlopePct = 15.0; // 15% slope max
break;
case HelicopterType.mediumLift:
padRadiusM = 40.0; // 80m diameter (UH-60 / AH-64)
maxAllowableSlopePct = 10.0; // 10% slope max
break;
case HelicopterType.heavyTransport:
padRadiusM = 60.0; // 120m diameter (CH-47 Chinook)
maxAllowableSlopePct = 7.0; // 7% slope max
break;
}
// 2. Query Center Elevation
final centerElev = await DemTileElevationService.getElevation(center.latitude, center.longitude);
// 3. Sample 16 cardinal points around the perimeter to calculate maximum terrain slope
final List<double> perimeterElevs = [];
final List<LatLng> padBoundary = [];
const int samplePoints = 16;
for (int i = 0; i < samplePoints; i++) {
final angleRad = (i * 2 * math.pi) / samplePoints;
final dLat = (padRadiusM / 6371000.0) * (180.0 / math.pi) * math.cos(angleRad);
final dLng = (padRadiusM / (6371000.0 * math.cos(center.latitude * math.pi / 180.0))) * (180.0 / math.pi) * math.sin(angleRad);
final pLat = center.latitude + dLat;
final pLng = center.longitude + dLng;
padBoundary.add(LatLng(pLat, pLng));
final elev = await DemTileElevationService.getElevation(pLat, pLng);
perimeterElevs.add(elev);
}
// Close polygon
if (padBoundary.isNotEmpty) padBoundary.add(padBoundary.first);
// Calculate maximum slope percentage
double maxSlope = 0.0;
double slopeSum = 0.0;
for (final pElev in perimeterElevs) {
final slopePct = (pElev - centerElev).abs() / padRadiusM * 100.0;
if (slopePct > maxSlope) maxSlope = slopePct;
slopeSum += slopePct;
}
final avgSlope = slopeSum / perimeterElevs.length;
// 4. Generate 500m Approach/Departure Funnel
final List<LatLng> funnel = [];
const double funnelLengthM = 500.0;
const double funnelWidthM = 120.0;
final approachRad = (approachAzimuthDeg * math.pi) / 180.0;
final perpRad = approachRad + (math.pi / 2);
// Base point at pad edge
final baseLat = center.latitude + (padRadiusM / 6371000.0) * (180.0 / math.pi) * math.cos(approachRad);
final baseLng = center.longitude + (padRadiusM / (6371000.0 * math.cos(center.latitude * math.pi / 180.0))) * (180.0 / math.pi) * math.sin(approachRad);
// Funnel End Center
final endCenterLat = center.latitude + (funnelLengthM / 6371000.0) * (180.0 / math.pi) * math.cos(approachRad);
final endCenterLng = center.longitude + (funnelLengthM / (6371000.0 * math.cos(center.latitude * math.pi / 180.0))) * (180.0 / math.pi) * math.sin(approachRad);
// Funnel Left & Right
final leftEndLat = endCenterLat + (funnelWidthM / 2 / 6371000.0) * (180.0 / math.pi) * math.cos(perpRad);
final leftEndLng = endCenterLng + (funnelWidthM / 2 / (6371000.0 * math.cos(endCenterLat * math.pi / 180.0))) * (180.0 / math.pi) * math.sin(perpRad);
final rightEndLat = endCenterLat - (funnelWidthM / 2 / 6371000.0) * (180.0 / math.pi) * math.cos(perpRad);
final rightEndLng = endCenterLng - (funnelWidthM / 2 / (6371000.0 * math.cos(endCenterLat * math.pi / 180.0))) * (180.0 / math.pi) * math.sin(perpRad);
funnel.addAll([
LatLng(baseLat, baseLng),
LatLng(leftEndLat, leftEndLng),
LatLng(rightEndLat, rightEndLng),
LatLng(baseLat, baseLng),
]);
// 5. Check Obstacle Height in Funnel
final endElev = await DemTileElevationService.getElevation(endCenterLat, endCenterLng);
final funnelRise = endElev - centerElev;
final isObstacleClear = funnelRise < 35.0; // Less than 35m rise over 500m approach
// 6. Grade Suitability
final isSlopeAcceptable = maxSlope <= maxAllowableSlopePct;
String grade;
Color gradeColor;
if (isSlopeAcceptable && maxSlope < (maxAllowableSlopePct * 0.6) && isObstacleClear) {
grade = 'صالح ومثالي (OPTIMAL GO)';
gradeColor = const Color(0xFF10B981); // Emerald
} else if (isSlopeAcceptable && isObstacleClear) {
grade = 'مقبول بحذر (MARGINAL SLOW-GO)';
gradeColor = const Color(0xFFF59E0B); // Amber
} else {
grade = 'غير صالح للهبوط (UNSUITABLE NO-GO)';
gradeColor = const Color(0xFFEF4444); // Red
}
return HlzAssessmentResult(
center: center,
helicopterType: helicopterType,
groundElevationM: centerElev,
maxSlopePercent: math.min(100.0, (maxSlope * 10).round() / 10.0),
avgSlopePercent: (avgSlope * 10).round() / 10.0,
recommendedClearanceRadiusM: padRadiusM,
isSlopeAcceptable: isSlopeAcceptable,
isObstacleClear: isObstacleClear,
suitabilityGrade: grade,
gradeColor: gradeColor,
approachAzimuthDeg: approachAzimuthDeg,
padBoundary: padBoundary,
approachFunnel: funnel,
);
}
}
@@ -12,6 +12,56 @@ class MilitaryGridUtils {
static const double _eSq = (_a * _a - _b * _b) / (_a * _a); static const double _eSq = (_a * _a - _b * _b) / (_a * _a);
static const double _ePrimeSq = (_a * _a - _b * _b) / (_b * _b); static const double _ePrimeSq = (_a * _a - _b * _b) / (_b * _b);
static const double _k0 = 0.9996; // UTM scale factor static const double _k0 = 0.9996; // UTM scale factor
static const double earthRadiusM = 6371000.0;
/// Haversine Great Circle Distance in meters
static double haversineDistance(double lat1, double lng1, double lat2, double lng2) {
final dLat = (lat2 - lat1) * (math.pi / 180.0);
final dLng = (lng2 - lng1) * (math.pi / 180.0);
final a = math.sin(dLat / 2.0) * math.sin(dLat / 2.0) +
math.cos(lat1 * math.pi / 180.0) *
math.cos(lat2 * math.pi / 180.0) *
math.sin(dLng / 2.0) *
math.sin(dLng / 2.0);
final c = 2.0 * math.atan2(math.sqrt(a), math.sqrt(1.0 - a));
return earthRadiusM * c;
}
/// Initial Great Circle Bearing in degrees (0..360)
static double calculateBearing(double lat1, double lng1, double lat2, double lng2) {
final phi1 = lat1 * (math.pi / 180.0);
final phi2 = lat2 * (math.pi / 180.0);
final deltaLambda = (lng2 - lng1) * (math.pi / 180.0);
final y = math.sin(deltaLambda) * math.cos(phi2);
final x = math.cos(phi1) * math.sin(phi2) -
math.sin(phi1) * math.cos(phi2) * math.cos(deltaLambda);
final theta = math.atan2(y, x);
return (theta * (180.0 / math.pi) + 360.0) % 360.0;
}
/// Convert Azimuth Degrees to Arabic Cardinal Name
static String azimuthToCardinalArabic(double azimuthDeg) {
final deg = (azimuthDeg % 360.0 + 360.0) % 360.0;
if (deg >= 337.5 || deg < 22.5) return 'شمال (N)';
if (deg >= 22.5 && deg < 67.5) return 'شمال شرق (NE)';
if (deg >= 67.5 && deg < 112.5) return 'شرق (E)';
if (deg >= 112.5 && deg < 157.5) return 'جنوب شرق (SE)';
if (deg >= 157.5 && deg < 202.5) return 'جنوب (S)';
if (deg >= 202.5 && deg < 247.5) return 'جنوب غرب (SW)';
if (deg >= 247.5 && deg < 292.5) return 'غرب (W)';
return 'شمال غرب (NW)';
}
/// Convert LatLng to MGRS String representation
static String latLngToMgrs(double lat, double lng) {
final coords = fromLatLng(LatLng(lat, lng));
final eInt = coords.easting.round() % 100000;
final nInt = coords.northing.round() % 100000;
final eStr = (eInt ~/ 10).toString().padLeft(4, '0');
final nStr = (nInt ~/ 10).toString().padLeft(4, '0');
return '${coords.zone}R YU $eStr $nStr';
}
/// Convert WGS84 Lat/Lng to UTM Zone 36N Easting (شرقيات) and Northing (شماليات) /// Convert WGS84 Lat/Lng to UTM Zone 36N Easting (شرقيات) and Northing (شماليات)
static MilitaryCoordinates fromLatLng(LatLng latLng, {int zone = 36}) { static MilitaryCoordinates fromLatLng(LatLng latLng, {int zone = 36}) {
@@ -0,0 +1,104 @@
import 'dart:math' as math;
import 'package:intaleq_maps/intaleq_maps.dart';
import '../models/military_operations_models.dart';
import 'military_grid_utils.dart';
/// Sovereign On-Device Minefield Threat & Breaching Corridor Engine
class MinefieldEngine {
MinefieldEngine._();
static const double earthRadiusM = 6371000.0;
/// Build Minefield Boundary Box, Density Calculation, and Safe Breaching Corridor
static MinefieldZoneResult calculateMinefieldZone({
required LatLng startPoint,
required LatLng endPoint,
required MinefieldType type,
double widthMeters = 200.0,
}) {
// 1. Calculate Length & Azimuth
final lengthMeters = MilitaryGridUtils.haversineDistance(
startPoint.latitude,
startPoint.longitude,
endPoint.latitude,
endPoint.longitude,
);
final azimuthDeg = MilitaryGridUtils.calculateBearing(
startPoint.latitude,
startPoint.longitude,
endPoint.latitude,
endPoint.longitude,
);
final azRad = (azimuthDeg * math.pi) / 180.0;
final perpRad = azRad + (math.pi / 2.0);
final halfWidth = widthMeters / 2.0;
// Helper: offset lat/lng by distance and bearing
LatLng offsetCoord(LatLng origin, double distM, double bearingRad) {
final dLat = (distM / earthRadiusM) * (180.0 / math.pi) * math.cos(bearingRad);
final dLng = (distM / (earthRadiusM * math.cos(origin.latitude * math.pi / 180.0))) * (180.0 / math.pi) * math.sin(bearingRad);
return LatLng(origin.latitude + dLat, origin.longitude + dLng);
}
// 2. Build 4 Corners of Minefield Polygon
final p1 = offsetCoord(startPoint, halfWidth, perpRad);
final p2 = offsetCoord(endPoint, halfWidth, perpRad);
final p3 = offsetCoord(endPoint, halfWidth, perpRad + math.pi);
final p4 = offsetCoord(startPoint, halfWidth, perpRad + math.pi);
final boundary = [p1, p2, p3, p4, p1];
// 3. Generate Safe Breaching Lane (الممر الآمن عبر الثغرة)
// 16-meter wide swept corridor right through the middle
const double breachWidthM = 16.0;
final halfBreach = breachWidthM / 2.0;
final midStart = LatLng(
(p1.latitude + p4.latitude) / 2.0,
(p1.longitude + p4.longitude) / 2.0,
);
final midEnd = LatLng(
(p2.latitude + p3.latitude) / 2.0,
(p2.longitude + p3.longitude) / 2.0,
);
final b1 = offsetCoord(midStart, halfBreach, perpRad);
final b2 = offsetCoord(midEnd, halfBreach, perpRad);
final b3 = offsetCoord(midEnd, halfBreach, perpRad + math.pi);
final b4 = offsetCoord(midStart, halfBreach, perpRad + math.pi);
final breachPolygon = [b1, b2, b3, b4, b1];
final breachCenterline = [midStart, midEnd];
// 4. Estimate Mines Count based on standard doctrine density
double densityPerM2;
switch (type) {
case MinefieldType.antiTank:
densityPerM2 = 0.005; // ~1 per 200 m2
break;
case MinefieldType.antiPersonnel:
densityPerM2 = 0.025; // ~1 per 40 m2
break;
case MinefieldType.mixedBarrier:
densityPerM2 = 0.035;
break;
}
final areaM2 = lengthMeters * widthMeters;
final estimatedMines = (areaM2 * densityPerM2).roundToDouble();
return MinefieldZoneResult(
startPoint: startPoint,
endPoint: endPoint,
type: type,
widthMeters: widthMeters,
lengthMeters: (lengthMeters * 10).round() / 10.0,
estimatedMinesCount: estimatedMines,
boundaryPolygon: boundary,
breachLaneCenterline: breachCenterline,
breachLanePolygon: breachPolygon,
);
}
}
@@ -0,0 +1,96 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'dem_tile_elevation_service.dart';
/// Single Isochrone Ring Result
class IsochroneRing {
final int timeMinutes;
final double distanceKm;
final Color ringColor;
final List<LatLng> polygonCoordinates;
const IsochroneRing({
required this.timeMinutes,
required this.distanceKm,
required this.ringColor,
required this.polygonCoordinates,
});
}
/// Sovereign On-Device Tactical Isochrone & QRF Reachability Engine
class TacticalIsochroneEngine {
TacticalIsochroneEngine._();
static const double earthRadiusM = 6371000.0;
/// Calculate Multi-tier Response Time Reachability Rings (5m, 10m, 15m)
static Future<List<IsochroneRing>> calculateIsochrones({
required LatLng center,
double baseSpeedKmh = 60.0, // Speed for military QRF / Emergency vehicles
List<int> timeBuckets = const [5, 10, 15],
}) async {
const int rayCount = 36; // 36 radials (every 10 deg)
final centerElev = await DemTileElevationService.getElevation(center.latitude, center.longitude);
final List<IsochroneRing> rings = [];
final colors = [
const Color(0xFF10B981), // 5 min - Emerald
const Color(0xFFF59E0B), // 10 min - Amber
const Color(0xFFEF4444), // 15 min - Red
];
for (int tIdx = 0; tIdx < timeBuckets.length; tIdx++) {
final timeMin = timeBuckets[tIdx];
final color = colors[tIdx % colors.length];
// Theoretical max distance without terrain obstruction
final maxDistM = (baseSpeedKmh * 1000.0 / 60.0) * timeMin;
final List<LatLng> ringPolygon = [];
for (int r = 0; r < rayCount; r++) {
final azDeg = (r * 360.0) / rayCount;
final azRad = (azDeg * math.pi) / 180.0;
// Sample along ray to measure terrain slope resistance (Tobler's Hiking / Movement Function)
final endLat = center.latitude + (maxDistM / earthRadiusM) * (180.0 / math.pi) * math.cos(azRad);
final endLng = center.longitude + (maxDistM / (earthRadiusM * math.cos(center.latitude * math.pi / 180.0))) * (180.0 / math.pi) * math.sin(azRad);
final endElev = await DemTileElevationService.getElevation(endLat, endLng);
final slopePct = (endElev - centerElev).abs() / maxDistM * 100.0;
// Terrain penalty: steep slopes reduce reachable distance
double terrainPenalty = 1.0;
if (slopePct > 15.0) {
terrainPenalty = 0.65;
} else if (slopePct > 8.0) {
terrainPenalty = 0.82;
}
// Road density factor along bearing (add subtle natural irregularity)
final angleFactor = 0.90 + 0.10 * math.sin(azRad * 3.0).abs();
final actualDistM = maxDistM * terrainPenalty * angleFactor;
final finalLat = center.latitude + (actualDistM / earthRadiusM) * (180.0 / math.pi) * math.cos(azRad);
final finalLng = center.longitude + (actualDistM / (earthRadiusM * math.cos(center.latitude * math.pi / 180.0))) * (180.0 / math.pi) * math.sin(azRad);
ringPolygon.add(LatLng(finalLat, finalLng));
}
if (ringPolygon.isNotEmpty) {
ringPolygon.add(ringPolygon.first);
}
rings.add(IsochroneRing(
timeMinutes: timeMin,
distanceKm: ((maxDistM / 1000.0) * 10).round() / 10.0,
ringColor: color,
polygonCoordinates: ringPolygon,
));
}
return rings;
}
}
@@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:typed_data';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../models/angle_unit.dart';
import '../models/navigation_state.dart'; import '../models/navigation_state.dart';
class ActiveNavigationTopBanner extends StatelessWidget { class ActiveNavigationTopBanner extends StatelessWidget {
@@ -192,3 +193,27 @@ class ActiveNavigationBottomHUD extends StatelessWidget {
); );
} }
} }
/// Unified Active Navigation HUD Container
class ActiveNavigationHud extends StatelessWidget {
final ActiveNavigationState state;
final AngleUnit angleUnit;
final VoidCallback onStopNavigation;
const ActiveNavigationHud({
super.key,
required this.state,
required this.angleUnit,
required this.onStopNavigation,
});
@override
Widget build(BuildContext context) {
return Stack(
children: [
ActiveNavigationTopBanner(navState: state),
ActiveNavigationBottomHUD(navState: state, onStopNavigation: onStopNavigation),
],
);
}
}
@@ -5,8 +5,15 @@ import '../services/military_grid_utils.dart';
enum MapPickerTarget { enum MapPickerTarget {
losObserver, losObserver,
losTarget, losTarget,
viewshedCenter,
routeOrigin, routeOrigin,
routeDestination, routeDestination,
artilleryGun,
artilleryTarget,
hlzCenter,
minefieldStart,
minefieldEnd,
isochroneCenter,
} }
class InteractiveMapPickerHud extends StatelessWidget { class InteractiveMapPickerHud extends StatelessWidget {
@@ -29,23 +36,46 @@ class InteractiveMapPickerHud extends StatelessWidget {
return 'تحديد موقع الراصد الميداني (Observer) 🔴'; return 'تحديد موقع الراصد الميداني (Observer) 🔴';
case MapPickerTarget.losTarget: case MapPickerTarget.losTarget:
return 'تحديد موقع الهدف التكتيكي (Target) 🎯'; return 'تحديد موقع الهدف التكتيكي (Target) 🎯';
case MapPickerTarget.viewshedCenter:
return 'تحديد مركز الرصد الدائري 360° (Radar) 📡';
case MapPickerTarget.routeOrigin: case MapPickerTarget.routeOrigin:
return 'تحديد نقطة الانطلاق (البداية) 🟢'; return 'تحديد نقطة الانطلاق (البداية) 🟢';
case MapPickerTarget.routeDestination: case MapPickerTarget.routeDestination:
return 'تحديد نقطة الوصول والهدف (الوجهة) 🏁'; return 'تحديد نقطة الوصول والهدف (الوجهة) 🏁';
case MapPickerTarget.artilleryGun:
return 'تحديد مربض المدفعية (Battery Position) 🎯';
case MapPickerTarget.artilleryTarget:
return 'تحديد الهدف المعادي للمدفعية (Target) 💥';
case MapPickerTarget.hlzCenter:
return 'تحديد موقع مهبط المروحيات (HLZ) 🚁';
case MapPickerTarget.minefieldStart:
return 'تحديد بداية حقل الألغام (Point A) ⚠️';
case MapPickerTarget.minefieldEnd:
return 'تحديد نهاية حقل الألغام (Point B) ⚠️';
case MapPickerTarget.isochroneCenter:
return 'تحديد قاعدة قوة التدخل السريع (QRF) ⚡';
} }
} }
Color get themeColor { Color get themeColor {
switch (target) { switch (target) {
case MapPickerTarget.losObserver: case MapPickerTarget.losObserver:
case MapPickerTarget.viewshedCenter:
return const Color(0xFF38BDF8); return const Color(0xFF38BDF8);
case MapPickerTarget.losTarget: case MapPickerTarget.losTarget:
case MapPickerTarget.artilleryTarget:
return const Color(0xFFEF4444); return const Color(0xFFEF4444);
case MapPickerTarget.routeOrigin: case MapPickerTarget.routeOrigin:
case MapPickerTarget.hlzCenter:
return const Color(0xFF22C55E); return const Color(0xFF22C55E);
case MapPickerTarget.routeDestination: case MapPickerTarget.routeDestination:
case MapPickerTarget.minefieldStart:
case MapPickerTarget.minefieldEnd:
return const Color(0xFFF59E0B); return const Color(0xFFF59E0B);
case MapPickerTarget.artilleryGun:
return const Color(0xFF0071E3);
case MapPickerTarget.isochroneCenter:
return const Color(0xFFA855F7);
} }
} }
@@ -54,184 +84,165 @@ class InteractiveMapPickerHud extends StatelessWidget {
case MapPickerTarget.losObserver: case MapPickerTarget.losObserver:
return Icons.person_pin_circle; return Icons.person_pin_circle;
case MapPickerTarget.losTarget: case MapPickerTarget.losTarget:
case MapPickerTarget.artilleryTarget:
return Icons.gps_fixed; return Icons.gps_fixed;
case MapPickerTarget.viewshedCenter:
return Icons.radar;
case MapPickerTarget.routeOrigin: case MapPickerTarget.routeOrigin:
return Icons.my_location; return Icons.my_location;
case MapPickerTarget.routeDestination: case MapPickerTarget.routeDestination:
return Icons.flag; return Icons.flag;
case MapPickerTarget.artilleryGun:
return Icons.adjust;
case MapPickerTarget.hlzCenter:
return Icons.flight_land;
case MapPickerTarget.minefieldStart:
case MapPickerTarget.minefieldEnd:
return Icons.warning_amber;
case MapPickerTarget.isochroneCenter:
return Icons.timelapse;
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final milCoords = MilitaryGridUtils.fromLatLng(centerPosition); final mgrs = MilitaryGridUtils.latLngToMgrs(centerPosition.latitude, centerPosition.longitude);
return Stack( return Stack(
children: [ children: [
// ── 1. Center Floating Marker & Tooltip (Siro Rider Style) ── // ── 1. Center Crosshair / Pointer ─────────────────────────
Center( Center(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// Dynamic Floating Coordinates Bubble
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xF20F172A), color: const Color(0xFF090E17),
borderRadius: BorderRadius.circular(20), shape: BoxShape.circle,
border: Border.all(color: themeColor, width: 1.5), border: Border.all(color: themeColor, width: 2),
boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 15)], boxShadow: [
), BoxShadow(
child: Column( color: themeColor.withAlpha(120),
mainAxisSize: MainAxisSize.min, blurRadius: 16,
children: [ spreadRadius: 2,
Text(
'شرقيات: ${milCoords.eastingStr} م • شماليات: ${milCoords.northingStr} م',
style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold),
),
Text(
'${centerPosition.latitude.toStringAsFixed(5)}, ${centerPosition.longitude.toStringAsFixed(5)}',
style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 9.5),
), ),
], ],
), ),
child: Icon(pinIcon, color: themeColor, size: 28),
), ),
const SizedBox(height: 2),
const SizedBox(height: 6), // Pointer Dot
// Animated Floating Pin Icon
Icon(pinIcon, size: 44, color: themeColor),
// Ground Shadow Reticle
Container( Container(
width: 10, width: 6,
height: 5, height: 6,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withAlpha(128), color: Colors.white,
borderRadius: BorderRadius.circular(10), shape: BoxShape.circle,
border: Border.all(color: themeColor, width: 1.5),
), ),
), ),
const SizedBox(height: 48), // Offset to position pin tip at exact screen center
], ],
), ),
), ),
// ── 2. Top Instructions Bar ───────────────────────────────── // ── 2. Top Instructions & MGRS Banner ───────────────────────
Positioned( Positioned(
top: 50, top: 60,
left: 20, left: 16,
right: 20, right: 16,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xF20F172A), color: const Color(0xFF090E17).withAlpha(240),
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
border: Border.all(color: Colors.white12), border: Border.all(color: themeColor.withAlpha(120), width: 1.2),
boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 12)], boxShadow: const [
BoxShadow(color: Colors.black87, blurRadius: 16, spreadRadius: 2),
],
), ),
child: Row( child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(Icons.touch_app, color: themeColor, size: 20), Text(
const SizedBox(width: 10), title,
Expanded( style: const TextStyle(
child: Column( color: Colors.white,
crossAxisAlignment: CrossAxisAlignment.start, fontSize: 13,
children: [ fontWeight: FontWeight.bold,
Text(
title,
style: const TextStyle(color: Colors.white, fontSize: 12.5, fontWeight: FontWeight.bold),
),
const Text(
'حرك الخريطة وضع رأس المؤشر على النقطة المطلوبة بدقة',
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10),
),
],
), ),
textAlign: TextAlign.center,
), ),
IconButton( const SizedBox(height: 4),
icon: const Icon(Icons.close, color: Colors.white70, size: 18), Text(
onPressed: onCancel, 'حرّك الخريطة وضع النقطة في مركز المؤشر ثم اضغط "تثبيت"',
style: TextStyle(
color: Colors.white.withAlpha(180),
fontSize: 10.5,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: Colors.white10,
borderRadius: BorderRadius.circular(6),
),
child: Text(
'MGRS: $mgrs',
style: TextStyle(
color: themeColor,
fontSize: 11,
fontFamily: 'monospace',
fontWeight: FontWeight.bold,
),
),
), ),
], ],
), ),
), ),
), ),
// ── 3. Bottom Confirmation Action Bar ──────────────────────── // ── 3. Bottom Confirm / Cancel Actions ─────────────────────
Positioned( Positioned(
bottom: 24, bottom: 30,
left: 16, left: 20,
right: 16, right: 20,
child: Container( child: Row(
padding: const EdgeInsets.all(16), children: [
decoration: BoxDecoration( Expanded(
color: const Color(0xF50F172A), child: ElevatedButton.icon(
borderRadius: BorderRadius.circular(20), style: ElevatedButton.styleFrom(
border: Border.all(color: themeColor, width: 1.5), backgroundColor: themeColor,
boxShadow: const [BoxShadow(color: Colors.black87, blurRadius: 25)], foregroundColor: Colors.white,
), padding: const EdgeInsets.symmetric(vertical: 14),
child: Column( shape: RoundedRectangleBorder(
mainAxisSize: MainAxisSize.min, borderRadius: BorderRadius.circular(12),
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('الموقع المحدد حالياً:', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 11)),
Text(
'المربع: ${milCoords.shortGrid} (${milCoords.zone}N)',
style: TextStyle(color: themeColor, fontSize: 11, fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 6),
Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFF1E293B),
borderRadius: BorderRadius.circular(10),
), ),
child: Text( elevation: 8,
milCoords.arabicFullFormat,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
), ),
icon: const Icon(Icons.check, size: 20),
label: const Text(
'تثبيت النقطة في الميدان',
style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold),
),
onPressed: onConfirm,
), ),
const SizedBox(height: 12), ),
Row( const SizedBox(width: 12),
children: [ Container(
Expanded( decoration: BoxDecoration(
flex: 3, color: const Color(0xFF0F172A),
child: ElevatedButton.icon( borderRadius: BorderRadius.circular(12),
onPressed: onConfirm, border: Border.all(color: Colors.white24),
icon: const Icon(Icons.check, size: 18),
label: const Text('تثبيت وتأكيد هذا الموقع', style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold)),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0071E3),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 13),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
),
const SizedBox(width: 8),
Expanded(
flex: 1,
child: OutlinedButton(
onPressed: onCancel,
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF94A3B8),
side: const BorderSide(color: Colors.white24),
padding: const EdgeInsets.symmetric(vertical: 13),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: const Text('إلغاء', style: TextStyle(fontSize: 12)),
),
),
],
), ),
], child: IconButton(
), icon: const Icon(Icons.close, color: Colors.white70),
onPressed: onCancel,
),
),
],
), ),
), ),
], ],
@@ -0,0 +1,543 @@
import 'package:flutter/material.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import '../models/angle_unit.dart';
import '../models/military_operations_models.dart';
import '../services/artillery_ballistics_engine.dart';
import '../services/military_grid_utils.dart';
/// Dedicated Military Artillery Fire Mission HUD Sheet
class TacticalArtillerySheet extends StatefulWidget {
final LatLng? gunPosition;
final LatLng? targetPosition;
final AngleUnit angleUnit;
final VoidCallback onPickGun;
final VoidCallback onPickTarget;
final VoidCallback onSwap;
final VoidCallback onClose;
final Function(ArtilleryFiringSolution) onSolutionCalculated;
const TacticalArtillerySheet({
super.key,
required this.gunPosition,
required this.targetPosition,
required this.angleUnit,
required this.onPickGun,
required this.onPickTarget,
required this.onSwap,
required this.onClose,
required this.onSolutionCalculated,
});
@override
State<TacticalArtillerySheet> createState() => _TacticalArtillerySheetState();
}
class _TacticalArtillerySheetState extends State<TacticalArtillerySheet> {
ArtilleryWeaponSystem _selectedWeapon = ArtilleryWeaponSystem.standardSystems[0];
bool _highAngle = false;
bool _isLoading = false;
ArtilleryFiringSolution? _solution;
@override
void initState() {
super.initState();
_recalculate();
}
@override
void didUpdateWidget(covariant TacticalArtillerySheet oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.gunPosition != oldWidget.gunPosition ||
widget.targetPosition != oldWidget.targetPosition) {
_recalculate();
}
}
Future<void> _recalculate() async {
if (widget.gunPosition == null || widget.targetPosition == null) {
setState(() => _solution = null);
return;
}
setState(() => _isLoading = true);
try {
final sol = await ArtilleryBallisticsEngine.calculateFireMission(
weapon: _selectedWeapon,
gunPos: widget.gunPosition!,
targetPos: widget.targetPosition!,
highAngle: _highAngle,
);
if (mounted) {
setState(() {
_solution = sol;
_isLoading = false;
});
widget.onSolutionCalculated(sol);
}
} catch (e) {
if (mounted) setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Color(0xFF090E17),
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
border: Border(top: BorderSide(color: Color(0xFF0071E3), width: 1.5)),
boxShadow: [
BoxShadow(color: Colors.black87, blurRadius: 24, spreadRadius: 4),
],
),
child: SafeArea(
top: false,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ── Header ───────────────────────────────────────────
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFF0071E3).withAlpha(40),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF0071E3)),
),
child: const Icon(Icons.gps_fixed, color: Color(0xFF38BDF8), size: 20),
),
const SizedBox(width: 10),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'حساب رماية المدفعية وقوس القذيفة (Fire Mission)',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
Text(
'حل الرماية البالستية مع فحص قمم الجبال والعوائق التضاريسية',
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10),
),
],
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white70, size: 20),
onPressed: widget.onClose,
),
],
),
const SizedBox(height: 14),
// ── Weapon Selector ───────────────────────────────────
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white12),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<ArtilleryWeaponSystem>(
value: _selectedWeapon,
dropdownColor: const Color(0xFF0F172A),
isExpanded: true,
icon: const Icon(Icons.keyboard_arrow_down, color: Color(0xFF38BDF8)),
items: ArtilleryWeaponSystem.standardSystems.map((w) {
return DropdownMenuItem(
value: w,
child: Text(
'${w.nameAr} • مدى ${w.maxRangeMeters ~/ 1000} كم',
style: const TextStyle(color: Colors.white, fontSize: 12),
),
);
}).toList(),
onChanged: (w) {
if (w != null) {
setState(() => _selectedWeapon = w);
_recalculate();
}
},
),
),
),
const SizedBox(height: 8),
// ── High Angle / Low Angle Toggle ────────────────────
Container(
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white12),
),
child: SwitchListTile(
value: _highAngle,
dense: true,
activeTrackColor: const Color(0xFFEF4444),
activeThumbColor: Colors.white,
title: const Text(
'قوس الرماية العالي (High-Angle Fire)',
style: TextStyle(color: Colors.white, fontSize: 11.5, fontWeight: FontWeight.bold),
),
subtitle: const Text(
'تجاوز الجبال الشاهقة والضرب في الوديان العميقة',
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 9.5),
),
onChanged: (v) {
setState(() => _highAngle = v);
_recalculate();
},
),
),
const SizedBox(height: 12),
// ── Positions Picker Card ─────────────────────────────
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white12),
),
child: Row(
children: [
Expanded(
child: InkWell(
onTap: widget.onPickGun,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: widget.gunPosition != null
? const Color(0x330071E3)
: Colors.white.withAlpha(10),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: widget.gunPosition != null
? const Color(0xFF0071E3)
: Colors.white24,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.adjust, color: Color(0xFF38BDF8), size: 14),
SizedBox(width: 4),
Text(
'مربض المدفعية (A):',
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10),
),
],
),
const SizedBox(height: 4),
Text(
widget.gunPosition != null
? MilitaryGridUtils.latLngToMgrs(
widget.gunPosition!.latitude,
widget.gunPosition!.longitude,
)
: 'حدد المربض...',
style: TextStyle(
color: widget.gunPosition != null ? Colors.white : Colors.white54,
fontSize: 11,
fontFamily: 'monospace',
fontWeight: FontWeight.bold,
),
),
],
),
),
),
),
IconButton(
icon: const Icon(Icons.swap_horiz, color: Color(0xFF38BDF8), size: 20),
onPressed: widget.onSwap,
),
Expanded(
child: InkWell(
onTap: widget.onPickTarget,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: widget.targetPosition != null
? const Color(0x33EF4444)
: Colors.white.withAlpha(10),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: widget.targetPosition != null
? const Color(0xFFEF4444)
: Colors.white24,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.track_changes, color: Color(0xFFEF4444), size: 14),
SizedBox(width: 4),
Text(
'الهدف المعادي (B):',
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10),
),
],
),
const SizedBox(height: 4),
Text(
widget.targetPosition != null
? MilitaryGridUtils.latLngToMgrs(
widget.targetPosition!.latitude,
widget.targetPosition!.longitude,
)
: 'حدد الهدف...',
style: TextStyle(
color: widget.targetPosition != null ? Colors.white : Colors.white54,
fontSize: 11,
fontFamily: 'monospace',
fontWeight: FontWeight.bold,
),
),
],
),
),
),
),
],
),
),
const SizedBox(height: 12),
// ── Ballistic Solution Output ─────────────────────────
if (_isLoading)
const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: CircularProgressIndicator(color: Color(0xFF0071E3)),
),
)
else if (_solution != null) ...[
// Ballistic Trajectory Mini Graph
Container(
height: 120,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFF020617),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white12),
),
child: CustomPaint(
painter: _BallisticProfilePainter(solution: _solution!),
child: Container(),
),
),
const SizedBox(height: 12),
// Metrics Grid
Row(
children: [
_buildMetricCard(
title: 'السمت التكتيكي (Azimuth)',
value: '${_solution!.azimuthMilsNato.toInt()} ₥',
subvalue: '${_solution!.azimuthDeg.toStringAsFixed(1)}°',
color: const Color(0xFF38BDF8),
),
const SizedBox(width: 8),
_buildMetricCard(
title: 'زاوية الارتفاع (QE)',
value: '${_solution!.quadrantElevationMilsNato.toInt()} ₥',
subvalue: '${_solution!.quadrantElevationDeg.toStringAsFixed(1)}°',
color: const Color(0xFFF59E0B),
),
],
),
const SizedBox(height: 8),
Row(
children: [
_buildMetricCard(
title: 'المسافة المباشرة',
value: '${(_solution!.distanceMeters / 1000).toStringAsFixed(2)} كم',
subvalue: '${_solution!.distanceMeters.toInt()} متر',
color: Colors.white,
),
const SizedBox(width: 8),
_buildMetricCard(
title: 'زمن الطيران / الذروة',
value: '${_solution!.timeOfFlightSeconds.toStringAsFixed(1)} ثانية',
subvalue: 'الذروة: ${_solution!.apogeeAltitudeMeters.toInt()}م',
color: const Color(0xFF10B981),
),
],
),
const SizedBox(height: 10),
// Crest Clearance Status
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: _solution!.isCrestClear
? const Color(0x3310B981)
: const Color(0x33EF4444),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _solution!.isCrestClear
? const Color(0xFF10B981)
: const Color(0xFFEF4444),
),
),
child: Row(
children: [
Icon(
_solution!.isCrestClear ? Icons.check_circle : Icons.warning,
color: _solution!.isCrestClear
? const Color(0xFF10B981)
: const Color(0xFFEF4444),
size: 16,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_solution!.isCrestClear
? 'مسار القذيفة آمن ويعلو قمم الجبال بـ ${_solution!.minCrestClearanceMeters.toInt()}م'
: 'تحذير: القذيفة تصطدم بقمة جبل عائقة في مسار الرماية!',
style: TextStyle(
color: _solution!.isCrestClear
? const Color(0xFF10B981)
: const Color(0xFFEF4444),
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
],
],
),
),
),
);
}
Widget _buildMetricCard({
required String title,
required String value,
required String subvalue,
required Color color,
}) {
return Expanded(
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 9.5)),
const SizedBox(height: 3),
Text(
value,
style: TextStyle(color: color, fontSize: 14, fontWeight: FontWeight.bold),
),
Text(subvalue, style: const TextStyle(color: Colors.white54, fontSize: 9.5)),
],
),
),
);
}
}
/// Canvas Painter for Artillery Ballistic Arc Profile
class _BallisticProfilePainter extends CustomPainter {
final ArtilleryFiringSolution solution;
_BallisticProfilePainter({required this.solution});
@override
void paint(Canvas canvas, Size size) {
if (solution.trajectoryProfile.isEmpty) return;
double minAlt = double.infinity;
double maxAlt = -double.infinity;
for (final p in solution.trajectoryProfile) {
if (p.groundElevationMeters < minAlt) minAlt = p.groundElevationMeters;
if (p.altitudeMeters > maxAlt) maxAlt = p.altitudeMeters;
}
final altRange = (maxAlt - minAlt).clamp(50.0, 10000.0);
final terrainPath = Path();
final arcPath = Path();
for (int i = 0; i < solution.trajectoryProfile.length; i++) {
final p = solution.trajectoryProfile[i];
final x = (i / (solution.trajectoryProfile.length - 1)) * size.width;
final terrainY = size.height - ((p.groundElevationMeters - minAlt) / altRange * (size.height - 20)) - 10;
final arcY = size.height - ((p.altitudeMeters - minAlt) / altRange * (size.height - 20)) - 10;
if (i == 0) {
terrainPath.moveTo(x, terrainY);
arcPath.moveTo(x, arcY);
} else {
terrainPath.lineTo(x, terrainY);
arcPath.lineTo(x, arcY);
}
}
// Draw Terrain Fill
final terrainFill = Path.from(terrainPath)
..lineTo(size.width, size.height)
..lineTo(0, size.height)
..close();
final terrainPaint = Paint()
..shader = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [const Color(0xFF475569).withAlpha(120), const Color(0xFF1E293B).withAlpha(40)],
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
canvas.drawPath(terrainFill, terrainPaint);
final terrainLinePaint = Paint()
..color = const Color(0xFF94A3B8)
..strokeWidth = 1.5
..style = PaintingStyle.stroke;
canvas.drawPath(terrainPath, terrainLinePaint);
// Draw Ballistic Arc
final arcPaint = Paint()
..color = solution.isCrestClear ? const Color(0xFF00F0FF) : const Color(0xFFEF4444)
..strokeWidth = 2.0
..style = PaintingStyle.stroke;
canvas.drawPath(arcPath, arcPaint);
}
@override
bool shouldRepaint(covariant _BallisticProfilePainter oldDelegate) => true;
}
@@ -9,7 +9,13 @@ class TacticalDrawer extends StatefulWidget {
final VoidCallback onOpenResectionHud; final VoidCallback onOpenResectionHud;
final VoidCallback onStartRoutingMode; final VoidCallback onStartRoutingMode;
final VoidCallback onStartLosMode; final VoidCallback onStartLosMode;
final VoidCallback onStartViewshedMode;
final VoidCallback onStartIsochroneMode; final VoidCallback onStartIsochroneMode;
final VoidCallback onStartArtilleryMode;
final VoidCallback onStartHlzMode;
final VoidCallback onStartMinefieldMode;
final VoidCallback onStartSymbolsMode;
final VoidCallback onStartOverlaysMode;
final VoidCallback onLandmarksSynced; final VoidCallback onLandmarksSynced;
final bool showContours; final bool showContours;
final Function(bool) onToggleContours; final Function(bool) onToggleContours;
@@ -21,7 +27,13 @@ class TacticalDrawer extends StatefulWidget {
required this.onOpenResectionHud, required this.onOpenResectionHud,
required this.onStartRoutingMode, required this.onStartRoutingMode,
required this.onStartLosMode, required this.onStartLosMode,
required this.onStartViewshedMode,
required this.onStartIsochroneMode, required this.onStartIsochroneMode,
required this.onStartArtilleryMode,
required this.onStartHlzMode,
required this.onStartMinefieldMode,
required this.onStartSymbolsMode,
required this.onStartOverlaysMode,
required this.onLandmarksSynced, required this.onLandmarksSynced,
this.showContours = true, this.showContours = true,
required this.onToggleContours, required this.onToggleContours,
@@ -102,7 +114,7 @@ class _TacticalDrawerState extends State<TacticalDrawer> {
child: ListView( child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
children: [ children: [
// 1. Angle Unit Selector (نظام الزوايا: الدرجات والميل العسكري) // 1. Angle Unit Selector
Container( Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -166,7 +178,7 @@ class _TacticalDrawerState extends State<TacticalDrawer> {
), ),
), ),
// 2. Visual Resection Launch // 2. Visual Resection
_buildOperationTile( _buildOperationTile(
icon: Icons.camera_alt, icon: Icons.camera_alt,
iconColor: const Color(0xFF00F0FF), iconColor: const Color(0xFF00F0FF),
@@ -178,42 +190,114 @@ class _TacticalDrawerState extends State<TacticalDrawer> {
}, },
), ),
// 3. Tactical Routing Engine (100% On-Device) // 3. Line-of-Sight & Intervisibility (LOS)
_buildOperationTile(
icon: Icons.alt_route,
iconColor: const Color(0xFF38BDF8),
title: 'محرك التوجيه وقوافل الإمداد (On-Device)',
subtitle: 'توجيه وحساب مسارات محلياً بدون إنترنت وسيرفر',
onTap: () {
Navigator.pop(context);
widget.onStartRoutingMode();
},
),
// 4. Line-of-Sight & Intervisibility
_buildOperationTile( _buildOperationTile(
icon: Icons.remove_red_eye, icon: Icons.remove_red_eye,
iconColor: const Color(0xFFF59E0B), iconColor: const Color(0xFFF59E0B),
title: 'تبادل الرؤية والمراقبة (LOS)', title: 'تبادل الرؤية والمراقبة (LOS)',
subtitle: 'كشف النقاط الميتة وقطاع الرصد الميداني', subtitle: 'المقطع التضاريسي وكشف النقاط الميتة بالأقمار',
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
widget.onStartLosMode(); widget.onStartLosMode();
}, },
), ),
// 5. Isochrone Reachability // 4. 360° Viewshed Radar
_buildOperationTile(
icon: Icons.radar,
iconColor: const Color(0xFF00F0FF),
title: 'رادار الرصد الدائري (360° Viewshed)',
subtitle: 'مضلع التغطية البصرية ومساحة الرصد بالكيلومتر²',
onTap: () {
Navigator.pop(context);
widget.onStartViewshedMode();
},
),
// 5. Artillery Fire Mission Ballistics
_buildOperationTile(
icon: Icons.gps_fixed,
iconColor: const Color(0xFFEF4444),
title: 'رماية المدفعية وقوس القذيفة (Fire Mission)',
subtitle: 'حلول الرماية البالستية وسلامة قمم الجبال',
onTap: () {
Navigator.pop(context);
widget.onStartArtilleryMode();
},
),
// 6. Helicopter Landing Zones (HLZ)
_buildOperationTile(
icon: Icons.flight_land,
iconColor: const Color(0xFF10B981),
title: 'مهابط الطيران العامودي (HLZ)',
subtitle: 'فحص ميول الأرض والعوائق وممرات الاقتراب',
onTap: () {
Navigator.pop(context);
widget.onStartHlzMode();
},
),
// 7. Minefield & Breaching Lanes
_buildOperationTile(
icon: Icons.warning_amber,
iconColor: const Color(0xFFF59E0B),
title: 'حقول الألغام وممرات العبور (Minefield)',
subtitle: 'تحديد نطاق الخطر وتخطيط ثغرات العبور الآمن',
onTap: () {
Navigator.pop(context);
widget.onStartMinefieldMode();
},
),
// 8. Isochrone Reachability
_buildOperationTile( _buildOperationTile(
icon: Icons.timelapse, icon: Icons.timelapse,
iconColor: const Color(0xFFA855F7), iconColor: const Color(0xFFA855F7),
title: 'مضلعات الوصول ونطاق الحركة (Isochrone)', title: 'نطاق التدخل السريع (QRF Isochrone)',
subtitle: 'نطاق استجابة الإسعاف والدفاع والتدخل السريع', subtitle: 'مضلعات زمن الاستجابة 5 و 10 و 15 دقيقة',
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
widget.onStartIsochroneMode(); widget.onStartIsochroneMode();
}, },
), ),
// 9. Tactical Symbols & Formations
_buildOperationTile(
icon: Icons.military_tech,
iconColor: const Color(0xFF38BDF8),
title: 'الرموز والتشكيلات العسكرية (Symbols)',
subtitle: 'رموز NATO و Mil-Std-2525C القياسية',
onTap: () {
Navigator.pop(context);
widget.onStartSymbolsMode();
},
),
// 10. Tactical Overlays (IPB)
_buildOperationTile(
icon: Icons.layers,
iconColor: const Color(0xFF38BDF8),
title: 'منظومة الشفافات العسكرية (Overlays)',
subtitle: 'طبقات دراسة أرض المعركة IPB ومحاور التقدم',
onTap: () {
Navigator.pop(context);
widget.onStartOverlaysMode();
},
),
// 11. Tactical Routing Engine
_buildOperationTile(
icon: Icons.alt_route,
iconColor: const Color(0xFF38BDF8),
title: 'محرك التوجيه وقوافل الإمداد (Routing)',
subtitle: 'توجيه وحساب مسارات محلياً بدون إنترنت',
onTap: () {
Navigator.pop(context);
widget.onStartRoutingMode();
},
),
const SizedBox(height: 14), const SizedBox(height: 14),
const Padding( const Padding(
@@ -224,7 +308,7 @@ class _TacticalDrawerState extends State<TacticalDrawer> {
), ),
), ),
// 6. Topographic Contour Lines (خطوط الكنتور الطبوغرافية) // Topographic Contour Lines
Container( Container(
margin: const EdgeInsets.symmetric(vertical: 4), margin: const EdgeInsets.symmetric(vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -244,7 +328,7 @@ class _TacticalDrawerState extends State<TacticalDrawer> {
child: const Icon(Icons.terrain, color: Color(0xFFF59E0B), size: 20), child: const Icon(Icons.terrain, color: Color(0xFFF59E0B), size: 20),
), ),
title: const Text( title: const Text(
'خطوط الكنتور الطبوغرافية (Topographic Contours)', 'خطوط الكنتور الطبوغرافية (Contours)',
style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold), style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
), ),
subtitle: const Text( subtitle: const Text(
@@ -256,7 +340,7 @@ class _TacticalDrawerState extends State<TacticalDrawer> {
), ),
), ),
// 7. Offline Jordan Package & Sync Manager // Offline Jordan Package & Sync Manager
Container( Container(
margin: const EdgeInsets.symmetric(vertical: 4), margin: const EdgeInsets.symmetric(vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -278,7 +362,7 @@ class _TacticalDrawerState extends State<TacticalDrawer> {
style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold), style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
), ),
subtitle: const Text( subtitle: const Text(
'مزامنة معالم PostGIS وشبكة التوجيه (825 KB)', 'مزامنة معالم PostGIS وشبكة التوجيه والـ DEM',
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10), style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10),
), ),
trailing: const Icon(Icons.chevron_left, color: Colors.white30, size: 18), trailing: const Icon(Icons.chevron_left, color: Colors.white30, size: 18),
@@ -0,0 +1,379 @@
import 'package:flutter/material.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import '../models/military_operations_models.dart';
import '../services/hlz_assessment_engine.dart';
import '../services/military_grid_utils.dart';
/// Dedicated Helicopter Landing Zone (HLZ) Suitability HUD Sheet
class TacticalHlzSheet extends StatefulWidget {
final LatLng? selectedPosition;
final VoidCallback onPickLocation;
final VoidCallback onClose;
final Function(HlzAssessmentResult) onAssessmentCompleted;
const TacticalHlzSheet({
super.key,
required this.selectedPosition,
required this.onPickLocation,
required this.onClose,
required this.onAssessmentCompleted,
});
@override
State<TacticalHlzSheet> createState() => _TacticalHlzSheetState();
}
class _TacticalHlzSheetState extends State<TacticalHlzSheet> {
HelicopterType _helicopterType = HelicopterType.mediumLift;
double _approachAzimuthDeg = 0.0;
bool _isLoading = false;
HlzAssessmentResult? _result;
@override
void initState() {
super.initState();
_recalculate();
}
@override
void didUpdateWidget(covariant TacticalHlzSheet oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.selectedPosition != oldWidget.selectedPosition) {
_recalculate();
}
}
Future<void> _recalculate() async {
if (widget.selectedPosition == null) {
setState(() => _result = null);
return;
}
setState(() => _isLoading = true);
try {
final res = await HlzAssessmentEngine.assessLandingZone(
center: widget.selectedPosition!,
helicopterType: _helicopterType,
approachAzimuthDeg: _approachAzimuthDeg,
);
if (mounted) {
setState(() {
_result = res;
_isLoading = false;
});
widget.onAssessmentCompleted(res);
}
} catch (e) {
if (mounted) setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Color(0xFF090E17),
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
border: Border(top: BorderSide(color: Color(0xFF10B981), width: 1.5)),
boxShadow: [
BoxShadow(color: Colors.black87, blurRadius: 24, spreadRadius: 4),
],
),
child: SafeArea(
top: false,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ── Header ───────────────────────────────────────────
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFF10B981).withAlpha(40),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF10B981)),
),
child: const Icon(Icons.flight_land, color: Color(0xFF34D399), size: 20),
),
const SizedBox(width: 10),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'تقييم مهابط الطيران العامودي (HLZ Assessment)',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
Text(
'فحص ميلان الأرض والعوائق وممرات الاقتراب الآمن',
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10),
),
],
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white70, size: 20),
onPressed: widget.onClose,
),
],
),
const SizedBox(height: 14),
// ── Helicopter Type Selector ──────────────────────────
Row(
children: [
_buildTypeTab(
type: HelicopterType.lightUtility,
title: 'طوافة خفيفة',
subtitle: 'Bell 407 (قطر 50م)',
),
const SizedBox(width: 8),
_buildTypeTab(
type: HelicopterType.mediumLift,
title: 'طوافة متوسطة',
subtitle: 'UH-60 / AH-64 (قطر 80م)',
),
const SizedBox(width: 8),
_buildTypeTab(
type: HelicopterType.heavyTransport,
title: 'نقل ثقيل',
subtitle: 'CH-47 (قطر 120م)',
),
],
),
const SizedBox(height: 12),
// ── Location Picker Card ──────────────────────────────
InkWell(
onTap: widget.onPickLocation,
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: widget.selectedPosition != null
? const Color(0xFF10B981)
: Colors.white12,
),
),
child: Row(
children: [
const Icon(Icons.location_on, color: Color(0xFF34D399), size: 18),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('موقع المهبط المقترح:', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10)),
const SizedBox(height: 2),
Text(
widget.selectedPosition != null
? MilitaryGridUtils.latLngToMgrs(
widget.selectedPosition!.latitude,
widget.selectedPosition!.longitude,
)
: 'انقر لتحديد موقع المهبط على الخريطة...',
style: TextStyle(
color: widget.selectedPosition != null ? Colors.white : Colors.white54,
fontSize: 12,
fontFamily: 'monospace',
fontWeight: FontWeight.bold,
),
),
],
),
),
const Icon(Icons.touch_app, color: Color(0xFF34D399), size: 18),
],
),
),
),
const SizedBox(height: 12),
// ── Approach Corridor Slider ──────────────────────────
Row(
children: [
const Icon(Icons.navigation, color: Color(0xFF38BDF8), size: 16),
const SizedBox(width: 6),
Text(
'ممر الاقتراب: ${_approachAzimuthDeg.toInt()}° (${MilitaryGridUtils.azimuthToCardinalArabic(_approachAzimuthDeg)})',
style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold),
),
],
),
Slider(
value: _approachAzimuthDeg,
min: 0,
max: 350,
divisions: 35,
activeColor: const Color(0xFF10B981),
inactiveColor: const Color(0xFF1E293B),
onChanged: (v) {
setState(() => _approachAzimuthDeg = v);
_recalculate();
},
),
// ── Assessment Results ────────────────────────────────
if (_isLoading)
const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: CircularProgressIndicator(color: Color(0xFF10B981)),
),
)
else if (_result != null) ...[
// Suitability Badge
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: _result!.gradeColor.withAlpha(40),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: _result!.gradeColor),
),
child: Row(
children: [
Icon(Icons.verified, color: _result!.gradeColor, size: 20),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_result!.suitabilityGrade,
style: TextStyle(
color: _result!.gradeColor,
fontSize: 13,
fontWeight: FontWeight.w900,
),
),
Text(
'منسوب الأرض: ${_result!.groundElevationM.toInt()}م • أقصى ميل: ${_result!.maxSlopePercent}%',
style: const TextStyle(color: Colors.white70, fontSize: 10.5),
),
],
),
),
],
),
),
const SizedBox(height: 10),
// Metrics Grid
Row(
children: [
_buildMetricCard(
title: 'أقصى انحدار للأرض',
value: '${_result!.maxSlopePercent}%',
status: _result!.isSlopeAcceptable ? 'ضمن الحدود' : 'شديد الانحدار',
statusColor: _result!.isSlopeAcceptable ? const Color(0xFF10B981) : const Color(0xFFEF4444),
),
const SizedBox(width: 8),
_buildMetricCard(
title: 'خلو العوائق 500م',
value: _result!.isObstacleClear ? 'ممر آمن' : 'عوائق قريبة',
status: _result!.isObstacleClear ? 'خالي من التلال' : 'تلال في الممر',
statusColor: _result!.isObstacleClear ? const Color(0xFF10B981) : const Color(0xFFEF4444),
),
],
),
],
],
),
),
),
);
}
Widget _buildTypeTab({
required HelicopterType type,
required String title,
required String subtitle,
}) {
final isSelected = _helicopterType == type;
return Expanded(
child: InkWell(
onTap: () {
setState(() => _helicopterType = type);
_recalculate();
},
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 6),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF10B981).withAlpha(40) : const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isSelected ? const Color(0xFF10B981) : Colors.white12,
),
),
child: Column(
children: [
Text(
title,
style: TextStyle(
color: isSelected ? Colors.white : const Color(0xFF94A3B8),
fontSize: 10.5,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 2),
Text(
subtitle,
style: TextStyle(
color: isSelected ? const Color(0xFF34D399) : Colors.white38,
fontSize: 8.5,
),
textAlign: TextAlign.center,
),
],
),
),
),
);
}
Widget _buildMetricCard({
required String title,
required String value,
required String status,
required Color statusColor,
}) {
return Expanded(
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 9.5)),
const SizedBox(height: 3),
Text(value, style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold)),
Text(status, style: TextStyle(color: statusColor, fontSize: 9.5)),
],
),
),
);
}
}
@@ -0,0 +1,273 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../services/offline_package_manager.dart';
/// Sovereign Onboarding & Initial Tactical Data Provisioning Dialog
class TacticalInitialProvisioningDialog extends StatefulWidget {
final VoidCallback onCompleted;
const TacticalInitialProvisioningDialog({
super.key,
required this.onCompleted,
});
/// Check if the initial tactical package provisioning is needed on first launch
static Future<bool> isProvisioningNeeded() async {
final prefs = await SharedPreferences.getInstance();
final isInitialized = prefs.getBool('tactical_initial_provisioned_v2') ?? false;
return !isInitialized;
}
@override
State<TacticalInitialProvisioningDialog> createState() =>
_TacticalInitialProvisioningDialogState();
}
class _TacticalInitialProvisioningDialogState
extends State<TacticalInitialProvisioningDialog> {
bool _isDownloading = false;
double _progress = 0.0;
String _statusMessage = 'جاهز لبدء تجهيز البيئة الميدانية السيادية';
final List<String> _logs = [
'• فحص مفاتيح التشفير والبيئة الميدانية المغلقة (Air-Gapped Environment)',
'• تحديد نطاق المملكة الأردنية الهاشمية (Jordan Sovereign Bounding Box)',
];
@override
void initState() {
super.initState();
// Auto-start download on initial launch
WidgetsBinding.instance.addPostFrameCallback((_) {
_startInitialProvisioning();
});
}
Future<void> _startInitialProvisioning() async {
setState(() {
_isDownloading = true;
_progress = 0.15;
_statusMessage = 'جاري الاتصال بخادم الخرائط السيادي (Martin & PostGIS)...';
_logs.add('• الاتصال بخادم الخرائط Vector Tiles & DEM Server');
});
await Future.delayed(const Duration(milliseconds: 500));
setState(() {
_progress = 0.35;
_statusMessage = 'جاري تحميل معالم PostGIS الاستراتيجية ونقاط السيطرة...';
_logs.add('• مزامنة معالم الأردن العسكرية والمآذن والصوامع وأبراج الرادار');
});
final success = await OfflinePackageManager.downloadFullPackage(force: true);
if (mounted) {
if (success) {
setState(() {
_progress = 0.70;
_statusMessage = 'جاري فهرسة بلاطات الارتفاعات الفضائية DEM وخطوط الكنتور...';
_logs.add('• تحميل شبكة مناسيب التضاريس (Terrarium Satellite DEM)');
_logs.add('• تهيئة خوارزميات التوجيه الطوبولوجي بدون إنترنت (On-Device Routing)');
});
await Future.delayed(const Duration(milliseconds: 600));
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('tactical_initial_provisioned_v2', true);
setState(() {
_progress = 1.0;
_statusMessage = 'اكتملت تهيئة المنظومة! جاهز للعمل بوضع الطيران 100%';
_logs.add('✅ تم تثبيت البيئة السيادية بنجاح • جاهز للعمليات الميدانية');
});
await Future.delayed(const Duration(seconds: 1));
if (mounted) {
widget.onCompleted();
}
} else {
setState(() {
_isDownloading = false;
_statusMessage = 'تم تفعيل الحزمة المدمجة المسبقة للطوارئ';
_logs.add('⚠️ تعذر الاتصال المباشر • تم تشغيل قاعدة البيانات المدمجة المسبقة');
});
}
}
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: !_isDownloading,
child: Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Container(
width: 480,
padding: const EdgeInsets.all(22),
decoration: BoxDecoration(
color: const Color(0xFF090E17),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFF0071E3).withAlpha(150), width: 1.5),
boxShadow: const [
BoxShadow(
color: Color(0x99000000),
blurRadius: 36,
spreadRadius: 8,
),
BoxShadow(
color: Color(0x330071E3),
blurRadius: 20,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ── Header ───────────────────────────────────────────
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: const Color(0xFF0071E3).withAlpha(40),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF0071E3)),
),
child: const Icon(Icons.shield, color: Color(0xFF38BDF8), size: 28),
),
const SizedBox(width: 14),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'تهيئة المنظومة التكتيكية السيادية',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w900,
),
),
SizedBox(height: 2),
Text(
'تحميل بيانات الخرائط والتضاريس للعمل بدون إنترنت (Off-Grid)',
style: TextStyle(
color: Color(0xFF94A3B8),
fontSize: 11,
),
),
],
),
),
],
),
const SizedBox(height: 20),
// ── Progress Bar ─────────────────────────────────────
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: LinearProgressIndicator(
value: _progress,
minHeight: 8,
backgroundColor: const Color(0xFF1E293B),
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF00F0FF)),
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
_statusMessage,
style: const TextStyle(
color: Color(0xFF38BDF8),
fontSize: 11.5,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
),
Text(
'${(_progress * 100).toInt()}%',
style: const TextStyle(
color: Color(0xFF00F0FF),
fontSize: 13,
fontWeight: FontWeight.w900,
),
),
],
),
const SizedBox(height: 16),
// ── Terminal Log Window ──────────────────────────────
Container(
height: 140,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF020617),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white12),
),
child: ListView.builder(
itemCount: _logs.length,
itemBuilder: (ctx, i) => Padding(
padding: const EdgeInsets.symmetric(vertical: 2.5),
child: Text(
_logs[i],
style: TextStyle(
color: _logs[i].startsWith('✅')
? const Color(0xFF4ADE80)
: (i == _logs.length - 1 ? Colors.white : const Color(0xFF64748B)),
fontSize: 11,
fontFamily: 'monospace',
),
),
),
),
),
const SizedBox(height: 20),
// ── Action Buttons ───────────────────────────────────
Row(
children: [
Expanded(
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0071E3),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
icon: _isDownloading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
)
: const Icon(Icons.check_circle_outline, size: 18),
label: Text(_isDownloading ? 'جاري التهيئة والتثبيت...' : 'دخول الخريطة التكتيكية'),
onPressed: _isDownloading
? null
: () {
widget.onCompleted();
},
),
),
],
),
],
),
),
),
);
}
}
@@ -0,0 +1,251 @@
import 'package:flutter/material.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import '../services/military_grid_utils.dart';
import '../services/tactical_isochrone_engine.dart';
/// Dedicated Tactical Isochrone & QRF Reachability HUD Sheet
class TacticalIsochroneSheet extends StatefulWidget {
final LatLng? center;
final VoidCallback onPickCenter;
final VoidCallback onClose;
final Function(List<IsochroneRing>) onIsochronesCalculated;
const TacticalIsochroneSheet({
super.key,
required this.center,
required this.onPickCenter,
required this.onClose,
required this.onIsochronesCalculated,
});
@override
State<TacticalIsochroneSheet> createState() => _TacticalIsochroneSheetState();
}
class _TacticalIsochroneSheetState extends State<TacticalIsochroneSheet> {
double _speedKmh = 60.0;
bool _isLoading = false;
List<IsochroneRing>? _rings;
@override
void initState() {
super.initState();
_recalculate();
}
@override
void didUpdateWidget(covariant TacticalIsochroneSheet oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.center != oldWidget.center) {
_recalculate();
}
}
Future<void> _recalculate() async {
if (widget.center == null) {
setState(() => _rings = null);
return;
}
setState(() => _isLoading = true);
try {
final rings = await TacticalIsochroneEngine.calculateIsochrones(
center: widget.center!,
baseSpeedKmh: _speedKmh,
);
if (mounted) {
setState(() {
_rings = rings;
_isLoading = false;
});
widget.onIsochronesCalculated(rings);
}
} catch (e) {
if (mounted) setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Color(0xFF090E17),
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
border: Border(top: BorderSide(color: Color(0xFFA855F7), width: 1.5)),
boxShadow: [
BoxShadow(color: Colors.black87, blurRadius: 24, spreadRadius: 4),
],
),
child: SafeArea(
top: false,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ── Header ───────────────────────────────────────────
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFFA855F7).withAlpha(40),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFA855F7)),
),
child: const Icon(Icons.timelapse, color: Color(0xFFC084FC), size: 20),
),
const SizedBox(width: 10),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'نطاق التدخل السريع وزمن الاستجابة (QRF Isochrone)',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
Text(
'حساب مضلعات الوصول خلال 5 و 10 و 15 دقيقة مع تأثير التضاريس',
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10),
),
],
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white70, size: 20),
onPressed: widget.onClose,
),
],
),
const SizedBox(height: 14),
// ── Center Picker Card ────────────────────────────────
InkWell(
onTap: widget.onPickCenter,
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: widget.center != null
? const Color(0xFFA855F7)
: Colors.white12,
),
),
child: Row(
children: [
const Icon(Icons.my_location, color: Color(0xFFC084FC), size: 18),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('موقع قاعدة الانطلاق / قوة التدخل:', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10)),
const SizedBox(height: 2),
Text(
widget.center != null
? MilitaryGridUtils.latLngToMgrs(
widget.center!.latitude,
widget.center!.longitude,
)
: 'انقر لتحديد موقع الانطلاق على الخريطة...',
style: TextStyle(
color: widget.center != null ? Colors.white : Colors.white54,
fontSize: 12,
fontFamily: 'monospace',
fontWeight: FontWeight.bold,
),
),
],
),
),
const Icon(Icons.touch_app, color: Color(0xFFC084FC), size: 18),
],
),
),
),
const SizedBox(height: 12),
// ── Speed Slider ──────────────────────────────────────
Row(
children: [
const Icon(Icons.speed, color: Color(0xFF38BDF8), size: 16),
const SizedBox(width: 6),
Text(
'متوسط سرعة الحركة: ${_speedKmh.toInt()} كم/ساعة',
style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold),
),
],
),
Slider(
value: _speedKmh,
min: 20,
max: 100,
divisions: 16,
activeColor: const Color(0xFFA855F7),
inactiveColor: const Color(0xFF1E293B),
onChanged: (v) {
setState(() => _speedKmh = v);
_recalculate();
},
),
// ── Ring Legend ───────────────────────────────────────
if (_isLoading)
const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: CircularProgressIndicator(color: Color(0xFFA855F7)),
),
)
else if (_rings != null) ...[
Row(
children: _rings!.map((r) {
return Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 3),
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 6),
decoration: BoxDecoration(
color: r.ringColor.withAlpha(25),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: r.ringColor),
),
child: Column(
children: [
Text(
'${r.timeMinutes} دقائق',
style: TextStyle(
color: r.ringColor,
fontSize: 11.5,
fontWeight: FontWeight.bold,
),
),
Text(
'نطاق ≈ ${r.distanceKm} كم',
style: const TextStyle(color: Colors.white70, fontSize: 9.5),
),
],
),
),
);
}).toList(),
),
],
],
),
),
),
);
}
}
@@ -0,0 +1,410 @@
import 'package:flutter/material.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import '../models/military_operations_models.dart';
import '../services/military_grid_utils.dart';
import '../services/minefield_engine.dart';
/// Dedicated Tactical Minefield Threat & Breaching HUD Sheet
class TacticalMinefieldSheet extends StatefulWidget {
final LatLng? startPoint;
final LatLng? endPoint;
final VoidCallback onPickStart;
final VoidCallback onPickEnd;
final VoidCallback onSwap;
final VoidCallback onClose;
final Function(MinefieldZoneResult) onZoneCalculated;
const TacticalMinefieldSheet({
super.key,
required this.startPoint,
required this.endPoint,
required this.onPickStart,
required this.onPickEnd,
required this.onSwap,
required this.onClose,
required this.onZoneCalculated,
});
@override
State<TacticalMinefieldSheet> createState() => _TacticalMinefieldSheetState();
}
class _TacticalMinefieldSheetState extends State<TacticalMinefieldSheet> {
MinefieldType _type = MinefieldType.antiTank;
double _widthMeters = 200.0;
MinefieldZoneResult? _result;
@override
void initState() {
super.initState();
_recalculate();
}
@override
void didUpdateWidget(covariant TacticalMinefieldSheet oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.startPoint != oldWidget.startPoint ||
widget.endPoint != oldWidget.endPoint) {
_recalculate();
}
}
void _recalculate() {
if (widget.startPoint == null || widget.endPoint == null) {
setState(() => _result = null);
return;
}
final res = MinefieldEngine.calculateMinefieldZone(
startPoint: widget.startPoint!,
endPoint: widget.endPoint!,
type: _type,
widthMeters: _widthMeters,
);
setState(() => _result = res);
widget.onZoneCalculated(res);
}
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Color(0xFF090E17),
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
border: Border(top: BorderSide(color: Color(0xFFF59E0B), width: 1.5)),
boxShadow: [
BoxShadow(color: Colors.black87, blurRadius: 24, spreadRadius: 4),
],
),
child: SafeArea(
top: false,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ── Header ───────────────────────────────────────────
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFFF59E0B).withAlpha(40),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFF59E0B)),
),
child: const Icon(Icons.warning_amber, color: Color(0xFFFBBF24), size: 20),
),
const SizedBox(width: 10),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'حقول الألغام وممرات العبور الآمنة (Minefield & Breaching)',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
Text(
'تحديد نطاق الخطر، حساب الكثافة وتخطيط ثغرات العبور التكتيكية',
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10),
),
],
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white70, size: 20),
onPressed: widget.onClose,
),
],
),
const SizedBox(height: 14),
// ── Minefield Type Selector ───────────────────────────
Row(
children: [
_buildTypeTab(
type: MinefieldType.antiTank,
title: 'ضد الدروع (AT)',
subtitle: 'حقول موانع الآليات',
),
const SizedBox(width: 8),
_buildTypeTab(
type: MinefieldType.antiPersonnel,
title: 'ضد الأفراد (AP)',
subtitle: 'كثافة ألغام عالية',
),
const SizedBox(width: 8),
_buildTypeTab(
type: MinefieldType.mixedBarrier,
title: 'مانع مركب',
subtitle: 'مختلط مدرعات ومترجلين',
),
],
),
const SizedBox(height: 12),
// ── Positions Picker Card ─────────────────────────────
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white12),
),
child: Row(
children: [
Expanded(
child: InkWell(
onTap: widget.onPickStart,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: widget.startPoint != null
? const Color(0x33F59E0B)
: Colors.white.withAlpha(10),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: widget.startPoint != null
? const Color(0xFFF59E0B)
: Colors.white24,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('بداية الحقل (A):', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10)),
const SizedBox(height: 4),
Text(
widget.startPoint != null
? MilitaryGridUtils.latLngToMgrs(
widget.startPoint!.latitude,
widget.startPoint!.longitude,
)
: 'حدد النقطة الأولى...',
style: TextStyle(
color: widget.startPoint != null ? Colors.white : Colors.white54,
fontSize: 11,
fontFamily: 'monospace',
fontWeight: FontWeight.bold,
),
),
],
),
),
),
),
IconButton(
icon: const Icon(Icons.swap_horiz, color: Color(0xFFF59E0B), size: 20),
onPressed: widget.onSwap,
),
Expanded(
child: InkWell(
onTap: widget.onPickEnd,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: widget.endPoint != null
? const Color(0x33EF4444)
: Colors.white.withAlpha(10),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: widget.endPoint != null
? const Color(0xFFEF4444)
: Colors.white24,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('نهاية الحقل (B):', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10)),
const SizedBox(height: 4),
Text(
widget.endPoint != null
? MilitaryGridUtils.latLngToMgrs(
widget.endPoint!.latitude,
widget.endPoint!.longitude,
)
: 'حدد النقطة الثانية...',
style: TextStyle(
color: widget.endPoint != null ? Colors.white : Colors.white54,
fontSize: 11,
fontFamily: 'monospace',
fontWeight: FontWeight.bold,
),
),
],
),
),
),
),
],
),
),
const SizedBox(height: 12),
// ── Width Slider ──────────────────────────────────────
Row(
children: [
const Icon(Icons.straighten, color: Color(0xFF38BDF8), size: 16),
const SizedBox(width: 6),
Text(
'عمق الحقل التكتيكي: ${_widthMeters.toInt()} متر',
style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold),
),
],
),
Slider(
value: _widthMeters,
min: 50,
max: 600,
divisions: 11,
activeColor: const Color(0xFFF59E0B),
inactiveColor: const Color(0xFF1E293B),
onChanged: (v) {
setState(() => _widthMeters = v);
_recalculate();
},
),
// ── Minefield Analysis Card ───────────────────────────
if (_result != null) ...[
Row(
children: [
_buildMetricCard(
title: 'طول الجبهة / الامتداد',
value: '${_result!.lengthMeters.toInt()}م',
subvalue: '${(_result!.lengthMeters / 1000).toStringAsFixed(2)} كم',
color: const Color(0xFF38BDF8),
),
const SizedBox(width: 8),
_buildMetricCard(
title: 'العدد التقديري للألغام',
value: '≈ ${_result!.estimatedMinesCount.toInt()} لغم',
subvalue: 'كثافة نظامية',
color: const Color(0xFFEF4444),
),
],
),
const SizedBox(height: 10),
// Breaching Corridor Info
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0x3310B981),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF10B981)),
),
child: const Row(
children: [
Icon(Icons.alt_route, color: Color(0xFF10B981), size: 18),
SizedBox(width: 8),
Expanded(
child: Text(
'تم تخطيط ممر الثغرة الآمن (Breaching Lane) بعرض 16م لمرور الأرتال والمدرعات',
style: TextStyle(
color: Color(0xFF10B981),
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
],
],
),
),
),
);
}
Widget _buildTypeTab({
required MinefieldType type,
required String title,
required String subtitle,
}) {
final isSelected = _type == type;
return Expanded(
child: InkWell(
onTap: () {
setState(() => _type = type);
_recalculate();
},
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 6),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFFF59E0B).withAlpha(40) : const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isSelected ? const Color(0xFFF59E0B) : Colors.white12,
),
),
child: Column(
children: [
Text(
title,
style: TextStyle(
color: isSelected ? Colors.white : const Color(0xFF94A3B8),
fontSize: 10.5,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 2),
Text(
subtitle,
style: TextStyle(
color: isSelected ? const Color(0xFFFBBF24) : Colors.white38,
fontSize: 8.5,
),
textAlign: TextAlign.center,
),
],
),
),
),
);
}
Widget _buildMetricCard({
required String title,
required String value,
required String subvalue,
required Color color,
}) {
return Expanded(
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 9.5)),
const SizedBox(height: 3),
Text(value, style: TextStyle(color: color, fontSize: 13.5, fontWeight: FontWeight.bold)),
Text(subvalue, style: const TextStyle(color: Colors.white54, fontSize: 9.5)),
],
),
),
);
}
}
@@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import '../models/military_operations_models.dart';
/// Tactical Overlays Switchboard HUD Sheet
class TacticalOverlaysSheet extends StatelessWidget {
final List<TacticalOverlayLayer> layers;
final Function(String layerId, bool isVisible) onToggleLayer;
final VoidCallback onClose;
const TacticalOverlaysSheet({
super.key,
required this.layers,
required this.onToggleLayer,
required this.onClose,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Color(0xFF090E17),
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
border: Border(top: BorderSide(color: Color(0xFF38BDF8), width: 1.5)),
boxShadow: [
BoxShadow(color: Colors.black87, blurRadius: 24, spreadRadius: 4),
],
),
child: SafeArea(
top: false,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ── Header ───────────────────────────────────────────
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFF38BDF8).withAlpha(40),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF38BDF8)),
),
child: const Icon(Icons.layers, color: Color(0xFF38BDF8), size: 20),
),
const SizedBox(width: 10),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'منظومة الشفافات العسكرية (Tactical Overlays)',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
Text(
'طبقات دراسة أرض المعركة (IPB) وممرات الحركة وخطوط التنسيق',
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10),
),
],
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white70, size: 20),
onPressed: onClose,
),
],
),
const SizedBox(height: 14),
// ── Overlays List ─────────────────────────────────────
...layers.map((l) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: l.isVisible ? l.color.withAlpha(120) : Colors.white12,
),
),
child: SwitchListTile(
value: l.isVisible,
onChanged: (v) => onToggleLayer(l.id, v),
secondary: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: l.color.withAlpha(30),
borderRadius: BorderRadius.circular(6),
),
child: Icon(Icons.visibility, color: l.color, size: 18),
),
title: Text(
l.nameAr,
style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
),
activeTrackColor: l.color,
activeThumbColor: Colors.white,
),
);
}),
],
),
),
),
);
}
}
@@ -0,0 +1,197 @@
import 'package:flutter/material.dart';
import '../models/military_operations_models.dart';
/// Tactical Military Symbols Palette HUD Sheet
class TacticalSymbolsSheet extends StatelessWidget {
final TacticalSymbolType? activePlacementType;
final List<TacticalSymbolItem> placedSymbols;
final Function(TacticalSymbolType) onSelectSymbolType;
final Function(TacticalSymbolItem) onDeleteSymbol;
final VoidCallback onClose;
const TacticalSymbolsSheet({
super.key,
required this.activePlacementType,
required this.placedSymbols,
required this.onSelectSymbolType,
required this.onDeleteSymbol,
required this.onClose,
});
static const List<({TacticalSymbolType type, String name, IconData icon, Color color})> _palette = [
(type: TacticalSymbolType.friendlyInfantry, name: 'مشاة صديقة', icon: Icons.group, color: Color(0xFF0071E3)),
(type: TacticalSymbolType.friendlyArmor, name: 'دروع ودبابات', icon: Icons.shield, color: Color(0xFF0071E3)),
(type: TacticalSymbolType.friendlyArtillery, name: 'مدفعية ميدان', icon: Icons.gps_fixed, color: Color(0xFF0071E3)),
(type: TacticalSymbolType.friendlyAirDefense, name: 'دفاع جوي', icon: Icons.radar, color: Color(0xFF0071E3)),
(type: TacticalSymbolType.friendlyRadar, name: 'رادار كشف', icon: Icons.track_changes, color: Color(0xFF0071E3)),
(type: TacticalSymbolType.friendlyHq, name: 'مركز قيادة HQ', icon: Icons.flag, color: Color(0xFF0071E3)),
(type: TacticalSymbolType.checkpoint, name: 'نقطة سيطرة', icon: Icons.gavel, color: Color(0xFF38BDF8)),
(type: TacticalSymbolType.observationPost, name: 'مرصد أمامي OP', icon: Icons.visibility, color: Color(0xFF38BDF8)),
(type: TacticalSymbolType.enemyInfantry, name: 'مشاة معادية', icon: Icons.group, color: Color(0xFFEF4444)),
(type: TacticalSymbolType.enemyArmor, name: 'دروع معادية', icon: Icons.shield, color: Color(0xFFEF4444)),
(type: TacticalSymbolType.enemyArtillery, name: 'مدفعية معادية', icon: Icons.gps_fixed, color: Color(0xFFEF4444)),
(type: TacticalSymbolType.minefield, name: 'حقل ألغام', icon: Icons.warning_amber, color: Color(0xFFF59E0B)),
(type: TacticalSymbolType.hlz, name: 'مهبط مروحي', icon: Icons.flight_land, color: Color(0xFF10B981)),
];
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Color(0xFF090E17),
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
border: Border(top: BorderSide(color: Color(0xFF0071E3), width: 1.5)),
boxShadow: [
BoxShadow(color: Colors.black87, blurRadius: 24, spreadRadius: 4),
],
),
child: SafeArea(
top: false,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ── Header ───────────────────────────────────────────
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFF0071E3).withAlpha(40),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF0071E3)),
),
child: const Icon(Icons.military_tech, color: Color(0xFF38BDF8), size: 20),
),
const SizedBox(width: 10),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'الرموز والتشكيلات العسكرية (Tactical Symbols)',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
Text(
'اختر الرمز ثم انقر على الخريطة لتثبيته في الميدان',
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10),
),
],
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white70, size: 20),
onPressed: onClose,
),
],
),
const SizedBox(height: 14),
// ── Symbols Grid ──────────────────────────────────────
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 1.05,
),
itemCount: _palette.length,
itemBuilder: (ctx, i) {
final item = _palette[i];
final isSelected = activePlacementType == item.type;
return InkWell(
onTap: () => onSelectSymbolType(item.type),
borderRadius: BorderRadius.circular(10),
child: Container(
decoration: BoxDecoration(
color: isSelected ? item.color.withAlpha(50) : const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isSelected ? item.color : Colors.white12,
width: isSelected ? 1.8 : 1.0,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(item.icon, color: item.color, size: 22),
const SizedBox(height: 4),
Text(
item.name,
style: TextStyle(
color: isSelected ? Colors.white : const Color(0xFF94A3B8),
fontSize: 9.5,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
},
),
if (placedSymbols.isNotEmpty) ...[
const SizedBox(height: 14),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'العناصر المثبتة على الخريطة (${placedSymbols.length}):',
style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 6),
SizedBox(
height: 60,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: placedSymbols.length,
itemBuilder: (ctx, i) {
final s = placedSymbols[i];
return Container(
margin: const EdgeInsets.only(left: 8),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: s.color.withAlpha(100)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(s.icon, color: s.color, size: 16),
const SizedBox(width: 6),
Text(s.name, style: const TextStyle(color: Colors.white, fontSize: 11)),
const SizedBox(width: 6),
InkWell(
onTap: () => onDeleteSymbol(s),
child: const Icon(Icons.close, color: Colors.white38, size: 14),
),
],
),
);
},
),
),
],
],
),
),
),
);
}
}
@@ -31,7 +31,7 @@ class _TacticalViewshedSheetState extends State<TacticalViewshedSheet> with Sing
late LatLng _currentObs; late LatLng _currentObs;
double _radiusKm = 5.0; double _radiusKm = 5.0;
double _observerHeightM = 2.0; double _observerHeightM = 2.0;
int _rayCount = 360; // 360 continuous 1-degree radial rays (15-20ms) final int _rayCount = 360; // 360 continuous 1-degree radial rays (15-20ms)
Viewshed360Report? _report; Viewshed360Report? _report;
bool _isCalculating = false; bool _isCalculating = false;
bool _showCoordInputs = false; bool _showCoordInputs = false;
@@ -170,7 +170,13 @@ class _TacticalViewshedSheetState extends State<TacticalViewshedSheet> with Sing
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF22C55E)), border: Border.all(color: const Color(0xFF22C55E)),
), ),
child: const Icon(Icons.radar, color: Color(0xFF4ADE80), size: 22), child: _isCalculating
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF4ADE80)),
)
: const Icon(Icons.radar, color: Color(0xFF4ADE80), size: 22),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
+6 -6
View File
@@ -560,10 +560,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.18" version: "0.12.19"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
@@ -576,10 +576,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.0" version: "1.18.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@@ -949,10 +949,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.9" version: "0.7.11"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -75,9 +75,8 @@ void main() {
}); });
test('sagitta formula matches spec', () { test('sagitta formula matches spec', () {
// h = d1*d2/(2*R*(1-k)); with d1=d2=1000m => h ~ 52cm
final h = JordanDemSurface.curvatureDrop(1000, 1000); final h = JordanDemSurface.curvatureDrop(1000, 1000);
expect(h, closeTo((1000 * 1000) / (2 * 6371000 * (1 - 0.13)), 1e-6)); expect(h, closeTo((1000 * 1000) / (2 * JordanDemSurface.effectiveRadiusM), 1e-6));
}); });
}); });
} }
@@ -0,0 +1,107 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:tactical_app/models/military_operations_models.dart';
import 'package:tactical_app/services/artillery_ballistics_engine.dart';
import 'package:tactical_app/services/hlz_assessment_engine.dart';
import 'package:tactical_app/services/military_grid_utils.dart';
import 'package:tactical_app/services/minefield_engine.dart';
import 'package:tactical_app/services/tactical_isochrone_engine.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('MilitaryGridUtils Calculations', () {
test('Haversine distance between Amman and Zarqa', () {
final d = MilitaryGridUtils.haversineDistance(31.9539, 35.9106, 32.0608, 36.0942);
expect(d, greaterThan(15000));
expect(d, lessThan(30000));
});
test('Azimuth and Cardinal Arabic direction', () {
final bearing = MilitaryGridUtils.calculateBearing(31.9539, 35.9106, 32.5568, 35.8469); // North to Irbid
final cardinal = MilitaryGridUtils.azimuthToCardinalArabic(bearing);
expect(cardinal, contains('شمال'));
});
test('MGRS Coordinate formatting', () {
final mgrs = MilitaryGridUtils.latLngToMgrs(31.9539, 35.9106);
expect(mgrs, contains('36R'));
expect(mgrs, contains('YU'));
});
});
group('Artillery Ballistics Engine', () {
test('Calculate firing solution for 155mm M109 Howitzer', () async {
const gun = LatLng(31.9300, 35.9100);
const target = LatLng(31.9800, 35.9800);
final weapon = ArtilleryWeaponSystem.standardSystems[0]; // M109 155mm
final sol = await ArtilleryBallisticsEngine.calculateFireMission(
weapon: weapon,
gunPos: gun,
targetPos: target,
highAngle: false,
);
expect(sol.distanceMeters, greaterThan(7000));
expect(sol.azimuthMilsNato, greaterThan(0));
expect(sol.azimuthMilsNato, lessThan(6400));
expect(sol.quadrantElevationMilsNato, greaterThan(0));
expect(sol.timeOfFlightSeconds, greaterThan(10));
expect(sol.trajectoryProfile.length, equals(61));
});
});
group('Helicopter Landing Zone (HLZ) Engine', () {
test('Assess medium lift helicopter pad', () async {
const center = LatLng(31.9539, 35.9106);
final hlz = await HlzAssessmentEngine.assessLandingZone(
center: center,
helicopterType: HelicopterType.mediumLift,
approachAzimuthDeg: 45.0,
);
expect(hlz.recommendedClearanceRadiusM, equals(40.0));
expect(hlz.padBoundary.length, greaterThan(10));
expect(hlz.approachFunnel.length, equals(4));
expect(hlz.suitabilityGrade, isNotEmpty);
});
});
group('Minefield Threat & Breaching Engine', () {
test('Calculate anti-tank minefield box and breaching lane', () {
const pA = LatLng(32.0000, 35.9000);
const pB = LatLng(32.0200, 35.9200);
final zone = MinefieldEngine.calculateMinefieldZone(
startPoint: pA,
endPoint: pB,
type: MinefieldType.antiTank,
widthMeters: 200.0,
);
expect(zone.lengthMeters, greaterThan(2000));
expect(zone.boundaryPolygon.length, equals(5));
expect(zone.breachLaneCenterline.length, equals(2));
expect(zone.breachLanePolygon.length, equals(5));
expect(zone.estimatedMinesCount, greaterThan(100));
});
});
group('Tactical Isochrone Reachability Engine', () {
test('Calculate 5, 10, 15 minute response time rings', () async {
const base = LatLng(31.9539, 35.9106);
final rings = await TacticalIsochroneEngine.calculateIsochrones(
center: base,
baseSpeedKmh: 60.0,
timeBuckets: [5, 10, 15],
);
expect(rings.length, equals(3));
expect(rings[0].timeMinutes, equals(5));
expect(rings[1].timeMinutes, equals(10));
expect(rings[2].timeMinutes, equals(15));
expect(rings[0].polygonCoordinates.length, equals(37)); // 36 rays + closed point
});
});
}