feat(tactical): add map visualization layers for peaks, valleys, slope sectors, wadis, roads, urban, and basalt hazards
This commit is contained in:
@@ -29,7 +29,9 @@ import {
|
|||||||
Printer,
|
Printer,
|
||||||
FileText,
|
FileText,
|
||||||
Compass,
|
Compass,
|
||||||
Check
|
Check,
|
||||||
|
EyeOff,
|
||||||
|
Sliders
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
calculateLineOfSight,
|
calculateLineOfSight,
|
||||||
@@ -86,6 +88,14 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
const [terrainLoading, setTerrainLoading] = useState<boolean>(false);
|
const [terrainLoading, setTerrainLoading] = useState<boolean>(false);
|
||||||
const [copiedReport, setCopiedReport] = useState<boolean>(false);
|
const [copiedReport, setCopiedReport] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// Terrain Visual Layers Toggles on Map
|
||||||
|
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
|
// 2. Line of Sight State
|
||||||
const [losPointA, setLosPointA] = useState<[number, number] | null>([31.9539, 35.9106]);
|
const [losPointA, setLosPointA] = useState<[number, number] | null>([31.9539, 35.9106]);
|
||||||
const [losPointB, setLosPointB] = useState<[number, number] | null>([32.0125, 35.8540]);
|
const [losPointB, setLosPointB] = useState<[number, number] | null>([32.0125, 35.8540]);
|
||||||
@@ -187,6 +197,8 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
|
|
||||||
if (markersRef.current[id]) {
|
if (markersRef.current[id]) {
|
||||||
markersRef.current[id].setLngLat([coords[1], coords[0]]);
|
markersRef.current[id].setLngLat([coords[1], coords[0]]);
|
||||||
|
const el = markersRef.current[id].getElement();
|
||||||
|
if (el) el.innerText = label;
|
||||||
} else {
|
} else {
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = 'tactical-marker';
|
el.className = 'tactical-marker';
|
||||||
@@ -223,6 +235,32 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
if (!map.current) return;
|
if (!map.current) return;
|
||||||
// Terrain Study Center
|
// Terrain Study Center
|
||||||
updateMarker('terrain-c', terrainCenter, '⛰️ مركز دراسة الأرض', '#6366f1', (pos) => setTerrainCenter(pos));
|
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
|
// LOS
|
||||||
updateMarker('los-a', losPointA, '📍 راصد A', '#0284c7', (pos) => setLosPointA(pos));
|
updateMarker('los-a', losPointA, '📍 راصد A', '#0284c7', (pos) => setLosPointA(pos));
|
||||||
updateMarker('los-b', losPointB, '🎯 هدف B', '#d97706', (pos) => setLosPointB(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));
|
updateMarker('mine-e', mineEnd, '⛔ نهاية حقل الألغام', '#991b1b', (pos) => setMineEnd(pos));
|
||||||
// HLZ
|
// HLZ
|
||||||
updateMarker('hlz-c', hlzCenter, '🚁 مركز HLZ', '#059669', (pos) => setHlzCenter(pos));
|
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
|
// Map Initialization
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -379,7 +417,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
initialMap.on('load', () => {
|
initialMap.on('load', () => {
|
||||||
// 0. Terrain Study Circle Layer
|
// 0. Terrain Study Circle & Spatial Layers
|
||||||
initialMap.addSource('tactical-terrain-src', {
|
initialMap.addSource('tactical-terrain-src', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: { type: 'FeatureCollection', features: [] }
|
data: { type: 'FeatureCollection', features: [] }
|
||||||
@@ -391,7 +429,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
source: 'tactical-terrain-src',
|
source: 'tactical-terrain-src',
|
||||||
paint: {
|
paint: {
|
||||||
'fill-color': '#6366f1',
|
'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
|
// 1. Line of Sight Layer
|
||||||
initialMap.addSource('tactical-los-src', {
|
initialMap.addSource('tactical-los-src', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
@@ -556,7 +672,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
setTerrainResult(res);
|
setTerrainResult(res);
|
||||||
setTerrainLoading(false);
|
setTerrainLoading(false);
|
||||||
|
|
||||||
// Draw sector boundary polygon on map
|
// 1. Draw sector boundary polygon on map
|
||||||
if (map.current && map.current.getSource('tactical-terrain-src')) {
|
if (map.current && map.current.getSource('tactical-terrain-src')) {
|
||||||
const points = 48;
|
const points = 48;
|
||||||
const coords: [number, number][] = [];
|
const coords: [number, number][] = [];
|
||||||
@@ -586,7 +702,34 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
.catch(() => setTerrainLoading(false));
|
.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(() => {
|
useEffect(() => {
|
||||||
runTerrainStudy();
|
runTerrainStudy();
|
||||||
}, [terrainCenter, terrainRadius]);
|
}, [terrainCenter, terrainRadius]);
|
||||||
@@ -822,7 +965,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
نصف القطر: ${terrainResult.radiusMeters / 1000} كم | المساحة: ${terrainResult.areaKm2} كم²
|
نصف القطر: ${terrainResult.radiusMeters / 1000} كم | المساحة: ${terrainResult.areaKm2} كم²
|
||||||
طبيعة الأرض: ${terrainResult.terrainClassificationAr}
|
طبيعة الأرض: ${terrainResult.terrainClassificationAr}
|
||||||
حالة الحركة والمناورة: ${terrainResult.mobilityStatusAr}
|
حالة الحركة والمناورة: ${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}°
|
الانحدار: متوسط ${terrainResult.avgSlopeDegrees}° | أقصى انحدار ${terrainResult.maxSlopeDegrees}°
|
||||||
|
|
||||||
[الموانع الطبيعية]:
|
[الموانع الطبيعية]:
|
||||||
@@ -1051,7 +1194,7 @@ ${terrainResult.tacticalRecommendations.map((r, i) => `${i + 1}. ${r}`).join('\n
|
|||||||
<Mountain size={18} /> دراسة الأرض والاستخبارات الجغرافية
|
<Mountain size={18} /> دراسة الأرض والاستخبارات الجغرافية
|
||||||
</h3>
|
</h3>
|
||||||
<p style={{ margin: '4px 0 0 0', fontSize: '0.75rem', color: '#94a3b8' }}>
|
<p style={{ margin: '4px 0 0 0', fontSize: '0.75rem', color: '#94a3b8' }}>
|
||||||
تحليل طبيعة الأرض، الموانع الطبيعية والصناعية، والمناطق المأهولة
|
تحليل تضاريسي ملون على الخريطة للموانع الطبيعية، الصناعية، والعمران
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{terrainResult && (
|
{terrainResult && (
|
||||||
@@ -1077,6 +1220,145 @@ ${terrainResult.tacticalRecommendations.map((r, i) => `${i + 1}. ${r}`).join('\n
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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={() => 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 */}
|
{/* 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={{ 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 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||||||
@@ -1192,19 +1474,60 @@ ${terrainResult.tacticalRecommendations.map((r, i) => `${i + 1}. ${r}`).join('\n
|
|||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Elevation & Slope Metrics Grid */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 6 }}>
|
<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={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6, textAlign: 'center' }}>
|
||||||
<div style={{ fontSize: '0.62rem', color: '#94a3b8' }}>أعلى قمة</div>
|
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>الفارق التضاريسي (Relief)</div>
|
||||||
<div style={{ fontSize: '0.9rem', fontWeight: 800, color: '#38bdf8' }}>{terrainResult.maxElevation}م</div>
|
<div style={{ fontSize: '0.95rem', fontWeight: 800, color: '#fb923c' }}>{terrainResult.reliefMeters}م</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6, textAlign: 'center' }}>
|
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6, textAlign: 'center' }}>
|
||||||
<div style={{ fontSize: '0.62rem', color: '#94a3b8' }}>أخفض نقطة</div>
|
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>أقصى زاوية انحدار</div>
|
||||||
<div style={{ fontSize: '0.9rem', fontWeight: 800, color: '#38bdf8' }}>{terrainResult.minElevation}م</div>
|
<div style={{ fontSize: '0.95rem', fontWeight: 800, color: '#f87171' }}>{terrainResult.maxSlopeDegrees}°</div>
|
||||||
</div>
|
|
||||||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6, textAlign: 'center' }}>
|
|
||||||
<div style={{ fontSize: '0.62rem', color: '#94a3b8' }}>الفارق التضاريسي</div>
|
|
||||||
<div style={{ fontSize: '0.9rem', fontWeight: 800, color: '#fb923c' }}>{terrainResult.reliefMeters}م</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -597,6 +597,22 @@ export interface TerrainStudyResult {
|
|||||||
terrainClassificationAr: string;
|
terrainClassificationAr: string;
|
||||||
mobilityStatus: 'GO' | 'SLOW-GO' | 'NO-GO';
|
mobilityStatus: 'GO' | 'SLOW-GO' | 'NO-GO';
|
||||||
mobilityStatusAr: string;
|
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<{
|
naturalObstacles: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -632,6 +648,7 @@ export interface TerrainStudyResult {
|
|||||||
tacticalRecommendations: string[];
|
tacticalRecommendations: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Military Tactical Terrain Intelligence & Environmental Study (OAKOC Doctrine)
|
* Military Tactical Terrain Intelligence & Environmental Study (OAKOC Doctrine)
|
||||||
* دراسة الأرض الشاملة: التضاريس، الموانع الطبيعية والصناعية، والمناطق المأهولة
|
* دراسة الأرض الشاملة: التضاريس، الموانع الطبيعية والصناعية، والمناطق المأهولة
|
||||||
@@ -646,29 +663,41 @@ export async function calculateTerrainStudy(
|
|||||||
|
|
||||||
// Multi-point radial sampling across the sector
|
// Multi-point radial sampling across the sector
|
||||||
const samples = 24;
|
const samples = 24;
|
||||||
const elevations: number[] = [centerElev];
|
const sampledPoints: Array<{ lat: number; lng: number; elevation: number; slope: number; angleRad: number; dist: number }> = [
|
||||||
const slopes: 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++) {
|
for (let i = 0; i < samples; i++) {
|
||||||
const angleRad = (i / samples) * 2 * Math.PI;
|
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 R_earth = 6371000;
|
||||||
const dLat = (dist / R_earth) * (180 / Math.PI) * Math.cos(angleRad);
|
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 dLng = (dist / (R_earth * Math.cos((centerLat * Math.PI) / 180))) * (180 / Math.PI) * Math.sin(angleRad);
|
||||||
|
|
||||||
const sLat = centerLat + dLat;
|
const sLat = Number((centerLat + dLat).toFixed(5));
|
||||||
const sLng = centerLng + dLng;
|
const sLng = Number((centerLng + dLng).toFixed(5));
|
||||||
const el = await sampleElevationAt(sLat, sLng);
|
const el = await sampleElevationAt(sLat, sLng);
|
||||||
elevations.push(el);
|
|
||||||
|
|
||||||
const slopeDeg = Math.atan(Math.abs(el - centerElev) / dist) * (180 / Math.PI);
|
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 slopes = sampledPoints.slice(1).map(p => p.slope);
|
||||||
const maxElev = Math.max(...elevations);
|
|
||||||
const relief = maxElev - minElev;
|
const relief = maxElev - minElev;
|
||||||
|
|
||||||
const avgSlope = Math.round((slopes.reduce((a, b) => a + b, 0) / slopes.length) * 10) / 10;
|
const avgSlope = Math.round((slopes.reduce((a, b) => a + b, 0) / slopes.length) * 10) / 10;
|
||||||
const maxSlope = Math.round(Math.max(...slopes) * 10) / 10;
|
const maxSlope = Math.round(Math.max(...slopes) * 10) / 10;
|
||||||
|
|
||||||
@@ -757,6 +786,184 @@ export async function calculateTerrainStudy(
|
|||||||
mobilityStatusAr = 'حركة بطيئة تتطلب مسارب وتجهيزاً هندسياً (SLOW-GO)';
|
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 {
|
return {
|
||||||
centerLat,
|
centerLat,
|
||||||
centerLng,
|
centerLng,
|
||||||
@@ -778,6 +985,12 @@ export async function calculateTerrainStudy(
|
|||||||
terrainClassificationAr,
|
terrainClassificationAr,
|
||||||
mobilityStatus,
|
mobilityStatus,
|
||||||
mobilityStatusAr,
|
mobilityStatusAr,
|
||||||
|
highestPoint,
|
||||||
|
lowestPoint,
|
||||||
|
spatialGeoJson: {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: spatialFeatures
|
||||||
|
},
|
||||||
naturalObstacles: [
|
naturalObstacles: [
|
||||||
{
|
{
|
||||||
name: 'المنحدرات والجروف التضاريسية الحادة',
|
name: 'المنحدرات والجروف التضاريسية الحادة',
|
||||||
@@ -849,3 +1062,4 @@ export async function calculateTerrainStudy(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user