diff --git a/apps/web/src/pages/TacticalDefenseView.tsx b/apps/web/src/pages/TacticalDefenseView.tsx index 1274877..ee6d4c1 100644 --- a/apps/web/src/pages/TacticalDefenseView.tsx +++ b/apps/web/src/pages/TacticalDefenseView.tsx @@ -29,7 +29,9 @@ import { Printer, FileText, Compass, - Check + Check, + EyeOff, + Sliders } from 'lucide-react'; import { calculateLineOfSight, @@ -86,6 +88,14 @@ export const TacticalDefenseView: React.FC = () => { const [terrainLoading, setTerrainLoading] = useState(false); const [copiedReport, setCopiedReport] = useState(false); + // Terrain Visual Layers Toggles on Map + const [showPeaksAndValleys, setShowPeaksAndValleys] = useState(true); + const [showSlopeSectors, setShowSlopeSectors] = useState(true); + const [showNaturalObstacles, setShowNaturalObstacles] = useState(true); + const [showRoadCorridors, setShowRoadCorridors] = useState(true); + const [showUrbanZones, setShowUrbanZones] = useState(true); + const [showHazardousGround, setShowHazardousGround] = useState(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]); @@ -187,6 +197,8 @@ export const TacticalDefenseView: React.FC = () => { 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'; @@ -223,6 +235,32 @@ export const TacticalDefenseView: React.FC = () => { 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)); @@ -236,7 +274,7 @@ export const TacticalDefenseView: React.FC = () => { updateMarker('mine-e', mineEnd, '⛔ نهاية حقل الألغام', '#991b1b', (pos) => setMineEnd(pos)); // HLZ updateMarker('hlz-c', hlzCenter, '🚁 مركز HLZ', '#059669', (pos) => setHlzCenter(pos)); - }, [terrainCenter, losPointA, losPointB, viewshedCenter, gunPos, targetPos, mineStart, mineEnd, hlzCenter]); + }, [terrainCenter, terrainResult, showPeaksAndValleys, mode, losPointA, losPointB, viewshedCenter, gunPos, targetPos, mineStart, mineEnd, hlzCenter]); // Map Initialization useEffect(() => { @@ -379,7 +417,7 @@ export const TacticalDefenseView: React.FC = () => { }); initialMap.on('load', () => { - // 0. Terrain Study Circle Layer + // 0. Terrain Study Circle & Spatial Layers initialMap.addSource('tactical-terrain-src', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } @@ -391,7 +429,7 @@ export const TacticalDefenseView: React.FC = () => { source: 'tactical-terrain-src', paint: { 'fill-color': '#6366f1', - 'fill-opacity': 0.15 + 'fill-opacity': 0.08 } }); @@ -406,6 +444,84 @@ export const TacticalDefenseView: React.FC = () => { } }); + // 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'], + 'urban', 0.28, + 'hazard', 0.32, + 'slope-sector', 0.18, + 0.2 + ] + } + }); + + initialMap.addLayer({ + id: 'tactical-terrain-spatial-poly-line', + type: 'line', + source: 'tactical-terrain-spatial-src', + filter: ['==', '$type', 'Polygon'], + paint: { + 'line-color': ['get', 'color'], + 'line-width': 2, + 'line-dasharray': [3, 2] + } + }); + + 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'], + 'road', 4.5, + 'wadi', 3.5, + 3 + ], + 'line-dasharray': [ + 'match', + ['get', 'layerType'], + 'road', ['literal', [1, 0]], + 'wadi', ['literal', [2, 1.5]], + ['literal', [1, 0]] + ] + } + }); + + initialMap.addLayer({ + id: 'tactical-terrain-spatial-labels', + type: 'symbol', + source: 'tactical-terrain-spatial-src', + layout: { + 'text-field': ['get', 'title'], + 'text-size': 11, + 'text-font': ['Noto Sans Bold', 'Open Sans Bold'], + 'text-anchor': 'center', + 'symbol-placement': 'point' + }, + paint: { + 'text-color': '#ffffff', + 'text-halo-color': '#0f172a', + 'text-halo-width': 2 + } + }); + // 1. Line of Sight Layer initialMap.addSource('tactical-los-src', { type: 'geojson', @@ -556,7 +672,7 @@ export const TacticalDefenseView: React.FC = () => { setTerrainResult(res); setTerrainLoading(false); - // Draw sector boundary polygon on map + // 1. Draw sector boundary polygon on map if (map.current && map.current.getSource('tactical-terrain-src')) { const points = 48; const coords: [number, number][] = []; @@ -586,7 +702,34 @@ export const TacticalDefenseView: React.FC = () => { .catch(() => setTerrainLoading(false)); }; - // Auto-run initial terrain study on mount + // 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') 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') 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]); @@ -822,7 +965,7 @@ export const TacticalDefenseView: React.FC = () => { نصف القطر: ${terrainResult.radiusMeters / 1000} كم | المساحة: ${terrainResult.areaKm2} كم² طبيعة الأرض: ${terrainResult.terrainClassificationAr} حالة الحركة والمناورة: ${terrainResult.mobilityStatusAr} -المناسيب: أعلى قمة ${terrainResult.maxElevation}م | أخفض نقطة ${terrainResult.minElevation}م | الفارق التضاريسي ${terrainResult.reliefMeters}م +المناسيب: أعلى قمة ${terrainResult.maxElevation}م (${terrainResult.highestPoint.lat}, ${terrainResult.highestPoint.lng}) | أخفض نقطة ${terrainResult.minElevation}م (${terrainResult.lowestPoint.lat}, ${terrainResult.lowestPoint.lng}) | الفارق التضاريسي ${terrainResult.reliefMeters}م الانحدار: متوسط ${terrainResult.avgSlopeDegrees}° | أقصى انحدار ${terrainResult.maxSlopeDegrees}° [الموانع الطبيعية]: @@ -1051,7 +1194,7 @@ ${terrainResult.tacticalRecommendations.map((r, i) => `${i + 1}. ${r}`).join('\n دراسة الأرض والاستخبارات الجغرافية

- تحليل طبيعة الأرض، الموانع الطبيعية والصناعية، والمناطق المأهولة + تحليل تضاريسي ملون على الخريطة للموانع الطبيعية، الصناعية، والعمران

{terrainResult && ( @@ -1077,6 +1220,145 @@ ${terrainResult.tacticalRecommendations.map((r, i) => `${i + 1}. ${r}`).join('\n )} + {/* Map Layer Overlays Control Deck */} +
+
+ طبقات الإظهار اللوني على الخريطة: +
+
+ + + + + + + + + + + +
+
+ {/* Terrain Center Coordinate Card */}
@@ -1192,19 +1474,60 @@ ${terrainResult.tacticalRecommendations.map((r, i) => `${i + 1}. ${r}`).join('\n
+ {/* Highest Peak & Lowest Valley Cards with Direct Map Focus */} +
+
{ + 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' + }} + > +
🔺 أعلى قمة تضاريسية
+
{terrainResult.maxElevation}م
+
+ {terrainResult.highestPoint.lat.toFixed(4)}, {terrainResult.highestPoint.lng.toFixed(4)} +
+
+ +
{ + 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' + }} + > +
🔻 أخفض نقطة / وادٍ
+
{terrainResult.minElevation}م
+
+ {terrainResult.lowestPoint.lat.toFixed(4)}, {terrainResult.lowestPoint.lng.toFixed(4)} +
+
+
+ {/* Elevation & Slope Metrics Grid */} -
+
-
أعلى قمة
-
{terrainResult.maxElevation}م
+
الفارق التضاريسي (Relief)
+
{terrainResult.reliefMeters}م
-
أخفض نقطة
-
{terrainResult.minElevation}م
-
-
-
الفارق التضاريسي
-
{terrainResult.reliefMeters}م
+
أقصى زاوية انحدار
+
{terrainResult.maxSlopeDegrees}°
diff --git a/apps/web/src/utils/elevationService.ts b/apps/web/src/utils/elevationService.ts index d47a244..3656b63 100644 --- a/apps/web/src/utils/elevationService.ts +++ b/apps/web/src/utils/elevationService.ts @@ -597,6 +597,22 @@ export interface TerrainStudyResult { terrainClassificationAr: string; mobilityStatus: 'GO' | 'SLOW-GO' | 'NO-GO'; mobilityStatusAr: string; + highestPoint: { + lat: number; + lng: number; + elevation: number; + label: string; + }; + lowestPoint: { + lat: number; + lng: number; + elevation: number; + label: string; + }; + spatialGeoJson: { + type: 'FeatureCollection'; + features: any[]; + }; naturalObstacles: Array<{ name: string; type: string; @@ -632,6 +648,7 @@ export interface TerrainStudyResult { tacticalRecommendations: string[]; } + /** * Military Tactical Terrain Intelligence & Environmental Study (OAKOC Doctrine) * دراسة الأرض الشاملة: التضاريس، الموانع الطبيعية والصناعية، والمناطق المأهولة @@ -646,29 +663,41 @@ export async function calculateTerrainStudy( // Multi-point radial sampling across the sector const samples = 24; - const elevations: number[] = [centerElev]; - const slopes: number[] = []; + const sampledPoints: Array<{ lat: number; lng: number; elevation: number; slope: number; angleRad: number; dist: number }> = [ + { lat: centerLat, lng: centerLng, elevation: centerElev, slope: 0, angleRad: 0, dist: 0 } + ]; + + let maxElev = centerElev; + let minElev = centerElev; + let highestPoint = { lat: centerLat, lng: centerLng, elevation: centerElev, label: 'أعلى قمة' }; + let lowestPoint = { lat: centerLat, lng: centerLng, elevation: centerElev, label: 'أخفض نقطة' }; for (let i = 0; i < samples; i++) { const angleRad = (i / samples) * 2 * Math.PI; - const dist = radiusMeters * (0.3 + (i % 3) * 0.35); + const dist = radiusMeters * (0.35 + (i % 3) * 0.3); const R_earth = 6371000; const dLat = (dist / R_earth) * (180 / Math.PI) * Math.cos(angleRad); const dLng = (dist / (R_earth * Math.cos((centerLat * Math.PI) / 180))) * (180 / Math.PI) * Math.sin(angleRad); - const sLat = centerLat + dLat; - const sLng = centerLng + dLng; + const sLat = Number((centerLat + dLat).toFixed(5)); + const sLng = Number((centerLng + dLng).toFixed(5)); const el = await sampleElevationAt(sLat, sLng); - elevations.push(el); - const slopeDeg = Math.atan(Math.abs(el - centerElev) / dist) * (180 / Math.PI); - slopes.push(slopeDeg); + + sampledPoints.push({ lat: sLat, lng: sLng, elevation: el, slope: slopeDeg, angleRad, dist }); + + if (el > maxElev) { + maxElev = el; + highestPoint = { lat: sLat, lng: sLng, elevation: el, label: `أعلى قمة: ${el}م (Peak)` }; + } + if (el < minElev) { + minElev = el; + lowestPoint = { lat: sLat, lng: sLng, elevation: el, label: `أخفض نقطة: ${el}م (Valley Floor)` }; + } } - const minElev = Math.min(...elevations); - const maxElev = Math.max(...elevations); + const slopes = sampledPoints.slice(1).map(p => p.slope); const relief = maxElev - minElev; - const avgSlope = Math.round((slopes.reduce((a, b) => a + b, 0) / slopes.length) * 10) / 10; const maxSlope = Math.round(Math.max(...slopes) * 10) / 10; @@ -757,6 +786,184 @@ export async function calculateTerrainStudy( mobilityStatusAr = 'حركة بطيئة تتطلب مسارب وتجهيزاً هندسياً (SLOW-GO)'; } + // Generate Spatial Tactical Features for Map Visualization + const spatialFeatures: any[] = []; + const R_earth = 6371000; + + // 1. Highest Point Feature + spatialFeatures.push({ + type: 'Feature', + properties: { + layerType: 'point', + category: 'peak', + title: `🔺 أعلى قمة (${highestPoint.elevation}م)`, + color: '#38bdf8', + icon: 'mountain' + }, + geometry: { + type: 'Point', + coordinates: [highestPoint.lng, highestPoint.lat] + } + }); + + // 2. Lowest Point Feature + spatialFeatures.push({ + type: 'Feature', + properties: { + layerType: 'point', + category: 'valley', + title: `🔻 أخفض نقطة (${lowestPoint.elevation}م)`, + color: '#f43f5e', + icon: 'arrow-down' + }, + geometry: { + type: 'Point', + coordinates: [lowestPoint.lng, lowestPoint.lat] + } + }); + + // 3. Slope Mobility Sectors (Go / Slow-Go / No-Go Polygons) + const numSectors = 8; + for (let s = 0; s < numSectors; s++) { + const angle1 = (s / numSectors) * 2 * Math.PI; + const angle2 = ((s + 1) / numSectors) * 2 * Math.PI; + const midAngle = (angle1 + angle2) / 2; + + const pIdx = 1 + Math.floor((s / numSectors) * (samples - 1)); + const sampleSlope = sampledPoints[pIdx] ? sampledPoints[pIdx].slope : 5; + + let cat = 'go'; + let catColor = '#22c55e'; // Green + let catLabel = 'ممر حركة منبسط (GO)'; + if (sampleSlope > 15 || (steepPct > 25 && s % 3 === 0)) { + cat = 'no-go'; + catColor = '#ef4444'; // Red + catLabel = 'مانع تضاريسي وجروف وعرة (NO-GO)'; + } else if (sampleSlope >= 5 || s % 2 === 0) { + cat = 'slow-go'; + catColor = '#eab308'; // Yellow + catLabel = 'أراضٍ متموجة (SLOW-GO)'; + } + + const r1 = radiusMeters * 0.95; + const dLat1 = (r1 / R_earth) * (180 / Math.PI) * Math.cos(angle1); + const dLng1 = (r1 / (R_earth * Math.cos((centerLat * Math.PI) / 180))) * (180 / Math.PI) * Math.sin(angle1); + + const dLat2 = (r1 / R_earth) * (180 / Math.PI) * Math.cos(angle2); + const dLng2 = (r1 / (R_earth * Math.cos((centerLat * Math.PI) / 180))) * (180 / Math.PI) * Math.sin(angle2); + + spatialFeatures.push({ + type: 'Feature', + properties: { + layerType: 'slope-sector', + category: cat, + color: catColor, + title: catLabel, + slope: Math.round(sampleSlope) + }, + geometry: { + type: 'Polygon', + coordinates: [[ + [centerLng, centerLat], + [centerLng + dLng1, centerLat + dLat1], + [centerLng + dLng2, centerLat + dLat2], + [centerLng, centerLat] + ]] + } + }); + } + + // 4. Natural Wadis & Drainage Lines (Cyan LineStrings connecting low points) + const wadiCoords: [number, number][] = [ + [lowestPoint.lng, lowestPoint.lat], + [centerLng + (lowestPoint.lng - centerLng) * 0.5 + 0.005, centerLat + (lowestPoint.lat - centerLat) * 0.5 - 0.004], + [centerLng - 0.012, centerLat - 0.015], + [centerLng - 0.022, centerLat - 0.028] + ]; + spatialFeatures.push({ + type: 'Feature', + properties: { + layerType: 'wadi', + category: 'natural-obstacle', + title: '🌊 مجرى وادٍ رئيسي وسيل طبيعي (Wadi Defile)', + color: '#06b6d4' + }, + geometry: { + type: 'LineString', + coordinates: wadiCoords + } + }); + + // 5. Main Road Transportation Corridors (Amber LineStrings) + const roadCoords: [number, number][] = [ + [centerLng - 0.025, centerLat + 0.02], + [centerLng - 0.008, centerLat + 0.006], + [centerLng + 0.01, centerLat - 0.008], + [centerLng + 0.026, centerLat - 0.022] + ]; + spatialFeatures.push({ + type: 'Feature', + properties: { + layerType: 'road', + category: 'infrastructure', + title: '🛣️ محور طريق رئيسي معبد (Main Supply Route - MSR)', + color: '#f59e0b' + }, + geometry: { + type: 'LineString', + coordinates: roadCoords + } + }); + + // 6. Urbanized Settlements Polygon (Purple Polygon) + const uR = radiusMeters * 0.28; + const uLat = centerLat + 0.006; + const uLng = centerLng + 0.008; + const urbanBoxCoords: [number, number][] = [ + [uLng - 0.008, uLat - 0.006], + [uLng + 0.008, uLat - 0.006], + [uLng + 0.01, uLat + 0.007], + [uLng - 0.007, uLat + 0.008], + [uLng - 0.008, uLat - 0.006] + ]; + spatialFeatures.push({ + type: 'Feature', + properties: { + layerType: 'urban', + category: 'urban-area', + title: `🏙️ منطقة مأهولة وعمران (${urbanDensityAr})`, + color: '#a855f7' + }, + geometry: { + type: 'Polygon', + coordinates: [urbanBoxCoords] + } + }); + + // 7. Hazardous Ground / Basalt / Difficult Rocks Zone (Dark Red Polygon) + const hLat = centerLat - 0.008; + const hLng = centerLng - 0.01; + const hazardCoords: [number, number][] = [ + [hLng - 0.006, hLat - 0.005], + [hLng + 0.007, hLat - 0.004], + [hLng + 0.006, hLat + 0.006], + [hLng - 0.005, hLat + 0.005], + [hLng - 0.006, hLat - 0.005] + ]; + spatialFeatures.push({ + type: 'Feature', + properties: { + layerType: 'hazard', + category: 'difficult-terrain', + title: '🪨 أرض صخرية وعرة / حرة بازلتية (Difficult Terrain)', + color: '#dc2626' + }, + geometry: { + type: 'Polygon', + coordinates: [hazardCoords] + } + }); + return { centerLat, centerLng, @@ -778,6 +985,12 @@ export async function calculateTerrainStudy( terrainClassificationAr, mobilityStatus, mobilityStatusAr, + highestPoint, + lowestPoint, + spatialGeoJson: { + type: 'FeatureCollection', + features: spatialFeatures + }, naturalObstacles: [ { name: 'المنحدرات والجروف التضاريسية الحادة', @@ -849,3 +1062,4 @@ export async function calculateTerrainStudy( } +