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

This commit is contained in:
Hamza-Ayed
2026-08-22 15:24:09 +03:00
parent c10fd2396c
commit 5e3be672ee
23 changed files with 4096 additions and 1028 deletions
@@ -0,0 +1,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;