diff --git a/apps/web/src/pages/TacticalDefenseView.tsx b/apps/web/src/pages/TacticalDefenseView.tsx index 2bb142b..c1e9249 100644 --- a/apps/web/src/pages/TacticalDefenseView.tsx +++ b/apps/web/src/pages/TacticalDefenseView.tsx @@ -21,26 +21,52 @@ import { Play, RotateCcw, Zap, - ChevronDown + ChevronDown, + FileText, + MapPin, + Target, + Sliders, + Check, + Award, + AlertOctagon } from 'lucide-react'; -import { calculateLineOfSight, calculateRadialViewshed, LineOfSightResult, Viewshed360Result } from '../utils/elevationService'; +import { + calculateLineOfSight, + calculateRadialViewshed, + calculateMinefieldAnalysis, + LineOfSightResult, + Viewshed360Result, + MinefieldAnalysisResult, + calculateDistance, + calculateAzimuth +} from '../utils/elevationService'; -type TacticalMode = 'los' | 'viewshed' | 'artillery' | 'hlz' | 'symbols'; +type TacticalMode = 'los' | 'viewshed' | 'artillery' | 'minefield' | 'hlz' | 'symbols' | 'study'; interface TacticalSymbolItem { id: string; - type: 'friendly' | 'hostile' | 'artillery' | 'radar' | 'checkpoint' | 'minefield' | 'op'; + type: 'friendly' | 'hostile' | 'artillery' | 'radar' | 'checkpoint' | 'minefield' | 'op' | 'hlz'; name: string; lat: number; lng: number; + elev?: number; } +const REGIONAL_SECTORS = [ + { name: 'عمّان الوسطى', lat: 31.9539, lng: 35.9106, zoom: 12 }, + { name: 'إربد / اليرموك', lat: 32.5568, lng: 35.8469, zoom: 12 }, + { name: 'قاطع المفرق', lat: 32.3421, lng: 36.2081, zoom: 11 }, + { name: 'غور الأردن / البحر الميت', lat: 31.7500, lng: 35.5800, zoom: 12 }, + { name: 'مرتفعات الكرك والشوبك', lat: 31.1853, lng: 35.7048, zoom: 11 }, + { name: 'وادي رم الجنوبي', lat: 29.5700, lng: 35.4200, zoom: 12 }, +]; + const DEFAULT_SYMBOLS: TacticalSymbolItem[] = [ - { id: 'sym-1', type: 'friendly', name: 'كتيبة المشاة الآلية 4', lat: 31.98, lng: 35.88 }, - { id: 'sym-2', type: 'artillery', name: 'مربض مدفعية م109', lat: 31.93, lng: 35.91 }, - { id: 'sym-3', type: 'radar', name: 'رادار كشف أرضي تكتيكي', lat: 32.02, lng: 35.84 }, - { id: 'sym-4', type: 'checkpoint', name: 'نقطة سيطرة وغلق أمني', lat: 31.95, lng: 35.95 }, - { id: 'sym-5', type: 'op', name: 'نقطة مراقبة واستطلاع أمامي', lat: 32.06, lng: 35.81 }, + { id: 'sym-1', type: 'friendly', name: 'كتيبة المشاة الآلية 4', lat: 31.9800, lng: 35.8800 }, + { id: 'sym-2', type: 'artillery', name: 'مربض مدفعية م109', lat: 31.9300, lng: 35.9100 }, + { id: 'sym-3', type: 'radar', name: 'رادار كشف أرضي تكتيكي', lat: 32.0200, lng: 35.8400 }, + { id: 'sym-4', type: 'checkpoint', name: 'نقطة سيطرة وغلق أمني', lat: 31.9500, lng: 35.9500 }, + { id: 'sym-5', type: 'op', name: 'نقطة مراقبة واستطلاع أمامي', lat: 32.0600, lng: 35.8100 }, ]; export const TacticalDefenseView: React.FC = () => { @@ -49,6 +75,7 @@ export const TacticalDefenseView: React.FC = () => { const [mode, setMode] = useState('los'); const [cursorPos, setCursorPos] = useState({ lat: 31.95, lng: 35.93, elev: 850 }); + const [activePlacement, setActivePlacement] = useState(null); // Line of Sight State const [losPointA, setLosPointA] = useState<[number, number] | null>([31.9539, 35.9106]); @@ -73,6 +100,13 @@ export const TacticalDefenseView: React.FC = () => { const [artilleryResult, setArtilleryResult] = useState(null); const [artilleryLoading, setArtilleryLoading] = useState(false); + // 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(null); + const [mineLoading, setMineLoading] = useState(false); + // HLZ State const [hlzCenter, setHlzCenter] = useState<[number, number] | null>([31.9600, 35.9000]); const [hlzResult, setHlzResult] = useState(null); @@ -113,10 +147,52 @@ export const TacticalDefenseView: React.FC = () => { }); initialMap.on('click', (e) => { - const clickedLat = e.lngLat.lat; - const clickedLng = e.lngLat.lng; + const clickedLat = Number(e.lngLat.lat.toFixed(5)); + const clickedLng = Number(e.lngLat.lng.toFixed(5)); - // Handle map clicks based on active mode + // Handle explicit placement modes + if (activePlacement === 'los-a') { + setLosPointA([clickedLat, clickedLng]); + setActivePlacement(null); + return; + } + if (activePlacement === 'los-b') { + setLosPointB([clickedLat, clickedLng]); + setActivePlacement(null); + return; + } + if (activePlacement === 'gun') { + setGunPos([clickedLat, clickedLng]); + setActivePlacement(null); + return; + } + if (activePlacement === 'target') { + setTargetPos([clickedLat, clickedLng]); + setActivePlacement(null); + return; + } + if (activePlacement === 'mine-start') { + setMineStart([clickedLat, clickedLng]); + setActivePlacement(null); + return; + } + if (activePlacement === 'mine-end') { + setMineEnd([clickedLat, clickedLng]); + setActivePlacement(null); + return; + } + if (activePlacement === 'viewshed-center') { + setViewshedCenter([clickedLat, clickedLng]); + setActivePlacement(null); + return; + } + if (activePlacement === 'hlz-center') { + setHlzCenter([clickedLat, clickedLng]); + setActivePlacement(null); + return; + } + + // Default click actions by active tab if (mode === 'los') { if (!losPointA) { setLosPointA([clickedLat, clickedLng]); @@ -136,13 +212,32 @@ export const TacticalDefenseView: React.FC = () => { } else { setTargetPos([clickedLat, clickedLng]); } + } else if (mode === 'minefield') { + if (!mineStart) { + setMineStart([clickedLat, clickedLng]); + } else if (!mineEnd) { + setMineEnd([clickedLat, clickedLng]); + } else { + setMineStart([clickedLat, clickedLng]); + setMineEnd(null); + } } else if (mode === 'hlz') { setHlzCenter([clickedLat, clickedLng]); } else if (mode === 'symbols') { + const typeLabels: Record = { + friendly: 'وحدة صديقة', + hostile: 'هدف معادي', + artillery: 'مربض مدفعية', + radar: 'محطة رادار', + checkpoint: 'نقطة سيطرة', + op: 'نقطة مراقبة', + minefield: 'حقل ألغام', + hlz: 'مهبط مروحيات' + }; const newSym: TacticalSymbolItem = { id: `sym-${Date.now()}`, type: selectedSymbolType as any, - name: newSymbolName || `نقطة ${selectedSymbolType}`, + name: newSymbolName || `${typeLabels[selectedSymbolType] || 'رمز'} ${symbols.length + 1}`, lat: clickedLat, lng: clickedLng }; @@ -152,7 +247,7 @@ export const TacticalDefenseView: React.FC = () => { }); initialMap.on('load', () => { - // Add Sources and Layers for Tactical Defense Visualization + // 1. Line of Sight Layer initialMap.addSource('tactical-los-src', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } @@ -183,7 +278,7 @@ export const TacticalDefenseView: React.FC = () => { } }); - // Viewshed 360 Polygon Layer + // 2. Viewshed 360 Polygon Layer initialMap.addSource('tactical-viewshed-src', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } @@ -210,7 +305,7 @@ export const TacticalDefenseView: React.FC = () => { } }); - // Artillery Trajectory Layer + // 3. Artillery Trajectory Layer initialMap.addSource('tactical-artillery-src', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } @@ -226,7 +321,24 @@ export const TacticalDefenseView: React.FC = () => { } }); - // Tactical Symbols Layer + // 4. Minefield Barrier Layer + initialMap.addSource('tactical-minefield-src', { + type: 'geojson', + data: { type: 'FeatureCollection', features: [] } + }); + + initialMap.addLayer({ + id: 'tactical-minefield-line', + type: 'line', + source: 'tactical-minefield-src', + paint: { + 'line-color': '#dc2626', + 'line-width': 6, + 'line-dasharray': [1, 1] + } + }); + + // 5. Tactical Symbols Layer initialMap.addSource('tactical-symbols-src', { type: 'geojson', data: { @@ -337,6 +449,34 @@ export const TacticalDefenseView: React.FC = () => { .catch(() => setLosLoading(false)); }, [losPointA, losPointB, losObsHeight, losTgtHeight]); + // Update Minefield Analysis + useEffect(() => { + if (!mineStart || !mineEnd) return; + setMineLoading(true); + calculateMinefieldAnalysis(mineStart[0], mineStart[1], mineEnd[0], mineEnd[1], mineDensity) + .then((res) => { + setMineResult(res); + setMineLoading(false); + + if (map.current && map.current.getSource('tactical-minefield-src')) { + (map.current.getSource('tactical-minefield-src') as maplibregl.GeoJSONSource).setData({ + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: [[mineStart[1], mineStart[0]], [mineEnd[1], mineEnd[0]]] + } + } + ] + }); + } + }) + .catch(() => setMineLoading(false)); + }, [mineStart, mineEnd, mineDensity]); + // Update 360 Viewshed Calculations & Map Layer const runViewshed360 = () => { if (!viewshedCenter) return; @@ -433,6 +573,17 @@ export const TacticalDefenseView: React.FC = () => { }); }, [symbols]); + // Jump to Sector + const flyToSector = (sector: typeof REGIONAL_SECTORS[0]) => { + if (!map.current) return; + map.current.flyTo({ + center: [sector.lng, sector.lat], + zoom: sector.zoom, + pitch: 45, + essential: true + }); + }; + return (
{ justifyContent: 'space-between', padding: '0 18px', zIndex: 50, - gap: 14, + gap: 12, flexShrink: 0 }}> -
+ {/* Title */} +
{ justifyContent: 'center', boxShadow: '0 0 16px rgba(79, 70, 229, 0.5)' }}> - +
-
+
منظومة القيادة والسيطرة والتحليل التكتيكي - C4ISR Tactical Grid + C4ISR Grid
{/* Tactical Modes Selector */} -
+
{[ - { id: 'los', label: 'تبادل الرؤية (LOS)', icon: }, - { id: 'viewshed', label: 'كشف الميدان 360°', icon: }, - { id: 'artillery', label: 'رماية المدفعية', icon: }, - { id: 'hlz', label: 'مهابط المروحيات', icon: }, - { id: 'symbols', label: 'الرموز والعمليات', icon: }, + { id: 'los', label: 'تبادل الرؤية (LOS)', icon: }, + { id: 'viewshed', label: 'كشف 360°', icon: }, + { id: 'artillery', label: 'موقع المدفعية', icon: }, + { id: 'minefield', label: 'حقل الألغام', icon: }, + { id: 'hlz', label: 'مهابط المروحيات', icon: }, + { id: 'symbols', label: 'الرموز والتشكيلات', icon: }, + { id: 'study', label: 'دراسة التنظيم', icon: }, ].map(m => (
{/* Realtime Cursor HUD */} -
+
LAT: {cursorPos.lat}
LNG: {cursorPos.lng}
ELEV: {cursorPos.elev}m
+ {/* Quick Sector Presets Bar */} +
+ + الانتقال السريع لقطاعات العمليات: + + {REGIONAL_SECTORS.map((s, idx) => ( + + ))} +
+ {/* Main Tactical Workspace */}
{/* Left Side Tactical Control Deck */} {/* Tactical Map Container */} diff --git a/apps/web/src/utils/elevationService.ts b/apps/web/src/utils/elevationService.ts index 782b013..40332cf 100644 --- a/apps/web/src/utils/elevationService.ts +++ b/apps/web/src/utils/elevationService.ts @@ -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 { + 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) + } + }; +} +