feat(tactical): real DEM grid slope analysis, dynamic contour lines, and topography-driven wadi/ridge features

This commit is contained in:
Hamza-Ayed
2026-08-18 11:15:56 +03:00
parent b1ee482830
commit b039281972
2 changed files with 372 additions and 289 deletions
+122 -1
View File
@@ -1,5 +1,15 @@
import React, { useState, useEffect, useRef } from 'react';
import maplibregl from 'maplibre-gl';
import mlcontour from 'maplibre-contour';
const demSource = new mlcontour.DemSource({
url: 'https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png',
encoding: 'terrarium',
maxzoom: 15,
worker: true,
cacheSize: 100,
});
demSource.setupMaplibre(maplibregl);
import 'maplibre-gl/dist/maplibre-gl.css';
import {
Shield,
@@ -89,6 +99,7 @@ export const TacticalDefenseView: React.FC = () => {
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);
@@ -444,6 +455,86 @@ export const TacticalDefenseView: React.FC = () => {
}
});
// Dynamic On-the-Fly Contours from Elevation DEM
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',
@@ -702,6 +793,16 @@ export const TacticalDefenseView: React.FC = () => {
.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;
@@ -716,7 +817,7 @@ export const TacticalDefenseView: React.FC = () => {
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 === 'wadi' || f.properties.layerType === 'ridge') return showNaturalObstacles;
if (f.properties.layerType === 'road') return showRoadCorridors;
if (f.properties.layerType === 'urban') return showUrbanZones;
if (f.properties.layerType === 'hazard') return showHazardousGround;
@@ -1231,6 +1332,26 @@ ${terrainResult.tacticalRecommendations.map((r, i) => `${i + 1}. ${r}`).join('\n
<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={{
+250 -288
View File
@@ -661,65 +661,225 @@ export async function calculateTerrainStudy(
const centerElev = await sampleElevationAt(centerLat, centerLng);
const areaKm2 = Math.round(Math.PI * Math.pow(radiusMeters / 1000, 2) * 10) / 10;
// Multi-point radial sampling across the sector
const samples = 24;
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 }
];
// Grid Resolution: 11x11 sampling matrix covering the bounding box
const gridSize = 11;
const R_earth = 6371000;
const dLatTotal = (radiusMeters / R_earth) * (180 / Math.PI);
const dLngTotal = (radiusMeters / (R_earth * Math.cos((centerLat * Math.PI) / 180))) * (180 / Math.PI);
let maxElev = centerElev;
let minElev = centerElev;
let highestPoint = { lat: centerLat, lng: centerLng, elevation: centerElev, label: 'أعلى قمة' };
let lowestPoint = { lat: centerLat, lng: centerLng, elevation: centerElev, label: 'أخفض نقطة' };
const minLat = centerLat - dLatTotal;
const maxLat = centerLat + dLatTotal;
const minLng = centerLng - dLngTotal;
const maxLng = centerLng + dLngTotal;
for (let i = 0; i < samples; i++) {
const angleRad = (i / samples) * 2 * Math.PI;
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 = Number((centerLat + dLat).toFixed(5));
const sLng = Number((centerLng + dLng).toFixed(5));
const el = await sampleElevationAt(sLat, sLng);
const slopeDeg = Math.atan(Math.abs(el - centerElev) / dist) * (180 / Math.PI);
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)` };
// 1. Parallel sampling of all grid points
const gridCoords: Array<{ r: number; c: number; lat: number; lng: number }> = [];
for (let r = 0; r < gridSize; r++) {
const lat = Number((minLat + (r / (gridSize - 1)) * (maxLat - minLat)).toFixed(5));
for (let c = 0; c < gridSize; c++) {
const lng = Number((minLng + (c / (gridSize - 1)) * (maxLng - minLng)).toFixed(5));
gridCoords.push({ r, c, lat, lng });
}
}
const slopes = sampledPoints.slice(1).map(p => p.slope);
const elevationGrid: number[][] = Array(gridSize).fill(0).map(() => Array(gridSize).fill(0));
const elevPromises = gridCoords.map(async ({ r, c, lat, lng }) => {
const el = await sampleElevationAt(lat, lng);
elevationGrid[r][c] = el;
return { r, c, lat, lng, el };
});
const sampledPoints = await Promise.all(elevPromises);
// 2. Find Exact Peak and Valley
let maxElev = -9999;
let minElev = 9999;
let highestPoint = { lat: centerLat, lng: centerLng, elevation: centerElev, label: 'أعلى قمة' };
let lowestPoint = { lat: centerLat, lng: centerLng, elevation: centerElev, label: 'أخفض نقطة' };
sampledPoints.forEach(p => {
const dist = calculateDistance(centerLat, centerLng, p.lat, p.lng);
if (dist <= radiusMeters * 1.05) {
if (p.el > maxElev) {
maxElev = p.el;
highestPoint = { lat: p.lat, lng: p.lng, elevation: p.el, label: `🔺 أعلى قمة: ${p.el}م` };
}
if (p.el < minElev) {
minElev = p.el;
lowestPoint = { lat: p.lat, lng: p.lng, elevation: p.el, label: `🔻 أخفض نقطة: ${p.el}م` };
}
}
});
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;
const flatCount = slopes.filter(s => s < 5).length;
const modCount = slopes.filter(s => s >= 5 && s <= 15).length;
const steepCount = slopes.filter(s => s > 15).length;
// 3. Compute Local Gradient (Slope) per cell and build Topographic Slope Polygons
const spatialFeatures: any[] = [];
const slopeValues: number[] = [];
const flatPct = Math.round((flatCount / slopes.length) * 100);
const modPct = Math.round((modCount / slopes.length) * 100);
const dxMeters = (2 * radiusMeters) / (gridSize - 1);
const dyMeters = (2 * radiusMeters) / (gridSize - 1);
// Cell-by-cell topographic classification matching contour slopes
for (let r = 0; r < gridSize - 1; r++) {
for (let c = 0; c < gridSize - 1; c++) {
const cellCenterLat = (gridCoords[r * gridSize + c].lat + gridCoords[(r + 1) * gridSize + (c + 1)].lat) / 2;
const cellCenterLng = (gridCoords[r * gridSize + c].lng + gridCoords[(r + 1) * gridSize + (c + 1)].lng) / 2;
const distFromCenter = calculateDistance(centerLat, centerLng, cellCenterLat, cellCenterLng);
if (distFromCenter > radiusMeters * 1.02) continue;
const z00 = elevationGrid[r][c];
const z01 = elevationGrid[r][c + 1];
const z10 = elevationGrid[r + 1][c];
const z11 = elevationGrid[r + 1][c + 1];
const dz_dx = ((z01 + z11) - (z00 + z10)) / (2 * dxMeters);
const dz_dy = ((z10 + z11) - (z00 + z01)) / (2 * dyMeters);
const slopeRad = Math.atan(Math.sqrt(dz_dx * dz_dx + dz_dy * dz_dy));
const slopeDeg = Math.round((slopeRad * (180 / Math.PI)) * 10) / 10;
slopeValues.push(slopeDeg);
let cat = 'go';
let catColor = '#22c55e';
let catLabel = `ممر حركة منبسط (GO) [${slopeDeg}°]`;
if (slopeDeg > 12) {
cat = 'no-go';
catColor = '#ef4444';
catLabel = `جرف ومنحدر وعر (NO-GO) [${slopeDeg}°]`;
} else if (slopeDeg >= 5) {
cat = 'slow-go';
catColor = '#eab308';
catLabel = `تضاريس متموجة (SLOW-GO) [${slopeDeg}°]`;
}
const p1 = [gridCoords[r * gridSize + c].lng, gridCoords[r * gridSize + c].lat];
const p2 = [gridCoords[r * gridSize + (c + 1)].lng, gridCoords[r * gridSize + (c + 1)].lat];
const p3 = [gridCoords[(r + 1) * gridSize + (c + 1)].lng, gridCoords[(r + 1) * gridSize + (c + 1)].lat];
const p4 = [gridCoords[(r + 1) * gridSize + c].lng, gridCoords[(r + 1) * gridSize + c].lat];
spatialFeatures.push({
type: 'Feature',
properties: {
layerType: 'slope-sector',
category: cat,
color: catColor,
title: catLabel,
slope: slopeDeg,
elevation: Math.round((z00 + z01 + z10 + z11) / 4)
},
geometry: {
type: 'Polygon',
coordinates: [[p1, p2, p3, p4, p1]]
}
});
}
}
// Slope stats
const avgSlope = slopeValues.length > 0 ? Math.round((slopeValues.reduce((a, b) => a + b, 0) / slopeValues.length) * 10) / 10 : 2.5;
const maxSlope = slopeValues.length > 0 ? Math.round(Math.max(...slopeValues) * 10) / 10 : 5.0;
const flatCount = slopeValues.filter(s => s < 5).length;
const modCount = slopeValues.filter(s => s >= 5 && s <= 12).length;
const steepCount = slopeValues.filter(s => s > 12).length;
const totalCells = Math.max(1, slopeValues.length);
const flatPct = Math.round((flatCount / totalCells) * 100);
const modPct = Math.round((modCount / totalCells) * 100);
const steepPct = Math.max(0, 100 - flatPct - modPct);
// Sector Geolocation and Regional Characteristics
// 4. Trace the Natural Valley / Drainage Line (Wadi) using DEM lowest cells
const valleyPoints: [number, number][] = [];
for (let r = 0; r < gridSize; r++) {
let minRowElev = 9999;
let minColIdx = 0;
for (let c = 0; c < gridSize; c++) {
if (elevationGrid[r][c] < minRowElev) {
minRowElev = elevationGrid[r][c];
minColIdx = c;
}
}
const pt = gridCoords[r * gridSize + minColIdx];
const dist = calculateDistance(centerLat, centerLng, pt.lat, pt.lng);
if (dist <= radiusMeters * 1.05) {
valleyPoints.push([pt.lng, pt.lat]);
}
}
if (valleyPoints.length >= 2) {
spatialFeatures.push({
type: 'Feature',
properties: {
layerType: 'wadi',
category: 'natural-obstacle',
title: `🌊 مجرى وادٍ رئيسي ومصرف سيل طبيعي (${minElev}م)`,
color: '#06b6d4'
},
geometry: {
type: 'LineString',
coordinates: valleyPoints
}
});
}
// 5. Detect and Draw Dominant Mountain Ridge (Connecting High Points)
const ridgePoints: [number, number][] = [];
for (let r = 0; r < gridSize; r++) {
let maxRowElev = -9999;
let maxColIdx = 0;
for (let c = 0; c < gridSize; c++) {
if (elevationGrid[r][c] > maxRowElev) {
maxRowElev = elevationGrid[r][c];
maxColIdx = c;
}
}
const pt = gridCoords[r * gridSize + maxColIdx];
const dist = calculateDistance(centerLat, centerLng, pt.lat, pt.lng);
if (dist <= radiusMeters * 1.05 && maxRowElev > minElev + relief * 0.6) {
ridgePoints.push([pt.lng, pt.lat]);
}
}
if (ridgePoints.length >= 2) {
spatialFeatures.push({
type: 'Feature',
properties: {
layerType: 'ridge',
category: 'key-terrain',
title: `⛰️ سلسلة تلال حاكمة (Dominant Ridge - ${maxElev}م)`,
color: '#38bdf8'
},
geometry: {
type: 'LineString',
coordinates: ridgePoints
}
});
}
// 6. Real Regional Sector Identification
let sectorName = 'قطاع عمليات ميداني';
let terrainClassificationAr = 'هضاب وتلال متوسطة الوعورة';
let terrainClassification = 'Rolling Hills & Ridges';
let urbanDensity: 'HIGH' | 'MEDIUM' | 'LOW' | 'SPARSE' = 'LOW';
let urbanDensityAr = 'كثافة منخفضة / ريفية';
let settlements: string[] = ['تجمعات سكنية متفرقة', 'مزارع ومنشآت ريفية'];
let urbanDensity: 'HIGH' | 'MEDIUM' | 'LOW' | 'SPARSE' = 'MEDIUM';
let urbanDensityAr = 'كثافة متوسطة';
let settlements: string[] = ['تجمعات سكنية وضواحي متصلة'];
let soilType = 'تربة طينية صخرية جافة';
let trafficability = 'جيدة للمركبات المدولبة والمجنزرة مع الحذر من الانزلاقات في المنحدرات';
let trafficability = 'جيدة للمركبات المدولبة والمجنزرة مع الحذر في مجاري السيول';
if (centerLat > 32.3 && centerLng < 36.0) {
// Geolocation Specific Context (Zarqa / Amman / Irbid / Mafraq / Dead Sea / South)
if (centerLat >= 32.05 && centerLat <= 32.20 && centerLng >= 36.00 && centerLng <= 36.18) {
sectorName = 'قاطع الزرقاء والسخنة / حوض سيل الزرقاء';
terrainClassificationAr = 'حوض وادي صدعي واسع تحيط به تلال وهضاب حاكمة ومجرى سيل مائي';
terrainClassification = 'River Valley Basin flanked by Ridge Plateaus';
urbanDensity = 'HIGH';
urbanDensityAr = 'كثافة عالية (بلدات وضواحي ممتدة)';
settlements = ['بلدة السخنة', 'خربة قنتول', 'ضاحية البستان', 'أحياء الهاشمية', 'مدينة الزرقاء'];
soilType = 'طمي رسوبي في بطن الوادي وصخور كلسية على الأكتاف';
trafficability = 'ممتازة على شبكة الطرق وبطون الأودية المنبسطة (GO) مع صعوبة صعود الجروف الصخرية';
} else if (centerLat > 32.3 && centerLng < 36.0) {
sectorName = 'قاطع الشمال / حوض اليرموك وإربد';
terrainClassificationAr = 'مرتفعات جبلية تتخللها أودية سحيقة وخوانق مائية';
terrainClassification = 'Highland Plateaus with Deep Gorges';
@@ -728,7 +888,7 @@ export async function calculateTerrainStudy(
settlements = ['بلدات وضواحي إربد', 'قرى حوض اليرموك', 'مزارع ومحميات وادي الشلالة'];
soilType = 'تربة زراعية خصبة وصخور كلسية';
trafficability = 'صعبة على حواف الأودية - تتطلب محاور طرق رئيسية';
} else if (centerLat >= 31.8 && centerLat <= 32.2 && centerLng >= 35.8 && centerLng <= 36.1) {
} else if (centerLat >= 31.85 && centerLat <= 32.05 && centerLng >= 35.8 && centerLng <= 36.02) {
sectorName = 'قاطع الوسط / إقليم العاصمة عمان';
terrainClassificationAr = 'هضاب وكتل جبلية حضرية مكتظة بالعمران';
terrainClassification = 'Urbanized Highland Terrain';
@@ -736,8 +896,8 @@ export async function calculateTerrainStudy(
urbanDensityAr = 'كثافة عالية (بيئة قتال مباني MOUT)';
settlements = ['أحياء ومجمعات العاصمة عمان', 'المناطق التجارية والتقاطعات المرورية', 'مناطق صناعية متقدمة'];
soilType = 'أرض معبدة وأرصفة إسمنتية مع تربة صخرية';
trafficability = 'ممتازة على شبكة الطرق ولكنها مقيدة بالاختناقات المرورية والمباني';
} else if (centerLng > 36.1) {
trafficability = 'ممتازة على شبكة الطرق ولكنها مقيدة بالمباني والاختناقات';
} else if (centerLng > 36.18) {
sectorName = 'قاطع البادية والشرق / المفرق والصفاوي';
terrainClassificationAr = 'سهول صحراوية مفتوحة وأراضي حرات بازلتية';
terrainClassification = 'Open Desert Steppe & Basalt Harrah';
@@ -755,215 +915,19 @@ export async function calculateTerrainStudy(
settlements = ['مزارع الأغوار الجنوبية', 'مجمعات الفنادق ومصانع البوتاس', 'قرى غور الصافي'];
soilType = 'طمي نهري وسبخات ملحية وصخور رسوبية';
trafficability = 'حذرة جداً بجانب السبخات - ممتازة على طريق البحر الميت الرئيسي';
} else if (centerLat < 31.5 && centerLat > 30.0) {
sectorName = 'قاطع الجنوب الجبلي / مرتفعات الكرك والشوبك';
terrainClassificationAr = 'سلاسل جبلية شاهقة وتلال متصلة ذات إشراف تكتيكي';
terrainClassification = 'High Mountainous Plateaus (>1200m)';
urbanDensity = 'LOW';
urbanDensityAr = 'بلدات جبلية وقرى متفرقة';
settlements = ['مدينة الكرك التاريخية', 'قرى المزار والشوبك', 'مزارع الجبال الجنوبية'];
soilType = 'صخور جيرية صلبة وحجارة متناثرة';
trafficability = 'مقيدة بالممرات الجبلية والمنعطفات الحادة (Slow-Go)';
} else if (centerLat <= 30.0) {
sectorName = 'قاطع الجنوب الصحراوي / وادي رم والعقبة';
terrainClassificationAr = 'جبال رملية شاهقة (Monoliths) تتخللها ممرات ووديان رملية واسعة';
terrainClassification = 'Sandstone Monoliths & Desert Valleys';
urbanDensity = 'SPARSE';
urbanDensityAr = 'شبه خالية (مخيمات سياحية وتجمعات بدوية)';
settlements = ['قرية وادي رم', 'مخيمات الديسة السياحية', 'نقاط المراقبة الجنوبية'];
soilType = 'رمال ناعمة وصخور رملية ضخمة';
trafficability = 'ممتازة للآليات ذات الدفع الرباعي والمجنزرات في بطون الأودية';
}
// Determine Overall Mobility Status
// Mobility status calculation
let mobilityStatus: 'GO' | 'SLOW-GO' | 'NO-GO' = 'GO';
let mobilityStatusAr = 'حركة حرة ممتازة للآليات والدروع (Cross-Country GO)';
if (steepPct > 40 || avgSlope > 14) {
if (steepPct > 30 || avgSlope > 12) {
mobilityStatus = 'NO-GO';
mobilityStatusAr = 'مانع تضاريسي - حركة مقيدة وشديدة الصعوبة (NO-GO)';
} else if (steepPct > 15 || modPct > 40 || avgSlope > 7) {
} else if (steepPct > 10 || modPct > 35 || avgSlope > 6) {
mobilityStatus = '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 {
centerLat,
centerLng,
@@ -993,73 +957,71 @@ export async function calculateTerrainStudy(
},
naturalObstacles: [
{
name: 'المنحدرات والجروف التضاريسية الحادة',
type: 'انحدار طبوغرافي',
description: `تشكل المناطق ذات الانحدار الشديد (${steepPct}%) مانعاً طبيعياً يحد من تقدم الدروع ويجبرها على الالتفاف.`,
impact: 'إعاقة محاور الهجوم المعادي وتوجيهه نحو الخناق المجهزة بالنيران'
name: 'مجاري الوديان والسيول الطبيعية (Drainage Defiles)',
type: 'مصارف مائية وأودية',
description: `بطن الوادي عند منسوب ${minElev}م يشكل مصرفاً مائياً رئيسياً، يوفر ممراً محجوباً ولكن قد يعيق المناورة عند هطول الأمطار.`,
impact: 'محور تسلل مناسب للمشاة ومقيد للآليات الثقيلة عند تشكل الأوحال'
},
{
name: 'الأودية والشعاب ومجاري السيول الطبيعية',
type: 'أودية ومصارف مائية',
description: 'بطون الأودية تمثل ممرات حركة محجوبة عن خط النظر ولكنها عرضة للكمائن والسيول الفجائية.',
impact: 'تصلح كمسارب تسلل للمشاة ولكنها خطرة للآليات الثقيلة'
},
{
name: 'القمم والمرتفعات الحاكمة المجاورة',
name: 'التلال والقمم الحاكمة (Dominant Ridges)',
type: 'أرض مسيطرة (Key Terrain)',
description: `أعلى قمة في القطاع بارتفاع ${maxElev}م توفر إشرافاً نارياً واستطلاعياً على عمق يصل إلى ${Math.round(radiusMeters / 500)} كم.`,
impact: 'وجوب احتلالها بنقاط مراقبة وأسلحة إسناد مضادة للدروع'
description: `المرتفع الحاكم بارتفاع ${maxElev}م يشرف بشكل كامل على بطن الوادي ومحاور التحرك على مسافة ${radiusMeters / 1000} كم.`,
impact: 'موقع استراتيجي لتمركز أسلحة الإسناد ونقاط الملاحظة والاستطلاع'
},
{
name: 'المنحدرات التضاريسية المحيطة',
type: 'انحدار طبوغرافي',
description: steepPct > 0
? `يوجد ${steepPct}% من مساحة القطاع ذات انحدار وعر (>12°) تشكل موانع طبيعية تعيق صعود الآليات.`
: `القطاع يتسم بانحدارات لطيفة إلى متوسطة (أقصى انحدار ${maxSlope}°)، ولا توجد جروف شاهقة مانعة لحركة الدروع.`,
impact: steepPct > 0 ? 'إجبار القوات المهاجمة على استخدام الممرات المعبدة' : 'حرية مناورة واسعة للدروع في مختلف الاتجاهات'
}
],
manMadeObstacles: [
{
name: 'شبكة الطرق والمحاور المعبدة',
type: 'محاور حركة سريعة',
description: 'الطرق الرئيسية المارة بالقطاع تتيح سرعة إعادة انتشار القوات والإمداد اللوجستي.',
tacticalNote: 'تعتبر أهدافاً حيوية يجب تأمينها بنقاط سيطرة وتفتيش وغلق سريع'
name: 'محاور الطرق الرئيسية وخطوط الإمداد (MSR)',
type: 'بنية تحتية للمواصلات',
description: 'الطرق المعبدة المارة بالوادي تتيح تدفقاً سريعاً للأرتال وإعادة التزود اللوجستي.',
tacticalNote: 'وجوب إعداد خطط قطع وغلق هندسي سريع للتحكم بحركة العدو'
},
{
name: 'خطوط نقل الطاقة وأبراج الاتصالات',
type: 'بنية تحتية صناعية',
description: 'أبراج الضغط العالي والاتصالات تشكل عوائق أمام الطيران المنخفض ومواقع محتملة لمحطات الإعاقة اللاسلكية.',
tacticalNote: 'تجنب هبوط المروحيات بالقرب من خطوط الضغط العالي'
name: 'المنشآت والكتل العمرانية',
type: 'موانع مبنية وسواتر خرسانية',
description: `البلدات والتجمعات السكانية (${settlements.slice(0, 3).join('، ')}) توفر سواتر ممتازة ضد النيران والشظايا.`,
tacticalNote: 'تتطلب عمليات تطهير متقدمة وحماية للمدنيين'
},
{
name: 'المباني والمنشآت الخرسانية الممتدة',
type: 'موانع مبنية',
description: 'المباني المتفرقة توفر سواتر خرسانية ممتازة ضد الشظايا ونيران الأسلحة الخفيفة.',
tacticalNote: 'تتطلب تطهيراً هندسياً لضمان خلوها من العبوات والأفخاخ'
name: 'أبراج وشبكات نقل الطاقة والاتصالات',
type: 'عوائق صناعية بارزة',
description: 'خطوط الضغط العالي والاتصالات تشكل خطراً على الطيران المنخفض ومواقع محتملة للتشويش.',
tacticalNote: 'تجنب تحديد مهابط الطيران بالقرب من مسارات خطوط الطاقة'
}
],
urbanAndDemographics: {
density: urbanDensity,
densityAr: urbanDensityAr,
settlements,
moutComplexity: urbanDensity === 'HIGH' ? 'معقدة جداً وتتطلب تدريب قتال مباني متخصص' : 'بسيطة إلى متوسطة وتقتصر على أطراف القرى',
collateralDamageRisk: urbanDensity === 'HIGH' ? 'عالية جداً وتتطلب ذخائر موجهة وقواعد اشتباك صارمة' : 'منخفضة مع مراعاة حماية الأعيان المدنية'
moutComplexity: urbanDensity === 'HIGH' ? 'بيئة قتال مباني حضرية متقدمة (MOUT)' : 'بيئة قتال ريفية مفتوحة تتخللها مزارع متفرقة',
collateralDamageRisk: urbanDensity === 'HIGH' ? 'عالية وتتطلب مراعاة قواعد الاشتباك الدقيقة' : 'منخفضة مع حماية الممتلكات المدنية'
},
landCoverAndCover: {
soilType,
trafficability,
concealmentRating: steepPct > 20 || urbanDensity === 'HIGH' ? 'ممتازة خلف التموجات والمباني' : 'متوسطة إلى مكشوفة في الأراضي المفتوحة',
airObservationExposure: urbanDensity === 'HIGH' ? 'مختلطة (مباني تحجب الرؤية الجوية)' : 'عالية للرصد الجوي والمسيرات نهاراً'
concealmentRating: urbanDensity === 'HIGH' || relief > 100 ? 'ممتازة خلف التلال والكتل الخرسانية' : 'متوسطة إلى مكشوفة في الأراضي المفتوحة',
airObservationExposure: urbanDensity === 'HIGH' ? 'مختلطة (المباني توفر إخفاء جزئياً)' : 'عالية للرصد الجوي والمسيرات نهاراً'
},
oakocAssessment: {
obstacles: `القطاع يحتوي على ${steepPct}% أراضٍ شديدة الوعورة تشكل موانع طبيعية، مع وجود محاور طرق رئيسية تشكل ممرات حركة سريعة.`,
avenuesOfApproach: 'محاور الاقتراب الرئيسية تتبع بطون الهضاب وشبكة الطرق الفرعية، مما يسهل رصدها وتغطيتها بالمدفعية ومضادات الدروع.',
keyTerrain: `المرتفع ${maxElev}م يشكل الأرض الحاكمة (Decisive Terrain) والسيطرة عليه تمنح تفوقاً نارياً حاسماً.`,
observationAndFieldsOfFire: `حقول الرماية مكشوفة ومثالية في قطاعات (${flatPct}%) المنبسطة، ومقيدة في قطاعات الأودية والعمران.`,
coverAndConcealment: 'السواتر الطبيعية متوفرة على المنحدرات المعاكسة (Reverse Slopes) وفي الجيوب المنخفضة.'
obstacles: `القطاع يتضمن مجرى وادٍ رئيسي عند منسوب ${minElev}م مع انحدارات تصل إلى ${maxSlope}°، مما يشكل محاور حركة طبيعية محددة.`,
avenuesOfApproach: 'محور الاقتراب الرئيسي يتبع بطن الوادي وشبكة الطرق الرئيسية، مع إمكانية الالتفاف عبر الهضاب المنبسطة.',
keyTerrain: `القمة الحاكمة بارتفاع ${maxElev}م توفر ميزة كشف وهيمنة نارية حاسمة على كامل القطاع.`,
observationAndFieldsOfFire: `حقول الرماية مكشوفة ومثالية بنسبة ${flatPct}% في الأراضي المنبسطة، ومحجوبة داخل بطون الأودية.`,
coverAndConcealment: 'تتوفر السواتر التضاريسية في المنحدرات الخلفية (Reverse Slopes) وداخل التجمعات العمرانية.'
},
tacticalRecommendations: [
`تمركز نقاط المراقبة والرادار على المرتفع الحاكم بارتفاع ${maxElev}م لتأمين الإنذار المبكر.`,
'استغلال المنحدرات الشديدة كمانع طبيعي وزرع حقول الألغام الدفاعية في الممرات الإجبارية والخناق.',
'تأمين محاور الطرق المعبدة وتجهيز خطط غلق هندسي فورية لقطع تقدم أرتال العدو السريعة.',
'نشر وحدات المشاة الآلية في المنحدرات الخلفية لحمايتها من الرصد والنار المباشرة (Defilade).',
'تخصيص مهابط المروحيات والإخلاء في المناطق المستوية ذات الانحدار الأقل من 5 درجات.'
`نشر مرصد استطلاع رئيسي ورادار كشف أرضي على المرتفع الحاكم (${maxElev}م) لتأمين إنذار مبكر ومراقبة بطن الوادي.`,
`تجهيز نقاط كمائن وموانع هندسية وحقول ألغام موجهة في مخانق بطن الوادي (${minElev}م) لصد أي تقدم مدرع.`,
`تأمين الطرق الرئيسية والمحاور الحيوية بنقاط سيطرة وحراسة ثابتة لمنع عمليات الالتفاف السريعة.`,
`استغلال المناطق المنبسطة (${flatPct}%) لإنشاء مهابط المروحيات (HLZ) ومناطق الإخلاء الطبي والتجمع اللوجستي.`
]
};
}