Files
maps-saas/packages/tactical_app/lib/services/hlz_assessment_engine.dart
T

137 lines
5.6 KiB
Dart

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,
);
}
}