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:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user