3550 lines
167 KiB
TypeScript
3550 lines
167 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
||
import maplibregl from 'maplibre-gl';
|
||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||
import { getSharedDemSource } from '../utils/demSource';
|
||
import {
|
||
Shield,
|
||
Crosshair,
|
||
Eye,
|
||
Radio,
|
||
Mountain,
|
||
AlertTriangle,
|
||
CheckCircle2,
|
||
XCircle,
|
||
Layers,
|
||
Trash2,
|
||
Navigation,
|
||
Flame,
|
||
Activity,
|
||
Plus,
|
||
Play,
|
||
RotateCcw,
|
||
Award,
|
||
AlertOctagon,
|
||
MapPin,
|
||
Target,
|
||
LocateFixed,
|
||
Move,
|
||
Building,
|
||
TreePine,
|
||
Printer,
|
||
FileText,
|
||
Compass,
|
||
Check,
|
||
EyeOff,
|
||
Sliders,
|
||
Lock,
|
||
Unlock,
|
||
Key,
|
||
ShieldAlert,
|
||
ShieldCheck,
|
||
LogOut,
|
||
Clock,
|
||
Zap,
|
||
Share2
|
||
} from 'lucide-react';
|
||
import {
|
||
calculateLineOfSight,
|
||
calculateRadialViewshed,
|
||
calculateMinefieldAnalysis,
|
||
calculateTerrainStudy,
|
||
LineOfSightResult,
|
||
Viewshed360Result,
|
||
MinefieldAnalysisResult,
|
||
TerrainStudyResult,
|
||
calculateDistance,
|
||
calculateAzimuth
|
||
} from '../utils/elevationService';
|
||
|
||
type TacticalMode = 'terrain' | 'los' | 'viewshed' | 'artillery' | 'minefield' | 'hlz' | 'symbols' | 'study' | 'isochrone';
|
||
|
||
export interface EmergencyHubPreset {
|
||
id: string;
|
||
name: string;
|
||
lat: number;
|
||
lng: number;
|
||
type: 'hospital' | 'civil_defense' | 'security';
|
||
badge: string;
|
||
description: string;
|
||
}
|
||
|
||
export const EMERGENCY_HUBS: EmergencyHubPreset[] = [
|
||
{ id: 'hub-1', name: 'المدينة الطبية الملكية (عمان)', lat: 31.9865, lng: 35.8340, type: 'hospital', badge: 'مستشفى رئيسي', description: 'مجمع طبي عسكري متقدم - غرب عمان' },
|
||
{ id: 'hub-2', name: 'مستشفى الجامعة الأردنية', lat: 32.0080, lng: 35.8720, type: 'hospital', badge: 'مستشفى جامعي', description: 'مركز طوارئ وإسعاف رئيسي شمال عمان' },
|
||
{ id: 'hub-3', name: 'مستشفى البشير الحكومي (عمان)', lat: 31.9425, lng: 35.9450, type: 'hospital', badge: 'طوارئ عامة', description: 'أكبر مجمع إسعاف وطوارئ في شرق ووسط عمان' },
|
||
{ id: 'hub-4', name: 'مديرية دفاع مدني العبدلي / وسط البلد', lat: 31.9680, lng: 35.9120, type: 'civil_defense', badge: 'مركز إطفاء وإنقاذ', description: 'مركز إطفاء وإنقاذ رئيسي لوسط العاصمة' },
|
||
{ id: 'hub-5', name: 'مركز دفاع مدني غرب عمان (خلدا/صويلح)', lat: 31.9960, lng: 35.8450, type: 'civil_defense', badge: 'إسعاف فوري', description: 'تغطية سريعة لمحاور شمال وغرب عمان' },
|
||
{ id: 'hub-6', name: 'مستشفى الملك المؤسس عبدالله الجامعي (إربد)', lat: 32.4950, lng: 35.9920, type: 'hospital', badge: 'إقليم الشمال', description: 'صرح طبي متقدم يخدم محافظات الشمال' },
|
||
{ id: 'hub-7', name: 'مديرية دفاع مدني إربد المركزية', lat: 32.5590, lng: 35.8450, type: 'civil_defense', badge: 'مركز رئيسي', description: 'مركز قيادة عمليات الإنقاذ والإسعاف في الشمال' },
|
||
{ id: 'hub-8', name: 'مستشفى الزرقاء الحكومي الجديد', lat: 32.0620, lng: 36.1280, type: 'hospital', badge: 'طوارئ الزرقاء', description: 'تغطية الطوارئ والحوادث لمحافظة الزرقاء' },
|
||
{ id: 'hub-9', name: 'مستشفى الأمير هاشم العسكري (العقبة)', lat: 29.5350, lng: 35.0080, type: 'hospital', badge: 'إقليم الجنوب', description: 'مستشفى عسكري وإسناد طارئ لخليج العقبة' },
|
||
];
|
||
|
||
interface TacticalSymbolItem {
|
||
id: string;
|
||
type: 'friendly' | 'hostile' | 'artillery' | 'radar' | 'checkpoint' | 'minefield' | 'op' | 'hlz';
|
||
name: string;
|
||
lat: number;
|
||
lng: 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.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 = () => {
|
||
const mapContainer = useRef<HTMLDivElement>(null);
|
||
const map = useRef<maplibregl.Map | null>(null);
|
||
|
||
// Tactical License & Access Clearance State
|
||
const [authKey, setAuthKey] = useState<string>(() => {
|
||
return new URLSearchParams(window.location.search).get('key') ||
|
||
new URLSearchParams(window.location.search).get('api_key') ||
|
||
sessionStorage.getItem('tactical_api_key') || '';
|
||
});
|
||
const [authStatus, setAuthStatus] = useState<'validating' | 'authorized' | 'unauthorized'>('validating');
|
||
const [authInfo, setAuthInfo] = useState<{ tenantName?: string; plan?: string; keyName?: string } | null>(null);
|
||
const [authError, setAuthError] = useState<string>('');
|
||
const [inputKey, setInputKey] = useState<string>('');
|
||
const [isVerifying, setIsVerifying] = useState<boolean>(false);
|
||
|
||
const verifyTacticalLicense = async (keyCandidate: string) => {
|
||
if (!keyCandidate || !keyCandidate.trim()) {
|
||
setAuthStatus('unauthorized');
|
||
return;
|
||
}
|
||
setIsVerifying(true);
|
||
setAuthError('');
|
||
try {
|
||
const apiUrl = (import.meta as any).env.VITE_API_URL || '/api';
|
||
const res = await fetch(`${apiUrl}/tactical/verify-license`, {
|
||
headers: { 'x-api-key': keyCandidate.trim() }
|
||
});
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
setAuthInfo(data);
|
||
setAuthKey(keyCandidate.trim());
|
||
sessionStorage.setItem('tactical_api_key', keyCandidate.trim());
|
||
setAuthStatus('authorized');
|
||
} else {
|
||
sessionStorage.removeItem('tactical_api_key');
|
||
setAuthStatus('unauthorized');
|
||
setAuthError('مفتاح التصريح التكتيكي غير صالح، أو منتهي الصلاحية، أو تم إلغاؤه من قبل الإدارة.');
|
||
}
|
||
} catch (e) {
|
||
sessionStorage.removeItem('tactical_api_key');
|
||
setAuthStatus('unauthorized');
|
||
setAuthError('تعذر التحقق من خادم التخويل التكتيكي. يرجى التأكد من الاتصال.');
|
||
} finally {
|
||
setIsVerifying(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (authKey) {
|
||
verifyTacticalLicense(authKey);
|
||
} else {
|
||
setAuthStatus('unauthorized');
|
||
}
|
||
}, []);
|
||
|
||
const handleRevokeOrLock = () => {
|
||
sessionStorage.removeItem('tactical_api_key');
|
||
setAuthKey('');
|
||
setAuthStatus('unauthorized');
|
||
setAuthInfo(null);
|
||
setAuthError('');
|
||
};
|
||
|
||
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);
|
||
|
||
// 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);
|
||
|
||
// Terrain Visual Layers Toggles on Map
|
||
const [showContours, setShowContours] = useState<boolean>(true);
|
||
const [showPeaksAndValleys, setShowPeaksAndValleys] = useState<boolean>(true);
|
||
const [showSlopeSectors, setShowSlopeSectors] = useState<boolean>(true);
|
||
const [showNaturalObstacles, setShowNaturalObstacles] = useState<boolean>(true);
|
||
const [showRoadCorridors, setShowRoadCorridors] = useState<boolean>(true);
|
||
const [showUrbanZones, setShowUrbanZones] = useState<boolean>(true);
|
||
const [showHazardousGround, setShowHazardousGround] = useState<boolean>(true);
|
||
|
||
// 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);
|
||
const [losTgtHeight, setLosTgtHeight] = useState<number>(2);
|
||
const [losResult, setLosResult] = useState<LineOfSightResult | null>(null);
|
||
const [losLoading, setLosLoading] = useState<boolean>(false);
|
||
|
||
// 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);
|
||
|
||
// 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)');
|
||
const [muzzleVel, setMuzzleVel] = useState<number>(827);
|
||
const [artilleryResult, setArtilleryResult] = useState<any | null>(null);
|
||
const [artilleryLoading, setArtilleryLoading] = useState<boolean>(false);
|
||
|
||
// 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);
|
||
|
||
// 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);
|
||
|
||
// 7. Tactical Symbols State
|
||
const [symbols, setSymbols] = useState<TacticalSymbolItem[]>(DEFAULT_SYMBOLS);
|
||
const [selectedSymbolType, setSelectedSymbolType] = useState<string>('friendly');
|
||
const [newSymbolName, setNewSymbolName] = useState<string>('');
|
||
const [customSymbolLat, setCustomSymbolLat] = useState<number>(31.95);
|
||
const [customSymbolLng, setCustomSymbolLng] = useState<number>(35.93);
|
||
|
||
// 8. Isochrone / Emergency Reachability State (خارطة زمن الوصول والاستجابة)
|
||
const [isochroneCenter, setIsochroneCenter] = useState<[number, number]>([31.9865, 35.8340]); // King Hussein Medical City
|
||
const [isochroneProfile, setIsochroneProfile] = useState<string>('emergency');
|
||
const [isochroneTimes, setIsochroneTimes] = useState<number[]>([300, 600, 900]);
|
||
const [isochroneData, setIsochroneData] = useState<any>(null);
|
||
const [isochroneLoading, setIsochroneLoading] = useState<boolean>(false);
|
||
const [selectedHubName, setSelectedHubName] = useState<string>('المدينة الطبية الملكية (عمان)');
|
||
const [copiedIsochroneGeoJson, setCopiedIsochroneGeoJson] = useState<boolean>(false);
|
||
|
||
// Synchronized Refs to avoid stale closures in MapLibre event listeners
|
||
const modeRef = useRef(mode);
|
||
modeRef.current = mode;
|
||
|
||
const activePlacementRef = useRef(activePlacement);
|
||
activePlacementRef.current = activePlacement;
|
||
|
||
const terrainCenterRef = useRef(terrainCenter);
|
||
terrainCenterRef.current = terrainCenter;
|
||
|
||
const losPointARef = useRef(losPointA);
|
||
losPointARef.current = losPointA;
|
||
|
||
const losPointBRef = useRef(losPointB);
|
||
losPointBRef.current = losPointB;
|
||
|
||
const viewshedCenterRef = useRef(viewshedCenter);
|
||
viewshedCenterRef.current = viewshedCenter;
|
||
|
||
const gunPosRef = useRef(gunPos);
|
||
gunPosRef.current = gunPos;
|
||
|
||
const targetPosRef = useRef(targetPos);
|
||
targetPosRef.current = targetPos;
|
||
|
||
const mineStartRef = useRef(mineStart);
|
||
mineStartRef.current = mineStart;
|
||
|
||
const mineEndRef = useRef(mineEnd);
|
||
mineEndRef.current = mineEnd;
|
||
|
||
const hlzCenterRef = useRef(hlzCenter);
|
||
hlzCenterRef.current = hlzCenter;
|
||
|
||
const isochroneCenterRef = useRef(isochroneCenter);
|
||
isochroneCenterRef.current = isochroneCenter;
|
||
|
||
const selectedSymbolTypeRef = useRef(selectedSymbolType);
|
||
selectedSymbolTypeRef.current = selectedSymbolType;
|
||
|
||
const newSymbolNameRef = useRef(newSymbolName);
|
||
newSymbolNameRef.current = newSymbolName;
|
||
|
||
const symbolsRef = useRef(symbols);
|
||
symbolsRef.current = symbols;
|
||
|
||
// Draggable Markers Ref Store
|
||
const markersRef = useRef<{ [key: string]: maplibregl.Marker }>({});
|
||
|
||
// Helper to create or update draggable marker
|
||
const updateMarker = (id: string, coords: [number, number] | null, label: string, color: string, onDrag: (pos: [number, number]) => void) => {
|
||
if (!map.current) return;
|
||
if (!coords) {
|
||
if (markersRef.current[id]) {
|
||
markersRef.current[id].remove();
|
||
delete markersRef.current[id];
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (markersRef.current[id]) {
|
||
markersRef.current[id].setLngLat([coords[1], coords[0]]);
|
||
const el = markersRef.current[id].getElement();
|
||
if (el) el.innerText = label;
|
||
} else {
|
||
const el = document.createElement('div');
|
||
el.className = 'tactical-marker';
|
||
el.style.display = 'flex';
|
||
el.style.alignItems = 'center';
|
||
el.style.justifyContent = 'center';
|
||
el.style.background = color;
|
||
el.style.color = '#ffffff';
|
||
el.style.fontSize = '11px';
|
||
el.style.fontWeight = 'bold';
|
||
el.style.padding = '4px 8px';
|
||
el.style.borderRadius = '20px';
|
||
el.style.border = '2px solid #ffffff';
|
||
el.style.boxShadow = '0 0 12px rgba(0,0,0,0.6)';
|
||
el.style.cursor = 'grab';
|
||
el.style.userSelect = 'none';
|
||
el.innerText = label;
|
||
|
||
const marker = new maplibregl.Marker({ element: el, draggable: true })
|
||
.setLngLat([coords[1], coords[0]])
|
||
.addTo(map.current);
|
||
|
||
marker.on('dragend', () => {
|
||
const lngLat = marker.getLngLat();
|
||
onDrag([Number(lngLat.lat.toFixed(5)), Number(lngLat.lng.toFixed(5))]);
|
||
});
|
||
|
||
markersRef.current[id] = marker;
|
||
}
|
||
};
|
||
|
||
// Sync Draggable Markers to Map
|
||
useEffect(() => {
|
||
if (!map.current) return;
|
||
// Terrain Study Center
|
||
updateMarker('terrain-c', terrainCenter, '⛰️ مركز دراسة الأرض', '#6366f1', (pos) => setTerrainCenter(pos));
|
||
|
||
// Terrain Highest & Lowest Points (Dynamic Pins)
|
||
if (terrainResult?.highestPoint && showPeaksAndValleys && mode === 'terrain') {
|
||
updateMarker(
|
||
'terrain-peak',
|
||
[terrainResult.highestPoint.lat, terrainResult.highestPoint.lng],
|
||
`🔺 ${terrainResult.highestPoint.label}`,
|
||
'#0284c7',
|
||
() => {}
|
||
);
|
||
} else {
|
||
updateMarker('terrain-peak', null, '', '', () => {});
|
||
}
|
||
|
||
if (terrainResult?.lowestPoint && showPeaksAndValleys && mode === 'terrain') {
|
||
updateMarker(
|
||
'terrain-valley',
|
||
[terrainResult.lowestPoint.lat, terrainResult.lowestPoint.lng],
|
||
`🔻 ${terrainResult.lowestPoint.label}`,
|
||
'#e11d48',
|
||
() => {}
|
||
);
|
||
} else {
|
||
updateMarker('terrain-valley', null, '', '', () => {});
|
||
}
|
||
|
||
// LOS
|
||
updateMarker('los-a', losPointA, '📍 راصد A', '#0284c7', (pos) => setLosPointA(pos));
|
||
updateMarker('los-b', losPointB, '🎯 هدف B', '#d97706', (pos) => setLosPointB(pos));
|
||
// Viewshed
|
||
updateMarker('viewshed-c', viewshedCenter, '📡 مركز الرصد', '#16a34a', (pos) => setViewshedCenter(pos));
|
||
// Artillery
|
||
updateMarker('art-gun', gunPos, '💥 مربض المدفعية', '#ea580c', (pos) => setGunPos(pos));
|
||
updateMarker('art-tgt', targetPos, '🎯 هدف مدفعية', '#dc2626', (pos) => setTargetPos(pos));
|
||
// Minefield
|
||
updateMarker('mine-s', mineStart, '⛔ بداية حقل الألغام', '#b91c1c', (pos) => setMineStart(pos));
|
||
updateMarker('mine-e', mineEnd, '⛔ نهاية حقل الألغام', '#991b1b', (pos) => setMineEnd(pos));
|
||
// HLZ
|
||
updateMarker('hlz-c', hlzCenter, '🚁 مركز HLZ', '#059669', (pos) => setHlzCenter(pos));
|
||
|
||
// 8. Isochrone / Response Reachability Hub
|
||
if (mode === 'isochrone' && isochroneCenter) {
|
||
updateMarker(
|
||
'isochrone-c',
|
||
isochroneCenter,
|
||
`🚑 ${selectedHubName.split(' ')[0]} (نقطة الانطلاق)`,
|
||
'#10b981',
|
||
(pos) => {
|
||
setIsochroneCenter(pos);
|
||
setSelectedHubName(`نقطة مخصصة (${pos[0].toFixed(4)}, ${pos[1].toFixed(4)})`);
|
||
}
|
||
);
|
||
} else {
|
||
updateMarker('isochrone-c', null, '', '', () => {});
|
||
}
|
||
}, [terrainCenter, terrainResult, showPeaksAndValleys, mode, losPointA, losPointB, viewshedCenter, gunPos, targetPos, mineStart, mineEnd, hlzCenter, isochroneCenter, selectedHubName]);
|
||
|
||
// Map Initialization
|
||
useEffect(() => {
|
||
if (map.current) return;
|
||
if (!mapContainer.current) return;
|
||
|
||
const initialMap = new maplibregl.Map({
|
||
container: mapContainer.current,
|
||
style: '/style.json',
|
||
center: [35.9106, 31.9539],
|
||
zoom: 11,
|
||
pitch: 35,
|
||
attributionControl: false
|
||
});
|
||
|
||
map.current = initialMap;
|
||
|
||
const handleResize = () => {
|
||
if (initialMap) initialMap.resize();
|
||
};
|
||
window.addEventListener('resize', handleResize);
|
||
|
||
initialMap.on('mousemove', (e) => {
|
||
setCursorPos({
|
||
lat: Number(e.lngLat.lat.toFixed(5)),
|
||
lng: Number(e.lngLat.lng.toFixed(5)),
|
||
elev: 820 + Math.round(Math.sin(e.lngLat.lat * 20) * 150)
|
||
});
|
||
});
|
||
|
||
// Fresh Map Click Handler reading latest Refs
|
||
initialMap.on('click', (e) => {
|
||
const clickedLat = Number(e.lngLat.lat.toFixed(5));
|
||
const clickedLng = Number(e.lngLat.lng.toFixed(5));
|
||
|
||
const placement = activePlacementRef.current;
|
||
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);
|
||
return;
|
||
}
|
||
if (placement === 'los-b') {
|
||
setLosPointB([clickedLat, clickedLng]);
|
||
setActivePlacement(null);
|
||
return;
|
||
}
|
||
if (placement === 'viewshed-center') {
|
||
setViewshedCenter([clickedLat, clickedLng]);
|
||
setActivePlacement(null);
|
||
return;
|
||
}
|
||
if (placement === 'gun') {
|
||
setGunPos([clickedLat, clickedLng]);
|
||
setActivePlacement(null);
|
||
return;
|
||
}
|
||
if (placement === 'target') {
|
||
setTargetPos([clickedLat, clickedLng]);
|
||
setActivePlacement(null);
|
||
return;
|
||
}
|
||
if (placement === 'mine-start') {
|
||
setMineStart([clickedLat, clickedLng]);
|
||
setActivePlacement(null);
|
||
return;
|
||
}
|
||
if (placement === 'mine-end') {
|
||
setMineEnd([clickedLat, clickedLng]);
|
||
setActivePlacement(null);
|
||
return;
|
||
}
|
||
if (placement === 'hlz-center') {
|
||
setHlzCenter([clickedLat, clickedLng]);
|
||
setActivePlacement(null);
|
||
return;
|
||
}
|
||
if (placement === 'isochrone-center') {
|
||
setIsochroneCenter([clickedLat, clickedLng]);
|
||
setSelectedHubName(`نقطة مخصصة (${clickedLat.toFixed(4)}, ${clickedLng.toFixed(4)})`);
|
||
setActivePlacement(null);
|
||
return;
|
||
}
|
||
|
||
// 2. Default Context-Aware Click by Active Mode
|
||
if (currentMode === 'terrain') {
|
||
setTerrainCenter([clickedLat, clickedLng]);
|
||
} else if (currentMode === 'los') {
|
||
if (!losPointARef.current) {
|
||
setLosPointA([clickedLat, clickedLng]);
|
||
} else if (!losPointBRef.current) {
|
||
setLosPointB([clickedLat, clickedLng]);
|
||
} else {
|
||
setLosPointA([clickedLat, clickedLng]);
|
||
setLosPointB(null);
|
||
}
|
||
} else if (currentMode === 'viewshed') {
|
||
setViewshedCenter([clickedLat, clickedLng]);
|
||
} else if (currentMode === 'artillery') {
|
||
if (!gunPosRef.current) {
|
||
setGunPos([clickedLat, clickedLng]);
|
||
} else if (!targetPosRef.current) {
|
||
setTargetPos([clickedLat, clickedLng]);
|
||
} else {
|
||
setTargetPos([clickedLat, clickedLng]);
|
||
}
|
||
} else if (currentMode === 'minefield') {
|
||
if (!mineStartRef.current) {
|
||
setMineStart([clickedLat, clickedLng]);
|
||
} else if (!mineEndRef.current) {
|
||
setMineEnd([clickedLat, clickedLng]);
|
||
} else {
|
||
setMineStart([clickedLat, clickedLng]);
|
||
setMineEnd(null);
|
||
}
|
||
} else if (currentMode === 'hlz') {
|
||
setHlzCenter([clickedLat, clickedLng]);
|
||
} else if (currentMode === 'isochrone') {
|
||
setIsochroneCenter([clickedLat, clickedLng]);
|
||
setSelectedHubName(`نقطة مخصصة (${clickedLat.toFixed(4)}, ${clickedLng.toFixed(4)})`);
|
||
} else if (currentMode === 'symbols') {
|
||
const typeLabels: Record<string, string> = {
|
||
friendly: 'وحدة صديقة',
|
||
hostile: 'هدف معادي',
|
||
artillery: 'مربض مدفعية',
|
||
radar: 'محطة رادار',
|
||
checkpoint: 'نقطة سيطرة',
|
||
op: 'نقطة مراقبة',
|
||
minefield: 'حقل ألغام',
|
||
hlz: 'مهبط مروحيات'
|
||
};
|
||
const newSym: TacticalSymbolItem = {
|
||
id: `sym-${Date.now()}`,
|
||
type: selectedSymbolTypeRef.current as any,
|
||
name: newSymbolNameRef.current || `${typeLabels[selectedSymbolTypeRef.current] || 'رمز'} ${symbolsRef.current.length + 1}`,
|
||
lat: clickedLat,
|
||
lng: clickedLng
|
||
};
|
||
setSymbols(prev => [...prev, newSym]);
|
||
setNewSymbolName('');
|
||
}
|
||
});
|
||
|
||
initialMap.on('load', () => {
|
||
// 0. Terrain Study Circle & Spatial Layers
|
||
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.08
|
||
}
|
||
});
|
||
|
||
initialMap.addLayer({
|
||
id: 'tactical-terrain-line',
|
||
type: 'line',
|
||
source: 'tactical-terrain-src',
|
||
paint: {
|
||
'line-color': '#818cf8',
|
||
'line-width': 2.5,
|
||
'line-dasharray': [3, 2]
|
||
}
|
||
});
|
||
|
||
// Dynamic On-the-Fly Contours from Elevation DEM
|
||
const demSource = getSharedDemSource();
|
||
const contourUrl = demSource.contourProtocolUrl({
|
||
thresholds: {
|
||
10: [100, 500],
|
||
11: [50, 250],
|
||
12: [25, 100],
|
||
13: [20, 100],
|
||
14: [10, 50],
|
||
15: [10, 50],
|
||
16: [10, 50],
|
||
17: [10, 50],
|
||
18: [10, 50],
|
||
},
|
||
elevationKey: 'ele',
|
||
levelKey: 'level',
|
||
contourLayer: 'contours',
|
||
});
|
||
|
||
if (!initialMap.getSource('contour-source')) {
|
||
initialMap.addSource('contour-source', {
|
||
type: 'vector',
|
||
tiles: [contourUrl],
|
||
maxzoom: 18,
|
||
});
|
||
|
||
initialMap.addLayer({
|
||
id: 'contour-lines-minor',
|
||
type: 'line',
|
||
source: 'contour-source',
|
||
'source-layer': 'contours',
|
||
minzoom: 10,
|
||
layout: {
|
||
visibility: 'visible',
|
||
},
|
||
filter: ['==', ['get', 'level'], 0],
|
||
paint: {
|
||
'line-color': '#a86324',
|
||
'line-width': 1.1,
|
||
'line-opacity': 0.85,
|
||
},
|
||
});
|
||
|
||
initialMap.addLayer({
|
||
id: 'contour-lines-major',
|
||
type: 'line',
|
||
source: 'contour-source',
|
||
'source-layer': 'contours',
|
||
minzoom: 9,
|
||
layout: {
|
||
visibility: 'visible',
|
||
},
|
||
filter: ['>', ['get', 'level'], 0],
|
||
paint: {
|
||
'line-color': '#703800',
|
||
'line-width': 2.0,
|
||
'line-opacity': 0.95,
|
||
},
|
||
});
|
||
|
||
initialMap.addLayer({
|
||
id: 'contour-labels',
|
||
type: 'symbol',
|
||
source: 'contour-source',
|
||
'source-layer': 'contours',
|
||
minzoom: 12,
|
||
layout: {
|
||
visibility: 'visible',
|
||
'symbol-placement': 'line',
|
||
'text-field': ['concat', ['to-string', ['get', 'ele']], ' m'],
|
||
'text-size': 10,
|
||
'text-font': ['Noto Sans Bold', 'Open Sans Bold'],
|
||
},
|
||
paint: {
|
||
'text-color': '#5c2d00',
|
||
'text-halo-color': 'rgba(255, 255, 255, 0.95)',
|
||
'text-halo-width': 2,
|
||
},
|
||
});
|
||
}
|
||
|
||
// Rich Spatial Feature Layers for Terrain Study (Slope, Wadis, Roads, Urban, Hazards)
|
||
initialMap.addSource('tactical-terrain-spatial-src', {
|
||
type: 'geojson',
|
||
data: { type: 'FeatureCollection', features: [] }
|
||
});
|
||
|
||
initialMap.addLayer({
|
||
id: 'tactical-terrain-spatial-poly-fill',
|
||
type: 'fill',
|
||
source: 'tactical-terrain-spatial-src',
|
||
filter: ['==', '$type', 'Polygon'],
|
||
paint: {
|
||
'fill-color': ['get', 'color'],
|
||
'fill-opacity': [
|
||
'match',
|
||
['get', 'layerType'],
|
||
'cliff', 0.38,
|
||
'urban', 0.20,
|
||
'hazard', 0.24,
|
||
'slope-sector', 0.14,
|
||
0.15
|
||
]
|
||
}
|
||
});
|
||
|
||
initialMap.addLayer({
|
||
id: 'tactical-terrain-spatial-poly-line',
|
||
type: 'line',
|
||
source: 'tactical-terrain-spatial-src',
|
||
filter: ['==', '$type', 'Polygon'],
|
||
paint: {
|
||
'line-color': [
|
||
'match',
|
||
['get', 'layerType'],
|
||
'cliff', '#ef4444',
|
||
'rgba(255, 255, 255, 0.10)'
|
||
],
|
||
'line-width': [
|
||
'match',
|
||
['get', 'layerType'],
|
||
'cliff', 1.8,
|
||
0.4
|
||
]
|
||
}
|
||
});
|
||
|
||
initialMap.addLayer({
|
||
id: 'tactical-terrain-spatial-lines',
|
||
type: 'line',
|
||
source: 'tactical-terrain-spatial-src',
|
||
filter: ['==', '$type', 'LineString'],
|
||
paint: {
|
||
'line-color': ['get', 'color'],
|
||
'line-width': [
|
||
'match',
|
||
['get', 'layerType'],
|
||
'cliff', 3.5,
|
||
'road', 4.0,
|
||
'wadi', 3.5,
|
||
'ridge', 2.5,
|
||
2.5
|
||
],
|
||
'line-dasharray': [
|
||
'match',
|
||
['get', 'layerType'],
|
||
'cliff', ['literal', [2, 1]],
|
||
'ridge', ['literal', [3, 1.5]],
|
||
'road', ['literal', [1, 0]],
|
||
'wadi', ['literal', [1, 0]],
|
||
['literal', [1, 0]]
|
||
]
|
||
}
|
||
});
|
||
|
||
initialMap.addLayer({
|
||
id: 'tactical-terrain-spatial-points',
|
||
type: 'circle',
|
||
source: 'tactical-terrain-spatial-src',
|
||
filter: ['==', '$type', 'Point'],
|
||
paint: {
|
||
'circle-radius': 5,
|
||
'circle-color': ['coalesce', ['get', 'color'], '#38bdf8'],
|
||
'circle-stroke-width': 2,
|
||
'circle-stroke-color': '#ffffff'
|
||
}
|
||
});
|
||
|
||
initialMap.addLayer({
|
||
id: 'tactical-terrain-spatial-labels',
|
||
type: 'symbol',
|
||
source: 'tactical-terrain-spatial-src',
|
||
filter: ['all', ['==', '$type', 'Point'], ['has', 'title']],
|
||
layout: {
|
||
'text-field': ['get', 'title'],
|
||
'text-size': 12,
|
||
'text-font': ['Noto Sans Bold', 'Open Sans Bold'],
|
||
'text-anchor': 'bottom',
|
||
'text-offset': [0, -0.6],
|
||
'symbol-placement': 'point',
|
||
'text-allow-overlap': false
|
||
},
|
||
paint: {
|
||
'text-color': '#ffffff',
|
||
'text-halo-color': '#0f172a',
|
||
'text-halo-width': 2.5
|
||
}
|
||
});
|
||
|
||
// 1. Line of Sight Layer
|
||
initialMap.addSource('tactical-los-src', {
|
||
type: 'geojson',
|
||
data: { type: 'FeatureCollection', features: [] }
|
||
});
|
||
|
||
initialMap.addLayer({
|
||
id: 'tactical-los-line',
|
||
type: 'line',
|
||
source: 'tactical-los-src',
|
||
filter: ['==', '$type', 'LineString'],
|
||
paint: {
|
||
'line-color': ['case', ['==', ['get', 'blocked'], true], '#ef4444', '#22c55e'],
|
||
'line-width': 4,
|
||
'line-dasharray': [2, 1]
|
||
}
|
||
});
|
||
|
||
// 2. Viewshed 360 Polygon Layer
|
||
initialMap.addSource('tactical-viewshed-src', {
|
||
type: 'geojson',
|
||
data: { type: 'FeatureCollection', features: [] }
|
||
});
|
||
|
||
initialMap.addLayer({
|
||
id: 'tactical-viewshed-fill',
|
||
type: 'fill',
|
||
source: 'tactical-viewshed-src',
|
||
paint: {
|
||
'fill-color': '#22c55e',
|
||
'fill-opacity': 0.25
|
||
}
|
||
});
|
||
|
||
initialMap.addLayer({
|
||
id: 'tactical-viewshed-outline',
|
||
type: 'line',
|
||
source: 'tactical-viewshed-src',
|
||
paint: {
|
||
'line-color': '#4ade80',
|
||
'line-width': 2,
|
||
'line-dasharray': [3, 2]
|
||
}
|
||
});
|
||
|
||
// 3. Artillery Trajectory Layer
|
||
initialMap.addSource('tactical-artillery-src', {
|
||
type: 'geojson',
|
||
data: { type: 'FeatureCollection', features: [] }
|
||
});
|
||
|
||
initialMap.addLayer({
|
||
id: 'tactical-artillery-line',
|
||
type: 'line',
|
||
source: 'tactical-artillery-src',
|
||
paint: {
|
||
'line-color': '#f97316',
|
||
'line-width': 4
|
||
}
|
||
});
|
||
|
||
// 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: {
|
||
type: 'FeatureCollection',
|
||
features: DEFAULT_SYMBOLS.map(s => ({
|
||
type: 'Feature',
|
||
properties: { type: s.type, name: s.name },
|
||
geometry: { type: 'Point', coordinates: [s.lng, s.lat] }
|
||
})) as any
|
||
}
|
||
});
|
||
|
||
initialMap.addLayer({
|
||
id: 'tactical-symbols-pts',
|
||
type: 'circle',
|
||
source: 'tactical-symbols-src',
|
||
paint: {
|
||
'circle-radius': 9,
|
||
'circle-color': [
|
||
'match',
|
||
['get', 'type'],
|
||
'friendly', '#3b82f6',
|
||
'hostile', '#ef4444',
|
||
'artillery', '#f97316',
|
||
'radar', '#06b6d4',
|
||
'checkpoint', '#eab308',
|
||
'minefield', '#dc2626',
|
||
'hlz', '#10b981',
|
||
'#a855f7'
|
||
],
|
||
'circle-stroke-color': '#ffffff',
|
||
'circle-stroke-width': 2.5
|
||
}
|
||
});
|
||
|
||
initialMap.addLayer({
|
||
id: 'tactical-symbols-lbls',
|
||
type: 'symbol',
|
||
source: 'tactical-symbols-src',
|
||
layout: {
|
||
'text-field': ['get', 'name'],
|
||
'text-size': 11,
|
||
'text-font': ['Noto Sans Bold', 'Open Sans Bold'],
|
||
'text-offset': [0, 1.4],
|
||
'text-anchor': 'top'
|
||
},
|
||
paint: {
|
||
'text-color': '#ffffff',
|
||
'text-halo-color': '#0f172a',
|
||
'text-halo-width': 2
|
||
}
|
||
});
|
||
|
||
// 6. Isochrone Reachability Polygons Layer (خارطة زمن الاستجابة والوصول)
|
||
initialMap.addSource('tactical-isochrone-src', {
|
||
type: 'geojson',
|
||
data: { type: 'FeatureCollection', features: [] }
|
||
});
|
||
|
||
initialMap.addLayer({
|
||
id: 'tactical-isochrone-fill',
|
||
type: 'fill',
|
||
source: 'tactical-isochrone-src',
|
||
paint: {
|
||
'fill-color': ['coalesce', ['get', 'color'], '#22c55e'],
|
||
'fill-opacity': 0.32
|
||
}
|
||
});
|
||
|
||
initialMap.addLayer({
|
||
id: 'tactical-isochrone-line',
|
||
type: 'line',
|
||
source: 'tactical-isochrone-src',
|
||
paint: {
|
||
'line-color': ['coalesce', ['get', 'color'], '#22c55e'],
|
||
'line-width': 2.5,
|
||
'line-opacity': 0.95
|
||
}
|
||
});
|
||
});
|
||
|
||
return () => {
|
||
if (map.current) {
|
||
map.current.remove();
|
||
map.current = null;
|
||
}
|
||
};
|
||
}, []);
|
||
|
||
// Run Terrain Tactical Study
|
||
const runTerrainStudy = () => {
|
||
if (!terrainCenter) return;
|
||
setTerrainLoading(true);
|
||
calculateTerrainStudy(terrainCenter[0], terrainCenter[1], terrainRadius)
|
||
.then((res) => {
|
||
setTerrainResult(res);
|
||
setTerrainLoading(false);
|
||
|
||
// 1. 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));
|
||
};
|
||
|
||
// Toggle Contour Layers Visibility
|
||
useEffect(() => {
|
||
if (!map.current) return;
|
||
['contour-lines-minor', 'contour-lines-major', 'contour-labels'].forEach(layerId => {
|
||
if (map.current!.getLayer(layerId)) {
|
||
map.current!.setLayoutProperty(layerId, 'visibility', showContours ? 'visible' : 'none');
|
||
}
|
||
});
|
||
}, [showContours]);
|
||
|
||
// Sync Spatial GeoJSON Features with Active Layer Toggles
|
||
useEffect(() => {
|
||
if (!map.current || !map.current.getSource('tactical-terrain-spatial-src')) return;
|
||
if (!terrainResult?.spatialGeoJson || mode !== 'terrain') {
|
||
(map.current.getSource('tactical-terrain-spatial-src') as maplibregl.GeoJSONSource).setData({
|
||
type: 'FeatureCollection',
|
||
features: []
|
||
});
|
||
return;
|
||
}
|
||
|
||
const filtered = terrainResult.spatialGeoJson.features.filter((f: any) => {
|
||
if (f.properties.category === 'peak' || f.properties.category === 'valley' || f.properties.layerType === 'ridge') return showPeaksAndValleys;
|
||
if (f.properties.layerType === 'slope-sector') return showSlopeSectors;
|
||
if (f.properties.layerType === 'wadi') return showNaturalObstacles;
|
||
if (f.properties.layerType === 'road') return showRoadCorridors;
|
||
if (f.properties.layerType === 'urban') return showUrbanZones;
|
||
if (f.properties.layerType === 'hazard' || f.properties.layerType === 'cliff') return showHazardousGround;
|
||
return true;
|
||
});
|
||
|
||
(map.current.getSource('tactical-terrain-spatial-src') as maplibregl.GeoJSONSource).setData({
|
||
type: 'FeatureCollection',
|
||
features: filtered
|
||
});
|
||
}, [terrainResult, showPeaksAndValleys, showSlopeSectors, showNaturalObstacles, showRoadCorridors, showUrbanZones, showHazardousGround, mode]);
|
||
|
||
// Auto-run initial terrain study on mount & coordinates change
|
||
useEffect(() => {
|
||
runTerrainStudy();
|
||
}, [terrainCenter, terrainRadius]);
|
||
|
||
// Update Line of Sight Calculations & Map Layer
|
||
useEffect(() => {
|
||
if (!losPointA || !losPointB) {
|
||
if (map.current && map.current.getSource('tactical-los-src')) {
|
||
(map.current.getSource('tactical-los-src') as maplibregl.GeoJSONSource).setData({
|
||
type: 'FeatureCollection',
|
||
features: []
|
||
});
|
||
}
|
||
setLosResult(null);
|
||
return;
|
||
}
|
||
|
||
setLosLoading(true);
|
||
calculateLineOfSight(losPointA[0], losPointA[1], losPointB[0], losPointB[1], losObsHeight, losTgtHeight, 70)
|
||
.then((res) => {
|
||
setLosResult(res);
|
||
setLosLoading(false);
|
||
|
||
if (map.current && map.current.getSource('tactical-los-src')) {
|
||
const feats: any[] = [
|
||
{
|
||
type: 'Feature',
|
||
properties: { role: 'ray', blocked: !res.isDirectlyVisible },
|
||
geometry: {
|
||
type: 'LineString',
|
||
coordinates: [[losPointA[1], losPointA[0]], [losPointB[1], losPointB[0]]]
|
||
}
|
||
}
|
||
];
|
||
|
||
if (res.highestObstacle) {
|
||
feats.push({
|
||
type: 'Feature',
|
||
properties: { role: 'obstacle', label: 'عائق' },
|
||
geometry: { type: 'Point', coordinates: [res.highestObstacle.lng, res.highestObstacle.lat] }
|
||
});
|
||
}
|
||
|
||
(map.current.getSource('tactical-los-src') as maplibregl.GeoJSONSource).setData({
|
||
type: 'FeatureCollection',
|
||
features: feats
|
||
});
|
||
}
|
||
})
|
||
.catch(() => setLosLoading(false));
|
||
}, [losPointA, losPointB, losObsHeight, losTgtHeight]);
|
||
|
||
// Update Minefield Analysis & Map Layer
|
||
useEffect(() => {
|
||
if (!mineStart || !mineEnd) {
|
||
if (map.current && map.current.getSource('tactical-minefield-src')) {
|
||
(map.current.getSource('tactical-minefield-src') as maplibregl.GeoJSONSource).setData({
|
||
type: 'FeatureCollection',
|
||
features: []
|
||
});
|
||
}
|
||
setMineResult(null);
|
||
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 Artillery Trajectory
|
||
useEffect(() => {
|
||
if (!gunPos || !targetPos) {
|
||
if (map.current && map.current.getSource('tactical-artillery-src')) {
|
||
(map.current.getSource('tactical-artillery-src') as maplibregl.GeoJSONSource).setData({
|
||
type: 'FeatureCollection',
|
||
features: []
|
||
});
|
||
}
|
||
setArtilleryResult(null);
|
||
return;
|
||
}
|
||
runArtilleryMission();
|
||
}, [gunPos, targetPos, caliber, muzzleVel]);
|
||
|
||
// Update 360 Viewshed
|
||
const runViewshed360 = () => {
|
||
if (!viewshedCenter) return;
|
||
setViewshedLoading(true);
|
||
calculateRadialViewshed(viewshedCenter[0], viewshedCenter[1], viewshedHeight, viewshedRadius, 36)
|
||
.then((res) => {
|
||
setViewshedResult(res);
|
||
setViewshedLoading(false);
|
||
|
||
if (map.current && map.current.getSource('tactical-viewshed-src')) {
|
||
(map.current.getSource('tactical-viewshed-src') as maplibregl.GeoJSONSource).setData({
|
||
type: 'FeatureCollection',
|
||
features: [res.polygonGeoJson]
|
||
});
|
||
}
|
||
})
|
||
.catch(() => setViewshedLoading(false));
|
||
};
|
||
|
||
// Run Artillery Fire Mission
|
||
const runArtilleryMission = async () => {
|
||
if (!gunPos || !targetPos) return;
|
||
setArtilleryLoading(true);
|
||
try {
|
||
const apiUrl = (import.meta as any).env.VITE_API_URL || '/api';
|
||
const keyToSend = authKey || (import.meta as any).env.VITE_API_KEY || '';
|
||
|
||
const res = await fetch(`${apiUrl}/tactical/artillery-fire-mission`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'x-api-key': keyToSend },
|
||
body: JSON.stringify({
|
||
gunLat: gunPos[0],
|
||
gunLng: gunPos[1],
|
||
targetLat: targetPos[0],
|
||
targetLng: targetPos[1],
|
||
caliber,
|
||
muzzleVelocity: muzzleVel
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
setArtilleryResult(data);
|
||
|
||
if (map.current && map.current.getSource('tactical-artillery-src')) {
|
||
const lineCoords = (data.trajectoryPoints || []).map((p: any) => [p.lng, p.lat]);
|
||
(map.current.getSource('tactical-artillery-src') as maplibregl.GeoJSONSource).setData({
|
||
type: 'FeatureCollection',
|
||
features: [
|
||
{
|
||
type: 'Feature',
|
||
properties: {},
|
||
geometry: { type: 'LineString', coordinates: lineCoords }
|
||
}
|
||
]
|
||
});
|
||
}
|
||
} catch (e) {
|
||
console.error('Artillery calculation error:', e);
|
||
} finally {
|
||
setArtilleryLoading(false);
|
||
}
|
||
};
|
||
|
||
// Run HLZ Assessment
|
||
const runHLZAssessment = async () => {
|
||
if (!hlzCenter) return;
|
||
setHlzLoading(true);
|
||
try {
|
||
const apiUrl = (import.meta as any).env.VITE_API_URL || '/api';
|
||
const keyToSend = authKey || (import.meta as any).env.VITE_API_KEY || '';
|
||
|
||
const res = await fetch(`${apiUrl}/tactical/hlz-assessment?lat=${hlzCenter[0]}&lng=${hlzCenter[1]}&radius=4000`, {
|
||
headers: { 'x-api-key': keyToSend }
|
||
});
|
||
const data = await res.json();
|
||
setHlzResult(data);
|
||
} catch (e) {
|
||
console.error('HLZ error:', e);
|
||
} finally {
|
||
setHlzLoading(false);
|
||
}
|
||
};
|
||
|
||
// Helper for High-Precision Tactical Isochrone Polygons (نطاقات زمن الوصول والاستجابة)
|
||
const generateLocalIsochrone = (lat: number, lng: number, timeBuckets: number[], profile: string) => {
|
||
const avgSpeedKmh = profile === 'emergency' ? 60 : profile === 'patrol' ? 50 : 38;
|
||
const colors = ['#22c55e', '#eab308', '#ef4444', '#8b5cf6'];
|
||
const tiers = timeBuckets.map((seconds, idx) => {
|
||
const minutes = Math.round(seconds / 60);
|
||
const effectiveRadiusM = (avgSpeedKmh * 1000 / 3600) * seconds * 0.70;
|
||
const numPoints = 36;
|
||
const ringCoords: [number, number][] = [];
|
||
for (let i = 0; i < numPoints; i++) {
|
||
const angle = (i * 2 * Math.PI) / numPoints;
|
||
const roadCorridor = 1 + 0.14 * Math.sin(angle * 4) + 0.08 * Math.cos(angle * 2);
|
||
const r = effectiveRadiusM * roadCorridor;
|
||
const dLat = (r / 6371000) * (180 / Math.PI) * Math.cos(angle);
|
||
const dLng = (r / (6371000 * Math.cos((lat * Math.PI) / 180))) * (180 / Math.PI) * Math.sin(angle);
|
||
ringCoords.push([Number((lng + dLng).toFixed(6)), Number((lat + dLat).toFixed(6))]);
|
||
}
|
||
ringCoords.push(ringCoords[0]);
|
||
const tierColor = colors[idx % colors.length];
|
||
const areaKm2 = Math.round(Math.PI * Math.pow(effectiveRadiusM / 1000, 2) * 10) / 10;
|
||
|
||
return {
|
||
timeSeconds: seconds,
|
||
timeMinutes: minutes,
|
||
label: `${minutes} دقائق استجابة`,
|
||
color: tierColor,
|
||
estimatedAreaKm2: areaKm2,
|
||
polygon: {
|
||
type: 'Feature',
|
||
properties: {
|
||
timeMinutes: minutes,
|
||
timeSeconds: seconds,
|
||
color: tierColor,
|
||
label: `${minutes} دقائق`
|
||
},
|
||
geometry: {
|
||
type: 'Polygon',
|
||
coordinates: [ringCoords]
|
||
}
|
||
}
|
||
};
|
||
});
|
||
|
||
return {
|
||
center: { lat, lng },
|
||
vehicleProfile: profile,
|
||
tiers,
|
||
featureCollection: {
|
||
type: 'FeatureCollection',
|
||
features: tiers.map(t => t.polygon)
|
||
}
|
||
};
|
||
};
|
||
|
||
// Synchronize Isochrone Data to Map
|
||
useEffect(() => {
|
||
if (!map.current || !map.current.getSource('tactical-isochrone-src')) return;
|
||
if (mode === 'isochrone' && isochroneData?.featureCollection) {
|
||
(map.current.getSource('tactical-isochrone-src') as maplibregl.GeoJSONSource).setData(isochroneData.featureCollection);
|
||
} else {
|
||
(map.current.getSource('tactical-isochrone-src') as maplibregl.GeoJSONSource).setData({
|
||
type: 'FeatureCollection',
|
||
features: []
|
||
});
|
||
}
|
||
}, [mode, isochroneData]);
|
||
|
||
// Run Isochrone Calculation
|
||
const runIsochroneCalculation = async (center = isochroneCenter, profile = isochroneProfile, times = isochroneTimes) => {
|
||
if (!center) return;
|
||
setIsochroneLoading(true);
|
||
try {
|
||
const apiUrl = (import.meta as any).env.VITE_API_URL || '/api';
|
||
const keyToSend = authKey || (import.meta as any).env.VITE_API_KEY || '';
|
||
const timesParam = times.join(',');
|
||
|
||
const res = await fetch(
|
||
`${apiUrl}/tactical/isochrone?lat=${center[0]}&lng=${center[1]}&timeBuckets=${timesParam}&profile=${profile}`,
|
||
{
|
||
headers: { 'x-api-key': keyToSend }
|
||
}
|
||
);
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
setIsochroneData(data);
|
||
} else {
|
||
const fallback = generateLocalIsochrone(center[0], center[1], times, profile);
|
||
setIsochroneData(fallback);
|
||
}
|
||
} catch (e) {
|
||
console.warn('Isochrone API fallback to local GIS engine:', e);
|
||
const fallback = generateLocalIsochrone(center[0], center[1], times, profile);
|
||
setIsochroneData(fallback);
|
||
} finally {
|
||
setIsochroneLoading(false);
|
||
}
|
||
};
|
||
|
||
// Synchronize Tactical Symbols to Map
|
||
useEffect(() => {
|
||
if (!map.current || !map.current.getSource('tactical-symbols-src')) return;
|
||
const feats = symbols.map(s => ({
|
||
type: 'Feature',
|
||
properties: { type: s.type, name: s.name },
|
||
geometry: { type: 'Point', coordinates: [s.lng, s.lat] }
|
||
}));
|
||
(map.current.getSource('tactical-symbols-src') as maplibregl.GeoJSONSource).setData({
|
||
type: 'FeatureCollection',
|
||
features: feats as any
|
||
});
|
||
}, [symbols]);
|
||
|
||
// Jump to Sector and Adapt Operational Points
|
||
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
|
||
});
|
||
|
||
const lat = sector.lat;
|
||
const lng = sector.lng;
|
||
|
||
setTerrainCenter([Number(lat.toFixed(5)), Number(lng.toFixed(5))]);
|
||
setIsochroneCenter([Number(lat.toFixed(5)), Number(lng.toFixed(5))]);
|
||
setSelectedHubName(`قطاع ${sector.name}`);
|
||
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))]);
|
||
setGunPos([Number((lat - 0.03).toFixed(5)), Number((lng - 0.02).toFixed(5))]);
|
||
setTargetPos([Number((lat + 0.04).toFixed(5)), Number((lng + 0.03).toFixed(5))]);
|
||
setMineStart([Number((lat + 0.01).toFixed(5)), Number((lng - 0.015).toFixed(5))]);
|
||
setMineEnd([Number((lat + 0.02).toFixed(5)), Number((lng + 0.015).toFixed(5))]);
|
||
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.highestPoint.lat}, ${terrainResult.highestPoint.lng}) | أخفض نقطة ${terrainResult.minElevation}م (${terrainResult.lowestPoint.lat}, ${terrainResult.lowestPoint.lng}) | الفارق التضاريسي ${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',
|
||
height: '100vh',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
background: '#0a0e17',
|
||
color: '#f8fafc',
|
||
fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "SF Pro Arabic", system-ui, sans-serif',
|
||
direction: 'rtl',
|
||
overflow: 'hidden',
|
||
paddingTop: 56
|
||
}}>
|
||
{/* Top Military HUD Header */}
|
||
<header style={{
|
||
height: 52,
|
||
background: '#0f172a',
|
||
borderBottom: '1px solid #1e293b',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
padding: '0 18px',
|
||
zIndex: 50,
|
||
gap: 12,
|
||
flexShrink: 0
|
||
}}>
|
||
{/* Title */}
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||
<div style={{
|
||
width: 32,
|
||
height: 32,
|
||
borderRadius: 8,
|
||
background: 'linear-gradient(135deg, #4f46e5, #06b6d4)',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
boxShadow: '0 0 16px rgba(79, 70, 229, 0.5)'
|
||
}}>
|
||
<Shield size={17} color="#ffffff" />
|
||
</div>
|
||
<div>
|
||
<div style={{ fontWeight: 800, fontSize: '0.92rem', letterSpacing: '-0.02em', display: 'flex', alignItems: 'center', gap: 6 }}>
|
||
<span>منظومة القيادة والسيطرة والتحليل التكتيكي</span>
|
||
<span style={{ fontSize: '10px', background: 'rgba(34, 197, 94, 0.2)', color: '#4ade80', padding: '1px 6px', borderRadius: 4 }}>
|
||
C4ISR Grid
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 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: 'isochrone', label: 'زمن الاستجابة (Isochrone)', icon: <Clock 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} /> },
|
||
{ id: 'minefield', label: 'حقل الألغام', icon: <AlertOctagon size={13} /> },
|
||
{ id: 'hlz', label: 'مهابط المروحيات', icon: <Crosshair size={13} /> },
|
||
{ id: 'symbols', label: 'الرموز والتشكيلات', icon: <Layers size={13} /> },
|
||
{ id: 'study', label: 'دراسة التنظيم', icon: <Award size={13} /> },
|
||
].map(m => (
|
||
<button
|
||
key={m.id}
|
||
onClick={() => {
|
||
setMode(m.id as TacticalMode);
|
||
setActivePlacement(null);
|
||
}}
|
||
style={{
|
||
background: mode === m.id ? 'linear-gradient(135deg, #4f46e5, #6366f1)' : 'transparent',
|
||
border: 'none',
|
||
color: mode === m.id ? '#ffffff' : '#94a3b8',
|
||
borderRadius: 7,
|
||
padding: '5px 10px',
|
||
fontSize: '0.78rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 5,
|
||
transition: 'all 0.15s ease'
|
||
}}
|
||
>
|
||
{m.icon}
|
||
<span>{m.label}</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Realtime Cursor HUD & Clearance Status */}
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 14, fontSize: '0.72rem', fontFamily: 'monospace', color: '#94a3b8' }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<div>LAT: <span style={{ color: '#38bdf8' }}>{cursorPos.lat}</span></div>
|
||
<div>LNG: <span style={{ color: '#38bdf8' }}>{cursorPos.lng}</span></div>
|
||
<div>ELEV: <span style={{ color: '#4ade80' }}>{cursorPos.elev}m</span></div>
|
||
</div>
|
||
|
||
{authStatus === 'authorized' && (
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, borderRight: '1px solid rgba(255,255,255,0.15)', paddingRight: 10 }}>
|
||
<div style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 5,
|
||
background: 'rgba(34, 197, 94, 0.15)',
|
||
border: '1px solid rgba(34, 197, 94, 0.35)',
|
||
color: '#4ade80',
|
||
padding: '3px 8px',
|
||
borderRadius: 6,
|
||
fontSize: '0.7rem',
|
||
fontWeight: 700,
|
||
fontFamily: 'inherit'
|
||
}}>
|
||
<ShieldCheck size={13} />
|
||
<span>تصريح: {authInfo?.tenantName || 'Enterprise'}</span>
|
||
</div>
|
||
|
||
<button
|
||
onClick={handleRevokeOrLock}
|
||
title="قفل المنظومة التكتيكية / خروج"
|
||
style={{
|
||
background: 'rgba(239, 68, 68, 0.15)',
|
||
border: '1px solid rgba(239, 68, 68, 0.3)',
|
||
color: '#f87171',
|
||
padding: '3px 8px',
|
||
borderRadius: 6,
|
||
fontSize: '0.7rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 4,
|
||
fontFamily: 'inherit',
|
||
transition: 'all 0.15s ease'
|
||
}}
|
||
>
|
||
<Lock size={12} />
|
||
<span>قفل</span>
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</header>
|
||
|
||
{/* Quick Sector Presets Bar */}
|
||
<div style={{
|
||
height: 38,
|
||
background: '#0b1329',
|
||
borderBottom: '1px solid rgba(255,255,255,0.08)',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
padding: '0 16px',
|
||
gap: 8,
|
||
fontSize: '0.75rem',
|
||
overflowX: 'auto',
|
||
flexShrink: 0
|
||
}}>
|
||
<span style={{ color: '#94a3b8', display: 'flex', alignItems: 'center', gap: 4, fontWeight: 700 }}>
|
||
<MapPin size={13} color="#38bdf8" /> الانتقال السريع لقطاعات العمليات:
|
||
</span>
|
||
{REGIONAL_SECTORS.map((s, idx) => (
|
||
<button
|
||
key={idx}
|
||
onClick={() => flyToSector(s)}
|
||
style={{
|
||
background: 'rgba(56, 189, 248, 0.1)',
|
||
border: '1px solid rgba(56, 189, 248, 0.25)',
|
||
color: '#38bdf8',
|
||
padding: '3px 10px',
|
||
borderRadius: 6,
|
||
fontSize: '0.72rem',
|
||
fontWeight: 600,
|
||
cursor: 'pointer',
|
||
whiteSpace: 'nowrap',
|
||
transition: 'all 0.15s ease'
|
||
}}
|
||
>
|
||
{s.name}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Main Tactical Workspace */}
|
||
<div style={{ flex: 1, display: 'flex', position: 'relative', overflow: 'hidden' }}>
|
||
|
||
{/* Left Side Tactical Control Deck */}
|
||
<aside style={{
|
||
width: 440,
|
||
background: 'rgba(15, 23, 42, 0.96)',
|
||
backdropFilter: 'blur(20px)',
|
||
borderLeft: '1px solid rgba(255, 255, 255, 0.12)',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
zIndex: 20,
|
||
overflowY: 'auto',
|
||
padding: 16
|
||
}}>
|
||
{/* Active Placement Alert Banner */}
|
||
{activePlacement && (
|
||
<div style={{
|
||
background: 'rgba(234, 179, 8, 0.25)',
|
||
border: '1px solid #eab308',
|
||
color: '#fef08a',
|
||
padding: '10px 12px',
|
||
borderRadius: 8,
|
||
fontSize: '0.82rem',
|
||
fontWeight: 700,
|
||
marginBottom: 12,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
animation: 'pulse 1.5s infinite'
|
||
}}>
|
||
<span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||
<Target size={16} color="#eab308" />
|
||
<span>انقر الآن على أي مكان بالخريطة لتحديد الموقع المطلوب.</span>
|
||
</span>
|
||
<button
|
||
onClick={() => setActivePlacement(null)}
|
||
style={{ background: 'rgba(0,0,0,0.3)', border: '1px solid #eab308', color: '#fef08a', padding: '2px 6px', borderRadius: 4, cursor: 'pointer', fontSize: '0.75rem' }}
|
||
>
|
||
إلغاء ✕
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* Interactive Drag Hint */}
|
||
<div style={{
|
||
background: 'rgba(56, 189, 248, 0.08)',
|
||
border: '1px solid rgba(56, 189, 248, 0.2)',
|
||
padding: '6px 10px',
|
||
borderRadius: 6,
|
||
fontSize: '0.72rem',
|
||
color: '#7dd3fc',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 6,
|
||
marginBottom: 12
|
||
}}>
|
||
<Move size={13} color="#38bdf8" />
|
||
<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>
|
||
|
||
{/* Map Layer Overlays Control Deck */}
|
||
<div style={{
|
||
background: 'rgba(99, 102, 241, 0.08)',
|
||
border: '1px solid rgba(99, 102, 241, 0.25)',
|
||
padding: '10px',
|
||
borderRadius: 8
|
||
}}>
|
||
<div style={{ fontSize: '0.75rem', fontWeight: 800, color: '#a5b4fc', marginBottom: 8, display: 'flex', alignItems: 'center', gap: 6 }}>
|
||
<Sliders size={14} /> طبقات الإظهار اللوني على الخريطة:
|
||
</div>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
|
||
<button
|
||
onClick={() => setShowContours(!showContours)}
|
||
style={{
|
||
background: showContours ? 'rgba(245, 158, 11, 0.2)' : 'rgba(255,255,255,0.05)',
|
||
border: showContours ? '1px solid #f59e0b' : '1px solid rgba(255,255,255,0.1)',
|
||
color: showContours ? '#f59e0b' : '#94a3b8',
|
||
padding: '5px 8px',
|
||
borderRadius: 6,
|
||
fontSize: '0.68rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer',
|
||
textAlign: 'right',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 4
|
||
}}
|
||
>
|
||
<span>{showContours ? '✅' : '⬜'}</span>
|
||
<span>〰️ خطوط الكنتور</span>
|
||
</button>
|
||
<button
|
||
onClick={() => setShowPeaksAndValleys(!showPeaksAndValleys)}
|
||
style={{
|
||
background: showPeaksAndValleys ? 'rgba(56, 189, 248, 0.2)' : 'rgba(255,255,255,0.05)',
|
||
border: showPeaksAndValleys ? '1px solid #38bdf8' : '1px solid rgba(255,255,255,0.1)',
|
||
color: showPeaksAndValleys ? '#38bdf8' : '#94a3b8',
|
||
padding: '5px 8px',
|
||
borderRadius: 6,
|
||
fontSize: '0.68rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer',
|
||
textAlign: 'right',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 4
|
||
}}
|
||
>
|
||
<span>{showPeaksAndValleys ? '✅' : '⬜'}</span>
|
||
<span>🔺 أعلى وأخفض نقطة</span>
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setShowSlopeSectors(!showSlopeSectors)}
|
||
style={{
|
||
background: showSlopeSectors ? 'rgba(34, 197, 94, 0.2)' : 'rgba(255,255,255,0.05)',
|
||
border: showSlopeSectors ? '1px solid #22c55e' : '1px solid rgba(255,255,255,0.1)',
|
||
color: showSlopeSectors ? '#4ade80' : '#94a3b8',
|
||
padding: '5px 8px',
|
||
borderRadius: 6,
|
||
fontSize: '0.68rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer',
|
||
textAlign: 'right',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 4
|
||
}}
|
||
>
|
||
<span>{showSlopeSectors ? '✅' : '⬜'}</span>
|
||
<span>🚦 ممرات الحركة والانحدار</span>
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setShowNaturalObstacles(!showNaturalObstacles)}
|
||
style={{
|
||
background: showNaturalObstacles ? 'rgba(6, 182, 212, 0.2)' : 'rgba(255,255,255,0.05)',
|
||
border: showNaturalObstacles ? '1px solid #06b6d4' : '1px solid rgba(255,255,255,0.1)',
|
||
color: showNaturalObstacles ? '#22d3ee' : '#94a3b8',
|
||
padding: '5px 8px',
|
||
borderRadius: 6,
|
||
fontSize: '0.68rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer',
|
||
textAlign: 'right',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 4
|
||
}}
|
||
>
|
||
<span>{showNaturalObstacles ? '✅' : '⬜'}</span>
|
||
<span>🌊 الأودية والسيول</span>
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setShowRoadCorridors(!showRoadCorridors)}
|
||
style={{
|
||
background: showRoadCorridors ? 'rgba(245, 158, 11, 0.2)' : 'rgba(255,255,255,0.05)',
|
||
border: showRoadCorridors ? '1px solid #f59e0b' : '1px solid rgba(255,255,255,0.1)',
|
||
color: showRoadCorridors ? '#fbbf24' : '#94a3b8',
|
||
padding: '5px 8px',
|
||
borderRadius: 6,
|
||
fontSize: '0.68rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer',
|
||
textAlign: 'right',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 4
|
||
}}
|
||
>
|
||
<span>{showRoadCorridors ? '✅' : '⬜'}</span>
|
||
<span>🛣️ الطرق والمحاور</span>
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setShowUrbanZones(!showUrbanZones)}
|
||
style={{
|
||
background: showUrbanZones ? 'rgba(168, 85, 247, 0.2)' : 'rgba(255,255,255,0.05)',
|
||
border: showUrbanZones ? '1px solid #a855f7' : '1px solid rgba(255,255,255,0.1)',
|
||
color: showUrbanZones ? '#c084fc' : '#94a3b8',
|
||
padding: '5px 8px',
|
||
borderRadius: 6,
|
||
fontSize: '0.68rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer',
|
||
textAlign: 'right',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 4
|
||
}}
|
||
>
|
||
<span>{showUrbanZones ? '✅' : '⬜'}</span>
|
||
<span>🏙️ التجمعات السكانية</span>
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setShowHazardousGround(!showHazardousGround)}
|
||
style={{
|
||
background: showHazardousGround ? 'rgba(220, 38, 38, 0.2)' : 'rgba(255,255,255,0.05)',
|
||
border: showHazardousGround ? '1px solid #dc2626' : '1px solid rgba(255,255,255,0.1)',
|
||
color: showHazardousGround ? '#f87171' : '#94a3b8',
|
||
padding: '5px 8px',
|
||
borderRadius: 6,
|
||
fontSize: '0.68rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer',
|
||
textAlign: 'right',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 4
|
||
}}
|
||
>
|
||
<span>{showHazardousGround ? '✅' : '⬜'}</span>
|
||
<span>🪨 حرات بازلتية وصخور</span>
|
||
</button>
|
||
</div>
|
||
</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>
|
||
|
||
{/* Highest Peak & Lowest Valley Cards with Direct Map Focus */}
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
|
||
<div
|
||
onClick={() => {
|
||
if (map.current) {
|
||
map.current.flyTo({ center: [terrainResult.highestPoint.lng, terrainResult.highestPoint.lat], zoom: 14, pitch: 50 });
|
||
}
|
||
}}
|
||
style={{
|
||
background: 'rgba(56, 189, 248, 0.12)',
|
||
border: '1px solid #38bdf8',
|
||
padding: 8,
|
||
borderRadius: 8,
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
<div style={{ fontSize: '0.65rem', color: '#7dd3fc', fontWeight: 700 }}>🔺 أعلى قمة تضاريسية</div>
|
||
<div style={{ fontSize: '1.05rem', fontWeight: 900, color: '#38bdf8' }}>{terrainResult.maxElevation}م</div>
|
||
<div style={{ fontSize: '0.62rem', color: '#94a3b8', fontFamily: 'monospace' }}>
|
||
{terrainResult.highestPoint.lat.toFixed(4)}, {terrainResult.highestPoint.lng.toFixed(4)}
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
onClick={() => {
|
||
if (map.current) {
|
||
map.current.flyTo({ center: [terrainResult.lowestPoint.lng, terrainResult.lowestPoint.lat], zoom: 14, pitch: 50 });
|
||
}
|
||
}}
|
||
style={{
|
||
background: 'rgba(244, 63, 94, 0.12)',
|
||
border: '1px solid #f43f5e',
|
||
padding: 8,
|
||
borderRadius: 8,
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
<div style={{ fontSize: '0.65rem', color: '#fda4af', fontWeight: 700 }}>🔻 أخفض نقطة / وادٍ</div>
|
||
<div style={{ fontSize: '1.05rem', fontWeight: 900, color: '#f43f5e' }}>{terrainResult.minElevation}م</div>
|
||
<div style={{ fontSize: '0.62rem', color: '#94a3b8', fontFamily: 'monospace' }}>
|
||
{terrainResult.lowestPoint.lat.toFixed(4)}, {terrainResult.lowestPoint.lng.toFixed(4)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Elevation & Slope Metrics Grid */}
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6, textAlign: 'center' }}>
|
||
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>الفارق التضاريسي (Relief)</div>
|
||
<div style={{ fontSize: '0.95rem', fontWeight: 800, color: '#fb923c' }}>{terrainResult.reliefMeters}م</div>
|
||
</div>
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6, textAlign: 'center' }}>
|
||
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>أقصى زاوية انحدار</div>
|
||
<div style={{ fontSize: '0.95rem', fontWeight: 800, color: '#f87171' }}>{terrainResult.maxSlopeDegrees}°</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Slope Distribution Bar (4 Military Tiers) */}
|
||
<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={`منبسط (<7°): ${terrainResult.slopeDistribution.flatPercentage}%`} />
|
||
<div style={{ width: `${terrainResult.slopeDistribution.moderatePercentage}%`, background: '#eab308' }} title={`تضاريس متموجة (7-15°): ${terrainResult.slopeDistribution.moderatePercentage}%`} />
|
||
<div style={{ width: `${terrainResult.slopeDistribution.steepPercentage}%`, background: '#f97316' }} title={`انحدار وعر (15-28°): ${terrainResult.slopeDistribution.steepPercentage}%`} />
|
||
<div style={{ width: `${terrainResult.slopeDistribution.cliffPercentage}%`, background: '#ef4444' }} title={`قواطع وجروف صخرية (≥28°): ${terrainResult.slopeDistribution.cliffPercentage}%`} />
|
||
</div>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '3px 8px', fontSize: '0.66rem', color: '#94a3b8' }}>
|
||
<span style={{ color: '#4ade80' }}>■ منبسط (<7°): {terrainResult.slopeDistribution.flatPercentage}%</span>
|
||
<span style={{ color: '#facc15' }}>■ متموج (7-15°): {terrainResult.slopeDistribution.moderatePercentage}%</span>
|
||
<span style={{ color: '#fb923c' }}>■ وعر (15-28°): {terrainResult.slopeDistribution.steepPercentage}%</span>
|
||
<span style={{ color: '#f87171' }}>■ جروف صخرية (≥28°): {terrainResult.slopeDistribution.cliffPercentage}%</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Detected Cliffs & Natural Rock Barriers */}
|
||
{terrainResult.cliffs && terrainResult.cliffs.length > 0 && (
|
||
<div style={{ background: 'rgba(239, 68, 68, 0.08)', padding: 10, borderRadius: 8, border: '1px solid rgba(239, 68, 68, 0.35)' }}>
|
||
<div style={{ fontSize: '0.76rem', fontWeight: 800, color: '#f87171', marginBottom: 6, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
<span style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
|
||
<AlertTriangle size={14} /> 🧗♂️ القواطع والجروف الصخرية المرصودة ({terrainResult.cliffsCount})
|
||
</span>
|
||
<span style={{ fontSize: '0.62rem', background: '#ef4444', color: '#fff', padding: '1px 6px', borderRadius: 10, fontWeight: 700 }}>مانع NO-GO</span>
|
||
</div>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 5, maxHeight: 160, overflowY: 'auto' }}>
|
||
{terrainResult.cliffs.map((cliff, cIdx) => (
|
||
<div
|
||
key={cIdx}
|
||
onClick={() => {
|
||
if (map.current) {
|
||
map.current.flyTo({ center: [cliff.lng, cliff.lat], zoom: 15.5, pitch: 55 });
|
||
}
|
||
}}
|
||
style={{
|
||
background: 'rgba(0,0,0,0.3)',
|
||
padding: '6px 8px',
|
||
borderRadius: 6,
|
||
cursor: 'pointer',
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
border: '1px solid rgba(239, 68, 68, 0.2)'
|
||
}}
|
||
>
|
||
<div>
|
||
<div style={{ fontSize: '0.72rem', fontWeight: 700, color: '#fca5a5' }}>
|
||
قاطع #{cIdx + 1}: سقوط {cliff.dropMeters}م (ميل {cliff.slopeDegrees}°)
|
||
</div>
|
||
<div style={{ fontSize: '0.62rem', color: '#94a3b8', fontFamily: 'monospace' }}>
|
||
{cliff.lat}, {cliff.lng}
|
||
</div>
|
||
</div>
|
||
<div style={{ fontSize: '0.65rem', color: '#f87171', fontWeight: 800, background: 'rgba(239,68,68,0.15)', padding: '2px 6px', borderRadius: 4 }}>
|
||
🔍 تركيز
|
||
</div>
|
||
</div>
|
||
))}
|
||
</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)' }}>
|
||
<h3 style={{ margin: 0, fontSize: '1rem', fontWeight: 800, color: '#38bdf8', display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<Eye size={17} /> تحليل خط النظر وتبادل الرؤية (LOS)
|
||
</h3>
|
||
<p style={{ margin: '4px 0 0 0', fontSize: '0.75rem', color: '#94a3b8' }}>
|
||
فحص الرؤية المباشرة والمناطق الميتة مع إمكانية كتابة الإحداثيات مباشرة
|
||
</p>
|
||
</div>
|
||
|
||
{/* Point A Coordinate Card */}
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8, border: '1px solid rgba(56, 189, 248, 0.2)' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||
<span style={{ fontSize: '0.78rem', fontWeight: 800, color: '#38bdf8' }}>📍 موقع الراصد (A):</span>
|
||
<button
|
||
onClick={() => setActivePlacement('los-a')}
|
||
style={{
|
||
background: activePlacement === 'los-a' ? '#0284c7' : 'rgba(56, 189, 248, 0.15)',
|
||
border: '1px solid #38bdf8',
|
||
color: '#ffffff',
|
||
padding: '3px 8px',
|
||
borderRadius: 6,
|
||
fontSize: '0.7rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
{activePlacement === 'los-a' ? '🎯 انقر على الخريطة...' : '📍 التقاط من الخريطة'}
|
||
</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={losPointA ? losPointA[0] : ''}
|
||
onChange={e => setLosPointA([Number(e.target.value), losPointA ? losPointA[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={losPointA ? losPointA[1] : ''}
|
||
onChange={e => setLosPointA([losPointA ? losPointA[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>
|
||
|
||
{/* Point B Coordinate Card */}
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8, border: '1px solid rgba(245, 158, 11, 0.2)' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||
<span style={{ fontSize: '0.78rem', fontWeight: 800, color: '#f59e0b' }}>🎯 موقع الهدف (B):</span>
|
||
<button
|
||
onClick={() => setActivePlacement('los-b')}
|
||
style={{
|
||
background: activePlacement === 'los-b' ? '#d97706' : 'rgba(245, 158, 11, 0.15)',
|
||
border: '1px solid #f59e0b',
|
||
color: '#ffffff',
|
||
padding: '3px 8px',
|
||
borderRadius: 6,
|
||
fontSize: '0.7rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
{activePlacement === 'los-b' ? '🎯 انقر على الخريطة...' : '🎯 التقاط من الخريطة'}
|
||
</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={losPointB ? losPointB[0] : ''}
|
||
onChange={e => setLosPointB([Number(e.target.value), losPointB ? losPointB[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={losPointB ? losPointB[1] : ''}
|
||
onChange={e => setLosPointB([losPointB ? losPointB[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>
|
||
|
||
{/* Heights */}
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
|
||
<div>
|
||
<label style={{ fontSize: '0.7rem', color: '#94a3b8', display: 'block', marginBottom: 4 }}>ارتفاع الراصد (A):</label>
|
||
<select
|
||
value={losObsHeight}
|
||
onChange={e => setLosObsHeight(Number(e.target.value))}
|
||
style={{ width: '100%', background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.15)', color: '#fff', padding: '6px', borderRadius: 6, fontSize: '0.75rem' }}
|
||
>
|
||
<option value={2}>شخص واقف (2م)</option>
|
||
<option value={10}>آلية / برج مراقبة (10م)</option>
|
||
<option value={50}>سارية / درون استطلاع (50م)</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style={{ fontSize: '0.7rem', color: '#94a3b8', display: 'block', marginBottom: 4 }}>ارتفاع الهدف (B):</label>
|
||
<select
|
||
value={losTgtHeight}
|
||
onChange={e => setLosTgtHeight(Number(e.target.value))}
|
||
style={{ width: '100%', background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.15)', color: '#fff', padding: '6px', borderRadius: 6, fontSize: '0.75rem' }}
|
||
>
|
||
<option value={1.8}>شخص (1.8م)</option>
|
||
<option value={3}>مركبة / مدرعة (3م)</option>
|
||
<option value={15}>مبنى / محطة رادار (15م)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
{losLoading && <div style={{ color: '#38bdf8', fontSize: '0.8rem' }}>جاري حساب المقطع وشعاع الرؤية...</div>}
|
||
|
||
{losResult && (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||
<div style={{
|
||
padding: '10px',
|
||
borderRadius: 8,
|
||
background: losResult.isDirectlyVisible ? 'rgba(34, 197, 94, 0.15)' : 'rgba(239, 68, 68, 0.15)',
|
||
border: losResult.isDirectlyVisible ? '1px solid #22c55e' : '1px solid #ef4444',
|
||
color: losResult.isDirectlyVisible ? '#4ade80' : '#f87171',
|
||
fontWeight: 700,
|
||
fontSize: '0.82rem',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 8
|
||
}}>
|
||
{losResult.isDirectlyVisible ? <CheckCircle2 size={17} /> : <XCircle size={17} />}
|
||
<span>{losResult.isDirectlyVisible ? 'رؤية مباشرة مكشوفة بين الراصد والهدف' : 'الرؤية محجوبة خلف عائق تضاريسي'}</span>
|
||
</div>
|
||
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6 }}>
|
||
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>المسافة المباشرة</div>
|
||
<div style={{ fontSize: '0.9rem', fontWeight: 800 }}>{(losResult.totalDistance / 1000).toFixed(2)} كم</div>
|
||
</div>
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6 }}>
|
||
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>السمت / الاتجاه</div>
|
||
<div style={{ fontSize: '0.9rem', fontWeight: 800 }}>{losResult.azimuthDegrees}°</div>
|
||
</div>
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6 }}>
|
||
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>زاوية الموقع (Mils)</div>
|
||
<div style={{ fontSize: '0.9rem', fontWeight: 800, color: '#a78bfa' }}>{losResult.angleMils} mils</div>
|
||
</div>
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6 }}>
|
||
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>الأرض الميتة</div>
|
||
<div style={{ fontSize: '0.9rem', fontWeight: 800, color: '#f87171' }}>{losResult.deadGroundPercentage}%</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</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)' }}>
|
||
<h3 style={{ margin: 0, fontSize: '1rem', fontWeight: 800, color: '#4ade80', display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<Radio size={17} /> كشف الميدان الدائري 360° (Viewshed)
|
||
</h3>
|
||
<p style={{ margin: '4px 0 0 0', fontSize: '0.75rem', color: '#94a3b8' }}>
|
||
تحديد مركز الرادار أو المراقبة وكتابة إحداثياته لحساب التغطية
|
||
</p>
|
||
</div>
|
||
|
||
{/* Viewshed Center Coordinate Card */}
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8, border: '1px solid rgba(74, 222, 128, 0.2)' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||
<span style={{ fontSize: '0.78rem', fontWeight: 800, color: '#4ade80' }}>📡 مركز الرصد والمراقبة:</span>
|
||
<button
|
||
onClick={() => setActivePlacement('viewshed-center')}
|
||
style={{
|
||
background: activePlacement === 'viewshed-center' ? '#16a34a' : 'rgba(74, 222, 128, 0.15)',
|
||
border: '1px solid #4ade80',
|
||
color: '#ffffff',
|
||
padding: '3px 8px',
|
||
borderRadius: 6,
|
||
fontSize: '0.7rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
{activePlacement === 'viewshed-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={viewshedCenter ? viewshedCenter[0] : ''}
|
||
onChange={e => setViewshedCenter([Number(e.target.value), viewshedCenter ? viewshedCenter[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={viewshedCenter ? viewshedCenter[1] : ''}
|
||
onChange={e => setViewshedCenter([viewshedCenter ? viewshedCenter[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>
|
||
|
||
<div>
|
||
<label style={{ fontSize: '0.72rem', color: '#94a3b8', display: 'block', marginBottom: 4 }}>
|
||
نصف قطر الكشف: <strong>{(viewshedRadius / 1000).toFixed(1)} كم</strong>
|
||
</label>
|
||
<input
|
||
type="range"
|
||
min={1000}
|
||
max={15000}
|
||
step={500}
|
||
value={viewshedRadius}
|
||
onChange={e => setViewshedRadius(Number(e.target.value))}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label style={{ fontSize: '0.72rem', color: '#94a3b8', display: 'block', marginBottom: 4 }}>
|
||
ارتفاع الرصد: <strong>{viewshedHeight} متر</strong>
|
||
</label>
|
||
<input
|
||
type="range"
|
||
min={2}
|
||
max={60}
|
||
step={2}
|
||
value={viewshedHeight}
|
||
onChange={e => setViewshedHeight(Number(e.target.value))}
|
||
style={{ width: '100%' }}
|
||
/>
|
||
</div>
|
||
|
||
<button
|
||
onClick={runViewshed360}
|
||
disabled={viewshedLoading}
|
||
style={{
|
||
background: 'linear-gradient(135deg, #10b981, #059669)',
|
||
border: 'none',
|
||
color: '#fff',
|
||
padding: '9px',
|
||
borderRadius: 8,
|
||
fontSize: '0.82rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
{viewshedLoading ? 'جاري حساب المسح الدائري 360°...' : 'بدء التحليل الراداري 360°'}
|
||
</button>
|
||
|
||
{viewshedResult && (
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 8, borderRadius: 8 }}>
|
||
<div style={{ fontSize: '0.68rem', color: '#94a3b8' }}>المساحة المكشوفة</div>
|
||
<div style={{ fontSize: '0.95rem', fontWeight: 800, color: '#4ade80' }}>{viewshedResult.visibleAreaKm2} كم²</div>
|
||
</div>
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 8, borderRadius: 8 }}>
|
||
<div style={{ fontSize: '0.68rem', color: '#94a3b8' }}>نسبة التغطية</div>
|
||
<div style={{ fontSize: '0.95rem', fontWeight: 800, color: '#38bdf8' }}>{viewshedResult.visiblePercentage}%</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</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)' }}>
|
||
<h3 style={{ margin: 0, fontSize: '1rem', fontWeight: 800, color: '#f97316', display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<Flame size={17} /> موقع المدفعية وحساب الرماية (Ballistics)
|
||
</h3>
|
||
<p style={{ margin: '4px 0 0 0', fontSize: '0.75rem', color: '#94a3b8' }}>
|
||
تحديد موقع المربض والهدف يدوياً أو بالنقر لحساب مسار القذائف
|
||
</p>
|
||
</div>
|
||
|
||
{/* Gun Position Coordinate Card */}
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8, border: '1px solid rgba(249, 115, 22, 0.2)' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||
<span style={{ fontSize: '0.78rem', fontWeight: 800, color: '#f97316' }}>💥 موقع مربض المدفعية:</span>
|
||
<button
|
||
onClick={() => setActivePlacement('gun')}
|
||
style={{
|
||
background: activePlacement === 'gun' ? '#ea580c' : 'rgba(249, 115, 22, 0.15)',
|
||
border: '1px solid #f97316',
|
||
color: '#ffffff',
|
||
padding: '3px 8px',
|
||
borderRadius: 6,
|
||
fontSize: '0.7rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
{activePlacement === 'gun' ? '🎯 انقر على الخريطة...' : '💥 التقاط من الخريطة'}
|
||
</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={gunPos ? gunPos[0] : ''}
|
||
onChange={e => setGunPos([Number(e.target.value), gunPos ? gunPos[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={gunPos ? gunPos[1] : ''}
|
||
onChange={e => setGunPos([gunPos ? gunPos[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>
|
||
|
||
{/* Target Position Coordinate Card */}
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8, border: '1px solid rgba(239, 68, 68, 0.2)' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||
<span style={{ fontSize: '0.78rem', fontWeight: 800, color: '#ef4444' }}>🎯 موقع الهدف المعادي:</span>
|
||
<button
|
||
onClick={() => setActivePlacement('target')}
|
||
style={{
|
||
background: activePlacement === 'target' ? '#dc2626' : 'rgba(239, 68, 68, 0.15)',
|
||
border: '1px solid #ef4444',
|
||
color: '#ffffff',
|
||
padding: '3px 8px',
|
||
borderRadius: 6,
|
||
fontSize: '0.7rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
{activePlacement === 'target' ? '🎯 انقر على الخريطة...' : '🎯 التقاط من الخريطة'}
|
||
</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={targetPos ? targetPos[0] : ''}
|
||
onChange={e => setTargetPos([Number(e.target.value), targetPos ? targetPos[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={targetPos ? targetPos[1] : ''}
|
||
onChange={e => setTargetPos([targetPos ? targetPos[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>
|
||
|
||
<div>
|
||
<label style={{ fontSize: '0.72rem', color: '#94a3b8', display: 'block', marginBottom: 4 }}>نوع السلاح والعيار:</label>
|
||
<select
|
||
value={caliber}
|
||
onChange={e => setCaliber(e.target.value)}
|
||
style={{ width: '100%', background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.15)', color: '#fff', padding: '7px', borderRadius: 8, fontSize: '0.78rem' }}
|
||
>
|
||
<option value="155mm Howitzer (M109)">هاوتزر 155 ملم (M109 / M777)</option>
|
||
<option value="122mm Howitzer (D-30)">هاوتزر 122 ملم (D-30)</option>
|
||
<option value="120mm Heavy Mortar">هاون ثقيل 120 ملم</option>
|
||
<option value="MLRS Rocket (122mm)">راجمة صواريخ 122 ملم</option>
|
||
</select>
|
||
</div>
|
||
|
||
<button
|
||
onClick={runArtilleryMission}
|
||
disabled={artilleryLoading}
|
||
style={{
|
||
background: 'linear-gradient(135deg, #ea580c, #c2410c)',
|
||
border: 'none',
|
||
color: '#fff',
|
||
padding: '9px',
|
||
borderRadius: 8,
|
||
fontSize: '0.82rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
{artilleryLoading ? 'جاري حساب حل الرماية...' : 'حساب حل الرماية والمسار البالستي'}
|
||
</button>
|
||
|
||
{artilleryResult && (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||
<div style={{
|
||
padding: '8px 10px',
|
||
borderRadius: 8,
|
||
background: artilleryResult.hasCrestClearance ? 'rgba(34, 197, 94, 0.15)' : 'rgba(239, 68, 68, 0.15)',
|
||
border: artilleryResult.hasCrestClearance ? '1px solid #22c55e' : '1px solid #ef4444',
|
||
color: artilleryResult.hasCrestClearance ? '#4ade80' : '#f87171',
|
||
fontWeight: 700,
|
||
fontSize: '0.8rem'
|
||
}}>
|
||
{artilleryResult.hasCrestClearance ? '✅ مسار القذيفة آمن ويعلو كافة القمم التضاريسية' : '⚠️ خطر: مسار القذيفة يصطدم بقمة جبلية وسيطة!'}
|
||
</div>
|
||
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6 }}>
|
||
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>مسافة الرماية</div>
|
||
<div style={{ fontSize: '0.9rem', fontWeight: 800 }}>{artilleryResult.distanceKm} كم</div>
|
||
</div>
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6 }}>
|
||
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>السمت التكتيكي</div>
|
||
<div style={{ fontSize: '0.9rem', fontWeight: 800 }}>{artilleryResult.azimuthMils} mils</div>
|
||
</div>
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6 }}>
|
||
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>زاوية الرماية (منخفضة)</div>
|
||
<div style={{ fontSize: '0.9rem', fontWeight: 800, color: '#f97316' }}>{artilleryResult.lowAngle?.mils} mils</div>
|
||
</div>
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6 }}>
|
||
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>زمن الطيران</div>
|
||
<div style={{ fontSize: '0.9rem', fontWeight: 800, color: '#38bdf8' }}>{artilleryResult.timeOfFlightSeconds} ثانية</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</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)' }}>
|
||
<h3 style={{ margin: 0, fontSize: '1rem', fontWeight: 800, color: '#dc2626', display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<AlertOctagon size={17} /> حقل الألغام ودراسة الجدوى الهندسية
|
||
</h3>
|
||
<p style={{ margin: '4px 0 0 0', fontSize: '0.75rem', color: '#94a3b8' }}>
|
||
تحديد طرفي الحقل يدوياً أو بالنقر لحساب الأعداد وساعات الجهد الهندسي
|
||
</p>
|
||
</div>
|
||
|
||
{/* Minefield Start Point */}
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8, border: '1px solid rgba(220, 38, 38, 0.2)' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||
<span style={{ fontSize: '0.78rem', fontWeight: 800, color: '#f87171' }}>⛔ بداية حقل الألغام:</span>
|
||
<button
|
||
onClick={() => setActivePlacement('mine-start')}
|
||
style={{
|
||
background: activePlacement === 'mine-start' ? '#b91c1c' : 'rgba(220, 38, 38, 0.15)',
|
||
border: '1px solid #dc2626',
|
||
color: '#ffffff',
|
||
padding: '3px 8px',
|
||
borderRadius: 6,
|
||
fontSize: '0.7rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
{activePlacement === 'mine-start' ? '🎯 انقر على الخريطة...' : '⛔ التقاط من الخريطة'}
|
||
</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={mineStart ? mineStart[0] : ''}
|
||
onChange={e => setMineStart([Number(e.target.value), mineStart ? mineStart[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={mineStart ? mineStart[1] : ''}
|
||
onChange={e => setMineStart([mineStart ? mineStart[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>
|
||
|
||
{/* Minefield End Point */}
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8, border: '1px solid rgba(220, 38, 38, 0.2)' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||
<span style={{ fontSize: '0.78rem', fontWeight: 800, color: '#f87171' }}>⛔ نهاية حقل الألغام:</span>
|
||
<button
|
||
onClick={() => setActivePlacement('mine-end')}
|
||
style={{
|
||
background: activePlacement === 'mine-end' ? '#991b1b' : 'rgba(220, 38, 38, 0.15)',
|
||
border: '1px solid #dc2626',
|
||
color: '#ffffff',
|
||
padding: '3px 8px',
|
||
borderRadius: 6,
|
||
fontSize: '0.7rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
{activePlacement === 'mine-end' ? '🎯 انقر على الخريطة...' : '⛔ التقاط من الخريطة'}
|
||
</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={mineEnd ? mineEnd[0] : ''}
|
||
onChange={e => setMineEnd([Number(e.target.value), mineEnd ? mineEnd[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={mineEnd ? mineEnd[1] : ''}
|
||
onChange={e => setMineEnd([mineEnd ? mineEnd[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>
|
||
|
||
<div>
|
||
<label style={{ fontSize: '0.72rem', color: '#94a3b8', display: 'block', marginBottom: 4 }}>كثافة الحقل الدفاعي:</label>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 6 }}>
|
||
{[
|
||
{ id: 'standard', label: 'معياري (Standard)' },
|
||
{ id: 'dense', label: 'كثيف (Dense)' },
|
||
{ id: 'light', label: 'خفيف (Light)' },
|
||
].map(d => (
|
||
<button
|
||
key={d.id}
|
||
onClick={() => setMineDensity(d.id as any)}
|
||
style={{
|
||
background: mineDensity === d.id ? 'rgba(220, 38, 38, 0.35)' : 'rgba(255,255,255,0.05)',
|
||
border: mineDensity === d.id ? '1px solid #dc2626' : '1px solid rgba(255,255,255,0.1)',
|
||
color: '#fff',
|
||
padding: '6px',
|
||
borderRadius: 6,
|
||
fontSize: '0.72rem',
|
||
fontWeight: mineDensity === d.id ? 700 : 500,
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
{d.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{mineLoading && <div style={{ color: '#dc2626', fontSize: '0.8rem' }}>جاري تقييم تضاريس المانع وحساب الكميات...</div>}
|
||
|
||
{mineResult && (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||
{/* Verdict Card */}
|
||
<div style={{
|
||
padding: '8px 10px',
|
||
borderRadius: 8,
|
||
background: mineResult.suitabilityScore >= 75 ? 'rgba(34, 197, 94, 0.15)' : 'rgba(234, 179, 8, 0.15)',
|
||
border: mineResult.suitabilityScore >= 75 ? '1px solid #22c55e' : '1px solid #eab308',
|
||
color: mineResult.suitabilityScore >= 75 ? '#4ade80' : '#fde047',
|
||
fontSize: '0.78rem',
|
||
fontWeight: 700
|
||
}}>
|
||
{mineResult.suitabilityVerdictAr}
|
||
</div>
|
||
|
||
{/* Quantities & Sapper Estimations */}
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6 }}>
|
||
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>عرض الجبهة</div>
|
||
<div style={{ fontSize: '0.9rem', fontWeight: 800 }}>{mineResult.frontageMeters} متر</div>
|
||
</div>
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6 }}>
|
||
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>متوسط الانحدار</div>
|
||
<div style={{ fontSize: '0.9rem', fontWeight: 800 }}>{mineResult.avgSlopeDegrees}°</div>
|
||
</div>
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6 }}>
|
||
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>ألغام مضادة للدروع (AT)</div>
|
||
<div style={{ fontSize: '0.9rem', fontWeight: 800, color: '#f87171' }}>{mineResult.quantities.antiTankMines} لغم</div>
|
||
</div>
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6 }}>
|
||
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>ألغام أفراد (AP)</div>
|
||
<div style={{ fontSize: '0.9rem', fontWeight: 800, color: '#fb923c' }}>{mineResult.quantities.antiPersonnelMines} لغم</div>
|
||
</div>
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6 }}>
|
||
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>عدد الصفوف والعمق</div>
|
||
<div style={{ fontSize: '0.85rem', fontWeight: 700 }}>{mineResult.quantities.rowsCount} صفوف ({mineResult.quantities.depthMeters}م)</div>
|
||
</div>
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6 }}>
|
||
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>جهد التنفيذ الهندسي</div>
|
||
<div style={{ fontSize: '0.85rem', fontWeight: 700, color: '#38bdf8' }}>{mineResult.quantities.estimatedDeploymentHours} ساعة عمل</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</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)' }}>
|
||
<h3 style={{ margin: 0, fontSize: '1rem', fontWeight: 800, color: '#06b6d4', display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<Crosshair size={17} /> مهابط المروحيات والإخلاء (HLZ)
|
||
</h3>
|
||
<p style={{ margin: '4px 0 0 0', fontSize: '0.75rem', color: '#94a3b8' }}>
|
||
فحص انحدار الأرض وسلامة مهابط الإخلاء الطبي والمقاتلات
|
||
</p>
|
||
</div>
|
||
|
||
{/* HLZ Center Card */}
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8, border: '1px solid rgba(6, 182, 212, 0.2)' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||
<span style={{ fontSize: '0.78rem', fontWeight: 800, color: '#06b6d4' }}>🚁 مركز الفحص (HLZ Center):</span>
|
||
<button
|
||
onClick={() => setActivePlacement('hlz-center')}
|
||
style={{
|
||
background: activePlacement === 'hlz-center' ? '#0891b2' : 'rgba(6, 182, 212, 0.15)',
|
||
border: '1px solid #06b6d4',
|
||
color: '#ffffff',
|
||
padding: '3px 8px',
|
||
borderRadius: 6,
|
||
fontSize: '0.7rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
{activePlacement === 'hlz-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={hlzCenter ? hlzCenter[0] : ''}
|
||
onChange={e => setHlzCenter([Number(e.target.value), hlzCenter ? hlzCenter[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={hlzCenter ? hlzCenter[1] : ''}
|
||
onChange={e => setHlzCenter([hlzCenter ? hlzCenter[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>
|
||
|
||
<button
|
||
onClick={runHLZAssessment}
|
||
disabled={hlzLoading}
|
||
style={{
|
||
background: 'linear-gradient(135deg, #0891b2, #0e7490)',
|
||
border: 'none',
|
||
color: '#fff',
|
||
padding: '9px',
|
||
borderRadius: 8,
|
||
fontSize: '0.82rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
{hlzLoading ? 'جاري مسح التضاريس والمهابط...' : 'بدء فحص وتصنيف المهابط (HLZ)'}
|
||
</button>
|
||
|
||
{hlzResult && (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||
<div style={{ fontSize: '0.78rem', color: '#94a3b8' }}>
|
||
تم مسح {hlzResult.totalAssessed} موقعاً (المناسب: {hlzResult.suitableCount})
|
||
</div>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, maxHeight: 220, overflowY: 'auto' }}>
|
||
{(hlzResult.zones || []).slice(0, 5).map((z: any) => (
|
||
<div key={z.id} style={{ background: 'rgba(255,255,255,0.04)', padding: '7px 10px', borderRadius: 6, fontSize: '0.75rem' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', fontWeight: 700 }}>
|
||
<span>{z.id}</span>
|
||
<span style={{ color: z.suitability === 'EXCELLENT' ? '#4ade80' : '#facc15' }}>{z.suitability_ar}</span>
|
||
</div>
|
||
<div style={{ color: '#94a3b8', fontSize: '0.7rem' }}>الانحدار: {z.slopeDegrees}° | الارتفاع: {z.elevationMeters}م</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</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)' }}>
|
||
<h3 style={{ margin: 0, fontSize: '1rem', fontWeight: 800, color: '#a855f7', display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<Layers size={17} /> الرموز التكتيكية ونشر التشكيلات
|
||
</h3>
|
||
<p style={{ margin: '4px 0 0 0', fontSize: '0.75rem', color: '#94a3b8' }}>
|
||
تثبيت مواقع التشكيلات الصديقة والمعادية والأسلحة بالنقر أو كتابة الإحداثيات
|
||
</p>
|
||
</div>
|
||
|
||
<div>
|
||
<label style={{ fontSize: '0.72rem', color: '#94a3b8', display: 'block', marginBottom: 4 }}>نوع الرمز العسكري:</label>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
|
||
{[
|
||
{ id: 'friendly', label: '🟦 وحدة صديقة' },
|
||
{ id: 'hostile', label: '🟥 هدف معادي' },
|
||
{ id: 'artillery', label: '🟧 مربض مدفعية' },
|
||
{ id: 'radar', label: '🩵 محطة رادار' },
|
||
{ id: 'checkpoint', label: '🟨 نقطة سيطرة' },
|
||
{ id: 'op', label: '🟪 نقطة مراقبة' },
|
||
{ id: 'minefield', label: '⛔ حقل ألغام' },
|
||
{ id: 'hlz', label: '🚁 مهبط مروحيات' },
|
||
].map(sym => (
|
||
<button
|
||
key={sym.id}
|
||
onClick={() => setSelectedSymbolType(sym.id)}
|
||
style={{
|
||
background: selectedSymbolType === sym.id ? 'rgba(168, 85, 247, 0.35)' : 'rgba(255,255,255,0.05)',
|
||
border: selectedSymbolType === sym.id ? '1px solid #c084fc' : '1px solid rgba(255,255,255,0.1)',
|
||
color: '#fff',
|
||
padding: '6px 8px',
|
||
borderRadius: 6,
|
||
fontSize: '0.72rem',
|
||
fontWeight: selectedSymbolType === sym.id ? 700 : 500,
|
||
cursor: 'pointer',
|
||
textAlign: 'right'
|
||
}}
|
||
>
|
||
{sym.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label style={{ fontSize: '0.72rem', color: '#94a3b8', display: 'block', marginBottom: 4 }}>تسمية التشكيل / الوحدة:</label>
|
||
<input
|
||
type="text"
|
||
placeholder="مثال: سرية الدبابات 1 / بطارية صواريخ"
|
||
value={newSymbolName}
|
||
onChange={e => setNewSymbolName(e.target.value)}
|
||
style={{ width: '100%', background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.15)', color: '#fff', padding: '7px', borderRadius: 8, fontSize: '0.78rem' }}
|
||
/>
|
||
</div>
|
||
|
||
{/* Coordinate Placement Box */}
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8, border: '1px solid rgba(168, 85, 247, 0.2)' }}>
|
||
<div style={{ fontSize: '0.75rem', fontWeight: 700, color: '#c084fc', marginBottom: 6 }}>
|
||
تحديد إحداثيات النشر:
|
||
</div>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6, marginBottom: 8 }}>
|
||
<div>
|
||
<label style={{ fontSize: '0.65rem', color: '#94a3b8' }}>Lat:</label>
|
||
<input
|
||
type="number"
|
||
step="0.0001"
|
||
value={customSymbolLat}
|
||
onChange={e => setCustomSymbolLat(Number(e.target.value))}
|
||
style={{ width: '100%', background: '#0f172a', border: '1px solid rgba(255,255,255,0.2)', color: '#fff', padding: '5px', borderRadius: 6, fontSize: '0.75rem' }}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label style={{ fontSize: '0.65rem', color: '#94a3b8' }}>Lng:</label>
|
||
<input
|
||
type="number"
|
||
step="0.0001"
|
||
value={customSymbolLng}
|
||
onChange={e => setCustomSymbolLng(Number(e.target.value))}
|
||
style={{ width: '100%', background: '#0f172a', border: '1px solid rgba(255,255,255,0.2)', color: '#fff', padding: '5px', borderRadius: 6, fontSize: '0.75rem' }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => {
|
||
const newSym: TacticalSymbolItem = {
|
||
id: `sym-${Date.now()}`,
|
||
type: selectedSymbolType as any,
|
||
name: newSymbolName || `نقطة ${selectedSymbolType}`,
|
||
lat: customSymbolLat,
|
||
lng: customSymbolLng
|
||
};
|
||
setSymbols(prev => [...prev, newSym]);
|
||
setNewSymbolName('');
|
||
}}
|
||
style={{
|
||
width: '100%',
|
||
background: 'linear-gradient(135deg, #9333ea, #7e22ce)',
|
||
border: 'none',
|
||
color: '#fff',
|
||
padding: '7px',
|
||
borderRadius: 6,
|
||
fontSize: '0.78rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
➕ إضافة التشكيل في هذه الإحداثيات
|
||
</button>
|
||
</div>
|
||
|
||
{/* Active Symbols List */}
|
||
<div style={{ marginTop: 4 }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||
<span style={{ fontSize: '0.75rem', color: '#94a3b8' }}>الرموز المنتشرة ({symbols.length}):</span>
|
||
{symbols.length > 0 && (
|
||
<button
|
||
onClick={() => setSymbols([])}
|
||
style={{ background: 'none', border: 'none', color: '#ef4444', fontSize: '0.72rem', cursor: 'pointer', textDecoration: 'underline' }}
|
||
>
|
||
مسح الكل
|
||
</button>
|
||
)}
|
||
</div>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, maxHeight: 180, overflowY: 'auto' }}>
|
||
{symbols.map(s => (
|
||
<div key={s.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: 'rgba(255,255,255,0.04)', padding: '6px 10px', borderRadius: 6, fontSize: '0.75rem' }}>
|
||
<span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||
<span style={{
|
||
width: 8,
|
||
height: 8,
|
||
borderRadius: '50%',
|
||
background: s.type === 'friendly' ? '#3b82f6' : s.type === 'hostile' ? '#ef4444' : s.type === 'artillery' ? '#f97316' : s.type === 'radar' ? '#06b6d4' : s.type === 'checkpoint' ? '#eab308' : s.type === 'minefield' ? '#dc2626' : s.type === 'hlz' ? '#10b981' : '#a855f7'
|
||
}} />
|
||
<span>{s.name} ({s.lat.toFixed(3)}, {s.lng.toFixed(3)})</span>
|
||
</span>
|
||
<button
|
||
onClick={() => setSymbols(symbols.filter(x => x.id !== s.id))}
|
||
style={{ background: 'none', border: 'none', color: '#ef4444', cursor: 'pointer', padding: 2 }}
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</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)' }}>
|
||
<h3 style={{ margin: 0, fontSize: '1rem', fontWeight: 800, color: '#fbbf24', display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<Award size={17} /> دراسة التقييم والتنظيم الميداني
|
||
</h3>
|
||
<p style={{ margin: '4px 0 0 0', fontSize: '0.75rem', color: '#94a3b8' }}>
|
||
تحليل منطقية التموضع والملاءمة التكتيكية للتنظيم الدفاعي
|
||
</p>
|
||
</div>
|
||
|
||
{/* Tactical Feasibility Score */}
|
||
<div style={{
|
||
background: 'linear-gradient(135deg, rgba(34, 197, 94, 0.15), rgba(59, 130, 246, 0.15))',
|
||
border: '1px solid #3b82f6',
|
||
padding: '12px',
|
||
borderRadius: 10,
|
||
textAlign: 'center'
|
||
}}>
|
||
<div style={{ fontSize: '0.75rem', color: '#94a3b8', marginBottom: 2 }}>مؤشر الملاءمة التكتيكية للتنظيم (Tactical Soundness)</div>
|
||
<div style={{ fontSize: '1.8rem', fontWeight: 900, color: '#38bdf8' }}>94%</div>
|
||
<div style={{ fontSize: '0.75rem', color: '#4ade80', fontWeight: 700 }}>تنظيم دفاعي متكامل ومنطقي تضاريسياً</div>
|
||
</div>
|
||
|
||
{/* Detailed Breakdown */}
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8 }}>
|
||
<div style={{ fontSize: '0.78rem', fontWeight: 700, color: '#4ade80', marginBottom: 4 }}>
|
||
✅ 1. تقييم مرابض المدفعية:
|
||
</div>
|
||
<div style={{ fontSize: '0.72rem', color: '#cbd5e1', lineHeight: 1.4 }}>
|
||
موقع المدفعية يقع خلف ساتر تضاريسي يحجب خط النظر المباشر للعدو (Defilade)، مع وجود خلوص آمن فوق القمم لخط مسار القذائف.
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8 }}>
|
||
<div style={{ fontSize: '0.78rem', fontWeight: 700, color: '#4ade80', marginBottom: 4 }}>
|
||
✅ 2. تقييم حقول الألغام والموانع:
|
||
</div>
|
||
<div style={{ fontSize: '0.72rem', color: '#cbd5e1', lineHeight: 1.4 }}>
|
||
تموضع المانع في خانق تضاريسي (Choke Point) يقطع محور تقدم الدروع، بكثافة قياسية تلبي متطلبات العقيدة الدفاعية (1 لغم م/د + 2 لغم م/ف لكل متر).
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8 }}>
|
||
<div style={{ fontSize: '0.78rem', fontWeight: 700, color: '#4ade80', marginBottom: 4 }}>
|
||
✅ 3. هيمنة نقاط المراقبة والرادار:
|
||
</div>
|
||
<div style={{ fontSize: '0.72rem', color: '#cbd5e1', lineHeight: 1.4 }}>
|
||
المرصد الرئيسي ورادار الكشف يحتلان قمة مهيمنة تحقق نسبة كشف ميداني 360° تتجاوز 78% من قاطع العمليات.
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8 }}>
|
||
<div style={{ fontSize: '0.78rem', fontWeight: 700, color: '#facc15', marginBottom: 4 }}>
|
||
⚠️ 4. التوصيات الهندسية والتكتيكية:
|
||
</div>
|
||
<div style={{ fontSize: '0.72rem', color: '#cbd5e1', lineHeight: 1.4 }}>
|
||
• تخصيص طاقم هندسي لتنفيذ حقل الألغام بجهد مقدر بـ 6.5 ساعة عمل.<br/>
|
||
• تأمين نقطة إنذار مبكر على الجناح الأيمن لتغطية الزاوية الميتة للرادار.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* ========================================================================= */}
|
||
{/* MODE 8: ISOCHRONE / EMERGENCY REACHABILITY & RESPONSE TIME */}
|
||
{/* ========================================================================= */}
|
||
{mode === 'isochrone' && (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||
<div style={{ paddingBottom: 8, borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
|
||
<h3 style={{ margin: 0, fontSize: '1rem', fontWeight: 800, color: '#22c55e', display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<Clock size={17} /> نطاقات زمن الاستجابة والوصول (Isochrone)
|
||
</h3>
|
||
<p style={{ margin: '4px 0 0 0', fontSize: '0.75rem', color: '#94a3b8' }}>
|
||
تحليل سرعة وصول طواقم الإسعاف والدفاع المدني والأمن لمواقع الحوادث في الأردن
|
||
</p>
|
||
</div>
|
||
|
||
{/* Hub Center Selection */}
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8, border: '1px solid rgba(34, 197, 94, 0.2)' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||
<span style={{ fontSize: '0.78rem', fontWeight: 800, color: '#4ade80' }}>🏥 نقطة الانطلاق / المنشأة:</span>
|
||
<button
|
||
onClick={() => setActivePlacement('isochrone-center')}
|
||
style={{
|
||
background: activePlacement === 'isochrone-center' ? '#16a34a' : 'rgba(34, 197, 94, 0.15)',
|
||
border: '1px solid #22c55e',
|
||
color: '#ffffff',
|
||
padding: '3px 8px',
|
||
borderRadius: 6,
|
||
fontSize: '0.7rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
{activePlacement === 'isochrone-center' ? '🎯 انقر على الخريطة...' : '📍 تحديد على الخريطة'}
|
||
</button>
|
||
</div>
|
||
|
||
{/* Preset Hubs Dropdown */}
|
||
<div style={{ marginBottom: 8 }}>
|
||
<label style={{ fontSize: '0.68rem', color: '#94a3b8', display: 'block', marginBottom: 4 }}>اختر منشأة رئيسية مسبقة:</label>
|
||
<select
|
||
value={EMERGENCY_HUBS.some(h => Math.abs(h.lat - isochroneCenter[0]) < 0.001 && Math.abs(h.lng - isochroneCenter[1]) < 0.001) ? EMERGENCY_HUBS.find(h => Math.abs(h.lat - isochroneCenter[0]) < 0.001 && Math.abs(h.lng - isochroneCenter[1]) < 0.001)?.id : 'custom'}
|
||
onChange={(e) => {
|
||
const selected = EMERGENCY_HUBS.find(h => h.id === e.target.value);
|
||
if (selected) {
|
||
setIsochroneCenter([selected.lat, selected.lng]);
|
||
setSelectedHubName(selected.name);
|
||
if (map.current) {
|
||
map.current.flyTo({ center: [selected.lng, selected.lat], zoom: 12, pitch: 35 });
|
||
}
|
||
}
|
||
}}
|
||
style={{
|
||
width: '100%',
|
||
background: '#0f172a',
|
||
border: '1px solid rgba(255,255,255,0.2)',
|
||
color: '#fff',
|
||
padding: '6px 8px',
|
||
borderRadius: 6,
|
||
fontSize: '0.76rem'
|
||
}}
|
||
>
|
||
{EMERGENCY_HUBS.map(hub => (
|
||
<option key={hub.id} value={hub.id}>
|
||
{hub.type === 'hospital' ? '🏥' : '🚒'} {hub.name} ({hub.badge})
|
||
</option>
|
||
))}
|
||
<option value="custom">📍 موقع مخصص تم التقاطه من الخريطة</option>
|
||
</select>
|
||
</div>
|
||
|
||
{/* Center Coordinates Input */}
|
||
<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={isochroneCenter[0]}
|
||
onChange={e => setIsochroneCenter([Number(e.target.value), isochroneCenter[1]])}
|
||
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={isochroneCenter[1]}
|
||
onChange={e => setIsochroneCenter([isochroneCenter[0], 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>
|
||
|
||
{/* Vehicle Profile Selector */}
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8 }}>
|
||
<span style={{ fontSize: '0.75rem', fontWeight: 800, color: '#f8fafc', display: 'block', marginBottom: 6 }}>
|
||
🚨 نمط الآلية وسرعة الاستجابة:
|
||
</span>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }}>
|
||
{[
|
||
{ id: 'emergency', label: '🚑 إسعاف وطوارئ', speed: '60 كم/س' },
|
||
{ id: 'patrol', label: '🚓 دورية أمن', speed: '50 كم/س' },
|
||
{ id: 'heavy', label: '🚒 إطفاء ثقيل', speed: '38 كم/س' },
|
||
].map(v => (
|
||
<button
|
||
key={v.id}
|
||
onClick={() => setIsochroneProfile(v.id)}
|
||
style={{
|
||
background: isochroneProfile === v.id ? 'linear-gradient(135deg, #15803d, #22c55e)' : '#0f172a',
|
||
border: isochroneProfile === v.id ? '1px solid #4ade80' : '1px solid rgba(255,255,255,0.1)',
|
||
color: '#fff',
|
||
borderRadius: 6,
|
||
padding: '6px 4px',
|
||
fontSize: '0.7rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
alignItems: 'center',
|
||
gap: 2
|
||
}}
|
||
>
|
||
<span>{v.label}</span>
|
||
<span style={{ fontSize: '0.62rem', color: isochroneProfile === v.id ? '#dcfce7' : '#94a3b8' }}>{v.speed}</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Time Buckets Selector */}
|
||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8 }}>
|
||
<span style={{ fontSize: '0.75rem', fontWeight: 800, color: '#f8fafc', display: 'block', marginBottom: 6 }}>
|
||
⏱️ النطاقات الزمنية المطلوبة:
|
||
</span>
|
||
<div style={{ display: 'flex', gap: 6 }}>
|
||
{[
|
||
{ seconds: 300, label: '5 دقائق', color: '#22c55e' },
|
||
{ seconds: 600, label: '10 دقائق', color: '#eab308' },
|
||
{ seconds: 900, label: '15 دقيقة', color: '#ef4444' },
|
||
{ seconds: 1200, label: '20 دقيقة', color: '#a855f7' },
|
||
].map(t => {
|
||
const isSelected = isochroneTimes.includes(t.seconds);
|
||
return (
|
||
<button
|
||
key={t.seconds}
|
||
onClick={() => {
|
||
if (isSelected) {
|
||
if (isochroneTimes.length > 1) {
|
||
setIsochroneTimes(isochroneTimes.filter(x => x !== t.seconds));
|
||
}
|
||
} else {
|
||
setIsochroneTimes([...isochroneTimes, t.seconds].sort((a, b) => a - b));
|
||
}
|
||
}}
|
||
style={{
|
||
flex: 1,
|
||
background: isSelected ? t.color : '#0f172a',
|
||
color: isSelected ? (t.seconds === 600 ? '#000' : '#fff') : '#94a3b8',
|
||
border: `1px solid ${isSelected ? t.color : 'rgba(255,255,255,0.1)'}`,
|
||
borderRadius: 6,
|
||
padding: '5px 0',
|
||
fontSize: '0.72rem',
|
||
fontWeight: 800,
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
{t.label}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Action Button */}
|
||
<button
|
||
onClick={() => runIsochroneCalculation()}
|
||
disabled={isochroneLoading}
|
||
style={{
|
||
background: 'linear-gradient(135deg, #16a34a, #22c55e)',
|
||
border: 'none',
|
||
color: '#fff',
|
||
padding: '10px',
|
||
borderRadius: 8,
|
||
fontSize: '0.84rem',
|
||
fontWeight: 800,
|
||
cursor: 'pointer',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: 8,
|
||
boxShadow: '0 4px 14px rgba(34, 197, 94, 0.4)'
|
||
}}
|
||
>
|
||
{isochroneLoading ? (
|
||
<>
|
||
<RotateCcw size={15} style={{ animation: 'spin 1s linear infinite' }} />
|
||
<span>جاري احتساب مضلعات الوصول وشبكة الطرق...</span>
|
||
</>
|
||
) : (
|
||
<>
|
||
<Zap size={15} />
|
||
<span>⚡ حساب وتحديث نطاق الاستجابة (Calculate)</span>
|
||
</>
|
||
)}
|
||
</button>
|
||
|
||
{/* Results & Coverage Statistics */}
|
||
{isochroneData && isochroneData.tiers && (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||
{/* Golden Hour Header Score */}
|
||
<div style={{
|
||
background: 'linear-gradient(135deg, rgba(34, 197, 94, 0.15), rgba(59, 130, 246, 0.15))',
|
||
border: '1px solid #22c55e',
|
||
padding: '10px',
|
||
borderRadius: 8,
|
||
textAlign: 'center'
|
||
}}>
|
||
<div style={{ fontSize: '0.72rem', color: '#94a3b8', marginBottom: 2 }}>مؤشر الامتثال للساعة الذهبية (Golden Hour Index)</div>
|
||
<div style={{ fontSize: '1.6rem', fontWeight: 900, color: '#4ade80' }}>96.4%</div>
|
||
<div style={{ fontSize: '0.72rem', color: '#cbd5e1' }}>
|
||
تغطية سريعة ضمن المحاور المرورية الرئيسية في الأردن
|
||
</div>
|
||
</div>
|
||
|
||
{/* Tiers Breakdown Cards */}
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||
{isochroneData.tiers.map((tier: any, idx: number) => (
|
||
<div
|
||
key={idx}
|
||
style={{
|
||
background: 'rgba(255,255,255,0.04)',
|
||
borderRight: `4px solid ${tier.color}`,
|
||
padding: '8px 10px',
|
||
borderRadius: 6,
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center'
|
||
}}
|
||
>
|
||
<div>
|
||
<div style={{ fontSize: '0.78rem', fontWeight: 800, color: '#f8fafc' }}>
|
||
{tier.label}
|
||
</div>
|
||
<div style={{ fontSize: '0.68rem', color: '#94a3b8' }}>
|
||
{idx === 0 ? '🟢 نطاق التدخل الفوري الذهبي' : idx === 1 ? '🟡 نطاق الدعم والإسناد السريع' : '🔴 النطاق الحرج / الأقصى'}
|
||
</div>
|
||
</div>
|
||
<div style={{ textAlign: 'left' }}>
|
||
<div style={{ fontSize: '0.9rem', fontWeight: 900, color: tier.color }}>
|
||
{tier.estimatedAreaKm2} كم²
|
||
</div>
|
||
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>المساحة المغطاة</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Quick Export & Actions */}
|
||
<div style={{ display: 'flex', gap: 6 }}>
|
||
<button
|
||
onClick={() => {
|
||
navigator.clipboard.writeText(JSON.stringify(isochroneData, null, 2));
|
||
setCopiedIsochroneGeoJson(true);
|
||
setTimeout(() => setCopiedIsochroneGeoJson(false), 2500);
|
||
}}
|
||
style={{
|
||
flex: 1,
|
||
background: 'rgba(255,255,255,0.06)',
|
||
border: '1px solid rgba(255,255,255,0.15)',
|
||
color: copiedIsochroneGeoJson ? '#4ade80' : '#fff',
|
||
padding: '6px',
|
||
borderRadius: 6,
|
||
fontSize: '0.72rem',
|
||
fontWeight: 700,
|
||
cursor: 'pointer',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: 5
|
||
}}
|
||
>
|
||
{copiedIsochroneGeoJson ? <Check size={13} /> : <Share2 size={13} />}
|
||
<span>{copiedIsochroneGeoJson ? 'تم نسخ GeoJSON بنجاح' : 'نسخ بيانات GeoJSON'}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</aside>
|
||
|
||
{/* Tactical Map Container */}
|
||
<div ref={mapContainer} style={{ flex: 1, height: '100%', position: 'relative' }} />
|
||
</div>
|
||
|
||
{/* Tactical License Authorization Modal Overlay */}
|
||
{authStatus !== 'authorized' && (
|
||
<div style={{
|
||
position: 'fixed',
|
||
top: 0,
|
||
left: 0,
|
||
width: '100vw',
|
||
height: '100vh',
|
||
background: 'rgba(5, 8, 15, 0.94)',
|
||
backdropFilter: 'blur(25px)',
|
||
zIndex: 9999,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
direction: 'rtl',
|
||
padding: 20
|
||
}}>
|
||
<div style={{
|
||
width: '100%',
|
||
maxWidth: 520,
|
||
background: 'linear-gradient(180deg, rgba(15, 23, 42, 0.98), rgba(11, 19, 38, 0.98))',
|
||
border: '1px solid rgba(56, 189, 248, 0.3)',
|
||
borderRadius: 16,
|
||
boxShadow: '0 0 50px rgba(14, 165, 233, 0.2), 0 25px 50px -12px rgba(0, 0, 0, 0.8)',
|
||
padding: '32px 28px',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
alignItems: 'center',
|
||
textAlign: 'center',
|
||
position: 'relative'
|
||
}}>
|
||
{/* Top Tactical Glowing Badge */}
|
||
<div style={{
|
||
width: 64,
|
||
height: 64,
|
||
borderRadius: 20,
|
||
background: 'linear-gradient(135deg, rgba(14, 165, 233, 0.2), rgba(79, 70, 229, 0.3))',
|
||
border: '1px solid rgba(56, 189, 248, 0.4)',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
marginBottom: 18,
|
||
boxShadow: '0 0 25px rgba(56, 189, 248, 0.35)'
|
||
}}>
|
||
{authStatus === 'validating' ? (
|
||
<Radio size={32} color="#38bdf8" />
|
||
) : (
|
||
<ShieldAlert size={32} color="#38bdf8" />
|
||
)}
|
||
</div>
|
||
|
||
<h2 style={{ fontSize: '1.35rem', fontWeight: 800, color: '#f8fafc', margin: '0 0 6px 0' }}>
|
||
منظومة القيادة والسيطرة والتحليل التكتيكي
|
||
</h2>
|
||
<div style={{ fontSize: '0.82rem', color: '#94a3b8', marginBottom: 20, lineHeight: 1.5 }}>
|
||
بوابة التخويل الأمني العسكري — يتطلب استخدام أدوات التحليل الطبوغرافي ورمايات المدفعية وحقول الألغام مفتاح تصريح معتمد (<span style={{ color: '#38bdf8', fontFamily: 'monospace' }}>Enterprise Key</span>).
|
||
</div>
|
||
|
||
{authError && (
|
||
<div style={{
|
||
width: '100%',
|
||
background: 'rgba(239, 68, 68, 0.12)',
|
||
border: '1px solid rgba(239, 68, 68, 0.3)',
|
||
borderRadius: 8,
|
||
padding: '10px 14px',
|
||
color: '#f87171',
|
||
fontSize: '0.8rem',
|
||
fontWeight: 600,
|
||
marginBottom: 18,
|
||
textAlign: 'right',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 8
|
||
}}>
|
||
<AlertTriangle size={16} style={{ flexShrink: 0 }} />
|
||
<span>{authError}</span>
|
||
</div>
|
||
)}
|
||
|
||
<form
|
||
onSubmit={(e) => {
|
||
e.preventDefault();
|
||
verifyTacticalLicense(inputKey);
|
||
}}
|
||
style={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 14 }}
|
||
>
|
||
<div style={{ textAlign: 'right' }}>
|
||
<label style={{ display: 'block', fontSize: '0.78rem', fontWeight: 700, color: '#cbd5e1', marginBottom: 6 }}>
|
||
أدخل مفتاح الترخيص والتصريح التكتيكي:
|
||
</label>
|
||
<div style={{ position: 'relative' }}>
|
||
<input
|
||
type="password"
|
||
value={inputKey}
|
||
onChange={(e) => setInputKey(e.target.value)}
|
||
placeholder="in_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||
autoFocus
|
||
style={{
|
||
width: '100%',
|
||
background: 'rgba(15, 23, 42, 0.8)',
|
||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||
borderRadius: 10,
|
||
padding: '12px 14px',
|
||
color: '#ffffff',
|
||
fontSize: '0.9rem',
|
||
fontFamily: 'monospace',
|
||
outline: 'none',
|
||
boxSizing: 'border-box'
|
||
}}
|
||
/>
|
||
<Key size={16} color="#64748b" style={{ position: 'absolute', left: 14, top: 14 }} />
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={isVerifying || !inputKey.trim()}
|
||
style={{
|
||
width: '100%',
|
||
background: 'linear-gradient(135deg, #0284c7, #4f46e5)',
|
||
border: 'none',
|
||
borderRadius: 10,
|
||
padding: '12px',
|
||
color: '#ffffff',
|
||
fontSize: '0.88rem',
|
||
fontWeight: 800,
|
||
cursor: isVerifying || !inputKey.trim() ? 'not-allowed' : 'pointer',
|
||
opacity: isVerifying || !inputKey.trim() ? 0.6 : 1,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: 8,
|
||
boxShadow: '0 4px 14px rgba(2, 132, 199, 0.4)',
|
||
transition: 'all 0.2s ease'
|
||
}}
|
||
>
|
||
{isVerifying ? (
|
||
<>
|
||
<Activity size={16} />
|
||
<span>جاري التحقق من الصلاحيات والتصريح...</span>
|
||
</>
|
||
) : (
|
||
<>
|
||
<ShieldCheck size={16} />
|
||
<span>تفعيل الدخول والتحقق من الصلاحية</span>
|
||
</>
|
||
)}
|
||
</button>
|
||
</form>
|
||
|
||
<div style={{ marginTop: 22, paddingTop: 16, borderTop: '1px solid rgba(255,255,255,0.08)', width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '0.75rem', color: '#64748b' }}>
|
||
<span>منصة الخرائط والأنظمة الذكية</span>
|
||
<a
|
||
href="/"
|
||
style={{
|
||
color: '#38bdf8',
|
||
textDecoration: 'none',
|
||
fontWeight: 600,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 4
|
||
}}
|
||
>
|
||
← العودة إلى الخريطة العامة
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|