feat(tactical): high-resolution 21x21 DEM terrain study, bilinear interpolation, cliff/rock barrier detection, and 4-tier slope classification
This commit is contained in:
@@ -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
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Slope Distribution Bar */}
|
||||
{/* Slope Distribution Bar (4 Military Tiers) */}
|
||||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8 }}>
|
||||
<div style={{ fontSize: '0.72rem', fontWeight: 700, color: '#cbd5e1', marginBottom: 6, display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span>توزيع درجات الانحدار (Slope Analysis):</span>
|
||||
<span style={{ color: '#94a3b8' }}>متوسط: {terrainResult.avgSlopeDegrees}°</span>
|
||||
</div>
|
||||
<div style={{ height: 10, width: '100%', display: 'flex', borderRadius: 5, overflow: 'hidden', marginBottom: 6 }}>
|
||||
<div style={{ width: `${terrainResult.slopeDistribution.flatPercentage}%`, background: '#22c55e' }} title={`منبسط: ${terrainResult.slopeDistribution.flatPercentage}%`} />
|
||||
<div style={{ width: `${terrainResult.slopeDistribution.moderatePercentage}%`, background: '#eab308' }} title={`متوسط: ${terrainResult.slopeDistribution.moderatePercentage}%`} />
|
||||
<div style={{ width: `${terrainResult.slopeDistribution.steepPercentage}%`, background: '#ef4444' }} title={`شديد الوعورة: ${terrainResult.slopeDistribution.steepPercentage}%`} />
|
||||
<div style={{ width: `${terrainResult.slopeDistribution.flatPercentage}%`, background: '#22c55e' }} title={`منبسط (<7°): ${terrainResult.slopeDistribution.flatPercentage}%`} />
|
||||
<div style={{ width: `${terrainResult.slopeDistribution.moderatePercentage}%`, background: '#eab308' }} title={`تضاريس متموجة (7-15°): ${terrainResult.slopeDistribution.moderatePercentage}%`} />
|
||||
<div style={{ width: `${terrainResult.slopeDistribution.steepPercentage}%`, background: '#f97316' }} title={`انحدار وعر (15-25°): ${terrainResult.slopeDistribution.steepPercentage}%`} />
|
||||
<div style={{ width: `${terrainResult.slopeDistribution.cliffPercentage}%`, background: '#ef4444' }} title={`قواطع وجروف صخرية (>25°): ${terrainResult.slopeDistribution.cliffPercentage}%`} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.68rem', color: '#94a3b8' }}>
|
||||
<span style={{ color: '#4ade80' }}>■ منبسط (<5°): {terrainResult.slopeDistribution.flatPercentage}%</span>
|
||||
<span style={{ color: '#facc15' }}>■ متوسط (5-15°): {terrainResult.slopeDistribution.moderatePercentage}%</span>
|
||||
<span style={{ color: '#f87171' }}>■ وعر (>15°): {terrainResult.slopeDistribution.steepPercentage}%</span>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '3px 8px', fontSize: '0.66rem', color: '#94a3b8' }}>
|
||||
<span style={{ color: '#4ade80' }}>■ منبسط (<7°): {terrainResult.slopeDistribution.flatPercentage}%</span>
|
||||
<span style={{ color: '#facc15' }}>■ متموج (7-15°): {terrainResult.slopeDistribution.moderatePercentage}%</span>
|
||||
<span style={{ color: '#fb923c' }}>■ وعر (15-25°): {terrainResult.slopeDistribution.steepPercentage}%</span>
|
||||
<span style={{ color: '#f87171' }}>■ جروف صخرية (>25°): {terrainResult.slopeDistribution.cliffPercentage}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detected Cliffs & Natural Rock Barriers */}
|
||||
{terrainResult.cliffs && terrainResult.cliffs.length > 0 && (
|
||||
<div style={{ background: 'rgba(239, 68, 68, 0.08)', padding: 10, borderRadius: 8, border: '1px solid rgba(239, 68, 68, 0.35)' }}>
|
||||
<div style={{ fontSize: '0.76rem', fontWeight: 800, color: '#f87171', marginBottom: 6, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
|
||||
<AlertTriangle size={14} /> 🧗♂️ القواطع والجروف الصخرية المرصودة ({terrainResult.cliffsCount})
|
||||
</span>
|
||||
<span style={{ fontSize: '0.62rem', background: '#ef4444', color: '#fff', padding: '1px 6px', borderRadius: 10, fontWeight: 700 }}>مانع NO-GO</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 5, maxHeight: 160, overflowY: 'auto' }}>
|
||||
{terrainResult.cliffs.map((cliff, cIdx) => (
|
||||
<div
|
||||
key={cIdx}
|
||||
onClick={() => {
|
||||
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)'
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.72rem', fontWeight: 700, color: '#fca5a5' }}>
|
||||
قاطع #{cIdx + 1}: سقوط {cliff.dropMeters}م (ميل {cliff.slopeDegrees}°)
|
||||
</div>
|
||||
<div style={{ fontSize: '0.62rem', color: '#94a3b8', fontFamily: 'monospace' }}>
|
||||
{cliff.lat}, {cliff.lng}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.65rem', color: '#f87171', fontWeight: 800, background: 'rgba(239,68,68,0.15)', padding: '2px 6px', borderRadius: 4 }}>
|
||||
🔍 تركيز
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* SECTION 1: NATURAL OBSTACLES */}
|
||||
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 10, borderRadius: 8, border: '1px solid rgba(239, 68, 68, 0.2)' }}>
|
||||
<div style={{ fontSize: '0.78rem', fontWeight: 800, color: '#f87171', marginBottom: 6, display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
|
||||
@@ -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<ImageData | null> {
|
||||
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<HTMLImageElement>((resolve, reject) => {
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = (e) => reject(e);
|
||||
img.src = tileUrl;
|
||||
});
|
||||
|
||||
const loadedImg = await Promise.race([
|
||||
loadPromise,
|
||||
new Promise<never>((_, 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<number[]> {
|
||||
const n = Math.pow(2, zoom);
|
||||
|
||||
// Group coordinates by tile
|
||||
const tileMap = new Map<string, { zoom: number; x: number; y: number; points: Array<{ index: number; subX: number; subY: number; lat: number; lng: number }> }>();
|
||||
|
||||
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<number> {
|
||||
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<HTMLImageElement>((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<never>((_, 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) ومناطق الإخلاء الطبي والتجمع اللوجستي.`
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user