diff --git a/apps/web/src/pages/TacticalDefenseView.tsx b/apps/web/src/pages/TacticalDefenseView.tsx index 744a206..d61775b 100644 --- a/apps/web/src/pages/TacticalDefenseView.tsx +++ b/apps/web/src/pages/TacticalDefenseView.tsx @@ -551,6 +551,7 @@ export const TacticalDefenseView: React.FC = () => { 'fill-opacity': [ 'match', ['get', 'layerType'], + 'cliff', 0.50, 'urban', 0.28, 'hazard', 0.32, 'slope-sector', 0.18, @@ -566,8 +567,18 @@ export const TacticalDefenseView: React.FC = () => { filter: ['==', '$type', 'Polygon'], paint: { 'line-color': ['get', 'color'], - 'line-width': 2, - 'line-dasharray': [3, 2] + 'line-width': [ + 'match', + ['get', 'layerType'], + 'cliff', 3.5, + 2 + ], + 'line-dasharray': [ + 'match', + ['get', 'layerType'], + 'cliff', ['literal', [2, 1]], + ['literal', [3, 2]] + ] } }); @@ -581,6 +592,7 @@ export const TacticalDefenseView: React.FC = () => { 'line-width': [ 'match', ['get', 'layerType'], + 'cliff', 4.5, 'road', 4.5, 'wadi', 3.5, 3 @@ -588,6 +600,7 @@ export const TacticalDefenseView: React.FC = () => { 'line-dasharray': [ 'match', ['get', 'layerType'], + 'cliff', ['literal', [2, 1]], 'road', ['literal', [1, 0]], 'wadi', ['literal', [2, 1.5]], ['literal', [1, 0]] @@ -817,10 +830,10 @@ 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' || f.properties.layerType === 'ridge') return showNaturalObstacles; + if (f.properties.layerType === 'wadi' || f.properties.layerType === 'ridge' || f.properties.layerType === 'cliff') return showNaturalObstacles; if (f.properties.layerType === 'road') return showRoadCorridors; if (f.properties.layerType === 'urban') return showUrbanZones; - if (f.properties.layerType === 'hazard') return showHazardousGround; + if (f.properties.layerType === 'hazard' || f.properties.layerType === 'cliff') return showHazardousGround; return true; }); @@ -1652,24 +1665,72 @@ ${terrainResult.tacticalRecommendations.map((r, i) => `${i + 1}. ${r}`).join('\n - {/* Slope Distribution Bar */} + {/* Slope Distribution Bar (4 Military Tiers) */}
توزيع درجات الانحدار (Slope Analysis): متوسط: {terrainResult.avgSlopeDegrees}°
-
-
-
+
+
+
+
25°): ${terrainResult.slopeDistribution.cliffPercentage}%`} />
-
- ■ منبسط (<5°): {terrainResult.slopeDistribution.flatPercentage}% - ■ متوسط (5-15°): {terrainResult.slopeDistribution.moderatePercentage}% - ■ وعر (>15°): {terrainResult.slopeDistribution.steepPercentage}% +
+ ■ منبسط (<7°): {terrainResult.slopeDistribution.flatPercentage}% + ■ متموج (7-15°): {terrainResult.slopeDistribution.moderatePercentage}% + ■ وعر (15-25°): {terrainResult.slopeDistribution.steepPercentage}% + ■ جروف صخرية (>25°): {terrainResult.slopeDistribution.cliffPercentage}%
+ {/* Detected Cliffs & Natural Rock Barriers */} + {terrainResult.cliffs && terrainResult.cliffs.length > 0 && ( +
+
+ + 🧗‍♂️ القواطع والجروف الصخرية المرصودة ({terrainResult.cliffsCount}) + + مانع NO-GO +
+
+ {terrainResult.cliffs.map((cliff, cIdx) => ( +
{ + if (map.current) { + map.current.flyTo({ center: [cliff.lng, cliff.lat], zoom: 15.5, pitch: 55 }); + } + }} + style={{ + background: 'rgba(0,0,0,0.3)', + padding: '6px 8px', + borderRadius: 6, + cursor: 'pointer', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + border: '1px solid rgba(239, 68, 68, 0.2)' + }} + > +
+
+ قاطع #{cIdx + 1}: سقوط {cliff.dropMeters}م (ميل {cliff.slopeDegrees}°) +
+
+ {cliff.lat}, {cliff.lng} +
+
+
+ 🔍 تركيز +
+
+ ))} +
+
+ )} + {/* SECTION 1: NATURAL OBSTACLES */}
diff --git a/apps/web/src/utils/elevationService.ts b/apps/web/src/utils/elevationService.ts index 6dec2d0..2a66376 100644 --- a/apps/web/src/utils/elevationService.ts +++ b/apps/web/src/utils/elevationService.ts @@ -70,69 +70,145 @@ export function calculateAzimuth(lat1: number, lon1: number, lat2: number, lon2: return (theta * (180 / Math.PI) + 360) % 360; } +/** + * Samples elevation from Terrarium DEM tile or fallback topographic model + */ +/** + * Fetches or retrieves cached DEM tile ImageData + */ +async function fetchTileImageData(zoom: number, x: number, y: number): Promise { + const tileKey = `${zoom}/${x}/${y}`; + const cached = tileCache.get(tileKey); + if (cached) return cached; + + try { + const tileUrl = `https://s3.amazonaws.com/elevation-tiles-prod/terrarium/${zoom}/${x}/${y}.png`; + const img = new Image(); + img.crossOrigin = 'anonymous'; + + const loadPromise = new Promise((resolve, reject) => { + img.onload = () => resolve(img); + img.onerror = (e) => reject(e); + img.src = tileUrl; + }); + + const loadedImg = await Promise.race([ + loadPromise, + new Promise((_, reject) => setTimeout(() => reject(new Error('DEM Timeout')), 2000)) + ]); + + const canvas = document.createElement('canvas'); + canvas.width = 256; + canvas.height = 256; + const ctx = canvas.getContext('2d'); + if (ctx) { + ctx.drawImage(loadedImg, 0, 0); + const imgData = ctx.getImageData(0, 0, 256, 256); + tileCache.set(tileKey, imgData); + return imgData; + } + } catch { + // Offline or network error + } + return null; +} + +/** + * Samples sub-pixel elevation from ImageData using Bilinear Interpolation + */ +function interpolateElevation(imgData: ImageData, subX: number, subY: number): number { + const clampedX = Math.max(0, Math.min(254.99, subX)); + const clampedY = Math.max(0, Math.min(254.99, subY)); + + const x0 = Math.floor(clampedX); + const x1 = x0 + 1; + const y0 = Math.floor(clampedY); + const y1 = y0 + 1; + + const fx = clampedX - x0; + const fy = clampedY - y0; + + const decodePixel = (px: number, py: number): number => { + const idx = (py * 256 + px) * 4; + const r = imgData.data[idx]; + const g = imgData.data[idx + 1]; + const b = imgData.data[idx + 2]; + return (r * 256 + g + b / 256) - 32768; + }; + + const z00 = decodePixel(x0, y0); + const z10 = decodePixel(x1, y0); + const z01 = decodePixel(x0, y1); + const z11 = decodePixel(x1, y1); + + const zTop = z00 * (1 - fx) + z10 * fx; + const zBottom = z01 * (1 - fx) + z11 * fx; + return Math.round((zTop * (1 - fy) + zBottom * fy) * 10) / 10; +} + +/** + * Samples elevation for multiple coordinates in batch with tile grouping & cache + */ +export async function sampleElevationsBatch( + coords: Array<{ lat: number; lng: number }>, + zoom: number = 13 +): Promise { + const n = Math.pow(2, zoom); + + // Group coordinates by tile + const tileMap = new Map }>(); + + coords.forEach((coord, index) => { + const x = Math.floor(((coord.lng + 180) / 360) * n); + const latRad = (coord.lat * Math.PI) / 180; + const y = Math.floor((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * n); + + const subX = (((coord.lng + 180) / 360) * n - x) * 256; + const subY = ((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * n - y) * 256; + + const tileKey = `${zoom}/${x}/${y}`; + if (!tileMap.has(tileKey)) { + tileMap.set(tileKey, { zoom, x, y, points: [] }); + } + tileMap.get(tileKey)!.points.push({ index, subX, subY, lat: coord.lat, lng: coord.lng }); + }); + + const results: number[] = new Array(coords.length).fill(0); + + // Load all unique tiles in parallel + await Promise.all( + Array.from(tileMap.entries()).map(async ([_key, entry]) => { + const imgData = await fetchTileImageData(entry.zoom, entry.x, entry.y); + entry.points.forEach((pt) => { + if (imgData) { + results[pt.index] = interpolateElevation(imgData, pt.subX, pt.subY); + } else { + results[pt.index] = getApproximateElevation(pt.lat, pt.lng); + } + }); + }) + ); + + return results; +} + /** * Samples elevation from Terrarium DEM tile or fallback topographic model */ async function sampleElevationAt(lat: number, lng: number): Promise { - const zoom = 12; + const zoom = 13; const n = Math.pow(2, zoom); const x = Math.floor(((lng + 180) / 360) * n); const latRad = (lat * Math.PI) / 180; const y = Math.floor((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * n); - const tileKey = `${zoom}/${x}/${y}`; + const subX = (((lng + 180) / 360) * n - x) * 256; + const subY = ((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * n - y) * 256; - try { - let imgData = tileCache.get(tileKey); - - if (!imgData) { - const tileUrl = `https://s3.amazonaws.com/elevation-tiles-prod/terrarium/${zoom}/${x}/${y}.png`; - const img = new Image(); - img.crossOrigin = 'anonymous'; - - const loadPromise = new Promise((resolve, reject) => { - img.onload = () => resolve(img); - img.onerror = (e) => reject(e); - img.src = tileUrl; - }); - - // 1.5s timeout for fast responsiveness - const loadedImg = await Promise.race([ - loadPromise, - new Promise((_, reject) => setTimeout(() => reject(new Error('DEM Timeout')), 1500)) - ]); - - const canvas = document.createElement('canvas'); - canvas.width = 256; - canvas.height = 256; - const ctx = canvas.getContext('2d'); - if (ctx) { - ctx.drawImage(loadedImg, 0, 0); - imgData = ctx.getImageData(0, 0, 256, 256); - tileCache.set(tileKey, imgData); - } - } - - if (imgData) { - // Calculate exact sub-pixel inside tile - const subX = (((lng + 180) / 360) * n - x) * 256; - const subY = ((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * n - y) * 256; - const px = Math.min(255, Math.max(0, Math.floor(subX))); - const py = Math.min(255, Math.max(0, Math.floor(subY))); - - const index = (py * 256 + px) * 4; - const r = imgData.data[index]; - const g = imgData.data[index + 1]; - const b = imgData.data[index + 2]; - - // Terrarium formula: (R * 256 + G + B / 256) - 32768 - const elev = (r * 256 + g + b / 256) - 32768; - return Math.round(elev); - } - } catch (err) { - // Fallback topographic approximation for Jordan/Levant region + const imgData = await fetchTileImageData(zoom, x, y); + if (imgData) { + return Math.round(interpolateElevation(imgData, subX, subY)); } - return getApproximateElevation(lat, lng); } @@ -143,34 +219,26 @@ function getApproximateElevation(lat: number, lng: number): number { // Jordan Valley & Dead Sea trench model if (lng < 35.6 && lat < 32.2 && lat > 31.0) { const distFromRift = Math.abs(lng - 35.5); - return -400 + distFromRift * 3000; + return Math.round(-400 + distFromRift * 3000); } // Northern Highlands (Ajloun / Jerash / Salt) if (lat >= 32.1 && lng < 36.0) { - return 850 + Math.sin(lat * 50) * 250 + Math.cos(lng * 40) * 150; + return Math.round(850 + Math.sin(lat * 50) * 250 + Math.cos(lng * 40) * 150); } // Amman Plateau if (lat >= 31.8 && lat < 32.1 && lng >= 35.8 && lng < 36.2) { - return 900 + Math.sin((lat - 31.95) * 100) * 120 + Math.cos((lng - 35.9) * 100) * 100; + return Math.round(900 + Math.sin((lat - 31.95) * 100) * 120 + Math.cos((lng - 35.9) * 100) * 100); } // Southern Highlands (Karak / Tafilah / Shobak / Petra) if (lat < 31.5 && lat > 30.0 && lng < 35.7) { - return 1100 + Math.sin(lat * 30) * 350; + return Math.round(1100 + Math.sin(lat * 30) * 350); } // Eastern Desert (Badia) - return 650 + (lng - 36.0) * 30; + return Math.round(650 + (lng - 36.0) * 30); } /** * Calculates Line of Sight and Elevation Profile between two coordinates - * - * @param startLat Observer Latitude - * @param startLng Observer Longitude - * @param endLat Target Latitude - * @param endLng Target Longitude - * @param obsHeight Observer Eye Level offset above ground (default: 2 meters) - * @param tgtHeight Target Height offset above ground (default: 2 meters) - * @param samples Number of sampling steps along the ray (default: 60) */ export async function calculateLineOfSight( startLat: number, @@ -233,7 +301,7 @@ export async function calculateLineOfSight( }; } } catch { - // API not available or timed out, fall back to local DEM tile processing + // Fall back to local DEM processing } // Generate sample coordinates along the geodesic path @@ -246,17 +314,14 @@ export async function calculateLineOfSight( sampleCoords.push({ lat, lng, dist }); } - // Fetch elevations for all sample points in parallel - const elevations = await Promise.all( - sampleCoords.map((coord) => sampleElevationAt(coord.lat, coord.lng)) - ); + // Fetch elevations for all sample points in fast batch + const elevations = await sampleElevationsBatch(sampleCoords, 13); const observerGroundElev = elevations[0]; const targetGroundElev = elevations[elevations.length - 1]; const observerElevation = observerGroundElev + obsHeight; const targetElevation = targetGroundElev + tgtHeight; - // Earth curvature & atmospheric refraction parameter (k ≈ 0.13 for standard atmosphere) const R_earth = 6371000; const k_refraction = 0.13; const effectiveEarthRadius = R_earth / (1 - k_refraction); @@ -265,8 +330,6 @@ export async function calculateLineOfSight( let highestObstacle: ObstacleInfo | null = null; let maxObstacleExcess = 0; let deadGroundCount = 0; - - // Horizon angle tracking from observer (tan of highest angle encountered so far) let maxAngleSoFar = -Infinity; const points: ElevationPoint[] = []; @@ -279,16 +342,10 @@ export async function calculateLineOfSight( minElev = Math.min(minElev, elev); maxElev = Math.max(maxElev, elev); - // Earth curvature sagitta at distance d: deltaH = (d * (totalDistance - d)) / (2 * R_effective) const earthCurvatureDrop = (d * (totalDistance - d)) / (2 * effectiveEarthRadius); - - // Theoretical straight ray height AMSL connecting Observer to Target const rayHeight = observerElevation + ((targetElevation - observerElevation) * (d / totalDistance)) - earthCurvatureDrop; - - // Clearance (positive = ray above terrain, negative = obstacle) const clearance = rayHeight - elev; - // Check if this point blocks the direct ray to the target (ignore start and end margins) let isTargetRayBlocked = false; if (i > 1 && i < samples) { if (elev > rayHeight) { @@ -299,7 +356,7 @@ export async function calculateLineOfSight( maxObstacleExcess = excess; highestObstacle = { distance: Math.round(d), - elevation: elev, + elevation: Math.round(elev), lat: sampleCoords[i].lat, lng: sampleCoords[i].lng, excessHeight: Math.round(excess * 10) / 10, @@ -308,7 +365,6 @@ export async function calculateLineOfSight( } } - // Check visibility from observer's eye (Viewshed / Shadowing along profile) let isVisible = true; if (i === 0) { isVisible = true; @@ -327,7 +383,7 @@ export async function calculateLineOfSight( distance: Math.round(d), lat: sampleCoords[i].lat, lng: sampleCoords[i].lng, - elevation: elev, + elevation: Math.round(elev), rayHeight: Math.round(rayHeight * 10) / 10, isVisible, isTargetRayBlocked, @@ -335,13 +391,10 @@ export async function calculateLineOfSight( }); } - // Calculate Vertical Angle (Degrees & Artillery Mils) - // 1 Degree = 17.7778 Artillery Mils (6400 Mils in full circle) const verticalDiff = targetElevation - observerElevation; const angleRad = Math.atan2(verticalDiff, totalDistance); const angleDegrees = Math.round((angleRad * (180 / Math.PI)) * 100) / 100; const angleMils = Math.round((angleDegrees * (6400 / 360)) * 10) / 10; - const deadGroundPercentage = Math.round((deadGroundCount / samples) * 100); return { @@ -374,7 +427,7 @@ export interface Viewshed360Result { } /** - * Calculates 360-degree Radial Viewshed (كشف الميدان الدائري) around observer + * Calculates 360-degree Radial Viewshed around observer */ export async function calculateRadialViewshed( centerLat: number, @@ -390,9 +443,8 @@ export async function calculateRadialViewshed( const k_refraction = 0.13; const effectiveEarthRadius = R_earth / (1 - k_refraction); - const polygonCoordinates: [number, number][] = []; - const samplesPerRay = 12; - let totalVisibleDistanceSum = 0; + const samplesPerRay = 14; + const rayCoords: Array<{ rayIdx: number; step: number; sDist: number; sLat: number; sLng: number }> = []; for (let rayIdx = 0; rayIdx < numRays; rayIdx++) { const angleDeg = (rayIdx * 360) / numRays; @@ -404,29 +456,41 @@ export async function calculateRadialViewshed( const endLat = centerLat + dLat; const endLng = centerLng + dLng; - let maxAngleSoFar = -Infinity; - let visibleHorizonDist = radiusMeters; - let visibleHorizonLat = endLat; - let visibleHorizonLng = endLng; - for (let step = 1; step <= samplesPerRay; step++) { const frac = step / samplesPerRay; const sLat = centerLat + (endLat - centerLat) * frac; const sLng = centerLng + (endLng - centerLng) * frac; const sDist = radiusMeters * frac; + rayCoords.push({ rayIdx, step, sDist, sLat, sLng }); + } + } - const sElev = await sampleElevationAt(sLat, sLng); - const earthCurvatureDrop = (sDist * sDist) / (2 * effectiveEarthRadius); + // Batch sample all ray points + const elevations = await sampleElevationsBatch(rayCoords.map(rc => ({ lat: rc.sLat, lng: rc.sLng })), 13); + + const polygonCoordinates: [number, number][] = []; + let totalVisibleDistanceSum = 0; + + for (let rayIdx = 0; rayIdx < numRays; rayIdx++) { + let maxAngleSoFar = -Infinity; + let visibleHorizonDist = radiusMeters; + let visibleHorizonLat = centerLat; + let visibleHorizonLng = centerLng; + + const rayPoints = rayCoords.filter(rc => rc.rayIdx === rayIdx); + rayPoints.forEach(rc => { + const sElev = elevations[rc.rayIdx * samplesPerRay + (rc.step - 1)]; + const earthCurvatureDrop = (rc.sDist * rc.sDist) / (2 * effectiveEarthRadius); const apparentElev = sElev - earthCurvatureDrop; - const angle = (apparentElev - observerElevation) / sDist; + const angle = (apparentElev - observerElevation) / rc.sDist; if (angle >= maxAngleSoFar) { maxAngleSoFar = angle; - visibleHorizonDist = sDist; - visibleHorizonLat = sLat; - visibleHorizonLng = sLng; + visibleHorizonDist = rc.sDist; + visibleHorizonLat = rc.sLat; + visibleHorizonLng = rc.sLng; } - } + }); totalVisibleDistanceSum += visibleHorizonDist; polygonCoordinates.push([visibleHorizonLng, visibleHorizonLat]); @@ -493,7 +557,6 @@ export interface MinefieldAnalysisResult { /** * Tactical Minefield Barrier & Sapper Engineering Analysis - * دراسة جدوى وكميات حقل الألغام الدفاعي وفحص التضاريس */ export async function calculateMinefieldAnalysis( startLat: number, @@ -505,25 +568,23 @@ export async function calculateMinefieldAnalysis( const frontageMeters = calculateDistance(startLat, startLng, endLat, endLng); const azimuthDegrees = calculateAzimuth(startLat, startLng, endLat, endLng); - const startElev = await sampleElevationAt(startLat, startLng); - const endElev = await sampleElevationAt(endLat, endLng); - - // Sample intermediate points to determine valley / ridge / choke point profile - const samples = 10; - const elevs: number[] = []; + const samples = 12; + const sampleCoords: Array<{ lat: number; lng: number }> = []; for (let i = 0; i <= samples; i++) { const frac = i / samples; const lat = startLat + (endLat - startLat) * frac; const lng = startLng + (endLng - startLng) * frac; - const el = await sampleElevationAt(lat, lng); - elevs.push(el); + sampleCoords.push({ lat, lng }); } + const elevs = await sampleElevationsBatch(sampleCoords, 13); + const startElev = elevs[0]; + const endElev = elevs[elevs.length - 1]; + const elevDiff = Math.abs(endElev - startElev); const avgSlopeRad = frontageMeters > 0 ? Math.atan(elevDiff / frontageMeters) : 0; const avgSlopeDegrees = Math.round((avgSlopeRad * (180 / Math.PI)) * 10) / 10; - // Check if center is lower than edges (a defile / valley choke point) const midElev = elevs[Math.floor(samples / 2)]; const isChokePoint = midElev < Math.min(startElev, endElev) - 10 || avgSlopeDegrees <= 10; @@ -576,6 +637,15 @@ export async function calculateMinefieldAnalysis( }; } +export interface CliffFeature { + lat: number; + lng: number; + dropMeters: number; + slopeDegrees: number; + label: string; + tacticalImpact: string; +} + export interface TerrainStudyResult { centerLat: number; centerLng: number; @@ -589,10 +659,13 @@ export interface TerrainStudyResult { avgSlopeDegrees: number; maxSlopeDegrees: number; slopeDistribution: { - flatPercentage: number; // < 5 deg (Go) - moderatePercentage: number; // 5-15 deg (Slow-Go) - steepPercentage: number; // > 15 deg (No-Go / Obstacle) + flatPercentage: number; // < 7 deg (Go) + moderatePercentage: number; // 7-15 deg (Slow-Go / Rolling) + steepPercentage: number; // 15-25 deg (Severe Slow-Go / Rugged) + cliffPercentage: number; // > 25 deg (No-Go / Cliffs & Rock Barriers) }; + cliffsCount: number; + cliffs: CliffFeature[]; terrainClassification: string; terrainClassificationAr: string; mobilityStatus: 'GO' | 'SLOW-GO' | 'NO-GO'; @@ -648,10 +721,9 @@ export interface TerrainStudyResult { tacticalRecommendations: string[]; } - /** * Military Tactical Terrain Intelligence & Environmental Study (OAKOC Doctrine) - * دراسة الأرض الشاملة: التضاريس، الموانع الطبيعية والصناعية، والمناطق المأهولة + * دراسة الأرض الشاملة عالية الدقة: حساب الميول الدقيقة، كشف القواطع والجروف الصخرية، وممرات الحركة */ export async function calculateTerrainStudy( centerLat: number, @@ -661,8 +733,8 @@ export async function calculateTerrainStudy( const centerElev = await sampleElevationAt(centerLat, centerLng); const areaKm2 = Math.round(Math.PI * Math.pow(radiusMeters / 1000, 2) * 10) / 10; - // Grid Resolution: 11x11 sampling matrix covering the bounding box - const gridSize = 11; + // High-Density Sampling Matrix: 21x21 = 441 nodes (400 terrain cells) + const gridSize = 21; 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); @@ -672,68 +744,81 @@ export async function calculateTerrainStudy( const minLng = centerLng - dLngTotal; const maxLng = centerLng + dLngTotal; - // 1. Parallel sampling of all grid points + // 1. Generate all grid coordinates 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)); + const lat = Number((minLat + (r / (gridSize - 1)) * (maxLat - minLat)).toFixed(6)); for (let c = 0; c < gridSize; c++) { - const lng = Number((minLng + (c / (gridSize - 1)) * (maxLng - minLng)).toFixed(5)); + const lng = Number((minLng + (c / (gridSize - 1)) * (maxLng - minLng)).toFixed(6)); gridCoords.push({ r, c, lat, lng }); } } + // 2. High-speed parallel batch sampling from DEM tiles + const sampleResults = await sampleElevationsBatch(gridCoords, 13); 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 }; + + gridCoords.forEach((pt, idx) => { + elevationGrid[pt.r][pt.c] = sampleResults[idx]; }); - const sampledPoints = await Promise.all(elevPromises); - - // 2. Find Exact Peak and Valley + // 3. Precise Peak and Valley Identification 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 => { + gridCoords.forEach((p, idx) => { + const el = sampleResults[idx]; 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 (el > maxElev) { + maxElev = el; + highestPoint = { lat: p.lat, lng: p.lng, elevation: Math.round(el), label: `🔺 أعلى قمة: ${Math.round(el)}م` }; } - if (p.el < minElev) { - minElev = p.el; - lowestPoint = { lat: p.lat, lng: p.lng, elevation: p.el, label: `🔻 أخفض نقطة: ${p.el}م` }; + if (el < minElev) { + minElev = el; + lowestPoint = { lat: p.lat, lng: p.lng, elevation: Math.round(el), label: `🔻 أخفض نقطة: ${Math.round(el)}م` }; } } }); - const relief = maxElev - minElev; + const relief = Math.round(maxElev - minElev); - // 3. Compute Local Gradient (Slope) per cell and build Topographic Slope Polygons + // 4. Compute Local Gradients, Slopes, and Detect Natural Rock Barriers / Cliffs const spatialFeatures: any[] = []; const slopeValues: number[] = []; + const detectedCliffs: CliffFeature[] = []; const dxMeters = (2 * radiusMeters) / (gridSize - 1); const dyMeters = (2 * radiusMeters) / (gridSize - 1); - // Cell-by-cell topographic classification matching contour slopes + let flatCount = 0; // < 7 deg (GO) + let rollingCount = 0; // 7-15 deg (SLOW-GO) + let ruggedCount = 0; // 15-25 deg (SEVERE SLOW-GO) + let cliffCount = 0; // > 25 deg (NO-GO / Cliffs & Rock Barriers) + 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 p1Coord = gridCoords[r * gridSize + c]; + const p2Coord = gridCoords[r * gridSize + (c + 1)]; + const p3Coord = gridCoords[(r + 1) * gridSize + (c + 1)]; + const p4Coord = gridCoords[(r + 1) * gridSize + c]; + + const cellCenterLat = (p1Coord.lat + p3Coord.lat) / 2; + const cellCenterLng = (p1Coord.lng + p3Coord.lng) / 2; const distFromCenter = calculateDistance(centerLat, centerLng, cellCenterLat, cellCenterLng); - if (distFromCenter > radiusMeters * 1.02) continue; + if (distFromCenter > radiusMeters * 1.03) 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 cellElevAvg = Math.round((z00 + z01 + z10 + z11) / 4); + + // Central difference gradient const dz_dx = ((z01 + z11) - (z00 + z10)) / (2 * dxMeters); const dz_dy = ((z10 + z11) - (z00 + z01)) / (2 * dyMeters); @@ -741,58 +826,118 @@ export async function calculateTerrainStudy( const slopeDeg = Math.round((slopeRad * (180 / Math.PI)) * 10) / 10; slopeValues.push(slopeDeg); - let cat = 'go'; - let catColor = '#22c55e'; - let catLabel = `ممر حركة منبسط (GO) [${slopeDeg}°]`; + // Local maximum vertical step drop across cell corners + const localMaxZ = Math.max(z00, z01, z10, z11); + const localMinZ = Math.min(z00, z01, z10, z11); + const localDrop = Math.round(localMaxZ - localMinZ); - 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 = [p1Coord.lng, p1Coord.lat]; + const p2 = [p2Coord.lng, p2Coord.lat]; + const p3 = [p3Coord.lng, p3Coord.lat]; + const p4 = [p4Coord.lng, p4Coord.lat]; + + // Military Grade Slope & Barrier Classification: + // - Flat (GO): < 7° + // - Rolling (SLOW-GO): 7° - 15° + // - Rugged / Steep (SEVERE SLOW-GO): 15° - 25° + // - Cliffs & Natural Rock Barriers (NO-GO): > 25° or vertical drop >= 12m + const isCliff = slopeDeg >= 25 || (slopeDeg >= 18 && localDrop >= 10) || localDrop >= 18; + + if (isCliff) { + cliffCount++; + detectedCliffs.push({ + lat: Number(cellCenterLat.toFixed(5)), + lng: Number(cellCenterLng.toFixed(5)), + dropMeters: localDrop, + slopeDegrees: slopeDeg, + label: `🧗‍♂️ قاطع صخري وجرف حاد (سقوط ${localDrop}م - ميل ${slopeDeg}°)`, + tacticalImpact: 'مانع طبيعي قطعي - غير قابل لاجتياز الدروع والآليات، يفرض ممرات إجبارية' + }); + + // Add prominent cliff barrier polygon & line + spatialFeatures.push({ + type: 'Feature', + properties: { + layerType: 'cliff', + category: 'natural-obstacle', + color: '#dc2626', + title: `🧗‍♂️ قاطع صخري وجرف حاد (${localDrop}م / ${slopeDeg}°)`, + slope: slopeDeg, + drop: localDrop, + elevation: cellElevAvg + }, + geometry: { + type: 'Polygon', + coordinates: [[p1, p2, p3, p4, p1]] + } + }); + } else if (slopeDeg >= 15) { + ruggedCount++; + spatialFeatures.push({ + type: 'Feature', + properties: { + layerType: 'slope-sector', + category: 'severe-slow-go', + color: '#f97316', + title: `انحدار جبلي وعر (SEVERE SLOW-GO) [${slopeDeg}°]`, + slope: slopeDeg, + elevation: cellElevAvg + }, + geometry: { + type: 'Polygon', + coordinates: [[p1, p2, p3, p4, p1]] + } + }); + } else if (slopeDeg >= 7) { + rollingCount++; + spatialFeatures.push({ + type: 'Feature', + properties: { + layerType: 'slope-sector', + category: 'slow-go', + color: '#eab308', + title: `تضاريس متموجة (SLOW-GO) [${slopeDeg}°]`, + slope: slopeDeg, + elevation: cellElevAvg + }, + geometry: { + type: 'Polygon', + coordinates: [[p1, p2, p3, p4, p1]] + } + }); + } else { + flatCount++; + spatialFeatures.push({ + type: 'Feature', + properties: { + layerType: 'slope-sector', + category: 'go', + color: '#22c55e', + title: `ممر حركة منبسط (GO) [${slopeDeg}°]`, + slope: slopeDeg, + elevation: cellElevAvg + }, + geometry: { + type: 'Polygon', + coordinates: [[p1, p2, p3, p4, p1]] + } + }); } - - 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 + // 5. Statistics of Slope Distribution + const totalCells = Math.max(1, slopeValues.length); + const flatPct = Math.round((flatCount / totalCells) * 100); + const modPct = Math.round((rollingCount / totalCells) * 100); + const ruggedPct = Math.round((ruggedCount / totalCells) * 100); + const cliffPct = Math.max(0, 100 - flatPct - modPct - ruggedPct); + 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); - - // 4. Trace the Natural Valley / Drainage Line (Wadi) using DEM lowest cells - const valleyPoints: [number, number][] = []; + // 6. Trace Drainage Defiles & Natural Wadis (Lowland Thalweg Corridors) + const valleySegments: [number, number][] = []; for (let r = 0; r < gridSize; r++) { let minRowElev = 9999; let minColIdx = 0; @@ -804,29 +949,29 @@ export async function calculateTerrainStudy( } 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 (dist <= radiusMeters * 1.02) { + valleySegments.push([pt.lng, pt.lat]); } } - if (valleyPoints.length >= 2) { + if (valleySegments.length >= 3) { spatialFeatures.push({ type: 'Feature', properties: { layerType: 'wadi', category: 'natural-obstacle', - title: `🌊 مجرى وادٍ رئيسي ومصرف سيل طبيعي (${minElev}م)`, + title: `🌊 بطن وادٍ ومصرف سيل طبيعي (${minElev}م)`, color: '#06b6d4' }, geometry: { type: 'LineString', - coordinates: valleyPoints + coordinates: valleySegments } }); } - // 5. Detect and Draw Dominant Mountain Ridge (Connecting High Points) - const ridgePoints: [number, number][] = []; + // 7. Trace Dominant Mountain Ridges (Key Terrain Crest Lines) + const ridgeSegments: [number, number][] = []; for (let r = 0; r < gridSize; r++) { let maxRowElev = -9999; let maxColIdx = 0; @@ -838,28 +983,28 @@ export async function calculateTerrainStudy( } 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 (dist <= radiusMeters * 1.02 && maxRowElev >= minElev + relief * 0.5) { + ridgeSegments.push([pt.lng, pt.lat]); } } - if (ridgePoints.length >= 2) { + if (ridgeSegments.length >= 3) { spatialFeatures.push({ type: 'Feature', properties: { layerType: 'ridge', category: 'key-terrain', - title: `⛰️ سلسلة تلال حاكمة (Dominant Ridge - ${maxElev}م)`, + title: `⛰️ سلسلة تلال حاكمة (Dominant Crest - ${maxElev}م)`, color: '#38bdf8' }, geometry: { type: 'LineString', - coordinates: ridgePoints + coordinates: ridgeSegments } }); } - // 6. Real Regional Sector Identification + // 8. Regional Sector Intelligence Context let sectorName = 'قطاع عمليات ميداني'; let terrainClassificationAr = 'هضاب وتلال متوسطة الوعورة'; let terrainClassification = 'Rolling Hills & Ridges'; @@ -869,7 +1014,6 @@ export async function calculateTerrainStudy( let soilType = 'تربة طينية صخرية جافة'; let trafficability = 'جيدة للمركبات المدولبة والمجنزرة مع الحذر في مجاري السيول'; - // 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 = 'حوض وادي صدعي واسع تحيط به تلال وهضاب حاكمة ومجرى سيل مائي'; @@ -878,16 +1022,16 @@ export async function calculateTerrainStudy( urbanDensityAr = 'كثافة عالية (بلدات وضواحي ممتدة)'; settlements = ['بلدة السخنة', 'خربة قنتول', 'ضاحية البستان', 'أحياء الهاشمية', 'مدينة الزرقاء']; soilType = 'طمي رسوبي في بطن الوادي وصخور كلسية على الأكتاف'; - trafficability = 'ممتازة على شبكة الطرق وبطون الأودية المنبسطة (GO) مع صعوبة صعود الجروف الصخرية'; + trafficability = 'ممتازة على شبكة الطرق وبطون الأودية المنبسطة (GO) مع وجود قواطع صخرية على الأكتاف'; } else if (centerLat > 32.3 && centerLng < 36.0) { sectorName = 'قاطع الشمال / حوض اليرموك وإربد'; - terrainClassificationAr = 'مرتفعات جبلية تتخللها أودية سحيقة وخوانق مائية'; - terrainClassification = 'Highland Plateaus with Deep Gorges'; + terrainClassificationAr = 'مرتفعات جبلية تتخللها أودية سحيقة وخوانق مائية وجروف صخرية'; + terrainClassification = 'Highland Plateaus with Deep Gorges & Rock Cliffs'; urbanDensity = 'MEDIUM'; urbanDensityAr = 'كثافة متوسطة (قرى وضواحي ممتدة)'; settlements = ['بلدات وضواحي إربد', 'قرى حوض اليرموك', 'مزارع ومحميات وادي الشلالة']; - soilType = 'تربة زراعية خصبة وصخور كلسية'; - trafficability = 'صعبة على حواف الأودية - تتطلب محاور طرق رئيسية'; + soilType = 'تربة زراعية خصبة وصخور كلسية صلبة'; + trafficability = 'مقيدة بالجروف الصخرية الحادة - تتطلب محاور طرق معبدة'; } else if (centerLat >= 31.85 && centerLat <= 32.05 && centerLng >= 35.8 && centerLng <= 36.02) { sectorName = 'قاطع الوسط / إقليم العاصمة عمان'; terrainClassificationAr = 'هضاب وكتل جبلية حضرية مكتظة بالعمران'; @@ -908,26 +1052,35 @@ export async function calculateTerrainStudy( trafficability = 'حركة حرة مفتوحة للدروع والآليات الصحراوية (Cross-Country GO)'; } else if (centerLat < 31.8 && centerLng < 35.7) { sectorName = 'قاطع الغور / المنخفض الصدعي والبحر الميت'; - terrainClassificationAr = 'أخفـض نقطة على سطح الأرض (جروف حادة ومنحدرات قاسية)'; - terrainClassification = 'Rift Valley Escarpment (-400m MSL)'; + terrainClassificationAr = 'أخفـض نقطة على سطح الأرض (جروف صخرية وقواطع حادة)'; + terrainClassification = 'Rift Valley Escarpment (-400m MSL) with Rock Cliffs'; urbanDensity = 'LOW'; urbanDensityAr = 'كثافة منخفضة (مزارع الغور والمنتجعات)'; settlements = ['مزارع الأغوار الجنوبية', 'مجمعات الفنادق ومصانع البوتاس', 'قرى غور الصافي']; - soilType = 'طمي نهري وسبخات ملحية وصخور رسوبية'; - trafficability = 'حذرة جداً بجانب السبخات - ممتازة على طريق البحر الميت الرئيسي'; + soilType = 'طمي نهري وسبخات ملحية وصخور رسوبية قاسية'; + trafficability = 'جروف صخرية مانعة (NO-GO) على الحواف الغربية والشرقية'; } - // Mobility status calculation + // 9. Mobility Assessment let mobilityStatus: 'GO' | 'SLOW-GO' | 'NO-GO' = 'GO'; let mobilityStatusAr = 'حركة حرة ممتازة للآليات والدروع (Cross-Country GO)'; - if (steepPct > 30 || avgSlope > 12) { + if (cliffPct > 12 || ruggedPct > 25 || avgSlope > 14) { mobilityStatus = 'NO-GO'; - mobilityStatusAr = 'مانع تضاريسي - حركة مقيدة وشديدة الصعوبة (NO-GO)'; - } else if (steepPct > 10 || modPct > 35 || avgSlope > 6) { + mobilityStatusAr = 'مانع تضاريسي - قواطع صخرية وانحدارات حادة تعيق المناورة (NO-GO)'; + } else if (cliffPct > 3 || ruggedPct > 10 || modPct > 30 || avgSlope > 6) { mobilityStatus = 'SLOW-GO'; - mobilityStatusAr = 'حركة بطيئة تتطلب مسارب وتجهيزاً هندسياً (SLOW-GO)'; + mobilityStatusAr = 'تضاريس متموجة مقيدة تتطلب مسارب وتجهيزاً هندسياً (SLOW-GO)'; } + // Sort and pick top distinct cliffs + const topCliffs = detectedCliffs + .sort((a, b) => b.dropMeters - a.dropMeters || b.slopeDegrees - a.slopeDegrees) + .slice(0, 8); + + const cliffSummaryText = topCliffs.length > 0 + ? `تم رصد ${detectedCliffs.length} قاطعاً وجرفاً صخرياً حاداً (أقصى سقوط ${topCliffs[0].dropMeters}م بميل ${topCliffs[0].slopeDegrees}°). تشكل هذه القواطع موانع طبيعية مانعة لحركة الآليات والدروع.` + : `القطاع خالٍ من الجروف الصخرية العمودية، والميول العامة تتراوح بين منبسطة ومتموجة معتدلة (أقصى انحدار ${maxSlope}°).`; + return { centerLat, centerLng, @@ -937,14 +1090,17 @@ export async function calculateTerrainStudy( centerElevation: Math.round(centerElev), minElevation: Math.round(minElev), maxElevation: Math.round(maxElev), - reliefMeters: Math.round(relief), + reliefMeters: relief, avgSlopeDegrees: avgSlope, maxSlopeDegrees: maxSlope, slopeDistribution: { flatPercentage: flatPct, moderatePercentage: modPct, - steepPercentage: steepPct + steepPercentage: ruggedPct, + cliffPercentage: cliffPct }, + cliffsCount: detectedCliffs.length, + cliffs: topCliffs, terrainClassification, terrainClassificationAr, mobilityStatus, @@ -956,32 +1112,32 @@ export async function calculateTerrainStudy( features: spatialFeatures }, naturalObstacles: [ + { + name: 'القواطع والجروف الصخرية الطبيعية (Cliffs & Rock Escarpments)', + type: 'موانع وقواطع صخرية حادة', + description: cliffSummaryText, + impact: topCliffs.length > 0 + ? 'إغلاق محاور الالتفاف الجانبي وإجبار القوات المهاجمة على استخدام ممرات إجبارية (Choke Points)' + : 'حرية مناورة واسعة بدون عوائق صخرية عمودية' + }, { name: 'مجاري الوديان والسيول الطبيعية (Drainage Defiles)', type: 'مصارف مائية وأودية', - description: `بطن الوادي عند منسوب ${minElev}م يشكل مصرفاً مائياً رئيسياً، يوفر ممراً محجوباً ولكن قد يعيق المناورة عند هطول الأمطار.`, - impact: 'محور تسلل مناسب للمشاة ومقيد للآليات الثقيلة عند تشكل الأوحال' + description: `بطن الوادي عند منسوب ${minElev}م يشكل مصرفاً مائياً ومحور حركة منخفضاً، يوفر ممراً محجوباً ولكن قد يعيق المناورة عند هطول الأمطار.`, + impact: 'محور تسلل مناسب للمشاة ومقيد للآليات الثقيلة عند تشكل الأوحال والسيول' }, { - name: 'التلال والقمم الحاكمة (Dominant Ridges)', + name: 'التلال والقمم الحاكمة (Dominant Ridges & Crests)', type: 'أرض مسيطرة (Key Terrain)', - description: `المرتفع الحاكم بارتفاع ${maxElev}م يشرف بشكل كامل على بطن الوادي ومحاور التحرك على مسافة ${radiusMeters / 1000} كم.`, - impact: 'موقع استراتيجي لتمركز أسلحة الإسناد ونقاط الملاحظة والاستطلاع' - }, - { - name: 'المنحدرات التضاريسية المحيطة', - type: 'انحدار طبوغرافي', - description: steepPct > 0 - ? `يوجد ${steepPct}% من مساحة القطاع ذات انحدار وعر (>12°) تشكل موانع طبيعية تعيق صعود الآليات.` - : `القطاع يتسم بانحدارات لطيفة إلى متوسطة (أقصى انحدار ${maxSlope}°)، ولا توجد جروف شاهقة مانعة لحركة الدروع.`, - impact: steepPct > 0 ? 'إجبار القوات المهاجمة على استخدام الممرات المعبدة' : 'حرية مناورة واسعة للدروع في مختلف الاتجاهات' + description: `المرتفع الحاكم بارتفاع ${maxElev}م يشرف بشكل كامل على بطن الوادي ومحاور التحرك على مسافة ${radiusMeters / 1000} كم بفارق تضاريسي ${relief}م.`, + impact: 'موقع استراتيجي لتمركز أسلحة الإسناد ونقاط الملاحظة والاستطلاع والسيطرة النارية' } ], manMadeObstacles: [ { name: 'محاور الطرق الرئيسية وخطوط الإمداد (MSR)', type: 'بنية تحتية للمواصلات', - description: 'الطرق المعبدة المارة بالوادي تتيح تدفقاً سريعاً للأرتال وإعادة التزود اللوجستي.', + description: 'الطرق المعبدة المارة بالقطاع تتيح تدفقاً سريعاً للأرتال وإعادة التزود اللوجستي.', tacticalNote: 'وجوب إعداد خطط قطع وغلق هندسي سريع للتحكم بحركة العدو' }, { @@ -1011,17 +1167,20 @@ export async function calculateTerrainStudy( airObservationExposure: urbanDensity === 'HIGH' ? 'مختلطة (المباني توفر إخفاء جزئياً)' : 'عالية للرصد الجوي والمسيرات نهاراً' }, oakocAssessment: { - obstacles: `القطاع يتضمن مجرى وادٍ رئيسي عند منسوب ${minElev}م مع انحدارات تصل إلى ${maxSlope}°، مما يشكل محاور حركة طبيعية محددة.`, - avenuesOfApproach: 'محور الاقتراب الرئيسي يتبع بطن الوادي وشبكة الطرق الرئيسية، مع إمكانية الالتفاف عبر الهضاب المنبسطة.', + obstacles: `القطاع يضم ${detectedCliffs.length > 0 ? `${detectedCliffs.length} قاطعاً وجرفاً صخرياً و` : ''}مجرى وادٍ رئيسي عند منسوب ${minElev}م، مع انحدارات تصل إلى ${maxSlope}°، مما يشكل محاور حركة طبيعية محددة.`, + avenuesOfApproach: 'محور الاقتراب الرئيسي يتبع بطن الوادي وشبكة الطرق الرئيسية المنبسطة، مع تفادي الجروف الصخرية الحادة.', keyTerrain: `القمة الحاكمة بارتفاع ${maxElev}م توفر ميزة كشف وهيمنة نارية حاسمة على كامل القطاع.`, - observationAndFieldsOfFire: `حقول الرماية مكشوفة ومثالية بنسبة ${flatPct}% في الأراضي المنبسطة، ومحجوبة داخل بطون الأودية.`, - coverAndConcealment: 'تتوفر السواتر التضاريسية في المنحدرات الخلفية (Reverse Slopes) وداخل التجمعات العمرانية.' + observationAndFieldsOfFire: `حقول الرماية مكشوفة ومثالية بنسبة ${flatPct}% في الأراضي المنبسطة، ومحجوبة داخل بطون الأودية وخلف الجروف.`, + coverAndConcealment: 'تتوفر السواتر التضاريسية في المنحدرات الخلفية (Reverse Slopes) وأسفل الجروف الصخرية وداخل الكتل العمرانية.' }, tacticalRecommendations: [ - `نشر مرصد استطلاع رئيسي ورادار كشف أرضي على المرتفع الحاكم (${maxElev}م) لتأمين إنذار مبكر ومراقبة بطن الوادي.`, - `تجهيز نقاط كمائن وموانع هندسية وحقول ألغام موجهة في مخانق بطن الوادي (${minElev}م) لصد أي تقدم مدرع.`, + `نشر مرصد استطلاع رئيسي ورادار كشف أرضي على المرتفع الحاكم (${maxElev}م) لتأمين إنذار مبكر ومراقبة القطاع.`, + detectedCliffs.length > 0 + ? `استغلال القواطع والجروف الصخرية المكتشفة كحواجز طبيعية موجهة لحصر تقدم العدو داخل المخانق المستهدفة بنيران الأسلحة الثقيلة.` + : `تجهيز نقاط كمائن وموانع هندسية وحقول ألغام موجهة في مخانق بطن الوادي (${minElev}م) لصد أي تقدم مدرع.`, `تأمين الطرق الرئيسية والمحاور الحيوية بنقاط سيطرة وحراسة ثابتة لمنع عمليات الالتفاف السريعة.`, `استغلال المناطق المنبسطة (${flatPct}%) لإنشاء مهابط المروحيات (HLZ) ومناطق الإخلاء الطبي والتجمع اللوجستي.` ] }; } +