feat(tactical): add map visualization layers for peaks, valleys, slope sectors, wadis, roads, urban, and basalt hazards

This commit is contained in:
Hamza-Ayed
2026-08-18 10:56:58 +03:00
parent 6bd0bc0ce5
commit b1ee482830
2 changed files with 565 additions and 28 deletions
+340 -17
View File
@@ -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<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
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
<Mountain size={18} /> دراسة الأرض والاستخبارات الجغرافية
</h3>
<p style={{ margin: '4px 0 0 0', fontSize: '0.75rem', color: '#94a3b8' }}>
تحليل طبيعة الأرض، الموانع الطبيعية والصناعية، والمناطق المأهولة
تحليل تضاريسي ملون على الخريطة للموانع الطبيعية، الصناعية، والعمران
</p>
</div>
{terrainResult && (
@@ -1077,6 +1220,145 @@ ${terrainResult.tacticalRecommendations.map((r, i) => `${i + 1}. ${r}`).join('\n
)}
</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 */}
<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 }}>
@@ -1192,19 +1474,60 @@ ${terrainResult.tacticalRecommendations.map((r, i) => `${i + 1}. ${r}`).join('\n
</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 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={{ fontSize: '0.62rem', color: '#94a3b8' }}>أعلى قمة</div>
<div style={{ fontSize: '0.9rem', fontWeight: 800, color: '#38bdf8' }}>{terrainResult.maxElevation}م</div>
<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.62rem', color: '#94a3b8' }}>أخفض نقطة</div>
<div style={{ fontSize: '0.9rem', fontWeight: 800, color: '#38bdf8' }}>{terrainResult.minElevation}م</div>
</div>
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 7, borderRadius: 6, textAlign: 'center' }}>
<div style={{ fontSize: '0.62rem', color: '#94a3b8' }}>الفارق التضاريسي</div>
<div style={{ fontSize: '0.9rem', fontWeight: 800, color: '#fb923c' }}>{terrainResult.reliefMeters}م</div>
<div style={{ fontSize: '0.65rem', color: '#94a3b8' }}>أقصى زاوية انحدار</div>
<div style={{ fontSize: '0.95rem', fontWeight: 800, color: '#f87171' }}>{terrainResult.maxSlopeDegrees}°</div>
</div>
</div>