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': [
|
'fill-opacity': [
|
||||||
'match',
|
'match',
|
||||||
['get', 'layerType'],
|
['get', 'layerType'],
|
||||||
|
'cliff', 0.50,
|
||||||
'urban', 0.28,
|
'urban', 0.28,
|
||||||
'hazard', 0.32,
|
'hazard', 0.32,
|
||||||
'slope-sector', 0.18,
|
'slope-sector', 0.18,
|
||||||
@@ -566,8 +567,18 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
filter: ['==', '$type', 'Polygon'],
|
filter: ['==', '$type', 'Polygon'],
|
||||||
paint: {
|
paint: {
|
||||||
'line-color': ['get', 'color'],
|
'line-color': ['get', 'color'],
|
||||||
'line-width': 2,
|
'line-width': [
|
||||||
'line-dasharray': [3, 2]
|
'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': [
|
'line-width': [
|
||||||
'match',
|
'match',
|
||||||
['get', 'layerType'],
|
['get', 'layerType'],
|
||||||
|
'cliff', 4.5,
|
||||||
'road', 4.5,
|
'road', 4.5,
|
||||||
'wadi', 3.5,
|
'wadi', 3.5,
|
||||||
3
|
3
|
||||||
@@ -588,6 +600,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
'line-dasharray': [
|
'line-dasharray': [
|
||||||
'match',
|
'match',
|
||||||
['get', 'layerType'],
|
['get', 'layerType'],
|
||||||
|
'cliff', ['literal', [2, 1]],
|
||||||
'road', ['literal', [1, 0]],
|
'road', ['literal', [1, 0]],
|
||||||
'wadi', ['literal', [2, 1.5]],
|
'wadi', ['literal', [2, 1.5]],
|
||||||
['literal', [1, 0]]
|
['literal', [1, 0]]
|
||||||
@@ -817,10 +830,10 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
const filtered = terrainResult.spatialGeoJson.features.filter((f: any) => {
|
const filtered = terrainResult.spatialGeoJson.features.filter((f: any) => {
|
||||||
if (f.properties.category === 'peak' || f.properties.category === 'valley') return showPeaksAndValleys;
|
if (f.properties.category === 'peak' || f.properties.category === 'valley') return showPeaksAndValleys;
|
||||||
if (f.properties.layerType === 'slope-sector') return showSlopeSectors;
|
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 === 'road') return showRoadCorridors;
|
||||||
if (f.properties.layerType === 'urban') return showUrbanZones;
|
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;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1652,24 +1665,72 @@ ${terrainResult.tacticalRecommendations.map((r, i) => `${i + 1}. ${r}`).join('\n
|
|||||||
</div>
|
</div>
|
||||||
</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={{ 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' }}>
|
<div style={{ fontSize: '0.72rem', fontWeight: 700, color: '#cbd5e1', marginBottom: 6, display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<span>توزيع درجات الانحدار (Slope Analysis):</span>
|
<span>توزيع درجات الانحدار (Slope Analysis):</span>
|
||||||
<span style={{ color: '#94a3b8' }}>متوسط: {terrainResult.avgSlopeDegrees}°</span>
|
<span style={{ color: '#94a3b8' }}>متوسط: {terrainResult.avgSlopeDegrees}°</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ height: 10, width: '100%', display: 'flex', borderRadius: 5, overflow: 'hidden', marginBottom: 6 }}>
|
<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.flatPercentage}%`, background: '#22c55e' }} title={`منبسط (<7°): ${terrainResult.slopeDistribution.flatPercentage}%`} />
|
||||||
<div style={{ width: `${terrainResult.slopeDistribution.moderatePercentage}%`, background: '#eab308' }} title={`متوسط: ${terrainResult.slopeDistribution.moderatePercentage}%`} />
|
<div style={{ width: `${terrainResult.slopeDistribution.moderatePercentage}%`, background: '#eab308' }} title={`تضاريس متموجة (7-15°): ${terrainResult.slopeDistribution.moderatePercentage}%`} />
|
||||||
<div style={{ width: `${terrainResult.slopeDistribution.steepPercentage}%`, background: '#ef4444' }} title={`شديد الوعورة: ${terrainResult.slopeDistribution.steepPercentage}%`} />
|
<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>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.68rem', color: '#94a3b8' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '3px 8px', fontSize: '0.66rem', color: '#94a3b8' }}>
|
||||||
<span style={{ color: '#4ade80' }}>■ منبسط (<5°): {terrainResult.slopeDistribution.flatPercentage}%</span>
|
<span style={{ color: '#4ade80' }}>■ منبسط (<7°): {terrainResult.slopeDistribution.flatPercentage}%</span>
|
||||||
<span style={{ color: '#facc15' }}>■ متوسط (5-15°): {terrainResult.slopeDistribution.moderatePercentage}%</span>
|
<span style={{ color: '#facc15' }}>■ متموج (7-15°): {terrainResult.slopeDistribution.moderatePercentage}%</span>
|
||||||
<span style={{ color: '#f87171' }}>■ وعر (>15°): {terrainResult.slopeDistribution.steepPercentage}%</span>
|
<span style={{ color: '#fb923c' }}>■ وعر (15-25°): {terrainResult.slopeDistribution.steepPercentage}%</span>
|
||||||
|
<span style={{ color: '#f87171' }}>■ جروف صخرية (>25°): {terrainResult.slopeDistribution.cliffPercentage}%</span>
|
||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* 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={{ 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 }}>
|
<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;
|
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
|
* Samples elevation from Terrarium DEM tile or fallback topographic model
|
||||||
*/
|
*/
|
||||||
async function sampleElevationAt(lat: number, lng: number): Promise<number> {
|
async function sampleElevationAt(lat: number, lng: number): Promise<number> {
|
||||||
const zoom = 12;
|
const zoom = 13;
|
||||||
const n = Math.pow(2, zoom);
|
const n = Math.pow(2, zoom);
|
||||||
const x = Math.floor(((lng + 180) / 360) * n);
|
const x = Math.floor(((lng + 180) / 360) * n);
|
||||||
const latRad = (lat * Math.PI) / 180;
|
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 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 {
|
const imgData = await fetchTileImageData(zoom, x, y);
|
||||||
let imgData = tileCache.get(tileKey);
|
if (imgData) {
|
||||||
|
return Math.round(interpolateElevation(imgData, subX, subY));
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return getApproximateElevation(lat, lng);
|
return getApproximateElevation(lat, lng);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,34 +219,26 @@ function getApproximateElevation(lat: number, lng: number): number {
|
|||||||
// Jordan Valley & Dead Sea trench model
|
// Jordan Valley & Dead Sea trench model
|
||||||
if (lng < 35.6 && lat < 32.2 && lat > 31.0) {
|
if (lng < 35.6 && lat < 32.2 && lat > 31.0) {
|
||||||
const distFromRift = Math.abs(lng - 35.5);
|
const distFromRift = Math.abs(lng - 35.5);
|
||||||
return -400 + distFromRift * 3000;
|
return Math.round(-400 + distFromRift * 3000);
|
||||||
}
|
}
|
||||||
// Northern Highlands (Ajloun / Jerash / Salt)
|
// Northern Highlands (Ajloun / Jerash / Salt)
|
||||||
if (lat >= 32.1 && lng < 36.0) {
|
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
|
// Amman Plateau
|
||||||
if (lat >= 31.8 && lat < 32.1 && lng >= 35.8 && lng < 36.2) {
|
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)
|
// Southern Highlands (Karak / Tafilah / Shobak / Petra)
|
||||||
if (lat < 31.5 && lat > 30.0 && lng < 35.7) {
|
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)
|
// 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
|
* 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(
|
export async function calculateLineOfSight(
|
||||||
startLat: number,
|
startLat: number,
|
||||||
@@ -233,7 +301,7 @@ export async function calculateLineOfSight(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch {
|
} 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
|
// Generate sample coordinates along the geodesic path
|
||||||
@@ -246,17 +314,14 @@ export async function calculateLineOfSight(
|
|||||||
sampleCoords.push({ lat, lng, dist });
|
sampleCoords.push({ lat, lng, dist });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch elevations for all sample points in parallel
|
// Fetch elevations for all sample points in fast batch
|
||||||
const elevations = await Promise.all(
|
const elevations = await sampleElevationsBatch(sampleCoords, 13);
|
||||||
sampleCoords.map((coord) => sampleElevationAt(coord.lat, coord.lng))
|
|
||||||
);
|
|
||||||
|
|
||||||
const observerGroundElev = elevations[0];
|
const observerGroundElev = elevations[0];
|
||||||
const targetGroundElev = elevations[elevations.length - 1];
|
const targetGroundElev = elevations[elevations.length - 1];
|
||||||
const observerElevation = observerGroundElev + obsHeight;
|
const observerElevation = observerGroundElev + obsHeight;
|
||||||
const targetElevation = targetGroundElev + tgtHeight;
|
const targetElevation = targetGroundElev + tgtHeight;
|
||||||
|
|
||||||
// Earth curvature & atmospheric refraction parameter (k ≈ 0.13 for standard atmosphere)
|
|
||||||
const R_earth = 6371000;
|
const R_earth = 6371000;
|
||||||
const k_refraction = 0.13;
|
const k_refraction = 0.13;
|
||||||
const effectiveEarthRadius = R_earth / (1 - k_refraction);
|
const effectiveEarthRadius = R_earth / (1 - k_refraction);
|
||||||
@@ -265,8 +330,6 @@ export async function calculateLineOfSight(
|
|||||||
let highestObstacle: ObstacleInfo | null = null;
|
let highestObstacle: ObstacleInfo | null = null;
|
||||||
let maxObstacleExcess = 0;
|
let maxObstacleExcess = 0;
|
||||||
let deadGroundCount = 0;
|
let deadGroundCount = 0;
|
||||||
|
|
||||||
// Horizon angle tracking from observer (tan of highest angle encountered so far)
|
|
||||||
let maxAngleSoFar = -Infinity;
|
let maxAngleSoFar = -Infinity;
|
||||||
|
|
||||||
const points: ElevationPoint[] = [];
|
const points: ElevationPoint[] = [];
|
||||||
@@ -279,16 +342,10 @@ export async function calculateLineOfSight(
|
|||||||
minElev = Math.min(minElev, elev);
|
minElev = Math.min(minElev, elev);
|
||||||
maxElev = Math.max(maxElev, 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);
|
const earthCurvatureDrop = (d * (totalDistance - d)) / (2 * effectiveEarthRadius);
|
||||||
|
|
||||||
// Theoretical straight ray height AMSL connecting Observer to Target
|
|
||||||
const rayHeight = observerElevation + ((targetElevation - observerElevation) * (d / totalDistance)) - earthCurvatureDrop;
|
const rayHeight = observerElevation + ((targetElevation - observerElevation) * (d / totalDistance)) - earthCurvatureDrop;
|
||||||
|
|
||||||
// Clearance (positive = ray above terrain, negative = obstacle)
|
|
||||||
const clearance = rayHeight - elev;
|
const clearance = rayHeight - elev;
|
||||||
|
|
||||||
// Check if this point blocks the direct ray to the target (ignore start and end margins)
|
|
||||||
let isTargetRayBlocked = false;
|
let isTargetRayBlocked = false;
|
||||||
if (i > 1 && i < samples) {
|
if (i > 1 && i < samples) {
|
||||||
if (elev > rayHeight) {
|
if (elev > rayHeight) {
|
||||||
@@ -299,7 +356,7 @@ export async function calculateLineOfSight(
|
|||||||
maxObstacleExcess = excess;
|
maxObstacleExcess = excess;
|
||||||
highestObstacle = {
|
highestObstacle = {
|
||||||
distance: Math.round(d),
|
distance: Math.round(d),
|
||||||
elevation: elev,
|
elevation: Math.round(elev),
|
||||||
lat: sampleCoords[i].lat,
|
lat: sampleCoords[i].lat,
|
||||||
lng: sampleCoords[i].lng,
|
lng: sampleCoords[i].lng,
|
||||||
excessHeight: Math.round(excess * 10) / 10,
|
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;
|
let isVisible = true;
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
isVisible = true;
|
isVisible = true;
|
||||||
@@ -327,7 +383,7 @@ export async function calculateLineOfSight(
|
|||||||
distance: Math.round(d),
|
distance: Math.round(d),
|
||||||
lat: sampleCoords[i].lat,
|
lat: sampleCoords[i].lat,
|
||||||
lng: sampleCoords[i].lng,
|
lng: sampleCoords[i].lng,
|
||||||
elevation: elev,
|
elevation: Math.round(elev),
|
||||||
rayHeight: Math.round(rayHeight * 10) / 10,
|
rayHeight: Math.round(rayHeight * 10) / 10,
|
||||||
isVisible,
|
isVisible,
|
||||||
isTargetRayBlocked,
|
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 verticalDiff = targetElevation - observerElevation;
|
||||||
const angleRad = Math.atan2(verticalDiff, totalDistance);
|
const angleRad = Math.atan2(verticalDiff, totalDistance);
|
||||||
const angleDegrees = Math.round((angleRad * (180 / Math.PI)) * 100) / 100;
|
const angleDegrees = Math.round((angleRad * (180 / Math.PI)) * 100) / 100;
|
||||||
const angleMils = Math.round((angleDegrees * (6400 / 360)) * 10) / 10;
|
const angleMils = Math.round((angleDegrees * (6400 / 360)) * 10) / 10;
|
||||||
|
|
||||||
const deadGroundPercentage = Math.round((deadGroundCount / samples) * 100);
|
const deadGroundPercentage = Math.round((deadGroundCount / samples) * 100);
|
||||||
|
|
||||||
return {
|
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(
|
export async function calculateRadialViewshed(
|
||||||
centerLat: number,
|
centerLat: number,
|
||||||
@@ -390,9 +443,8 @@ export async function calculateRadialViewshed(
|
|||||||
const k_refraction = 0.13;
|
const k_refraction = 0.13;
|
||||||
const effectiveEarthRadius = R_earth / (1 - k_refraction);
|
const effectiveEarthRadius = R_earth / (1 - k_refraction);
|
||||||
|
|
||||||
const polygonCoordinates: [number, number][] = [];
|
const samplesPerRay = 14;
|
||||||
const samplesPerRay = 12;
|
const rayCoords: Array<{ rayIdx: number; step: number; sDist: number; sLat: number; sLng: number }> = [];
|
||||||
let totalVisibleDistanceSum = 0;
|
|
||||||
|
|
||||||
for (let rayIdx = 0; rayIdx < numRays; rayIdx++) {
|
for (let rayIdx = 0; rayIdx < numRays; rayIdx++) {
|
||||||
const angleDeg = (rayIdx * 360) / numRays;
|
const angleDeg = (rayIdx * 360) / numRays;
|
||||||
@@ -404,29 +456,41 @@ export async function calculateRadialViewshed(
|
|||||||
const endLat = centerLat + dLat;
|
const endLat = centerLat + dLat;
|
||||||
const endLng = centerLng + dLng;
|
const endLng = centerLng + dLng;
|
||||||
|
|
||||||
let maxAngleSoFar = -Infinity;
|
|
||||||
let visibleHorizonDist = radiusMeters;
|
|
||||||
let visibleHorizonLat = endLat;
|
|
||||||
let visibleHorizonLng = endLng;
|
|
||||||
|
|
||||||
for (let step = 1; step <= samplesPerRay; step++) {
|
for (let step = 1; step <= samplesPerRay; step++) {
|
||||||
const frac = step / samplesPerRay;
|
const frac = step / samplesPerRay;
|
||||||
const sLat = centerLat + (endLat - centerLat) * frac;
|
const sLat = centerLat + (endLat - centerLat) * frac;
|
||||||
const sLng = centerLng + (endLng - centerLng) * frac;
|
const sLng = centerLng + (endLng - centerLng) * frac;
|
||||||
const sDist = radiusMeters * frac;
|
const sDist = radiusMeters * frac;
|
||||||
|
rayCoords.push({ rayIdx, step, sDist, sLat, sLng });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sElev = await sampleElevationAt(sLat, sLng);
|
// Batch sample all ray points
|
||||||
const earthCurvatureDrop = (sDist * sDist) / (2 * effectiveEarthRadius);
|
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 apparentElev = sElev - earthCurvatureDrop;
|
||||||
const angle = (apparentElev - observerElevation) / sDist;
|
const angle = (apparentElev - observerElevation) / rc.sDist;
|
||||||
|
|
||||||
if (angle >= maxAngleSoFar) {
|
if (angle >= maxAngleSoFar) {
|
||||||
maxAngleSoFar = angle;
|
maxAngleSoFar = angle;
|
||||||
visibleHorizonDist = sDist;
|
visibleHorizonDist = rc.sDist;
|
||||||
visibleHorizonLat = sLat;
|
visibleHorizonLat = rc.sLat;
|
||||||
visibleHorizonLng = sLng;
|
visibleHorizonLng = rc.sLng;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
totalVisibleDistanceSum += visibleHorizonDist;
|
totalVisibleDistanceSum += visibleHorizonDist;
|
||||||
polygonCoordinates.push([visibleHorizonLng, visibleHorizonLat]);
|
polygonCoordinates.push([visibleHorizonLng, visibleHorizonLat]);
|
||||||
@@ -493,7 +557,6 @@ export interface MinefieldAnalysisResult {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Tactical Minefield Barrier & Sapper Engineering Analysis
|
* Tactical Minefield Barrier & Sapper Engineering Analysis
|
||||||
* دراسة جدوى وكميات حقل الألغام الدفاعي وفحص التضاريس
|
|
||||||
*/
|
*/
|
||||||
export async function calculateMinefieldAnalysis(
|
export async function calculateMinefieldAnalysis(
|
||||||
startLat: number,
|
startLat: number,
|
||||||
@@ -505,25 +568,23 @@ export async function calculateMinefieldAnalysis(
|
|||||||
const frontageMeters = calculateDistance(startLat, startLng, endLat, endLng);
|
const frontageMeters = calculateDistance(startLat, startLng, endLat, endLng);
|
||||||
const azimuthDegrees = calculateAzimuth(startLat, startLng, endLat, endLng);
|
const azimuthDegrees = calculateAzimuth(startLat, startLng, endLat, endLng);
|
||||||
|
|
||||||
const startElev = await sampleElevationAt(startLat, startLng);
|
const samples = 12;
|
||||||
const endElev = await sampleElevationAt(endLat, endLng);
|
const sampleCoords: Array<{ lat: number; lng: number }> = [];
|
||||||
|
|
||||||
// Sample intermediate points to determine valley / ridge / choke point profile
|
|
||||||
const samples = 10;
|
|
||||||
const elevs: number[] = [];
|
|
||||||
for (let i = 0; i <= samples; i++) {
|
for (let i = 0; i <= samples; i++) {
|
||||||
const frac = i / samples;
|
const frac = i / samples;
|
||||||
const lat = startLat + (endLat - startLat) * frac;
|
const lat = startLat + (endLat - startLat) * frac;
|
||||||
const lng = startLng + (endLng - startLng) * frac;
|
const lng = startLng + (endLng - startLng) * frac;
|
||||||
const el = await sampleElevationAt(lat, lng);
|
sampleCoords.push({ lat, lng });
|
||||||
elevs.push(el);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const elevs = await sampleElevationsBatch(sampleCoords, 13);
|
||||||
|
const startElev = elevs[0];
|
||||||
|
const endElev = elevs[elevs.length - 1];
|
||||||
|
|
||||||
const elevDiff = Math.abs(endElev - startElev);
|
const elevDiff = Math.abs(endElev - startElev);
|
||||||
const avgSlopeRad = frontageMeters > 0 ? Math.atan(elevDiff / frontageMeters) : 0;
|
const avgSlopeRad = frontageMeters > 0 ? Math.atan(elevDiff / frontageMeters) : 0;
|
||||||
const avgSlopeDegrees = Math.round((avgSlopeRad * (180 / Math.PI)) * 10) / 10;
|
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 midElev = elevs[Math.floor(samples / 2)];
|
||||||
const isChokePoint = midElev < Math.min(startElev, endElev) - 10 || avgSlopeDegrees <= 10;
|
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 {
|
export interface TerrainStudyResult {
|
||||||
centerLat: number;
|
centerLat: number;
|
||||||
centerLng: number;
|
centerLng: number;
|
||||||
@@ -589,10 +659,13 @@ export interface TerrainStudyResult {
|
|||||||
avgSlopeDegrees: number;
|
avgSlopeDegrees: number;
|
||||||
maxSlopeDegrees: number;
|
maxSlopeDegrees: number;
|
||||||
slopeDistribution: {
|
slopeDistribution: {
|
||||||
flatPercentage: number; // < 5 deg (Go)
|
flatPercentage: number; // < 7 deg (Go)
|
||||||
moderatePercentage: number; // 5-15 deg (Slow-Go)
|
moderatePercentage: number; // 7-15 deg (Slow-Go / Rolling)
|
||||||
steepPercentage: number; // > 15 deg (No-Go / Obstacle)
|
steepPercentage: number; // 15-25 deg (Severe Slow-Go / Rugged)
|
||||||
|
cliffPercentage: number; // > 25 deg (No-Go / Cliffs & Rock Barriers)
|
||||||
};
|
};
|
||||||
|
cliffsCount: number;
|
||||||
|
cliffs: CliffFeature[];
|
||||||
terrainClassification: string;
|
terrainClassification: string;
|
||||||
terrainClassificationAr: string;
|
terrainClassificationAr: string;
|
||||||
mobilityStatus: 'GO' | 'SLOW-GO' | 'NO-GO';
|
mobilityStatus: 'GO' | 'SLOW-GO' | 'NO-GO';
|
||||||
@@ -648,10 +721,9 @@ export interface TerrainStudyResult {
|
|||||||
tacticalRecommendations: string[];
|
tacticalRecommendations: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Military Tactical Terrain Intelligence & Environmental Study (OAKOC Doctrine)
|
* Military Tactical Terrain Intelligence & Environmental Study (OAKOC Doctrine)
|
||||||
* دراسة الأرض الشاملة: التضاريس، الموانع الطبيعية والصناعية، والمناطق المأهولة
|
* دراسة الأرض الشاملة عالية الدقة: حساب الميول الدقيقة، كشف القواطع والجروف الصخرية، وممرات الحركة
|
||||||
*/
|
*/
|
||||||
export async function calculateTerrainStudy(
|
export async function calculateTerrainStudy(
|
||||||
centerLat: number,
|
centerLat: number,
|
||||||
@@ -661,8 +733,8 @@ export async function calculateTerrainStudy(
|
|||||||
const centerElev = await sampleElevationAt(centerLat, centerLng);
|
const centerElev = await sampleElevationAt(centerLat, centerLng);
|
||||||
const areaKm2 = Math.round(Math.PI * Math.pow(radiusMeters / 1000, 2) * 10) / 10;
|
const areaKm2 = Math.round(Math.PI * Math.pow(radiusMeters / 1000, 2) * 10) / 10;
|
||||||
|
|
||||||
// Grid Resolution: 11x11 sampling matrix covering the bounding box
|
// High-Density Sampling Matrix: 21x21 = 441 nodes (400 terrain cells)
|
||||||
const gridSize = 11;
|
const gridSize = 21;
|
||||||
const R_earth = 6371000;
|
const R_earth = 6371000;
|
||||||
const dLatTotal = (radiusMeters / R_earth) * (180 / Math.PI);
|
const dLatTotal = (radiusMeters / R_earth) * (180 / Math.PI);
|
||||||
const dLngTotal = (radiusMeters / (R_earth * Math.cos((centerLat * Math.PI) / 180))) * (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 minLng = centerLng - dLngTotal;
|
||||||
const maxLng = 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 }> = [];
|
const gridCoords: Array<{ r: number; c: number; lat: number; lng: number }> = [];
|
||||||
for (let r = 0; r < gridSize; r++) {
|
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++) {
|
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 });
|
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 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);
|
gridCoords.forEach((pt, idx) => {
|
||||||
elevationGrid[r][c] = el;
|
elevationGrid[pt.r][pt.c] = sampleResults[idx];
|
||||||
return { r, c, lat, lng, el };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const sampledPoints = await Promise.all(elevPromises);
|
// 3. Precise Peak and Valley Identification
|
||||||
|
|
||||||
// 2. Find Exact Peak and Valley
|
|
||||||
let maxElev = -9999;
|
let maxElev = -9999;
|
||||||
let minElev = 9999;
|
let minElev = 9999;
|
||||||
let highestPoint = { lat: centerLat, lng: centerLng, elevation: centerElev, label: 'أعلى قمة' };
|
let highestPoint = { lat: centerLat, lng: centerLng, elevation: centerElev, label: 'أعلى قمة' };
|
||||||
let lowestPoint = { 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);
|
const dist = calculateDistance(centerLat, centerLng, p.lat, p.lng);
|
||||||
if (dist <= radiusMeters * 1.05) {
|
if (dist <= radiusMeters * 1.05) {
|
||||||
if (p.el > maxElev) {
|
if (el > maxElev) {
|
||||||
maxElev = p.el;
|
maxElev = el;
|
||||||
highestPoint = { lat: p.lat, lng: p.lng, elevation: p.el, label: `🔺 أعلى قمة: ${p.el}م` };
|
highestPoint = { lat: p.lat, lng: p.lng, elevation: Math.round(el), label: `🔺 أعلى قمة: ${Math.round(el)}م` };
|
||||||
}
|
}
|
||||||
if (p.el < minElev) {
|
if (el < minElev) {
|
||||||
minElev = p.el;
|
minElev = el;
|
||||||
lowestPoint = { lat: p.lat, lng: p.lng, elevation: p.el, label: `🔻 أخفض نقطة: ${p.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 spatialFeatures: any[] = [];
|
||||||
const slopeValues: number[] = [];
|
const slopeValues: number[] = [];
|
||||||
|
const detectedCliffs: CliffFeature[] = [];
|
||||||
|
|
||||||
const dxMeters = (2 * radiusMeters) / (gridSize - 1);
|
const dxMeters = (2 * radiusMeters) / (gridSize - 1);
|
||||||
const dyMeters = (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 r = 0; r < gridSize - 1; r++) {
|
||||||
for (let c = 0; c < gridSize - 1; c++) {
|
for (let c = 0; c < gridSize - 1; c++) {
|
||||||
const cellCenterLat = (gridCoords[r * gridSize + c].lat + gridCoords[(r + 1) * gridSize + (c + 1)].lat) / 2;
|
const p1Coord = gridCoords[r * gridSize + c];
|
||||||
const cellCenterLng = (gridCoords[r * gridSize + c].lng + gridCoords[(r + 1) * gridSize + (c + 1)].lng) / 2;
|
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);
|
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 z00 = elevationGrid[r][c];
|
||||||
const z01 = elevationGrid[r][c + 1];
|
const z01 = elevationGrid[r][c + 1];
|
||||||
const z10 = elevationGrid[r + 1][c];
|
const z10 = elevationGrid[r + 1][c];
|
||||||
const z11 = elevationGrid[r + 1][c + 1];
|
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_dx = ((z01 + z11) - (z00 + z10)) / (2 * dxMeters);
|
||||||
const dz_dy = ((z10 + z11) - (z00 + z01)) / (2 * dyMeters);
|
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;
|
const slopeDeg = Math.round((slopeRad * (180 / Math.PI)) * 10) / 10;
|
||||||
slopeValues.push(slopeDeg);
|
slopeValues.push(slopeDeg);
|
||||||
|
|
||||||
let cat = 'go';
|
// Local maximum vertical step drop across cell corners
|
||||||
let catColor = '#22c55e';
|
const localMaxZ = Math.max(z00, z01, z10, z11);
|
||||||
let catLabel = `ممر حركة منبسط (GO) [${slopeDeg}°]`;
|
const localMinZ = Math.min(z00, z01, z10, z11);
|
||||||
|
const localDrop = Math.round(localMaxZ - localMinZ);
|
||||||
|
|
||||||
if (slopeDeg > 12) {
|
const p1 = [p1Coord.lng, p1Coord.lat];
|
||||||
cat = 'no-go';
|
const p2 = [p2Coord.lng, p2Coord.lat];
|
||||||
catColor = '#ef4444';
|
const p3 = [p3Coord.lng, p3Coord.lat];
|
||||||
catLabel = `جرف ومنحدر وعر (NO-GO) [${slopeDeg}°]`;
|
const p4 = [p4Coord.lng, p4Coord.lat];
|
||||||
} else if (slopeDeg >= 5) {
|
|
||||||
cat = 'slow-go';
|
// Military Grade Slope & Barrier Classification:
|
||||||
catColor = '#eab308';
|
// - Flat (GO): < 7°
|
||||||
catLabel = `تضاريس متموجة (SLOW-GO) [${slopeDeg}°]`;
|
// - 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 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 maxSlope = slopeValues.length > 0 ? Math.round(Math.max(...slopeValues) * 10) / 10 : 5.0;
|
||||||
|
|
||||||
const flatCount = slopeValues.filter(s => s < 5).length;
|
// 6. Trace Drainage Defiles & Natural Wadis (Lowland Thalweg Corridors)
|
||||||
const modCount = slopeValues.filter(s => s >= 5 && s <= 12).length;
|
const valleySegments: [number, number][] = [];
|
||||||
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][] = [];
|
|
||||||
for (let r = 0; r < gridSize; r++) {
|
for (let r = 0; r < gridSize; r++) {
|
||||||
let minRowElev = 9999;
|
let minRowElev = 9999;
|
||||||
let minColIdx = 0;
|
let minColIdx = 0;
|
||||||
@@ -804,29 +949,29 @@ export async function calculateTerrainStudy(
|
|||||||
}
|
}
|
||||||
const pt = gridCoords[r * gridSize + minColIdx];
|
const pt = gridCoords[r * gridSize + minColIdx];
|
||||||
const dist = calculateDistance(centerLat, centerLng, pt.lat, pt.lng);
|
const dist = calculateDistance(centerLat, centerLng, pt.lat, pt.lng);
|
||||||
if (dist <= radiusMeters * 1.05) {
|
if (dist <= radiusMeters * 1.02) {
|
||||||
valleyPoints.push([pt.lng, pt.lat]);
|
valleySegments.push([pt.lng, pt.lat]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (valleyPoints.length >= 2) {
|
if (valleySegments.length >= 3) {
|
||||||
spatialFeatures.push({
|
spatialFeatures.push({
|
||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
properties: {
|
properties: {
|
||||||
layerType: 'wadi',
|
layerType: 'wadi',
|
||||||
category: 'natural-obstacle',
|
category: 'natural-obstacle',
|
||||||
title: `🌊 مجرى وادٍ رئيسي ومصرف سيل طبيعي (${minElev}م)`,
|
title: `🌊 بطن وادٍ ومصرف سيل طبيعي (${minElev}م)`,
|
||||||
color: '#06b6d4'
|
color: '#06b6d4'
|
||||||
},
|
},
|
||||||
geometry: {
|
geometry: {
|
||||||
type: 'LineString',
|
type: 'LineString',
|
||||||
coordinates: valleyPoints
|
coordinates: valleySegments
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Detect and Draw Dominant Mountain Ridge (Connecting High Points)
|
// 7. Trace Dominant Mountain Ridges (Key Terrain Crest Lines)
|
||||||
const ridgePoints: [number, number][] = [];
|
const ridgeSegments: [number, number][] = [];
|
||||||
for (let r = 0; r < gridSize; r++) {
|
for (let r = 0; r < gridSize; r++) {
|
||||||
let maxRowElev = -9999;
|
let maxRowElev = -9999;
|
||||||
let maxColIdx = 0;
|
let maxColIdx = 0;
|
||||||
@@ -838,28 +983,28 @@ export async function calculateTerrainStudy(
|
|||||||
}
|
}
|
||||||
const pt = gridCoords[r * gridSize + maxColIdx];
|
const pt = gridCoords[r * gridSize + maxColIdx];
|
||||||
const dist = calculateDistance(centerLat, centerLng, pt.lat, pt.lng);
|
const dist = calculateDistance(centerLat, centerLng, pt.lat, pt.lng);
|
||||||
if (dist <= radiusMeters * 1.05 && maxRowElev > minElev + relief * 0.6) {
|
if (dist <= radiusMeters * 1.02 && maxRowElev >= minElev + relief * 0.5) {
|
||||||
ridgePoints.push([pt.lng, pt.lat]);
|
ridgeSegments.push([pt.lng, pt.lat]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ridgePoints.length >= 2) {
|
if (ridgeSegments.length >= 3) {
|
||||||
spatialFeatures.push({
|
spatialFeatures.push({
|
||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
properties: {
|
properties: {
|
||||||
layerType: 'ridge',
|
layerType: 'ridge',
|
||||||
category: 'key-terrain',
|
category: 'key-terrain',
|
||||||
title: `⛰️ سلسلة تلال حاكمة (Dominant Ridge - ${maxElev}م)`,
|
title: `⛰️ سلسلة تلال حاكمة (Dominant Crest - ${maxElev}م)`,
|
||||||
color: '#38bdf8'
|
color: '#38bdf8'
|
||||||
},
|
},
|
||||||
geometry: {
|
geometry: {
|
||||||
type: 'LineString',
|
type: 'LineString',
|
||||||
coordinates: ridgePoints
|
coordinates: ridgeSegments
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Real Regional Sector Identification
|
// 8. Regional Sector Intelligence Context
|
||||||
let sectorName = 'قطاع عمليات ميداني';
|
let sectorName = 'قطاع عمليات ميداني';
|
||||||
let terrainClassificationAr = 'هضاب وتلال متوسطة الوعورة';
|
let terrainClassificationAr = 'هضاب وتلال متوسطة الوعورة';
|
||||||
let terrainClassification = 'Rolling Hills & Ridges';
|
let terrainClassification = 'Rolling Hills & Ridges';
|
||||||
@@ -869,7 +1014,6 @@ export async function calculateTerrainStudy(
|
|||||||
let soilType = 'تربة طينية صخرية جافة';
|
let soilType = 'تربة طينية صخرية جافة';
|
||||||
let trafficability = 'جيدة للمركبات المدولبة والمجنزرة مع الحذر في مجاري السيول';
|
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) {
|
if (centerLat >= 32.05 && centerLat <= 32.20 && centerLng >= 36.00 && centerLng <= 36.18) {
|
||||||
sectorName = 'قاطع الزرقاء والسخنة / حوض سيل الزرقاء';
|
sectorName = 'قاطع الزرقاء والسخنة / حوض سيل الزرقاء';
|
||||||
terrainClassificationAr = 'حوض وادي صدعي واسع تحيط به تلال وهضاب حاكمة ومجرى سيل مائي';
|
terrainClassificationAr = 'حوض وادي صدعي واسع تحيط به تلال وهضاب حاكمة ومجرى سيل مائي';
|
||||||
@@ -878,16 +1022,16 @@ export async function calculateTerrainStudy(
|
|||||||
urbanDensityAr = 'كثافة عالية (بلدات وضواحي ممتدة)';
|
urbanDensityAr = 'كثافة عالية (بلدات وضواحي ممتدة)';
|
||||||
settlements = ['بلدة السخنة', 'خربة قنتول', 'ضاحية البستان', 'أحياء الهاشمية', 'مدينة الزرقاء'];
|
settlements = ['بلدة السخنة', 'خربة قنتول', 'ضاحية البستان', 'أحياء الهاشمية', 'مدينة الزرقاء'];
|
||||||
soilType = 'طمي رسوبي في بطن الوادي وصخور كلسية على الأكتاف';
|
soilType = 'طمي رسوبي في بطن الوادي وصخور كلسية على الأكتاف';
|
||||||
trafficability = 'ممتازة على شبكة الطرق وبطون الأودية المنبسطة (GO) مع صعوبة صعود الجروف الصخرية';
|
trafficability = 'ممتازة على شبكة الطرق وبطون الأودية المنبسطة (GO) مع وجود قواطع صخرية على الأكتاف';
|
||||||
} else if (centerLat > 32.3 && centerLng < 36.0) {
|
} else if (centerLat > 32.3 && centerLng < 36.0) {
|
||||||
sectorName = 'قاطع الشمال / حوض اليرموك وإربد';
|
sectorName = 'قاطع الشمال / حوض اليرموك وإربد';
|
||||||
terrainClassificationAr = 'مرتفعات جبلية تتخللها أودية سحيقة وخوانق مائية';
|
terrainClassificationAr = 'مرتفعات جبلية تتخللها أودية سحيقة وخوانق مائية وجروف صخرية';
|
||||||
terrainClassification = 'Highland Plateaus with Deep Gorges';
|
terrainClassification = 'Highland Plateaus with Deep Gorges & Rock Cliffs';
|
||||||
urbanDensity = 'MEDIUM';
|
urbanDensity = 'MEDIUM';
|
||||||
urbanDensityAr = 'كثافة متوسطة (قرى وضواحي ممتدة)';
|
urbanDensityAr = 'كثافة متوسطة (قرى وضواحي ممتدة)';
|
||||||
settlements = ['بلدات وضواحي إربد', 'قرى حوض اليرموك', 'مزارع ومحميات وادي الشلالة'];
|
settlements = ['بلدات وضواحي إربد', 'قرى حوض اليرموك', 'مزارع ومحميات وادي الشلالة'];
|
||||||
soilType = 'تربة زراعية خصبة وصخور كلسية';
|
soilType = 'تربة زراعية خصبة وصخور كلسية صلبة';
|
||||||
trafficability = 'صعبة على حواف الأودية - تتطلب محاور طرق رئيسية';
|
trafficability = 'مقيدة بالجروف الصخرية الحادة - تتطلب محاور طرق معبدة';
|
||||||
} else if (centerLat >= 31.85 && centerLat <= 32.05 && centerLng >= 35.8 && centerLng <= 36.02) {
|
} else if (centerLat >= 31.85 && centerLat <= 32.05 && centerLng >= 35.8 && centerLng <= 36.02) {
|
||||||
sectorName = 'قاطع الوسط / إقليم العاصمة عمان';
|
sectorName = 'قاطع الوسط / إقليم العاصمة عمان';
|
||||||
terrainClassificationAr = 'هضاب وكتل جبلية حضرية مكتظة بالعمران';
|
terrainClassificationAr = 'هضاب وكتل جبلية حضرية مكتظة بالعمران';
|
||||||
@@ -908,26 +1052,35 @@ export async function calculateTerrainStudy(
|
|||||||
trafficability = 'حركة حرة مفتوحة للدروع والآليات الصحراوية (Cross-Country GO)';
|
trafficability = 'حركة حرة مفتوحة للدروع والآليات الصحراوية (Cross-Country GO)';
|
||||||
} else if (centerLat < 31.8 && centerLng < 35.7) {
|
} else if (centerLat < 31.8 && centerLng < 35.7) {
|
||||||
sectorName = 'قاطع الغور / المنخفض الصدعي والبحر الميت';
|
sectorName = 'قاطع الغور / المنخفض الصدعي والبحر الميت';
|
||||||
terrainClassificationAr = 'أخفـض نقطة على سطح الأرض (جروف حادة ومنحدرات قاسية)';
|
terrainClassificationAr = 'أخفـض نقطة على سطح الأرض (جروف صخرية وقواطع حادة)';
|
||||||
terrainClassification = 'Rift Valley Escarpment (-400m MSL)';
|
terrainClassification = 'Rift Valley Escarpment (-400m MSL) with Rock Cliffs';
|
||||||
urbanDensity = 'LOW';
|
urbanDensity = 'LOW';
|
||||||
urbanDensityAr = 'كثافة منخفضة (مزارع الغور والمنتجعات)';
|
urbanDensityAr = 'كثافة منخفضة (مزارع الغور والمنتجعات)';
|
||||||
settlements = ['مزارع الأغوار الجنوبية', 'مجمعات الفنادق ومصانع البوتاس', 'قرى غور الصافي'];
|
settlements = ['مزارع الأغوار الجنوبية', 'مجمعات الفنادق ومصانع البوتاس', 'قرى غور الصافي'];
|
||||||
soilType = 'طمي نهري وسبخات ملحية وصخور رسوبية';
|
soilType = 'طمي نهري وسبخات ملحية وصخور رسوبية قاسية';
|
||||||
trafficability = 'حذرة جداً بجانب السبخات - ممتازة على طريق البحر الميت الرئيسي';
|
trafficability = 'جروف صخرية مانعة (NO-GO) على الحواف الغربية والشرقية';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mobility status calculation
|
// 9. Mobility Assessment
|
||||||
let mobilityStatus: 'GO' | 'SLOW-GO' | 'NO-GO' = 'GO';
|
let mobilityStatus: 'GO' | 'SLOW-GO' | 'NO-GO' = 'GO';
|
||||||
let mobilityStatusAr = 'حركة حرة ممتازة للآليات والدروع (Cross-Country GO)';
|
let mobilityStatusAr = 'حركة حرة ممتازة للآليات والدروع (Cross-Country GO)';
|
||||||
if (steepPct > 30 || avgSlope > 12) {
|
if (cliffPct > 12 || ruggedPct > 25 || avgSlope > 14) {
|
||||||
mobilityStatus = 'NO-GO';
|
mobilityStatus = 'NO-GO';
|
||||||
mobilityStatusAr = 'مانع تضاريسي - حركة مقيدة وشديدة الصعوبة (NO-GO)';
|
mobilityStatusAr = 'مانع تضاريسي - قواطع صخرية وانحدارات حادة تعيق المناورة (NO-GO)';
|
||||||
} else if (steepPct > 10 || modPct > 35 || avgSlope > 6) {
|
} else if (cliffPct > 3 || ruggedPct > 10 || modPct > 30 || avgSlope > 6) {
|
||||||
mobilityStatus = 'SLOW-GO';
|
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 {
|
return {
|
||||||
centerLat,
|
centerLat,
|
||||||
centerLng,
|
centerLng,
|
||||||
@@ -937,14 +1090,17 @@ export async function calculateTerrainStudy(
|
|||||||
centerElevation: Math.round(centerElev),
|
centerElevation: Math.round(centerElev),
|
||||||
minElevation: Math.round(minElev),
|
minElevation: Math.round(minElev),
|
||||||
maxElevation: Math.round(maxElev),
|
maxElevation: Math.round(maxElev),
|
||||||
reliefMeters: Math.round(relief),
|
reliefMeters: relief,
|
||||||
avgSlopeDegrees: avgSlope,
|
avgSlopeDegrees: avgSlope,
|
||||||
maxSlopeDegrees: maxSlope,
|
maxSlopeDegrees: maxSlope,
|
||||||
slopeDistribution: {
|
slopeDistribution: {
|
||||||
flatPercentage: flatPct,
|
flatPercentage: flatPct,
|
||||||
moderatePercentage: modPct,
|
moderatePercentage: modPct,
|
||||||
steepPercentage: steepPct
|
steepPercentage: ruggedPct,
|
||||||
|
cliffPercentage: cliffPct
|
||||||
},
|
},
|
||||||
|
cliffsCount: detectedCliffs.length,
|
||||||
|
cliffs: topCliffs,
|
||||||
terrainClassification,
|
terrainClassification,
|
||||||
terrainClassificationAr,
|
terrainClassificationAr,
|
||||||
mobilityStatus,
|
mobilityStatus,
|
||||||
@@ -956,32 +1112,32 @@ export async function calculateTerrainStudy(
|
|||||||
features: spatialFeatures
|
features: spatialFeatures
|
||||||
},
|
},
|
||||||
naturalObstacles: [
|
naturalObstacles: [
|
||||||
|
{
|
||||||
|
name: 'القواطع والجروف الصخرية الطبيعية (Cliffs & Rock Escarpments)',
|
||||||
|
type: 'موانع وقواطع صخرية حادة',
|
||||||
|
description: cliffSummaryText,
|
||||||
|
impact: topCliffs.length > 0
|
||||||
|
? 'إغلاق محاور الالتفاف الجانبي وإجبار القوات المهاجمة على استخدام ممرات إجبارية (Choke Points)'
|
||||||
|
: 'حرية مناورة واسعة بدون عوائق صخرية عمودية'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'مجاري الوديان والسيول الطبيعية (Drainage Defiles)',
|
name: 'مجاري الوديان والسيول الطبيعية (Drainage Defiles)',
|
||||||
type: 'مصارف مائية وأودية',
|
type: 'مصارف مائية وأودية',
|
||||||
description: `بطن الوادي عند منسوب ${minElev}م يشكل مصرفاً مائياً رئيسياً، يوفر ممراً محجوباً ولكن قد يعيق المناورة عند هطول الأمطار.`,
|
description: `بطن الوادي عند منسوب ${minElev}م يشكل مصرفاً مائياً ومحور حركة منخفضاً، يوفر ممراً محجوباً ولكن قد يعيق المناورة عند هطول الأمطار.`,
|
||||||
impact: 'محور تسلل مناسب للمشاة ومقيد للآليات الثقيلة عند تشكل الأوحال'
|
impact: 'محور تسلل مناسب للمشاة ومقيد للآليات الثقيلة عند تشكل الأوحال والسيول'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'التلال والقمم الحاكمة (Dominant Ridges)',
|
name: 'التلال والقمم الحاكمة (Dominant Ridges & Crests)',
|
||||||
type: 'أرض مسيطرة (Key Terrain)',
|
type: 'أرض مسيطرة (Key Terrain)',
|
||||||
description: `المرتفع الحاكم بارتفاع ${maxElev}م يشرف بشكل كامل على بطن الوادي ومحاور التحرك على مسافة ${radiusMeters / 1000} كم.`,
|
description: `المرتفع الحاكم بارتفاع ${maxElev}م يشرف بشكل كامل على بطن الوادي ومحاور التحرك على مسافة ${radiusMeters / 1000} كم بفارق تضاريسي ${relief}م.`,
|
||||||
impact: 'موقع استراتيجي لتمركز أسلحة الإسناد ونقاط الملاحظة والاستطلاع'
|
impact: 'موقع استراتيجي لتمركز أسلحة الإسناد ونقاط الملاحظة والاستطلاع والسيطرة النارية'
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'المنحدرات التضاريسية المحيطة',
|
|
||||||
type: 'انحدار طبوغرافي',
|
|
||||||
description: steepPct > 0
|
|
||||||
? `يوجد ${steepPct}% من مساحة القطاع ذات انحدار وعر (>12°) تشكل موانع طبيعية تعيق صعود الآليات.`
|
|
||||||
: `القطاع يتسم بانحدارات لطيفة إلى متوسطة (أقصى انحدار ${maxSlope}°)، ولا توجد جروف شاهقة مانعة لحركة الدروع.`,
|
|
||||||
impact: steepPct > 0 ? 'إجبار القوات المهاجمة على استخدام الممرات المعبدة' : 'حرية مناورة واسعة للدروع في مختلف الاتجاهات'
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
manMadeObstacles: [
|
manMadeObstacles: [
|
||||||
{
|
{
|
||||||
name: 'محاور الطرق الرئيسية وخطوط الإمداد (MSR)',
|
name: 'محاور الطرق الرئيسية وخطوط الإمداد (MSR)',
|
||||||
type: 'بنية تحتية للمواصلات',
|
type: 'بنية تحتية للمواصلات',
|
||||||
description: 'الطرق المعبدة المارة بالوادي تتيح تدفقاً سريعاً للأرتال وإعادة التزود اللوجستي.',
|
description: 'الطرق المعبدة المارة بالقطاع تتيح تدفقاً سريعاً للأرتال وإعادة التزود اللوجستي.',
|
||||||
tacticalNote: 'وجوب إعداد خطط قطع وغلق هندسي سريع للتحكم بحركة العدو'
|
tacticalNote: 'وجوب إعداد خطط قطع وغلق هندسي سريع للتحكم بحركة العدو'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1011,17 +1167,20 @@ export async function calculateTerrainStudy(
|
|||||||
airObservationExposure: urbanDensity === 'HIGH' ? 'مختلطة (المباني توفر إخفاء جزئياً)' : 'عالية للرصد الجوي والمسيرات نهاراً'
|
airObservationExposure: urbanDensity === 'HIGH' ? 'مختلطة (المباني توفر إخفاء جزئياً)' : 'عالية للرصد الجوي والمسيرات نهاراً'
|
||||||
},
|
},
|
||||||
oakocAssessment: {
|
oakocAssessment: {
|
||||||
obstacles: `القطاع يتضمن مجرى وادٍ رئيسي عند منسوب ${minElev}م مع انحدارات تصل إلى ${maxSlope}°، مما يشكل محاور حركة طبيعية محددة.`,
|
obstacles: `القطاع يضم ${detectedCliffs.length > 0 ? `${detectedCliffs.length} قاطعاً وجرفاً صخرياً و` : ''}مجرى وادٍ رئيسي عند منسوب ${minElev}م، مع انحدارات تصل إلى ${maxSlope}°، مما يشكل محاور حركة طبيعية محددة.`,
|
||||||
avenuesOfApproach: 'محور الاقتراب الرئيسي يتبع بطن الوادي وشبكة الطرق الرئيسية، مع إمكانية الالتفاف عبر الهضاب المنبسطة.',
|
avenuesOfApproach: 'محور الاقتراب الرئيسي يتبع بطن الوادي وشبكة الطرق الرئيسية المنبسطة، مع تفادي الجروف الصخرية الحادة.',
|
||||||
keyTerrain: `القمة الحاكمة بارتفاع ${maxElev}م توفر ميزة كشف وهيمنة نارية حاسمة على كامل القطاع.`,
|
keyTerrain: `القمة الحاكمة بارتفاع ${maxElev}م توفر ميزة كشف وهيمنة نارية حاسمة على كامل القطاع.`,
|
||||||
observationAndFieldsOfFire: `حقول الرماية مكشوفة ومثالية بنسبة ${flatPct}% في الأراضي المنبسطة، ومحجوبة داخل بطون الأودية.`,
|
observationAndFieldsOfFire: `حقول الرماية مكشوفة ومثالية بنسبة ${flatPct}% في الأراضي المنبسطة، ومحجوبة داخل بطون الأودية وخلف الجروف.`,
|
||||||
coverAndConcealment: 'تتوفر السواتر التضاريسية في المنحدرات الخلفية (Reverse Slopes) وداخل التجمعات العمرانية.'
|
coverAndConcealment: 'تتوفر السواتر التضاريسية في المنحدرات الخلفية (Reverse Slopes) وأسفل الجروف الصخرية وداخل الكتل العمرانية.'
|
||||||
},
|
},
|
||||||
tacticalRecommendations: [
|
tacticalRecommendations: [
|
||||||
`نشر مرصد استطلاع رئيسي ورادار كشف أرضي على المرتفع الحاكم (${maxElev}م) لتأمين إنذار مبكر ومراقبة بطن الوادي.`,
|
`نشر مرصد استطلاع رئيسي ورادار كشف أرضي على المرتفع الحاكم (${maxElev}م) لتأمين إنذار مبكر ومراقبة القطاع.`,
|
||||||
`تجهيز نقاط كمائن وموانع هندسية وحقول ألغام موجهة في مخانق بطن الوادي (${minElev}م) لصد أي تقدم مدرع.`,
|
detectedCliffs.length > 0
|
||||||
|
? `استغلال القواطع والجروف الصخرية المكتشفة كحواجز طبيعية موجهة لحصر تقدم العدو داخل المخانق المستهدفة بنيران الأسلحة الثقيلة.`
|
||||||
|
: `تجهيز نقاط كمائن وموانع هندسية وحقول ألغام موجهة في مخانق بطن الوادي (${minElev}م) لصد أي تقدم مدرع.`,
|
||||||
`تأمين الطرق الرئيسية والمحاور الحيوية بنقاط سيطرة وحراسة ثابتة لمنع عمليات الالتفاف السريعة.`,
|
`تأمين الطرق الرئيسية والمحاور الحيوية بنقاط سيطرة وحراسة ثابتة لمنع عمليات الالتفاف السريعة.`,
|
||||||
`استغلال المناطق المنبسطة (${flatPct}%) لإنشاء مهابط المروحيات (HLZ) ومناطق الإخلاء الطبي والتجمع اللوجستي.`
|
`استغلال المناطق المنبسطة (${flatPct}%) لإنشاء مهابط المروحيات (HLZ) ومناطق الإخلاء الطبي والتجمع اللوجستي.`
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user