feat(tactical): add interactive location picker, minefield quantities analysis, and organizational feasibility study

This commit is contained in:
Hamza-Ayed
2026-08-18 10:15:15 +03:00
parent 97b8b27ddf
commit 23dde3c8e3
2 changed files with 792 additions and 184 deletions
File diff suppressed because it is too large Load Diff
+111
View File
@@ -465,3 +465,114 @@ export async function calculateRadialViewshed(
visiblePercentage
};
}
export interface MinefieldAnalysisResult {
startLat: number;
startLng: number;
endLat: number;
endLng: number;
frontageMeters: number;
azimuthDegrees: number;
startElevation: number;
endElevation: number;
avgSlopeDegrees: number;
suitabilityScore: number; // 0-100%
suitabilityVerdict: string;
suitabilityVerdictAr: string;
isChokePoint: boolean;
quantities: {
antiTankMines: number;
antiPersonnelMines: number;
fuseSets: number;
warningSigns: number;
rowsCount: number;
depthMeters: number;
estimatedDeploymentHours: number;
};
}
/**
* Tactical Minefield Barrier & Sapper Engineering Analysis
* دراسة جدوى وكميات حقل الألغام الدفاعي وفحص التضاريس
*/
export async function calculateMinefieldAnalysis(
startLat: number,
startLng: number,
endLat: number,
endLng: number,
densityLevel: 'standard' | 'dense' | 'light' = 'standard'
): Promise<MinefieldAnalysisResult> {
const frontageMeters = calculateDistance(startLat, startLng, endLat, endLng);
const azimuthDegrees = calculateAzimuth(startLat, startLng, endLat, endLng);
const startElev = await sampleElevationAt(startLat, startLng);
const endElev = await sampleElevationAt(endLat, endLng);
// Sample intermediate points to determine valley / ridge / choke point profile
const samples = 10;
const elevs: number[] = [];
for (let i = 0; i <= samples; i++) {
const frac = i / samples;
const lat = startLat + (endLat - startLat) * frac;
const lng = startLng + (endLng - startLng) * frac;
const el = await sampleElevationAt(lat, lng);
elevs.push(el);
}
const elevDiff = Math.abs(endElev - startElev);
const avgSlopeRad = frontageMeters > 0 ? Math.atan(elevDiff / frontageMeters) : 0;
const avgSlopeDegrees = Math.round((avgSlopeRad * (180 / Math.PI)) * 10) / 10;
// Check if center is lower than edges (a defile / valley choke point)
const midElev = elevs[Math.floor(samples / 2)];
const isChokePoint = midElev < Math.min(startElev, endElev) - 10 || avgSlopeDegrees <= 10;
const densityFactor = densityLevel === 'dense' ? 1.5 : densityLevel === 'light' ? 0.6 : 1.0;
const atMinesPerMeter = 1.0 * densityFactor;
const apMinesPerMeter = 2.0 * densityFactor;
const antiTankMines = Math.round(frontageMeters * atMinesPerMeter);
const antiPersonnelMines = Math.round(frontageMeters * apMinesPerMeter);
const warningSigns = Math.max(4, Math.round(frontageMeters / 30));
const deploymentHours = Math.round(((frontageMeters / 150) * 2 * densityFactor) * 10) / 10;
let suitabilityScore = 85;
let suitabilityVerdict = 'Highly Effective Defensive Barrier';
let suitabilityVerdictAr = 'موضع دفاعي نموذجي يسد محور اقتراب الدروع بفعالية';
if (avgSlopeDegrees > 20) {
suitabilityScore = 55;
suitabilityVerdict = 'Steep Terrain: Armor movement already restricted by natural slope';
suitabilityVerdictAr = 'انحدار جبلي شديد: التضاريس تشكل مانعاً طبيعياً لحركة الدروع';
} else if (isChokePoint) {
suitabilityScore = 96;
suitabilityVerdict = 'Optimal Choke Point Defile Barrier';
suitabilityVerdictAr = 'ممر ومضيق إجباري مثالي (Choke Point) يمنع التفاف آليات العدو';
}
return {
startLat,
startLng,
endLat,
endLng,
frontageMeters: Math.round(frontageMeters),
azimuthDegrees: Math.round(azimuthDegrees * 10) / 10,
startElevation: Math.round(startElev),
endElevation: Math.round(endElev),
avgSlopeDegrees,
suitabilityScore,
suitabilityVerdict,
suitabilityVerdictAr,
isChokePoint,
quantities: {
antiTankMines,
antiPersonnelMines,
fuseSets: Math.round((antiTankMines + antiPersonnelMines) * 1.1),
warningSigns,
rowsCount: densityLevel === 'dense' ? 4 : 3,
depthMeters: densityLevel === 'dense' ? 75 : 50,
estimatedDeploymentHours: Math.max(1, deploymentHours)
}
};
}