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:math' as math;
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
@@ -183,4 +182,9 @@ class DemTileElevationService {
}
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 _ePrimeSq = (_a * _a - _b * _b) / (_b * _b);
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 (شماليات)
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:math' as math;
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../models/angle_unit.dart';
import '../models/navigation_state.dart';
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 {
losObserver,
losTarget,
viewshedCenter,
routeOrigin,
routeDestination,
artilleryGun,
artilleryTarget,
hlzCenter,
minefieldStart,
minefieldEnd,
isochroneCenter,
}
class InteractiveMapPickerHud extends StatelessWidget {
@@ -29,23 +36,46 @@ class InteractiveMapPickerHud extends StatelessWidget {
return 'تحديد موقع الراصد الميداني (Observer) 🔴';
case MapPickerTarget.losTarget:
return 'تحديد موقع الهدف التكتيكي (Target) 🎯';
case MapPickerTarget.viewshedCenter:
return 'تحديد مركز الرصد الدائري 360° (Radar) 📡';
case MapPickerTarget.routeOrigin:
return 'تحديد نقطة الانطلاق (البداية) 🟢';
case MapPickerTarget.routeDestination:
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 {
switch (target) {
case MapPickerTarget.losObserver:
case MapPickerTarget.viewshedCenter:
return const Color(0xFF38BDF8);
case MapPickerTarget.losTarget:
case MapPickerTarget.artilleryTarget:
return const Color(0xFFEF4444);
case MapPickerTarget.routeOrigin:
case MapPickerTarget.hlzCenter:
return const Color(0xFF22C55E);
case MapPickerTarget.routeDestination:
case MapPickerTarget.minefieldStart:
case MapPickerTarget.minefieldEnd:
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:
return Icons.person_pin_circle;
case MapPickerTarget.losTarget:
case MapPickerTarget.artilleryTarget:
return Icons.gps_fixed;
case MapPickerTarget.viewshedCenter:
return Icons.radar;
case MapPickerTarget.routeOrigin:
return Icons.my_location;
case MapPickerTarget.routeDestination:
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
Widget build(BuildContext context) {
final milCoords = MilitaryGridUtils.fromLatLng(centerPosition);
final mgrs = MilitaryGridUtils.latLngToMgrs(centerPosition.latitude, centerPosition.longitude);
return Stack(
children: [
// ── 1. Center Floating Marker & Tooltip (Siro Rider Style) ──
// ── 1. Center Crosshair / Pointer ─────────────────────────
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Dynamic Floating Coordinates Bubble
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xF20F172A),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: themeColor, width: 1.5),
boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 15)],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
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),
color: const Color(0xFF090E17),
shape: BoxShape.circle,
border: Border.all(color: themeColor, width: 2),
boxShadow: [
BoxShadow(
color: themeColor.withAlpha(120),
blurRadius: 16,
spreadRadius: 2,
),
],
),
child: Icon(pinIcon, color: themeColor, size: 28),
),
const SizedBox(height: 6),
// Animated Floating Pin Icon
Icon(pinIcon, size: 44, color: themeColor),
// Ground Shadow Reticle
const SizedBox(height: 2),
// Pointer Dot
Container(
width: 10,
height: 5,
width: 6,
height: 6,
decoration: BoxDecoration(
color: Colors.black.withAlpha(128),
borderRadius: BorderRadius.circular(10),
color: Colors.white,
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(
top: 50,
left: 20,
right: 20,
top: 60,
left: 16,
right: 16,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: const Color(0xF20F172A),
color: const Color(0xFF090E17).withAlpha(240),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: Colors.white12),
boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 12)],
border: Border.all(color: themeColor.withAlpha(120), width: 1.2),
boxShadow: const [
BoxShadow(color: Colors.black87, blurRadius: 16, spreadRadius: 2),
],
),
child: Row(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.touch_app, color: themeColor, size: 20),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(color: Colors.white, fontSize: 12.5, fontWeight: FontWeight.bold),
),
const Text(
'حرك الخريطة وضع رأس المؤشر على النقطة المطلوبة بدقة',
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10),
),
],
Text(
title,
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white70, size: 18),
onPressed: onCancel,
const SizedBox(height: 4),
Text(
'حرّك الخريطة وضع النقطة في مركز المؤشر ثم اضغط "تثبيت"',
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(
bottom: 24,
left: 16,
right: 16,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xF50F172A),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: themeColor, width: 1.5),
boxShadow: const [BoxShadow(color: Colors.black87, blurRadius: 25)],
),
child: Column(
mainAxisSize: MainAxisSize.min,
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),
bottom: 30,
left: 20,
right: 20,
child: Row(
children: [
Expanded(
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: themeColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Text(
milCoords.arabicFullFormat,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
elevation: 8,
),
icon: const Icon(Icons.check, size: 20),
label: const Text(
'تثبيت النقطة في الميدان',
style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold),
),
onPressed: onConfirm,
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
flex: 3,
child: ElevatedButton.icon(
onPressed: onConfirm,
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)),
),
),
],
),
const SizedBox(width: 12),
Container(
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white24),
),
],
),
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 onStartRoutingMode;
final VoidCallback onStartLosMode;
final VoidCallback onStartViewshedMode;
final VoidCallback onStartIsochroneMode;
final VoidCallback onStartArtilleryMode;
final VoidCallback onStartHlzMode;
final VoidCallback onStartMinefieldMode;
final VoidCallback onStartSymbolsMode;
final VoidCallback onStartOverlaysMode;
final VoidCallback onLandmarksSynced;
final bool showContours;
final Function(bool) onToggleContours;
@@ -21,7 +27,13 @@ class TacticalDrawer extends StatefulWidget {
required this.onOpenResectionHud,
required this.onStartRoutingMode,
required this.onStartLosMode,
required this.onStartViewshedMode,
required this.onStartIsochroneMode,
required this.onStartArtilleryMode,
required this.onStartHlzMode,
required this.onStartMinefieldMode,
required this.onStartSymbolsMode,
required this.onStartOverlaysMode,
required this.onLandmarksSynced,
this.showContours = true,
required this.onToggleContours,
@@ -102,7 +114,7 @@ class _TacticalDrawerState extends State<TacticalDrawer> {
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
children: [
// 1. Angle Unit Selector (نظام الزوايا: الدرجات والميل العسكري)
// 1. Angle Unit Selector
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
@@ -166,7 +178,7 @@ class _TacticalDrawerState extends State<TacticalDrawer> {
),
),
// 2. Visual Resection Launch
// 2. Visual Resection
_buildOperationTile(
icon: Icons.camera_alt,
iconColor: const Color(0xFF00F0FF),
@@ -178,42 +190,114 @@ class _TacticalDrawerState extends State<TacticalDrawer> {
},
),
// 3. Tactical Routing Engine (100% On-Device)
_buildOperationTile(
icon: Icons.alt_route,
iconColor: const Color(0xFF38BDF8),
title: 'محرك التوجيه وقوافل الإمداد (On-Device)',
subtitle: 'توجيه وحساب مسارات محلياً بدون إنترنت وسيرفر',
onTap: () {
Navigator.pop(context);
widget.onStartRoutingMode();
},
),
// 4. Line-of-Sight & Intervisibility
// 3. Line-of-Sight & Intervisibility (LOS)
_buildOperationTile(
icon: Icons.remove_red_eye,
iconColor: const Color(0xFFF59E0B),
title: 'تبادل الرؤية والمراقبة (LOS)',
subtitle: 'كشف النقاط الميتة وقطاع الرصد الميداني',
subtitle: 'المقطع التضاريسي وكشف النقاط الميتة بالأقمار',
onTap: () {
Navigator.pop(context);
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(
icon: Icons.timelapse,
iconColor: const Color(0xFFA855F7),
title: 'مضلعات الوصول ونطاق الحركة (Isochrone)',
subtitle: 'نطاق استجابة الإسعاف والدفاع والتدخل السريع',
title: 'نطاق التدخل السريع (QRF Isochrone)',
subtitle: 'مضلعات زمن الاستجابة 5 و 10 و 15 دقيقة',
onTap: () {
Navigator.pop(context);
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 Padding(
@@ -224,7 +308,7 @@ class _TacticalDrawerState extends State<TacticalDrawer> {
),
),
// 6. Topographic Contour Lines (خطوط الكنتور الطبوغرافية)
// Topographic Contour Lines
Container(
margin: const EdgeInsets.symmetric(vertical: 4),
decoration: BoxDecoration(
@@ -244,7 +328,7 @@ class _TacticalDrawerState extends State<TacticalDrawer> {
child: const Icon(Icons.terrain, color: Color(0xFFF59E0B), size: 20),
),
title: const Text(
'خطوط الكنتور الطبوغرافية (Topographic Contours)',
'خطوط الكنتور الطبوغرافية (Contours)',
style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
),
subtitle: const Text(
@@ -256,7 +340,7 @@ class _TacticalDrawerState extends State<TacticalDrawer> {
),
),
// 7. Offline Jordan Package & Sync Manager
// Offline Jordan Package & Sync Manager
Container(
margin: const EdgeInsets.symmetric(vertical: 4),
decoration: BoxDecoration(
@@ -278,7 +362,7 @@ class _TacticalDrawerState extends State<TacticalDrawer> {
style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
),
subtitle: const Text(
'مزامنة معالم PostGIS وشبكة التوجيه (825 KB)',
'مزامنة معالم PostGIS وشبكة التوجيه والـ DEM',
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10),
),
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;
double _radiusKm = 5.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;
bool _isCalculating = false;
bool _showCoordInputs = false;
@@ -170,7 +170,13 @@ class _TacticalViewshedSheetState extends State<TacticalViewshedSheet> with Sing
borderRadius: BorderRadius.circular(10),
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),
Expanded(
+6 -6
View File
@@ -560,10 +560,10 @@ packages:
dependency: transitive
description:
name: matcher
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.18"
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
@@ -576,10 +576,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
version: "1.18.0"
mime:
dependency: transitive
description:
@@ -949,10 +949,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
version: "0.7.9"
version: "0.7.11"
typed_data:
dependency: transitive
description:
@@ -75,9 +75,8 @@ void main() {
});
test('sagitta formula matches spec', () {
// h = d1*d2/(2*R*(1-k)); with d1=d2=1000m => h ~ 52cm
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
});
});
}