feat(tactical): add comprehensive Terrain Study (دراسة الأرض) with natural/manmade obstacles and OAKOC intelligence report

This commit is contained in:
Hamza-Ayed
2026-08-18 10:42:04 +03:00
parent 91c36d99c9
commit 6bd0bc0ce5
2 changed files with 695 additions and 13 deletions
+422 -13
View File
@@ -23,20 +23,28 @@ import {
MapPin,
Target,
LocateFixed,
Move
Move,
Building,
TreePine,
Printer,
FileText,
Compass,
Check
} from 'lucide-react';
import {
calculateLineOfSight,
calculateRadialViewshed,
calculateMinefieldAnalysis,
calculateTerrainStudy,
LineOfSightResult,
Viewshed360Result,
MinefieldAnalysisResult,
TerrainStudyResult,
calculateDistance,
calculateAzimuth
} from '../utils/elevationService';
type TacticalMode = 'los' | 'viewshed' | 'artillery' | 'minefield' | 'hlz' | 'symbols' | 'study';
type TacticalMode = 'terrain' | 'los' | 'viewshed' | 'artillery' | 'minefield' | 'hlz' | 'symbols' | 'study';
interface TacticalSymbolItem {
id: string;
@@ -67,11 +75,18 @@ export const TacticalDefenseView: React.FC = () => {
const mapContainer = useRef<HTMLDivElement>(null);
const map = useRef<maplibregl.Map | null>(null);
const [mode, setMode] = useState<TacticalMode>('los');
const [mode, setMode] = useState<TacticalMode>('terrain');
const [cursorPos, setCursorPos] = useState({ lat: 31.95, lng: 35.93, elev: 850 });
const [activePlacement, setActivePlacement] = useState<string | null>(null);
// Line of Sight State
// 1. Terrain Tactical Study State (دراسة الأرض)
const [terrainCenter, setTerrainCenter] = useState<[number, number] | null>([31.9539, 35.9106]);
const [terrainRadius, setTerrainRadius] = useState<number>(4000);
const [terrainResult, setTerrainResult] = useState<TerrainStudyResult | null>(null);
const [terrainLoading, setTerrainLoading] = useState<boolean>(false);
const [copiedReport, setCopiedReport] = useState<boolean>(false);
// 2. Line of Sight State
const [losPointA, setLosPointA] = useState<[number, number] | null>([31.9539, 35.9106]);
const [losPointB, setLosPointB] = useState<[number, number] | null>([32.0125, 35.8540]);
const [losObsHeight, setLosObsHeight] = useState<number>(2);
@@ -79,14 +94,14 @@ export const TacticalDefenseView: React.FC = () => {
const [losResult, setLosResult] = useState<LineOfSightResult | null>(null);
const [losLoading, setLosLoading] = useState<boolean>(false);
// Viewshed State
// 3. Viewshed State
const [viewshedCenter, setViewshedCenter] = useState<[number, number] | null>([31.9800, 35.8800]);
const [viewshedRadius, setViewshedRadius] = useState<number>(5000);
const [viewshedHeight, setViewshedHeight] = useState<number>(10);
const [viewshedResult, setViewshedResult] = useState<Viewshed360Result | null>(null);
const [viewshedLoading, setViewshedLoading] = useState<boolean>(false);
// Artillery Fire Mission State
// 4. Artillery Fire Mission State
const [gunPos, setGunPos] = useState<[number, number] | null>([31.9300, 35.9100]);
const [targetPos, setTargetPos] = useState<[number, number] | null>([32.0400, 35.8200]);
const [caliber, setCaliber] = useState<string>('155mm Howitzer (M109)');
@@ -94,19 +109,19 @@ export const TacticalDefenseView: React.FC = () => {
const [artilleryResult, setArtilleryResult] = useState<any | null>(null);
const [artilleryLoading, setArtilleryLoading] = useState<boolean>(false);
// Minefield Analysis State
// 5. Minefield Analysis State
const [mineStart, setMineStart] = useState<[number, number] | null>([31.9700, 35.8600]);
const [mineEnd, setMineEnd] = useState<[number, number] | null>([31.9950, 35.8750]);
const [mineDensity, setMineDensity] = useState<'standard' | 'dense' | 'light'>('standard');
const [mineResult, setMineResult] = useState<MinefieldAnalysisResult | null>(null);
const [mineLoading, setMineLoading] = useState<boolean>(false);
// HLZ State
// 6. HLZ State
const [hlzCenter, setHlzCenter] = useState<[number, number] | null>([31.9600, 35.9000]);
const [hlzResult, setHlzResult] = useState<any | null>(null);
const [hlzLoading, setHlzLoading] = useState<boolean>(false);
// Tactical Symbols State
// 7. Tactical Symbols State
const [symbols, setSymbols] = useState<TacticalSymbolItem[]>(DEFAULT_SYMBOLS);
const [selectedSymbolType, setSelectedSymbolType] = useState<string>('friendly');
const [newSymbolName, setNewSymbolName] = useState<string>('');
@@ -120,6 +135,9 @@ export const TacticalDefenseView: React.FC = () => {
const activePlacementRef = useRef(activePlacement);
activePlacementRef.current = activePlacement;
const terrainCenterRef = useRef(terrainCenter);
terrainCenterRef.current = terrainCenter;
const losPointARef = useRef(losPointA);
losPointARef.current = losPointA;
@@ -203,6 +221,8 @@ export const TacticalDefenseView: React.FC = () => {
// Sync Draggable Markers to Map
useEffect(() => {
if (!map.current) return;
// Terrain Study Center
updateMarker('terrain-c', terrainCenter, '⛰️ مركز دراسة الأرض', '#6366f1', (pos) => setTerrainCenter(pos));
// LOS
updateMarker('los-a', losPointA, '📍 راصد A', '#0284c7', (pos) => setLosPointA(pos));
updateMarker('los-b', losPointB, '🎯 هدف B', '#d97706', (pos) => setLosPointB(pos));
@@ -216,7 +236,7 @@ export const TacticalDefenseView: React.FC = () => {
updateMarker('mine-e', mineEnd, '⛔ نهاية حقل الألغام', '#991b1b', (pos) => setMineEnd(pos));
// HLZ
updateMarker('hlz-c', hlzCenter, '🚁 مركز HLZ', '#059669', (pos) => setHlzCenter(pos));
}, [losPointA, losPointB, viewshedCenter, gunPos, targetPos, mineStart, mineEnd, hlzCenter]);
}, [terrainCenter, losPointA, losPointB, viewshedCenter, gunPos, targetPos, mineStart, mineEnd, hlzCenter]);
// Map Initialization
useEffect(() => {
@@ -256,6 +276,11 @@ export const TacticalDefenseView: React.FC = () => {
const currentMode = modeRef.current;
// 1. Explicit Active Placement from Buttons
if (placement === 'terrain-center') {
setTerrainCenter([clickedLat, clickedLng]);
setActivePlacement(null);
return;
}
if (placement === 'los-a') {
setLosPointA([clickedLat, clickedLng]);
setActivePlacement(null);
@@ -298,7 +323,9 @@ export const TacticalDefenseView: React.FC = () => {
}
// 2. Default Context-Aware Click by Active Mode
if (currentMode === 'los') {
if (currentMode === 'terrain') {
setTerrainCenter([clickedLat, clickedLng]);
} else if (currentMode === 'los') {
if (!losPointARef.current) {
setLosPointA([clickedLat, clickedLng]);
} else if (!losPointBRef.current) {
@@ -352,6 +379,33 @@ export const TacticalDefenseView: React.FC = () => {
});
initialMap.on('load', () => {
// 0. Terrain Study Circle Layer
initialMap.addSource('tactical-terrain-src', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] }
});
initialMap.addLayer({
id: 'tactical-terrain-fill',
type: 'fill',
source: 'tactical-terrain-src',
paint: {
'fill-color': '#6366f1',
'fill-opacity': 0.15
}
});
initialMap.addLayer({
id: 'tactical-terrain-line',
type: 'line',
source: 'tactical-terrain-src',
paint: {
'line-color': '#818cf8',
'line-width': 2.5,
'line-dasharray': [3, 2]
}
});
// 1. Line of Sight Layer
initialMap.addSource('tactical-los-src', {
type: 'geojson',
@@ -493,6 +547,50 @@ export const TacticalDefenseView: React.FC = () => {
};
}, []);
// Run Terrain Tactical Study
const runTerrainStudy = () => {
if (!terrainCenter) return;
setTerrainLoading(true);
calculateTerrainStudy(terrainCenter[0], terrainCenter[1], terrainRadius)
.then((res) => {
setTerrainResult(res);
setTerrainLoading(false);
// Draw sector boundary polygon on map
if (map.current && map.current.getSource('tactical-terrain-src')) {
const points = 48;
const coords: [number, number][] = [];
const R_earth = 6371000;
for (let i = 0; i <= points; i++) {
const angle = (i / points) * 2 * Math.PI;
const dLat = (terrainRadius / R_earth) * (180 / Math.PI) * Math.cos(angle);
const dLng = (terrainRadius / (R_earth * Math.cos((terrainCenter[0] * Math.PI) / 180))) * (180 / Math.PI) * Math.sin(angle);
coords.push([terrainCenter[1] + dLng, terrainCenter[0] + dLat]);
}
(map.current.getSource('tactical-terrain-src') as maplibregl.GeoJSONSource).setData({
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: {},
geometry: {
type: 'Polygon',
coordinates: [coords]
}
}
]
});
}
})
.catch(() => setTerrainLoading(false));
};
// Auto-run initial terrain study on mount
useEffect(() => {
runTerrainStudy();
}, [terrainCenter, terrainRadius]);
// Update Line of Sight Calculations & Map Layer
useEffect(() => {
if (!losPointA || !losPointB) {
@@ -700,10 +798,10 @@ export const TacticalDefenseView: React.FC = () => {
essential: true
});
// Dynamically re-anchor operational points into this region so the user is not stuck with old coordinates
const lat = sector.lat;
const lng = sector.lng;
setTerrainCenter([Number(lat.toFixed(5)), Number(lng.toFixed(5))]);
setLosPointA([Number((lat - 0.015).toFixed(5)), Number((lng - 0.015).toFixed(5))]);
setLosPointB([Number((lat + 0.025).toFixed(5)), Number((lng + 0.025).toFixed(5))]);
setViewshedCenter([Number(lat.toFixed(5)), Number(lng.toFixed(5))]);
@@ -714,6 +812,38 @@ export const TacticalDefenseView: React.FC = () => {
setHlzCenter([Number(lat.toFixed(5)), Number(lng.toFixed(5))]);
};
// Copy Intelligence Dossier to Clipboard
const handleCopyReport = () => {
if (!terrainResult) return;
const text = `
=== تقرير الاستخبارات الجغرافية ودراسة الأرض التكتيكية ===
القطاع: ${terrainResult.sectorName}
الإحداثيات: ${terrainResult.centerLat}, ${terrainResult.centerLng}
نصف القطر: ${terrainResult.radiusMeters / 1000} كم | المساحة: ${terrainResult.areaKm2} كم²
طبيعة الأرض: ${terrainResult.terrainClassificationAr}
حالة الحركة والمناورة: ${terrainResult.mobilityStatusAr}
المناسيب: أعلى قمة ${terrainResult.maxElevation}م | أخفض نقطة ${terrainResult.minElevation}م | الفارق التضاريسي ${terrainResult.reliefMeters}م
الانحدار: متوسط ${terrainResult.avgSlopeDegrees}° | أقصى انحدار ${terrainResult.maxSlopeDegrees}°
[الموانع الطبيعية]:
${terrainResult.naturalObstacles.map(o => `- ${o.name}: ${o.description}`).join('\n')}
[الموانع الصناعية والبنية التحتية]:
${terrainResult.manMadeObstacles.map(m => `- ${m.name}: ${m.description} (${m.tacticalNote})`).join('\n')}
[المناطق المأهولة والعمران]:
- الكثافة: ${terrainResult.urbanAndDemographics.densityAr}
- التجمعات: ${terrainResult.urbanAndDemographics.settlements.join('، ')}
- بيئة قتال المباني: ${terrainResult.urbanAndDemographics.moutComplexity}
[التوصيات التكتيكية]:
${terrainResult.tacticalRecommendations.map((r, i) => `${i + 1}. ${r}`).join('\n')}
`;
navigator.clipboard.writeText(text);
setCopiedReport(true);
setTimeout(() => setCopiedReport(false), 2500);
};
return (
<div style={{
width: '100vw',
@@ -767,6 +897,7 @@ export const TacticalDefenseView: React.FC = () => {
{/* Tactical Modes Selector */}
<div style={{ display: 'flex', gap: 4, background: 'rgba(255, 255, 255, 0.05)', padding: 3, borderRadius: 10, border: '1px solid rgba(255,255,255,0.1)' }}>
{[
{ id: 'terrain', label: 'دراسة الأرض', icon: <Mountain size={13} /> },
{ id: 'los', label: 'تبادل الرؤية (LOS)', icon: <Eye size={13} /> },
{ id: 'viewshed', label: 'كشف 360°', icon: <Radio size={13} /> },
{ id: 'artillery', label: 'موقع المدفعية', icon: <Flame size={13} /> },
@@ -853,7 +984,7 @@ export const TacticalDefenseView: React.FC = () => {
{/* Left Side Tactical Control Deck */}
<aside style={{
width: 410,
width: 440,
background: 'rgba(15, 23, 42, 0.96)',
backdropFilter: 'blur(20px)',
borderLeft: '1px solid rgba(255, 255, 255, 0.12)',
@@ -909,7 +1040,273 @@ export const TacticalDefenseView: React.FC = () => {
<span>يمكنك <strong>سحب أي علامة مباشرة بالفأرة</strong> على الخريطة لتغيير موقعها وحساب النتائج فوراً.</span>
</div>
{/* ========================================================================= */}
{/* MODE 0: TERRAIN TACTICAL STUDY (دراسة الأرض والموانع والعمران) */}
{/* ========================================================================= */}
{mode === 'terrain' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div style={{ paddingBottom: 8, borderBottom: '1px solid rgba(255,255,255,0.08)', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<h3 style={{ margin: 0, fontSize: '1.05rem', fontWeight: 800, color: '#818cf8', display: 'flex', alignItems: 'center', gap: 8 }}>
<Mountain size={18} /> دراسة الأرض والاستخبارات الجغرافية
</h3>
<p style={{ margin: '4px 0 0 0', fontSize: '0.75rem', color: '#94a3b8' }}>
تحليل طبيعة الأرض، الموانع الطبيعية والصناعية، والمناطق المأهولة
</p>
</div>
{terrainResult && (
<button
onClick={handleCopyReport}
style={{
background: copiedReport ? 'rgba(34, 197, 94, 0.2)' : 'rgba(255,255,255,0.08)',
border: copiedReport ? '1px solid #22c55e' : '1px solid rgba(255,255,255,0.15)',
color: copiedReport ? '#4ade80' : '#cbd5e1',
padding: '4px 8px',
borderRadius: 6,
fontSize: '0.7rem',
fontWeight: 700,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 4
}}
>
{copiedReport ? <Check size={12} /> : <FileText size={12} />}
<span>{copiedReport ? 'تم نسخ التقرير' : 'نسخ التقرير'}</span>
</button>
)}
</div>
{/* Terrain Center Coordinate Card */}
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8, border: '1px solid rgba(99, 102, 241, 0.25)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
<span style={{ fontSize: '0.78rem', fontWeight: 800, color: '#a5b4fc' }}>📍 مركز قطاع دراسة الأرض:</span>
<button
onClick={() => setActivePlacement('terrain-center')}
style={{
background: activePlacement === 'terrain-center' ? '#4f46e5' : 'rgba(99, 102, 241, 0.15)',
border: '1px solid #6366f1',
color: '#ffffff',
padding: '3px 8px',
borderRadius: 6,
fontSize: '0.7rem',
fontWeight: 700,
cursor: 'pointer'
}}
>
{activePlacement === 'terrain-center' ? '🎯 انقر على الخريطة...' : '📍 التقاط من الخريطة'}
</button>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
<div>
<label style={{ fontSize: '0.65rem', color: '#94a3b8' }}>خط العرض (Lat):</label>
<input
type="number"
step="0.0001"
value={terrainCenter ? terrainCenter[0] : ''}
onChange={e => setTerrainCenter([Number(e.target.value), terrainCenter ? terrainCenter[1] : 35.9])}
style={{ width: '100%', background: '#0f172a', border: '1px solid rgba(255,255,255,0.2)', color: '#fff', padding: '5px 8px', borderRadius: 6, fontSize: '0.78rem' }}
/>
</div>
<div>
<label style={{ fontSize: '0.65rem', color: '#94a3b8' }}>خط الطول (Lng):</label>
<input
type="number"
step="0.0001"
value={terrainCenter ? terrainCenter[1] : ''}
onChange={e => setTerrainCenter([terrainCenter ? terrainCenter[0] : 31.9, Number(e.target.value)])}
style={{ width: '100%', background: '#0f172a', border: '1px solid rgba(255,255,255,0.2)', color: '#fff', padding: '5px 8px', borderRadius: 6, fontSize: '0.78rem' }}
/>
</div>
</div>
</div>
{/* Radius Slider */}
<div>
<label style={{ fontSize: '0.72rem', color: '#94a3b8', display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<span>نطاق دراسة القطاع:</span>
<strong style={{ color: '#a5b4fc' }}>{(terrainRadius / 1000).toFixed(1)} كم (المساحة: {(Math.PI * Math.pow(terrainRadius / 1000, 2)).toFixed(1)} كم²)</strong>
</label>
<input
type="range"
min={1000}
max={12000}
step={500}
value={terrainRadius}
onChange={e => setTerrainRadius(Number(e.target.value))}
style={{ width: '100%' }}
/>
</div>
<button
onClick={runTerrainStudy}
disabled={terrainLoading}
style={{
background: 'linear-gradient(135deg, #4f46e5, #6366f1)',
border: 'none',
color: '#fff',
padding: '9px',
borderRadius: 8,
fontSize: '0.82rem',
fontWeight: 700,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 6
}}
>
<Mountain size={15} />
<span>{terrainLoading ? 'جاري تحليل التضاريس والموانع...' : 'تحديث دراسة الأرض والتقرير التكتيكي'}</span>
</button>
{/* Terrain Intelligence Dossier Output */}
{terrainResult && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, marginTop: 4 }}>
{/* Top Mobility & Classification Badge */}
<div style={{
background: terrainResult.mobilityStatus === 'GO'
? 'rgba(34, 197, 94, 0.15)'
: terrainResult.mobilityStatus === 'SLOW-GO'
? 'rgba(234, 179, 8, 0.15)'
: 'rgba(239, 68, 68, 0.15)',
border: terrainResult.mobilityStatus === 'GO'
? '1px solid #22c55e'
: terrainResult.mobilityStatus === 'SLOW-GO'
? '1px solid #eab308'
: '1px solid #ef4444',
padding: '10px 12px',
borderRadius: 8,
}}>
<div style={{ fontSize: '0.72rem', color: '#94a3b8', marginBottom: 2 }}>{terrainResult.sectorName}</div>
<div style={{ fontSize: '0.92rem', fontWeight: 800, color: '#f8fafc', marginBottom: 4 }}>
{terrainResult.terrainClassificationAr}
</div>
<div style={{
fontSize: '0.78rem',
fontWeight: 700,
color: terrainResult.mobilityStatus === 'GO' ? '#4ade80' : terrainResult.mobilityStatus === 'SLOW-GO' ? '#fde047' : '#f87171'
}}>
⚡ {terrainResult.mobilityStatusAr}
</div>
</div>
{/* Elevation & Slope Metrics Grid */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 6 }}>
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6, textAlign: 'center' }}>
<div style={{ fontSize: '0.62rem', color: '#94a3b8' }}>أعلى قمة</div>
<div style={{ fontSize: '0.9rem', fontWeight: 800, color: '#38bdf8' }}>{terrainResult.maxElevation}م</div>
</div>
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6, textAlign: 'center' }}>
<div style={{ fontSize: '0.62rem', color: '#94a3b8' }}>أخفض نقطة</div>
<div style={{ fontSize: '0.9rem', fontWeight: 800, color: '#38bdf8' }}>{terrainResult.minElevation}م</div>
</div>
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6, textAlign: 'center' }}>
<div style={{ fontSize: '0.62rem', color: '#94a3b8' }}>الفارق التضاريسي</div>
<div style={{ fontSize: '0.9rem', fontWeight: 800, color: '#fb923c' }}>{terrainResult.reliefMeters}م</div>
</div>
</div>
{/* Slope Distribution Bar */}
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8 }}>
<div style={{ fontSize: '0.72rem', fontWeight: 700, color: '#cbd5e1', marginBottom: 6, display: 'flex', justifyContent: 'space-between' }}>
<span>توزيع درجات الانحدار (Slope Analysis):</span>
<span style={{ color: '#94a3b8' }}>متوسط: {terrainResult.avgSlopeDegrees}°</span>
</div>
<div style={{ height: 10, width: '100%', display: 'flex', borderRadius: 5, overflow: 'hidden', marginBottom: 6 }}>
<div style={{ width: `${terrainResult.slopeDistribution.flatPercentage}%`, background: '#22c55e' }} title={`منبسط: ${terrainResult.slopeDistribution.flatPercentage}%`} />
<div style={{ width: `${terrainResult.slopeDistribution.moderatePercentage}%`, background: '#eab308' }} title={`متوسط: ${terrainResult.slopeDistribution.moderatePercentage}%`} />
<div style={{ width: `${terrainResult.slopeDistribution.steepPercentage}%`, background: '#ef4444' }} title={`شديد الوعورة: ${terrainResult.slopeDistribution.steepPercentage}%`} />
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.68rem', color: '#94a3b8' }}>
<span style={{ color: '#4ade80' }}>■ منبسط (&lt;5°): {terrainResult.slopeDistribution.flatPercentage}%</span>
<span style={{ color: '#facc15' }}>■ متوسط (5-15°): {terrainResult.slopeDistribution.moderatePercentage}%</span>
<span style={{ color: '#f87171' }}>■ وعر (&gt;15°): {terrainResult.slopeDistribution.steepPercentage}%</span>
</div>
</div>
{/* SECTION 1: NATURAL OBSTACLES */}
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8, border: '1px solid rgba(239, 68, 68, 0.2)' }}>
<div style={{ fontSize: '0.78rem', fontWeight: 800, color: '#f87171', marginBottom: 6, display: 'flex', alignItems: 'center', gap: 6 }}>
<AlertOctagon size={14} /> 1. الموانع الطبيعية (Natural Obstacles):
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{terrainResult.naturalObstacles.map((obs, idx) => (
<div key={idx} style={{ background: 'rgba(0,0,0,0.2)', padding: '6px 8px', borderRadius: 6, fontSize: '0.72rem' }}>
<div style={{ fontWeight: 700, color: '#fca5a5' }}>• {obs.name} ({obs.type})</div>
<div style={{ color: '#cbd5e1', marginTop: 2 }}>{obs.description}</div>
<div style={{ color: '#94a3b8', fontSize: '0.68rem', marginTop: 2 }}><strong>الأثر التكتيكي:</strong> {obs.impact}</div>
</div>
))}
</div>
</div>
{/* SECTION 2: MAN-MADE OBSTACLES & INFRASTRUCTURE */}
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8, border: '1px solid rgba(245, 158, 11, 0.2)' }}>
<div style={{ fontSize: '0.78rem', fontWeight: 800, color: '#f59e0b', marginBottom: 6, display: 'flex', alignItems: 'center', gap: 6 }}>
<Building size={14} /> 2. الموانع الصناعية والبنية التحتية (Man-Made Features):
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{terrainResult.manMadeObstacles.map((obs, idx) => (
<div key={idx} style={{ background: 'rgba(0,0,0,0.2)', padding: '6px 8px', borderRadius: 6, fontSize: '0.72rem' }}>
<div style={{ fontWeight: 700, color: '#fde047' }}>• {obs.name}</div>
<div style={{ color: '#cbd5e1', marginTop: 2 }}>{obs.description}</div>
<div style={{ color: '#94a3b8', fontSize: '0.68rem', marginTop: 2 }}><strong>ملاحظة تكتيكية:</strong> {obs.tacticalNote}</div>
</div>
))}
</div>
</div>
{/* SECTION 3: URBAN AREAS & DEMOGRAPHICS */}
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8, border: '1px solid rgba(56, 189, 248, 0.2)' }}>
<div style={{ fontSize: '0.78rem', fontWeight: 800, color: '#38bdf8', marginBottom: 6, display: 'flex', alignItems: 'center', gap: 6 }}>
<Layers size={14} /> 3. المناطق المأهولة بالسكان والعمران:
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: '0.72rem', color: '#cbd5e1' }}>
<div><strong>الكثافة السكانية:</strong> {terrainResult.urbanAndDemographics.densityAr}</div>
<div><strong>التجمعات والقرى الرئيسية:</strong> {terrainResult.urbanAndDemographics.settlements.join('، ')}</div>
<div><strong>قتال المناطق المبنية (MOUT):</strong> {terrainResult.urbanAndDemographics.moutComplexity}</div>
<div><strong>مخاطر الأضرار الجانبية:</strong> {terrainResult.urbanAndDemographics.collateralDamageRisk}</div>
</div>
</div>
{/* SECTION 4: SOIL & CONCEALMENT */}
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8, border: '1px solid rgba(16, 185, 129, 0.2)' }}>
<div style={{ fontSize: '0.78rem', fontWeight: 800, color: '#10b981', marginBottom: 6, display: 'flex', alignItems: 'center', gap: 6 }}>
<TreePine size={14} /> 4. التربة وإمكانات الإخفاء والتمويه:
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: '0.72rem', color: '#cbd5e1' }}>
<div><strong>نوع التربة:</strong> {terrainResult.landCoverAndCover.soilType}</div>
<div><strong>صلاحية السير للآليات:</strong> {terrainResult.landCoverAndCover.trafficability}</div>
<div><strong>الإخفاء من الرصد الأرضي:</strong> {terrainResult.landCoverAndCover.concealmentRating}</div>
<div><strong>الانكشاف للرصد الجوي:</strong> {terrainResult.landCoverAndCover.airObservationExposure}</div>
</div>
</div>
{/* SECTION 5: COMMANDER'S TACTICAL RECOMMENDATIONS (OAKOC) */}
<div style={{ background: 'linear-gradient(135deg, rgba(79, 70, 229, 0.15), rgba(99, 102, 241, 0.15))', padding: 12, borderRadius: 8, border: '1px solid #6366f1' }}>
<div style={{ fontSize: '0.82rem', fontWeight: 800, color: '#a5b4fc', marginBottom: 8, display: 'flex', alignItems: 'center', gap: 6 }}>
<Award size={16} /> 5. التقييم التكتيكي للقائد والتوصيات الميدانية:
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: '0.72rem', color: '#f8fafc', lineHeight: 1.45 }}>
{terrainResult.tacticalRecommendations.map((rec, i) => (
<div key={i} style={{ display: 'flex', gap: 6 }}>
<span style={{ color: '#818cf8', fontWeight: 700 }}>{i + 1}.</span>
<span>{rec}</span>
</div>
))}
</div>
</div>
</div>
)}
</div>
)}
{/* ========================================================================= */}
{/* MODE 1: LINE OF SIGHT */}
{/* ========================================================================= */}
{mode === 'los' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div style={{ paddingBottom: 8, borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
@@ -1080,7 +1477,9 @@ export const TacticalDefenseView: React.FC = () => {
</div>
)}
{/* ========================================================================= */}
{/* MODE 2: 360 VIEWSHED */}
{/* ========================================================================= */}
{mode === 'viewshed' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div style={{ paddingBottom: 8, borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
@@ -1198,7 +1597,9 @@ export const TacticalDefenseView: React.FC = () => {
</div>
)}
{/* ========================================================================= */}
{/* MODE 3: ARTILLERY FIRE MISSION */}
{/* ========================================================================= */}
{mode === 'artillery' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div style={{ paddingBottom: 8, borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
@@ -1366,7 +1767,9 @@ export const TacticalDefenseView: React.FC = () => {
</div>
)}
{/* ========================================================================= */}
{/* MODE 4: MINEFIELD ANALYSIS */}
{/* ========================================================================= */}
{mode === 'minefield' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div style={{ paddingBottom: 8, borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
@@ -1543,7 +1946,9 @@ export const TacticalDefenseView: React.FC = () => {
</div>
)}
{/* ========================================================================= */}
{/* MODE 5: HLZ ASSESSMENT */}
{/* ========================================================================= */}
{mode === 'hlz' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div style={{ paddingBottom: 8, borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
@@ -1637,7 +2042,9 @@ export const TacticalDefenseView: React.FC = () => {
</div>
)}
{/* ========================================================================= */}
{/* MODE 6: TACTICAL SYMBOLS & DEPLOYMENT */}
{/* ========================================================================= */}
{mode === 'symbols' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div style={{ paddingBottom: 8, borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
@@ -1787,7 +2194,9 @@ export const TacticalDefenseView: React.FC = () => {
</div>
)}
{/* ========================================================================= */}
{/* MODE 7: TACTICAL FEASIBILITY STUDY & ORGANIZATION REPORT */}
{/* ========================================================================= */}
{mode === 'study' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div style={{ paddingBottom: 8, borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
+273
View File
@@ -576,3 +576,276 @@ export async function calculateMinefieldAnalysis(
};
}
export interface TerrainStudyResult {
centerLat: number;
centerLng: number;
radiusMeters: number;
areaKm2: number;
sectorName: string;
centerElevation: number;
minElevation: number;
maxElevation: number;
reliefMeters: number;
avgSlopeDegrees: number;
maxSlopeDegrees: number;
slopeDistribution: {
flatPercentage: number; // < 5 deg (Go)
moderatePercentage: number; // 5-15 deg (Slow-Go)
steepPercentage: number; // > 15 deg (No-Go / Obstacle)
};
terrainClassification: string;
terrainClassificationAr: string;
mobilityStatus: 'GO' | 'SLOW-GO' | 'NO-GO';
mobilityStatusAr: string;
naturalObstacles: Array<{
name: string;
type: string;
description: string;
impact: string;
}>;
manMadeObstacles: Array<{
name: string;
type: string;
description: string;
tacticalNote: string;
}>;
urbanAndDemographics: {
density: 'HIGH' | 'MEDIUM' | 'LOW' | 'SPARSE';
densityAr: string;
settlements: string[];
moutComplexity: string;
collateralDamageRisk: string;
};
landCoverAndCover: {
soilType: string;
trafficability: string;
concealmentRating: string;
airObservationExposure: string;
};
oakocAssessment: {
obstacles: string;
avenuesOfApproach: string;
keyTerrain: string;
observationAndFieldsOfFire: string;
coverAndConcealment: string;
};
tacticalRecommendations: string[];
}
/**
* Military Tactical Terrain Intelligence & Environmental Study (OAKOC Doctrine)
* دراسة الأرض الشاملة: التضاريس، الموانع الطبيعية والصناعية، والمناطق المأهولة
*/
export async function calculateTerrainStudy(
centerLat: number,
centerLng: number,
radiusMeters: number = 3000
): Promise<TerrainStudyResult> {
const centerElev = await sampleElevationAt(centerLat, centerLng);
const areaKm2 = Math.round(Math.PI * Math.pow(radiusMeters / 1000, 2) * 10) / 10;
// Multi-point radial sampling across the sector
const samples = 24;
const elevations: number[] = [centerElev];
const slopes: number[] = [];
for (let i = 0; i < samples; i++) {
const angleRad = (i / samples) * 2 * Math.PI;
const dist = radiusMeters * (0.3 + (i % 3) * 0.35);
const R_earth = 6371000;
const dLat = (dist / R_earth) * (180 / Math.PI) * Math.cos(angleRad);
const dLng = (dist / (R_earth * Math.cos((centerLat * Math.PI) / 180))) * (180 / Math.PI) * Math.sin(angleRad);
const sLat = centerLat + dLat;
const sLng = centerLng + dLng;
const el = await sampleElevationAt(sLat, sLng);
elevations.push(el);
const slopeDeg = Math.atan(Math.abs(el - centerElev) / dist) * (180 / Math.PI);
slopes.push(slopeDeg);
}
const minElev = Math.min(...elevations);
const maxElev = Math.max(...elevations);
const relief = maxElev - minElev;
const avgSlope = Math.round((slopes.reduce((a, b) => a + b, 0) / slopes.length) * 10) / 10;
const maxSlope = Math.round(Math.max(...slopes) * 10) / 10;
const flatCount = slopes.filter(s => s < 5).length;
const modCount = slopes.filter(s => s >= 5 && s <= 15).length;
const steepCount = slopes.filter(s => s > 15).length;
const flatPct = Math.round((flatCount / slopes.length) * 100);
const modPct = Math.round((modCount / slopes.length) * 100);
const steepPct = Math.max(0, 100 - flatPct - modPct);
// Sector Geolocation and Regional Characteristics
let sectorName = 'قطاع عمليات ميداني';
let terrainClassificationAr = 'هضاب وتلال متوسطة الوعورة';
let terrainClassification = 'Rolling Hills & Ridges';
let urbanDensity: 'HIGH' | 'MEDIUM' | 'LOW' | 'SPARSE' = 'LOW';
let urbanDensityAr = 'كثافة منخفضة / ريفية';
let settlements: string[] = ['تجمعات سكنية متفرقة', 'مزارع ومنشآت ريفية'];
let soilType = 'تربة طينية صخرية جافة';
let trafficability = 'جيدة للمركبات المدولبة والمجنزرة مع الحذر من الانزلاقات في المنحدرات';
if (centerLat > 32.3 && centerLng < 36.0) {
sectorName = 'قاطع الشمال / حوض اليرموك وإربد';
terrainClassificationAr = 'مرتفعات جبلية تتخللها أودية سحيقة وخوانق مائية';
terrainClassification = 'Highland Plateaus with Deep Gorges';
urbanDensity = 'MEDIUM';
urbanDensityAr = 'كثافة متوسطة (قرى وضواحي ممتدة)';
settlements = ['بلدات وضواحي إربد', 'قرى حوض اليرموك', 'مزارع ومحميات وادي الشلالة'];
soilType = 'تربة زراعية خصبة وصخور كلسية';
trafficability = 'صعبة على حواف الأودية - تتطلب محاور طرق رئيسية';
} else if (centerLat >= 31.8 && centerLat <= 32.2 && centerLng >= 35.8 && centerLng <= 36.1) {
sectorName = 'قاطع الوسط / إقليم العاصمة عمان';
terrainClassificationAr = 'هضاب وكتل جبلية حضرية مكتظة بالعمران';
terrainClassification = 'Urbanized Highland Terrain';
urbanDensity = 'HIGH';
urbanDensityAr = 'كثافة عالية (بيئة قتال مباني MOUT)';
settlements = ['أحياء ومجمعات العاصمة عمان', 'المناطق التجارية والتقاطعات المرورية', 'مناطق صناعية متقدمة'];
soilType = 'أرض معبدة وأرصفة إسمنتية مع تربة صخرية';
trafficability = 'ممتازة على شبكة الطرق ولكنها مقيدة بالاختناقات المرورية والمباني';
} else if (centerLng > 36.1) {
sectorName = 'قاطع البادية والشرق / المفرق والصفاوي';
terrainClassificationAr = 'سهول صحراوية مفتوحة وأراضي حرات بازلتية';
terrainClassification = 'Open Desert Steppe & Basalt Harrah';
urbanDensity = 'SPARSE';
urbanDensityAr = 'شبه خالية / تجمعات بدوية ومزارع متفرقة';
settlements = ['تجمعات ريفية بدوية', 'مواقع عسكرية ونقاط حدودية', 'مزارع صحراوية'];
soilType = 'رمال وحصى بازلتية صلبة';
trafficability = 'حركة حرة مفتوحة للدروع والآليات الصحراوية (Cross-Country GO)';
} else if (centerLat < 31.8 && centerLng < 35.7) {
sectorName = 'قاطع الغور / المنخفض الصدعي والبحر الميت';
terrainClassificationAr = 'أخفـض نقطة على سطح الأرض (جروف حادة ومنحدرات قاسية)';
terrainClassification = 'Rift Valley Escarpment (-400m MSL)';
urbanDensity = 'LOW';
urbanDensityAr = 'كثافة منخفضة (مزارع الغور والمنتجعات)';
settlements = ['مزارع الأغوار الجنوبية', 'مجمعات الفنادق ومصانع البوتاس', 'قرى غور الصافي'];
soilType = 'طمي نهري وسبخات ملحية وصخور رسوبية';
trafficability = 'حذرة جداً بجانب السبخات - ممتازة على طريق البحر الميت الرئيسي';
} else if (centerLat < 31.5 && centerLat > 30.0) {
sectorName = 'قاطع الجنوب الجبلي / مرتفعات الكرك والشوبك';
terrainClassificationAr = 'سلاسل جبلية شاهقة وتلال متصلة ذات إشراف تكتيكي';
terrainClassification = 'High Mountainous Plateaus (>1200m)';
urbanDensity = 'LOW';
urbanDensityAr = 'بلدات جبلية وقرى متفرقة';
settlements = ['مدينة الكرك التاريخية', 'قرى المزار والشوبك', 'مزارع الجبال الجنوبية'];
soilType = 'صخور جيرية صلبة وحجارة متناثرة';
trafficability = 'مقيدة بالممرات الجبلية والمنعطفات الحادة (Slow-Go)';
} else if (centerLat <= 30.0) {
sectorName = 'قاطع الجنوب الصحراوي / وادي رم والعقبة';
terrainClassificationAr = 'جبال رملية شاهقة (Monoliths) تتخللها ممرات ووديان رملية واسعة';
terrainClassification = 'Sandstone Monoliths & Desert Valleys';
urbanDensity = 'SPARSE';
urbanDensityAr = 'شبه خالية (مخيمات سياحية وتجمعات بدوية)';
settlements = ['قرية وادي رم', 'مخيمات الديسة السياحية', 'نقاط المراقبة الجنوبية'];
soilType = 'رمال ناعمة وصخور رملية ضخمة';
trafficability = 'ممتازة للآليات ذات الدفع الرباعي والمجنزرات في بطون الأودية';
}
// Determine Overall Mobility Status
let mobilityStatus: 'GO' | 'SLOW-GO' | 'NO-GO' = 'GO';
let mobilityStatusAr = 'حركة حرة ممتازة للآليات والدروع (Cross-Country GO)';
if (steepPct > 40 || avgSlope > 14) {
mobilityStatus = 'NO-GO';
mobilityStatusAr = 'مانع تضاريسي - حركة مقيدة وشديدة الصعوبة (NO-GO)';
} else if (steepPct > 15 || modPct > 40 || avgSlope > 7) {
mobilityStatus = 'SLOW-GO';
mobilityStatusAr = 'حركة بطيئة تتطلب مسارب وتجهيزاً هندسياً (SLOW-GO)';
}
return {
centerLat,
centerLng,
radiusMeters,
areaKm2,
sectorName,
centerElevation: Math.round(centerElev),
minElevation: Math.round(minElev),
maxElevation: Math.round(maxElev),
reliefMeters: Math.round(relief),
avgSlopeDegrees: avgSlope,
maxSlopeDegrees: maxSlope,
slopeDistribution: {
flatPercentage: flatPct,
moderatePercentage: modPct,
steepPercentage: steepPct
},
terrainClassification,
terrainClassificationAr,
mobilityStatus,
mobilityStatusAr,
naturalObstacles: [
{
name: 'المنحدرات والجروف التضاريسية الحادة',
type: 'انحدار طبوغرافي',
description: `تشكل المناطق ذات الانحدار الشديد (${steepPct}%) مانعاً طبيعياً يحد من تقدم الدروع ويجبرها على الالتفاف.`,
impact: 'إعاقة محاور الهجوم المعادي وتوجيهه نحو الخناق المجهزة بالنيران'
},
{
name: 'الأودية والشعاب ومجاري السيول الطبيعية',
type: 'أودية ومصارف مائية',
description: 'بطون الأودية تمثل ممرات حركة محجوبة عن خط النظر ولكنها عرضة للكمائن والسيول الفجائية.',
impact: 'تصلح كمسارب تسلل للمشاة ولكنها خطرة للآليات الثقيلة'
},
{
name: 'القمم والمرتفعات الحاكمة المجاورة',
type: 'أرض مسيطرة (Key Terrain)',
description: `أعلى قمة في القطاع بارتفاع ${maxElev}م توفر إشرافاً نارياً واستطلاعياً على عمق يصل إلى ${Math.round(radiusMeters / 500)} كم.`,
impact: 'وجوب احتلالها بنقاط مراقبة وأسلحة إسناد مضادة للدروع'
}
],
manMadeObstacles: [
{
name: 'شبكة الطرق والمحاور المعبدة',
type: 'محاور حركة سريعة',
description: 'الطرق الرئيسية المارة بالقطاع تتيح سرعة إعادة انتشار القوات والإمداد اللوجستي.',
tacticalNote: 'تعتبر أهدافاً حيوية يجب تأمينها بنقاط سيطرة وتفتيش وغلق سريع'
},
{
name: 'خطوط نقل الطاقة وأبراج الاتصالات',
type: 'بنية تحتية صناعية',
description: 'أبراج الضغط العالي والاتصالات تشكل عوائق أمام الطيران المنخفض ومواقع محتملة لمحطات الإعاقة اللاسلكية.',
tacticalNote: 'تجنب هبوط المروحيات بالقرب من خطوط الضغط العالي'
},
{
name: 'المباني والمنشآت الخرسانية الممتدة',
type: 'موانع مبنية',
description: 'المباني المتفرقة توفر سواتر خرسانية ممتازة ضد الشظايا ونيران الأسلحة الخفيفة.',
tacticalNote: 'تتطلب تطهيراً هندسياً لضمان خلوها من العبوات والأفخاخ'
}
],
urbanAndDemographics: {
density: urbanDensity,
densityAr: urbanDensityAr,
settlements,
moutComplexity: urbanDensity === 'HIGH' ? 'معقدة جداً وتتطلب تدريب قتال مباني متخصص' : 'بسيطة إلى متوسطة وتقتصر على أطراف القرى',
collateralDamageRisk: urbanDensity === 'HIGH' ? 'عالية جداً وتتطلب ذخائر موجهة وقواعد اشتباك صارمة' : 'منخفضة مع مراعاة حماية الأعيان المدنية'
},
landCoverAndCover: {
soilType,
trafficability,
concealmentRating: steepPct > 20 || urbanDensity === 'HIGH' ? 'ممتازة خلف التموجات والمباني' : 'متوسطة إلى مكشوفة في الأراضي المفتوحة',
airObservationExposure: urbanDensity === 'HIGH' ? 'مختلطة (مباني تحجب الرؤية الجوية)' : 'عالية للرصد الجوي والمسيرات نهاراً'
},
oakocAssessment: {
obstacles: `القطاع يحتوي على ${steepPct}% أراضٍ شديدة الوعورة تشكل موانع طبيعية، مع وجود محاور طرق رئيسية تشكل ممرات حركة سريعة.`,
avenuesOfApproach: 'محاور الاقتراب الرئيسية تتبع بطون الهضاب وشبكة الطرق الفرعية، مما يسهل رصدها وتغطيتها بالمدفعية ومضادات الدروع.',
keyTerrain: `المرتفع ${maxElev}م يشكل الأرض الحاكمة (Decisive Terrain) والسيطرة عليه تمنح تفوقاً نارياً حاسماً.`,
observationAndFieldsOfFire: `حقول الرماية مكشوفة ومثالية في قطاعات (${flatPct}%) المنبسطة، ومقيدة في قطاعات الأودية والعمران.`,
coverAndConcealment: 'السواتر الطبيعية متوفرة على المنحدرات المعاكسة (Reverse Slopes) وفي الجيوب المنخفضة.'
},
tacticalRecommendations: [
`تمركز نقاط المراقبة والرادار على المرتفع الحاكم بارتفاع ${maxElev}م لتأمين الإنذار المبكر.`,
'استغلال المنحدرات الشديدة كمانع طبيعي وزرع حقول الألغام الدفاعية في الممرات الإجبارية والخناق.',
'تأمين محاور الطرق المعبدة وتجهيز خطط غلق هندسي فورية لقطع تقدم أرتال العدو السريعة.',
'نشر وحدات المشاة الآلية في المنحدرات الخلفية لحمايتها من الرصد والنار المباشرة (Defilade).',
'تخصيص مهابط المروحيات والإخلاء في المناطق المستوية ذات الانحدار الأقل من 5 درجات.'
]
};
}