25°): ${terrainResult.slopeDistribution.cliffPercentage}%`} />
+ {/* Detected Cliffs & Natural Rock Barriers */}
+ {terrainResult.cliffs && terrainResult.cliffs.length > 0 && (
+
+ )}
+
{/* 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) ومناطق الإخلاء الطبي والتجمع اللوجستي.`
]
};
}
+