chore: add build artifacts, map assets, and routing data

This commit is contained in:
Hamza-Ayed
2026-09-19 12:34:24 +03:00
parent aa2b9f131f
commit 43ad2e0ad4
327 changed files with 52421 additions and 1810 deletions
File diff suppressed because it is too large Load Diff
+109
View File
@@ -144,6 +144,9 @@ const MapComponent: React.FC<MapProps> = ({
if (map.current.getLayer('hillshading')) {
map.current.setLayoutProperty('hillshading', 'visibility', showTerrain ? 'visible' : 'none');
}
if (map.current.getLayer('terrain-hillshade')) {
map.current.setLayoutProperty('terrain-hillshade', 'visibility', showTerrain ? 'visible' : 'none');
}
// Toggle Contours
const contourLayers = ['contour-lines-minor', 'contour-lines-major', 'contour-labels'];
@@ -498,6 +501,33 @@ const MapComponent: React.FC<MapProps> = ({
});
}
// 3D Elevation Hillshade Relief (315° North-West Illumination)
if (!initialMap.getSource('terrain-dem')) {
initialMap.addSource('terrain-dem', {
type: 'raster-dem',
tiles: ['https://tiles.intaleqapp.com/raster_dem/{z}/{x}/{y}.png'],
tileSize: 256,
maxzoom: 14,
});
initialMap.addLayer({
id: 'terrain-hillshade',
type: 'hillshade',
source: 'terrain-dem',
layout: {
visibility: showTerrain ? 'visible' : 'none',
},
paint: {
'hillshade-illumination-direction': 315,
'hillshade-illumination-anchor': 'viewport',
'hillshade-shadow-color': '#3b1c06',
'hillshade-highlight-color': '#ffffff',
'hillshade-accent-color': '#9a3412',
'hillshade-exaggeration': 0.65,
},
});
}
const demSource = getSharedDemSource();
const contourUrl = demSource.contourProtocolUrl({
thresholds: {
@@ -576,6 +606,85 @@ const MapComponent: React.FC<MapProps> = ({
'text-halo-width': 2,
},
});
// Hydro-Flattening Water Mask over Contour Layers
if (!initialMap.getLayer('water-mask-hydroflat-map')) {
initialMap.addLayer({
id: 'water-mask-hydroflat-map',
type: 'fill',
source: 'local-osm-polygons',
'source-layer': 'planet_osm_polygon',
filter: [
'any',
['==', 'natural', 'water'],
['==', 'water', 'reservoir'],
['==', 'water', 'dam'],
['==', 'water', 'lake'],
['==', 'water', 'pond'],
['==', 'waterway', 'riverbank'],
['==', 'landuse', 'reservoir'],
['==', 'landuse', 'basin']
],
paint: {
'fill-color': '#90C3D4',
'fill-opacity': 1.0
}
});
}
// Military Mountain Peak Spot Heights (✕ [ele]m)
if (!initialMap.getLayer('tactical-peaks-spot-heights')) {
initialMap.addLayer({
id: 'tactical-peaks-spot-heights',
type: 'symbol',
source: 'local-osm-points',
'source-layer': 'planet_osm_point',
minzoom: 9,
filter: [
'any',
['in', 'natural', 'peak', 'volcano', 'ridge', 'hill', 'cliff'],
['in', 'place', 'isolated_dwelling', 'locality']
],
layout: {
'text-field': [
'case',
['has', 'ele'],
[
'concat',
'✕ ',
['to-string', ['get', 'ele']],
'م\n',
['coalesce', ['get', 'name:ar'], ['get', 'name'], '']
],
[
'concat',
'✕\n',
['coalesce', ['get', 'name:ar'], ['get', 'name'], 'مرتفع']
]
],
'text-font': ['Noto Sans Bold', 'Open Sans Bold'],
'text-size': [
'interpolate',
['linear'],
['zoom'],
9, 9,
11, 10,
13, 11.5,
15, 13,
17, 15
],
'text-anchor': 'center',
'text-justify': 'center',
'text-padding': 8,
'text-allow-overlap': false
},
paint: {
'text-color': '#7F1D1D',
'text-halo-color': 'rgba(255,255,255,0.95)',
'text-halo-width': 2.2
}
});
}
}
onMapLoad(map.current!);
@@ -0,0 +1,351 @@
import React from 'react';
interface TacticalMarkdownRendererProps {
content: string;
isChat?: boolean;
}
// Inline parser for bold, italic, inline-code, and tags
const parseInline = (text: string): React.ReactNode[] => {
if (!text) return [];
// Match bold (**text**), inline code (`text`), italic (*text* or _text_)
const tokens: React.ReactNode[] = [];
let remaining = text;
let key = 0;
while (remaining.length > 0) {
// Check for bold **...** or __...__
const boldMatch = remaining.match(/^(\*\*|__)(.*?)\1/);
if (boldMatch) {
tokens.push(
<strong
key={key++}
style={{
fontWeight: 800,
color: '#0f172a',
background: 'rgba(241, 245, 249, 0.6)',
padding: '1px 4px',
borderRadius: '4px',
margin: '0 1px'
}}
>
{parseInline(boldMatch[2])}
</strong>
);
remaining = remaining.slice(boldMatch[0].length);
continue;
}
// Check for inline code `...`
const codeMatch = remaining.match(/^`([^`]+)`/);
if (codeMatch) {
tokens.push(
<code
key={key++}
style={{
background: '#e0e7ff',
color: '#4338ca',
padding: '2px 6px',
borderRadius: '4px',
fontSize: '0.85em',
fontFamily: 'monospace',
fontWeight: 700,
direction: 'ltr',
display: 'inline-block'
}}
>
{codeMatch[1]}
</code>
);
remaining = remaining.slice(codeMatch[0].length);
continue;
}
// Check for italic *...* or _..._
const italicMatch = remaining.match(/^(\*|_)(.*?)\1/);
if (italicMatch) {
tokens.push(
<em key={key++} style={{ fontStyle: 'italic', color: '#475569' }}>
{parseInline(italicMatch[2])}
</em>
);
remaining = remaining.slice(italicMatch[0].length);
continue;
}
// Regular text until the next special character
const nextSpecial = remaining.search(/[\*_`]/);
if (nextSpecial === -1) {
tokens.push(remaining);
break;
} else if (nextSpecial === 0) {
// Stray character, consume it as plain text
tokens.push(remaining[0]);
remaining = remaining.slice(1);
} else {
tokens.push(remaining.slice(0, nextSpecial));
remaining = remaining.slice(nextSpecial);
}
}
return tokens;
};
export const TacticalMarkdownRenderer: React.FC<TacticalMarkdownRendererProps> = ({
content,
isChat = false
}) => {
if (!content) return null;
const lines = content.split('\n');
const elements: React.ReactNode[] = [];
let currentList: { type: 'ul' | 'ol'; items: React.ReactNode[] } | null = null;
let elementKey = 0;
const flushList = () => {
if (currentList) {
if (currentList.type === 'ul') {
elements.push(
<ul
key={elementKey++}
style={{
margin: '8px 0 12px 0',
paddingRight: isChat ? '16px' : '20px',
paddingLeft: 0,
display: 'flex',
flexDirection: 'column',
gap: isChat ? '4px' : '6px',
listStyleType: 'disc'
}}
>
{currentList.items.map((item, idx) => (
<li key={idx} style={{ lineHeight: isChat ? 1.6 : 1.85, color: '#1e293b' }}>
{item}
</li>
))}
</ul>
);
} else {
elements.push(
<ol
key={elementKey++}
style={{
margin: '8px 0 12px 0',
paddingRight: isChat ? '18px' : '24px',
paddingLeft: 0,
display: 'flex',
flexDirection: 'column',
gap: isChat ? '4px' : '6px'
}}
>
{currentList.items.map((item, idx) => (
<li key={idx} style={{ lineHeight: isChat ? 1.6 : 1.85, color: '#1e293b', fontWeight: 600 }}>
<span style={{ fontWeight: 400 }}>{item}</span>
</li>
))}
</ol>
);
}
currentList = null;
}
};
for (let i = 0; i < lines.length; i++) {
const rawLine = lines[i];
const trimmed = rawLine.trim();
// Empty lines
if (!trimmed) {
flushList();
continue;
}
// Horizontal Rule (--- or *** or ___)
if (/^(\-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
flushList();
elements.push(
<hr
key={elementKey++}
style={{
border: 'none',
height: '1px',
background: 'linear-gradient(to left, transparent, #cbd5e1, transparent)',
margin: isChat ? '10px 0' : '16px 0'
}}
/>
);
continue;
}
// Headers
const h1Match = trimmed.match(/^#\s+(.+)$/);
if (h1Match) {
flushList();
elements.push(
<h1
key={elementKey++}
style={{
fontSize: isChat ? '1.1rem' : '1.35rem',
fontWeight: 900,
color: '#0f172a',
marginTop: isChat ? '10px' : '18px',
marginBottom: isChat ? '6px' : '10px',
borderBottom: '2px solid #e2e8f0',
paddingBottom: '6px'
}}
>
{parseInline(h1Match[1])}
</h1>
);
continue;
}
const h2Match = trimmed.match(/^##\s+(.+)$/);
if (h2Match) {
flushList();
elements.push(
<h2
key={elementKey++}
style={{
fontSize: isChat ? '1rem' : '1.2rem',
fontWeight: 900,
color: '#1e293b',
marginTop: isChat ? '8px' : '16px',
marginBottom: isChat ? '4px' : '8px',
display: 'flex',
alignItems: 'center',
gap: 8
}}
>
<span style={{ width: 4, height: 16, background: '#9333ea', borderRadius: 2, display: 'inline-block' }}></span>
<span>{parseInline(h2Match[1])}</span>
</h2>
);
continue;
}
const h3Match = trimmed.match(/^###\s+(.+)$/);
if (h3Match) {
flushList();
elements.push(
<h3
key={elementKey++}
style={{
fontSize: isChat ? '0.92rem' : '1.05rem',
fontWeight: 800,
color: '#334155',
marginTop: isChat ? '8px' : '14px',
marginBottom: isChat ? '4px' : '6px',
background: isChat ? 'rgba(241, 245, 249, 0.8)' : '#f8fafc',
padding: '4px 10px',
borderRadius: 6,
borderRight: '3px solid #6366f1'
}}
>
{parseInline(h3Match[1])}
</h3>
);
continue;
}
const h4Match = trimmed.match(/^####\s+(.+)$/);
if (h4Match) {
flushList();
elements.push(
<h4
key={elementKey++}
style={{
fontSize: isChat ? '0.86rem' : '0.96rem',
fontWeight: 800,
color: '#475569',
marginTop: isChat ? '6px' : '10px',
marginBottom: '4px'
}}
>
{parseInline(h4Match[1])}
</h4>
);
continue;
}
// Unordered List Items (- or * or •)
const ulMatch = trimmed.match(/^[-*•]\s+(.+)$/);
if (ulMatch) {
if (!currentList || currentList.type !== 'ul') {
flushList();
currentList = { type: 'ul', items: [] };
}
currentList.items.push(parseInline(ulMatch[1]));
continue;
}
// Ordered List Items (1. , 2. )
const olMatch = trimmed.match(/^(\d+)\.\s+(.+)$/);
if (olMatch) {
if (!currentList || currentList.type !== 'ol') {
flushList();
currentList = { type: 'ol', items: [] };
}
currentList.items.push(parseInline(olMatch[2]));
continue;
}
// Blockquote (> ...)
const bqMatch = trimmed.match(/^>\s*(.+)$/);
if (bqMatch) {
flushList();
elements.push(
<blockquote
key={elementKey++}
style={{
margin: '8px 0',
padding: '6px 14px',
background: '#f8fafc',
borderRight: '4px solid #0071e3',
color: '#334155',
fontSize: isChat ? '0.84rem' : '0.9rem',
lineHeight: 1.7,
borderRadius: '0 8px 8px 0'
}}
>
{parseInline(bqMatch[1])}
</blockquote>
);
continue;
}
// Regular Paragraph
flushList();
elements.push(
<p
key={elementKey++}
style={{
margin: isChat ? '4px 0 6px 0' : '6px 0 10px 0',
lineHeight: isChat ? 1.7 : 1.9,
color: '#1e293b',
fontSize: isChat ? '0.86rem' : '0.92rem'
}}
>
{parseInline(trimmed)}
</p>
);
}
flushList();
return (
<div
className="tactical-markdown-content"
style={{
direction: 'rtl',
textAlign: 'right',
fontFamily: 'inherit',
color: '#1e293b',
wordBreak: 'break-word'
}}
>
{elements}
</div>
);
};
+4 -3
View File
@@ -25,14 +25,15 @@ body {
.app {
display: flex;
height: 100vh;
width: 100vw;
height: 100%;
width: 100%;
position: relative;
}
.sidebar {
width: 350px;
height: 100%;
max-height: 100vh;
max-height: 100%;
padding: 24px 20px;
z-index: 10;
display: flex;
+194 -78
View File
@@ -6,7 +6,6 @@ import CompareView from './pages/CompareView'
import IntelligenceDashboard from './pages/IntelligenceDashboard'
import { ExecutiveShowcase } from './pages/ExecutiveShowcase'
import { TacticalDefenseView } from './pages/TacticalDefenseView'
import { SiroStrategicReport } from './pages/SiroStrategicReport'
import './index.css'
maplibregl.setRTLTextPlugin(
@@ -15,62 +14,175 @@ maplibregl.setRTLTextPlugin(
true
);
// Lightweight hash router
// Lightweight hash router with Sovereign Executive Styling
const NAV = [
{ hash: '#siro', label: 'تقرير المركز الجغرافي (سيرو)', icon: '🏛️' },
{ hash: '#tactical', label: 'المنظومة التكتيكية العسكرية', icon: '🎖️' },
{ hash: '#map', label: 'الخريطة والطقس والملاحة', icon: '🗺️' },
{ hash: '#compare', label: 'مقارنة العمالقة والأسعار', icon: '⚖️' },
{ hash: '#review', label: 'تدقيق الطرق الذكي', icon: '🔍' },
]
function ViewNav({ hash }: { hash: string }) {
const currentHash = hash || '#siro'
function MasterHeader({ hash }: { hash: string }) {
const currentHash = hash || '#tactical'
const [isFullscreen, setIsFullscreen] = useState(false);
const toggleFullscreen = () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(() => {});
setIsFullscreen(true);
} else {
document.exitFullscreen().catch(() => {});
setIsFullscreen(false);
}
};
return (
<div style={{
<header style={{
position: 'fixed',
top: 12,
left: '50%',
transform: 'translateX(-50%)',
top: 0,
left: 0,
right: 0,
height: 52,
zIndex: 9999,
display: 'flex',
alignItems: 'center',
gap: 4,
background: 'rgba(255, 255, 255, 0.88)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(0, 0, 0, 0.1)',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)',
borderRadius: 999,
padding: '4px 6px',
justifyContent: 'space-between',
padding: '0 16px',
background: 'rgba(9, 14, 26, 0.94)',
backdropFilter: 'blur(24px)',
WebkitBackdropFilter: 'blur(24px)',
borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.4)',
fontFamily: "'IBM Plex Sans Arabic', Inter, system-ui, sans-serif",
direction: 'rtl',
maxWidth: 'calc(100vw - 24px)',
overflowX: 'auto'
userSelect: 'none'
}}>
{NAV.map(n => {
const active = currentHash === n.hash
return (
<a key={n.hash} href={n.hash}
style={{
textDecoration: 'none',
fontSize: 12,
fontWeight: 700,
color: active ? '#fff' : '#64748b',
background: active ? 'linear-gradient(135deg, #0071e3, #4f46e5)' : 'transparent',
boxShadow: active ? '0 2px 8px rgba(0, 113, 227, 0.35)' : 'none',
padding: '6px 14px',
borderRadius: 999,
{/* Right Section: Brand & Sovereign Security Clearance */}
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<a href="#tactical" style={{ textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{
width: 32,
height: 32,
borderRadius: 9,
background: 'linear-gradient(135deg, #0071e3, #6366f1)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 0 14px rgba(0, 113, 227, 0.5)'
}}>
<span style={{ fontSize: 16 }}>⚡</span>
</div>
<div>
<div style={{
color: '#ffffff',
fontSize: '0.85rem',
fontWeight: 800,
letterSpacing: '-0.02em',
display: 'flex',
alignItems: 'center',
gap: 6,
transition: 'all 0.15s ease'
gap: 6
}}>
<span>{n.icon}</span>
<span>{n.label}</span>
</a>
)
})}
</div>
<span>منظومة انطلاق المكانية</span>
<span style={{
fontSize: '9.5px',
background: 'rgba(0, 113, 227, 0.25)',
border: '1px solid rgba(0, 113, 227, 0.5)',
color: '#38bdf8',
padding: '1px 5px',
borderRadius: 4,
fontWeight: 700
}}>
Sovereign Cloud v3.2
</span>
</div>
</div>
</a>
</div>
{/* Center Section: Master Navigation Segmented Controller */}
<nav style={{
display: 'flex',
alignItems: 'center',
gap: 3,
background: 'rgba(255, 255, 255, 0.05)',
border: '1px solid rgba(255, 255, 255, 0.1)',
padding: '3px 4px',
borderRadius: 12,
boxShadow: 'inset 0 1px 4px rgba(0, 0, 0, 0.3)'
}}>
{NAV.map(n => {
const active = currentHash === n.hash
return (
<a key={n.hash} href={n.hash}
style={{
textDecoration: 'none',
fontSize: 12,
fontWeight: active ? 800 : 600,
color: active ? '#ffffff' : '#94a3b8',
background: active
? 'linear-gradient(135deg, #0071e3, #4f46e5)'
: 'transparent',
boxShadow: active ? '0 2px 10px rgba(0, 113, 227, 0.45)' : 'none',
padding: '6px 12px',
borderRadius: 9,
display: 'flex',
alignItems: 'center',
gap: 6,
transition: 'all 0.15s ease'
}}>
<span style={{ fontSize: 13 }}>{n.icon}</span>
<span>{n.label}</span>
</a>
)
})}
</nav>
{/* Left Section: Live Sovereign Telemetry & Fullscreen */}
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 6,
background: 'rgba(34, 197, 94, 0.12)',
border: '1px solid rgba(34, 197, 94, 0.3)',
color: '#4ade80',
padding: '4px 10px',
borderRadius: 8,
fontSize: '0.72rem',
fontWeight: 700
}}>
<span style={{
display: 'inline-block',
width: 7,
height: 7,
borderRadius: '50%',
background: '#22c55e',
boxShadow: '0 0 8px #22c55e'
}} />
<span>السحابة السيادية متصلة</span>
</div>
<button
onClick={toggleFullscreen}
title="ملء الشاشة"
style={{
background: 'rgba(255, 255, 255, 0.06)',
border: '1px solid rgba(255, 255, 255, 0.12)',
color: '#cbd5e1',
padding: '5px 9px',
borderRadius: 8,
fontSize: '0.75rem',
cursor: 'pointer',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: 4
}}
>
<span>{isFullscreen ? '🗗' : '🗖'}</span>
</button>
</div>
</header>
)
}
@@ -97,8 +209,8 @@ class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { has
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
background: '#f8fafc',
color: '#1d1d1f',
background: '#090e1a',
color: '#ffffff',
fontFamily: 'system-ui, sans-serif',
direction: 'rtl',
padding: 24,
@@ -106,7 +218,7 @@ class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { has
}}>
<div style={{ fontSize: '3rem', marginBottom: 16 }}>⚠️</div>
<h2 style={{ fontSize: '1.4rem', fontWeight: 700, marginBottom: 8 }}>حدث خطأ أثناء تحميل الواجهة</h2>
<p style={{ color: '#64748b', fontSize: '0.9rem', maxWidth: 450, marginBottom: 20 }}>
<p style={{ color: '#94a3b8', fontSize: '0.9rem', maxWidth: 450, marginBottom: 20 }}>
{this.state.error?.message || 'تعذر تحميل أحد مكونات الخريطة'}
</p>
<button
@@ -132,11 +244,11 @@ class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { has
}
function Root() {
const [hash, setHash] = useState(window.location.hash || '#executive');
const [hash, setHash] = useState(window.location.hash || '#tactical');
useEffect(() => {
const on = () => {
const h = window.location.hash || '#executive';
const h = window.location.hash || '#tactical';
setHash(h);
setTimeout(() => {
window.dispatchEvent(new Event('resize'));
@@ -146,8 +258,7 @@ function Root() {
return () => window.removeEventListener('hashchange', on);
}, []);
const isSiro = hash === '#siro' || hash === '#report' || hash === '' || hash === '#executive';
const isTactical = hash === '#tactical' || hash === '#military';
const isTactical = hash === '#tactical' || hash === '#military' || hash === '' || hash === '#siro';
const isExecutive = hash === '#showcase' || hash === '#pitch';
const isCompare = hash === '#compare';
const isReview = hash === '#review';
@@ -155,43 +266,48 @@ function Root() {
return (
<ErrorBoundary>
<ViewNav hash={hash} />
<MasterHeader hash={hash} />
{isSiro && (
<div style={{ width: '100vw', height: '100vh', position: 'absolute', top: 0, left: 0, overflowY: 'auto', overflowX: 'hidden' }}>
<SiroStrategicReport />
</div>
)}
<main style={{
position: 'absolute',
top: 52,
bottom: 0,
left: 0,
right: 0,
width: '100vw',
height: 'calc(100vh - 52px)',
overflow: 'hidden'
}}>
{isTactical && (
<div style={{ width: '100%', height: '100%' }}>
<TacticalDefenseView />
</div>
)}
{isTactical && (
<div style={{ width: '100vw', height: '100vh', position: 'absolute', top: 0, left: 0 }}>
<TacticalDefenseView />
</div>
)}
{isExecutive && (
<div style={{ width: '100%', height: '100%', overflowY: 'auto', overflowX: 'hidden' }}>
<ExecutiveShowcase />
</div>
)}
{isExecutive && (
<div style={{ width: '100vw', height: '100vh', position: 'absolute', top: 0, left: 0, overflowY: 'auto', overflowX: 'hidden' }}>
<ExecutiveShowcase />
</div>
)}
{isCompare && (
<div style={{ width: '100%', height: '100%' }}>
<CompareView />
</div>
)}
{isCompare && (
<div style={{ width: '100vw', height: '100vh', position: 'absolute', top: 0, left: 0 }}>
<CompareView />
</div>
)}
{isReview && (
<div style={{ width: '100%', height: '100%' }}>
<IntelligenceDashboard />
</div>
)}
{isReview && (
<div style={{ width: '100vw', height: '100vh', position: 'absolute', top: 0, left: 0 }}>
<IntelligenceDashboard />
</div>
)}
{isMap && (
<div style={{ width: '100vw', height: '100vh', position: 'absolute', top: 0, left: 0 }}>
<App />
</div>
)}
{isMap && (
<div style={{ width: '100%', height: '100%' }}>
<App />
</div>
)}
</main>
</ErrorBoundary>
);
}
+30 -12
View File
@@ -242,24 +242,42 @@ const CompareView: React.FC = () => {
});
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh', background: '#0f172a', color: '#e2e8f0', fontFamily: 'Inter, system-ui, sans-serif' }}>
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', width: '100%', background: '#090e1a', color: '#e2e8f0', fontFamily: 'Inter, system-ui, sans-serif' }}>
{/* Toolbar */}
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '8px 14px', paddingRight: '280px', borderBottom: '1px solid #1e293b', background: '#0f172a', flexWrap: 'wrap' }}>
<strong style={{ fontSize: '0.85rem', whiteSpace: 'nowrap' }}>⚖️ Compare</strong>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '12px',
padding: '8px 16px',
borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
background: '#0d1322',
flexWrap: 'nowrap',
overflowX: 'auto',
zIndex: 20
}}>
{/* Left Map Layer Toggles */}
<div style={{ display: 'flex', gap: '5px', alignItems: 'center', background: 'rgba(30,41,59,0.7)', padding: '2px 6px', borderRadius: '8px', border: '1px solid #334155' }}>
<span style={{ fontSize: '0.68rem', color: '#94a3b8' }}>Our Map:</span>
<button onClick={() => setShowTerrain(!showTerrain)} style={toggleBtn(showTerrain)}>🏔️ Relief</button>
<button onClick={() => setShowContours(!showContours)} style={toggleBtn(showContours)}>〰️ Contours</button>
<button onClick={() => setShowAdmin(!showAdmin)} style={toggleBtn(showAdmin)}>🏛️ Admin</button>
<button onClick={() => setShow3D(!show3D)} style={toggleBtn(show3D)}>🏢 3D</button>
<div style={{ display: 'flex', gap: '6px', alignItems: 'center', background: 'rgba(30,41,59,0.7)', padding: '3px 8px', borderRadius: '10px', border: '1px solid rgba(255,255,255,0.1)', flexShrink: 0 }}>
<span style={{ fontSize: '0.72rem', color: '#94a3b8', fontWeight: 600 }}>طبقات خريطتنا:</span>
<button onClick={() => setShowTerrain(!showTerrain)} style={toggleBtn(showTerrain)}>🏔️ تضاريس 3D</button>
<button onClick={() => setShowContours(!showContours)} style={toggleBtn(showContours)}>〰️ كنتور</button>
<button onClick={() => setShowAdmin(!showAdmin)} style={toggleBtn(showAdmin)}>🏛️ حدود</button>
<button onClick={() => setShow3D(!show3D)} style={toggleBtn(show3D)}>🏢 مباني 3D</button>
</div>
<div style={{ display: 'flex', gap: '5px', marginInlineStart: 'auto', overflowX: 'auto', paddingBottom: '2px' }}>
{/* Reference Map Selectors */}
<div style={{ display: 'flex', gap: '6px', alignItems: 'center', overflowX: 'auto', flexShrink: 0 }}>
<span style={{ fontSize: '0.72rem', color: '#94a3b8', fontWeight: 600, marginInlineEnd: 4 }}>المقارنة المرجعية:</span>
{(Object.keys(REFS) as RefKey[]).map(k => (
<button key={k} onClick={() => setRefKey(k)}
style={{ ...btn, whiteSpace: 'nowrap', flexShrink: 0, ...(refKey === k ? { borderColor: '#6366f1', color: '#fff', background: 'rgba(99,102,241,0.35)', fontWeight: 600 } : {}) }}>
style={{
...btn,
whiteSpace: 'nowrap',
flexShrink: 0,
...(refKey === k
? { borderColor: '#6366f1', color: '#fff', background: 'linear-gradient(135deg, #4f46e5, #6366f1)', fontWeight: 700, boxShadow: '0 2px 8px rgba(99,102,241,0.4)' }
: {})
}}>
{REFS[k].label}
</button>
))}
+1 -1
View File
@@ -570,7 +570,7 @@ const IntelligenceDashboard: React.FC = () => {
}
return (
<div style={{ display: 'flex', height: '100vh', background: '#0f172a', color: '#f8fafc', fontFamily: 'Inter,system-ui,sans-serif', overflow: 'hidden' }}>
<div style={{ display: 'flex', height: '100%', width: '100%', background: '#090e1a', color: '#f8fafc', fontFamily: 'Inter,system-ui,sans-serif', overflow: 'hidden' }}>
{/* ── SIDEBAR ── */}
<div style={{ width: '370px', flexShrink: 0, display: 'flex', flexDirection: 'column', borderRight: '1px solid #1e293b', overflow: 'hidden', zIndex: 10 }}>
+421 -235
View File
@@ -250,6 +250,7 @@ export const TacticalDefenseView: React.FC = () => {
const [showVisualResection, setShowVisualResection] = useState<boolean>(false);
const [showFileImporter, setShowFileImporter] = useState<boolean>(false);
const [showIPBStudio, setShowIPBStudio] = useState<boolean>(false);
const [ipbStudioTab, setIpbStudioTab] = useState<'overlays' | 'report' | 'ai_advisor'>('overlays');
const [resectionPosition, setResectionPosition] = useState<[number, number] | null>(null);
// 10. Live Digital IPB Overlays Engine (منظومة الشفافات التكتيكية الميدانية الحية)
@@ -615,13 +616,13 @@ export const TacticalDefenseView: React.FC = () => {
// Terrain Study Center
updateMarker('terrain-c', terrainCenter, '⛰️ مركز دراسة الأرض', '#6366f1', (pos) => setTerrainCenter(pos));
// Terrain Highest & Lowest Points (Dynamic Pins)
// Terrain Highest & Lowest Points (Dynamic Pins - Military Spot Heights)
if (terrainResult?.highestPoint && showPeaksAndValleys && mode === 'terrain') {
updateMarker(
'terrain-peak',
[terrainResult.highestPoint.lat, terrainResult.highestPoint.lng],
`🔺 ${terrainResult.highestPoint.label}`,
'#0284c7',
`✕ ${Math.round(terrainResult.highestPoint.elevation || 0)}م (القمة الحاكمة)`,
'#881337',
() => {}
);
} else {
@@ -678,7 +679,7 @@ export const TacticalDefenseView: React.FC = () => {
const initialMap = new maplibregl.Map({
container: mapContainer.current,
style: '/style.json',
style: '/tactical-style.json',
center: [35.9106, 31.9539],
zoom: 11,
pitch: 35,
@@ -875,6 +876,31 @@ export const TacticalDefenseView: React.FC = () => {
});
if (!initialMap.getSource('contour-source')) {
// ── 0. Terrain Hillshade Layer for Steep Slope & Cliff 3D Relief ──
if (!initialMap.getSource('raster-dem-src')) {
initialMap.addSource('raster-dem-src', {
type: 'raster-dem',
tiles: ['https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png'],
encoding: 'terrarium',
tileSize: 256,
maxzoom: 15
});
initialMap.addLayer({
id: 'tactical-hillshade',
type: 'hillshade',
source: 'raster-dem-src',
paint: {
'hillshade-exaggeration': 0.65,
'hillshade-shadow-color': '#3b1c06',
'hillshade-highlight-color': '#ffffff',
'hillshade-accent-color': '#9a3412',
'hillshade-illumination-direction': 315,
'hillshade-illumination-anchor': 'viewport'
}
});
}
initialMap.addSource('contour-source', {
type: 'vector',
tiles: [contourUrl],
@@ -934,6 +960,53 @@ export const TacticalDefenseView: React.FC = () => {
'text-halo-width': 2,
},
});
// ── Hydro-Flattening Water Mask: Covers and eliminates contour swirls over dams, lakes, and reservoirs ──
if (!initialMap.getLayer('tactical-water-mask-hydroflat')) {
initialMap.addLayer({
id: 'tactical-water-mask-hydroflat',
type: 'fill',
source: 'local-osm-polygons',
'source-layer': 'planet_osm_polygon',
filter: [
'any',
['==', 'natural', 'water'],
['==', 'water', 'reservoir'],
['==', 'water', 'dam'],
['==', 'water', 'lake'],
['==', 'water', 'pond'],
['==', 'waterway', 'riverbank'],
['==', 'landuse', 'reservoir'],
['==', 'landuse', 'basin']
],
paint: {
'fill-color': '#90C3D4',
'fill-opacity': 1.0
}
});
initialMap.addLayer({
id: 'tactical-water-mask-hydroflat-outline',
type: 'line',
source: 'local-osm-polygons',
'source-layer': 'planet_osm_polygon',
filter: [
'any',
['==', 'natural', 'water'],
['==', 'water', 'reservoir'],
['==', 'water', 'dam'],
['==', 'water', 'lake'],
['==', 'water', 'pond'],
['==', 'waterway', 'riverbank'],
['==', 'landuse', 'reservoir'],
['==', 'landuse', 'basin']
],
paint: {
'line-color': '#559cb3',
'line-width': 3.0
}
});
}
}
// Rich Spatial Feature Layers for Terrain Study (Slope, Wadis, Roads, Urban, Hazards)
@@ -1224,9 +1297,22 @@ export const TacticalDefenseView: React.FC = () => {
id: 'tactical-ipb-line',
type: 'line',
source: 'tactical-ipb-src',
filter: [
'any',
['==', '$type', 'LineString'],
['==', ['get', 'layer'], 'layer_10'],
['==', ['get', 'layer'], 'layer_7'],
['==', ['get', 'layer'], 'layer_4']
],
paint: {
'line-color': ['coalesce', ['get', 'strokeColor'], ['get', 'color'], '#0284c7'],
'line-width': 2.5,
'line-width': [
'case',
['==', ['get', 'layer'], 'layer_10'], 3.0,
['==', ['get', 'layer'], 'layer_7'], 4.0,
['==', ['get', 'layer'], 'layer_4'], 2.5,
2.0
],
'line-opacity': 0.95
}
});
@@ -1235,10 +1321,18 @@ export const TacticalDefenseView: React.FC = () => {
id: 'tactical-ipb-symbol',
type: 'symbol',
source: 'tactical-ipb-src',
filter: [
'all',
['has', 'label'],
['!=', ['get', 'layer'], 'layer_6'],
['!=', ['get', 'layer'], 'layer_9'],
['!=', ['get', 'layer'], 'layer_1'],
['!=', ['get', 'layer'], 'layer_2']
],
layout: {
'text-field': ['get', 'label'],
'text-font': ['Open Sans Bold', 'Arial Unicode MS Bold'],
'text-size': 11,
'text-font': ['Noto Sans Regular'],
'text-size': 12,
'text-offset': [0, 1.2],
'text-anchor': 'top',
'text-allow-overlap': false
@@ -1246,7 +1340,7 @@ export const TacticalDefenseView: React.FC = () => {
paint: {
'text-color': '#ffffff',
'text-halo-color': '#0f172a',
'text-halo-width': 2
'text-halo-width': 2.5
}
});
});
@@ -1311,7 +1405,7 @@ export const TacticalDefenseView: React.FC = () => {
// Sync Spatial GeoJSON Features with Active Layer Toggles
useEffect(() => {
if (!map.current || !map.current.getSource('tactical-terrain-spatial-src')) return;
if (!terrainResult?.spatialGeoJson || mode !== 'terrain') {
if (!terrainResult?.spatialGeoJson || (mode !== 'terrain' && mode !== 'ipb')) {
(map.current.getSource('tactical-terrain-spatial-src') as maplibregl.GeoJSONSource).setData({
type: 'FeatureCollection',
features: []
@@ -1339,6 +1433,9 @@ export const TacticalDefenseView: React.FC = () => {
useEffect(() => {
if (mode === 'ipb') {
runIPBCalculation(ipbCenter, ipbRadiusKm);
// Sync Terrain Study center to IPB center so the high-res terrain baseline displays perfectly underneath IPB
setTerrainCenter(ipbCenter);
setTerrainRadius(ipbRadiusKm * 1000);
}
}, [mode, ipbCenter, ipbRadiusKm]);
@@ -1722,8 +1819,8 @@ ${terrainResult.tacticalRecommendations.map((r, i) => `${i + 1}. ${r}`).join('\n
return (
<div style={{
width: '100vw',
height: '100vh',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
background: '#0a0e17',
@@ -1731,245 +1828,274 @@ ${terrainResult.tacticalRecommendations.map((r, i) => `${i + 1}. ${r}`).join('\n
fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "SF Pro Arabic", system-ui, sans-serif',
direction: 'rtl',
overflow: 'hidden',
paddingTop: 56
paddingTop: 0
}}>
{/* Top Military HUD Header */}
{/* Tier 1: C4ISR Tactical Operations Ribbon */}
<header style={{
height: 52,
background: '#0f172a',
borderBottom: '1px solid #1e293b',
height: 48,
background: '#0a0f1d',
borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 18px',
padding: '0 14px',
zIndex: 50,
gap: 12,
flexShrink: 0
gap: 10,
flexShrink: 0,
overflowX: 'auto'
}}>
{/* Title */}
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
{/* Right: C4ISR Title & Tactical Modes */}
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexShrink: 0 }}>
<div style={{
width: 32,
height: 32,
borderRadius: 8,
background: 'linear-gradient(135deg, #4f46e5, #06b6d4)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 0 16px rgba(79, 70, 229, 0.5)'
gap: 6,
background: 'rgba(79, 70, 229, 0.15)',
border: '1px solid rgba(99, 102, 241, 0.3)',
padding: '3px 8px',
borderRadius: 7,
fontSize: '0.78rem',
fontWeight: 800,
color: '#a5b4fc'
}}>
<Shield size={17} color="#ffffff" />
<Shield size={14} color="#818cf8" />
<span>C4ISR Grid</span>
</div>
<div>
<div style={{ fontWeight: 800, fontSize: '0.92rem', letterSpacing: '-0.02em', display: 'flex', alignItems: 'center', gap: 6 }}>
<span>منظومة القيادة والسيطرة والتحليل التكتيكي</span>
<span style={{ fontSize: '10px', background: 'rgba(34, 197, 94, 0.2)', color: '#4ade80', padding: '1px 6px', borderRadius: 4 }}>
C4ISR Grid
</span>
</div>
{/* Tactical Modes Selector Ribbon */}
<div style={{
display: 'flex',
gap: 3,
background: 'rgba(255, 255, 255, 0.04)',
padding: 3,
borderRadius: 10,
border: '1px solid rgba(255,255,255,0.08)'
}}>
{[
{ id: 'ipb', label: 'منظومة الشفافات (IPB)', icon: <LayersIcon size={12} /> },
{ id: 'terrain', label: 'دراسة الأرض', icon: <Mountain size={12} /> },
{ id: 'isochrone', label: 'زمن الاستجابة (Isochrone)', icon: <Clock size={12} /> },
{ id: 'los', label: 'تبادل الرؤية (LOS)', icon: <Eye size={12} /> },
{ id: 'viewshed', label: 'كشف 360°', icon: <Radio size={12} /> },
{ id: 'artillery', label: 'موقع المدفعية', icon: <Flame size={12} /> },
{ id: 'minefield', label: 'حقل الألغام', icon: <AlertOctagon size={12} /> },
{ id: 'hlz', label: 'مهابط المروحيات', icon: <Crosshair size={12} /> },
{ id: 'symbols', label: 'الرموز والتشكيلات', icon: <Layers size={12} /> },
{ id: 'study', label: 'دراسة التنظيم', icon: <Award size={12} /> },
].map(m => (
<button
key={m.id}
onClick={() => {
setMode(m.id as TacticalMode);
setActivePlacement(null);
}}
style={{
background: mode === m.id
? 'linear-gradient(135deg, #4f46e5, #6366f1)'
: 'transparent',
boxShadow: mode === m.id ? '0 2px 8px rgba(79, 70, 229, 0.45)' : 'none',
border: 'none',
color: mode === m.id ? '#ffffff' : '#94a3b8',
borderRadius: 7,
padding: '4px 9px',
fontSize: '0.74rem',
fontWeight: mode === m.id ? 800 : 600,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 5,
whiteSpace: 'nowrap',
transition: 'all 0.15s ease'
}}
>
{m.icon}
<span>{m.label}</span>
</button>
))}
</div>
</div>
{/* Tactical Modes Selector */}
<div style={{ display: 'flex', gap: 4, background: 'rgba(255, 255, 255, 0.05)', padding: 3, borderRadius: 10, border: '1px solid rgba(255,255,255,0.1)' }}>
{[
{ id: 'ipb', label: 'منظومة الشفافات (IPB)', icon: <LayersIcon size={13} /> },
{ id: 'terrain', label: 'دراسة الأرض', icon: <Mountain size={13} /> },
{ id: 'isochrone', label: 'زمن الاستجابة (Isochrone)', icon: <Clock size={13} /> },
{ id: 'los', label: 'تبادل الرؤية (LOS)', icon: <Eye size={13} /> },
{ id: 'viewshed', label: 'كشف 360°', icon: <Radio size={13} /> },
{ id: 'artillery', label: 'موقع المدفعية', icon: <Flame size={13} /> },
{ id: 'minefield', label: 'حقل الألغام', icon: <AlertOctagon size={13} /> },
{ id: 'hlz', label: 'مهابط المروحيات', icon: <Crosshair size={13} /> },
{ id: 'symbols', label: 'الرموز والتشكيلات', icon: <Layers size={13} /> },
{ id: 'study', label: 'دراسة التنظيم', icon: <Award size={13} /> },
].map(m => (
{/* Left: Strategic Quick Action Launchers */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexShrink: 0 }}>
{/* AI Strategic Advisor Button (Gemini 3.7 Flash) */}
<button
onClick={() => {
setIpbStudioTab('ai_advisor');
setShowIPBStudio(true);
}}
title="المستشار الاستراتيجي التفاعلي للذكاء الاصطناعي (Gemini 3.7 Flash)"
style={{
background: 'linear-gradient(135deg, rgba(147, 51, 234, 0.35), rgba(79, 70, 229, 0.4))',
border: '1px solid rgba(192, 132, 252, 0.7)',
color: '#f3e8ff',
padding: '4px 10px',
borderRadius: 7,
fontSize: '0.72rem',
fontWeight: 800,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 5,
boxShadow: '0 2px 8px rgba(147, 51, 234, 0.3)'
}}
>
<Zap size={13} color="#fde047" />
<span>المستشار الاستراتيجي AI</span>
</button>
{/* GPS-Denied Visual Resection Button */}
<button
onClick={() => setShowVisualResection(true)}
title="تحديد الموقع بالتقاطع البصري عند انقطاع الـ GPS"
style={{
background: 'linear-gradient(135deg, rgba(0, 240, 255, 0.15), rgba(0, 113, 227, 0.2))',
border: '1px solid rgba(0, 240, 255, 0.4)',
color: '#38bdf8',
padding: '4px 9px',
borderRadius: 7,
fontSize: '0.72rem',
fontWeight: 700,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 5
}}
>
<Camera size={12} />
<span>التقاطع البصري</span>
</button>
{/* Flash Drive Live Importer Button */}
<button
onClick={() => setShowFileImporter(true)}
title="استيراد ملفات المركز الجغرافي من الفلاشة (Shapefiles, GeoJSON)"
style={{
background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.15), rgba(79, 70, 229, 0.2))',
border: '1px solid rgba(99, 102, 241, 0.4)',
color: '#a5b4fc',
padding: '4px 9px',
borderRadius: 7,
fontSize: '0.72rem',
fontWeight: 700,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 5
}}
>
<FolderUp size={12} />
<span>استيراد ملفات</span>
</button>
{/* Digital IPB Overlay Studio Button */}
<button
onClick={() => {
setIpbStudioTab('overlays');
setShowIPBStudio(true);
}}
title="منظومة الشفافات التكتيكية الرقمية وإعداد ساحة المعركة (IPB Studio)"
style={{
background: 'linear-gradient(135deg, rgba(16, 185, 129, 0.2), rgba(5, 150, 105, 0.25))',
border: '1px solid rgba(16, 185, 129, 0.5)',
color: '#4ade80',
padding: '4px 9px',
borderRadius: 7,
fontSize: '0.72rem',
fontWeight: 700,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 5
}}
>
<LayersIcon size={12} />
<span>الشفافات (IPB)</span>
</button>
{authStatus === 'authorized' && (
<button
key={m.id}
onClick={() => {
setMode(m.id as TacticalMode);
setActivePlacement(null);
}}
onClick={handleRevokeOrLock}
title="قفل المنظومة التكتيكية / خروج"
style={{
background: mode === m.id ? 'linear-gradient(135deg, #4f46e5, #6366f1)' : 'transparent',
border: 'none',
color: mode === m.id ? '#ffffff' : '#94a3b8',
background: 'rgba(239, 68, 68, 0.12)',
border: '1px solid rgba(239, 68, 68, 0.3)',
color: '#f87171',
padding: '4px 7px',
borderRadius: 7,
padding: '5px 10px',
fontSize: '0.78rem',
fontSize: '0.7rem',
fontWeight: 700,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 5,
transition: 'all 0.15s ease'
gap: 4
}}
>
{m.icon}
<span>{m.label}</span>
<Lock size={12} />
<span>قفل</span>
</button>
))}
</div>
{/* Realtime Cursor HUD & Clearance Status */}
<div style={{ display: 'flex', alignItems: 'center', gap: 14, fontSize: '0.72rem', fontFamily: 'monospace', color: '#94a3b8' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div>LAT: <span style={{ color: '#38bdf8' }}>{cursorPos.lat}</span></div>
<div>LNG: <span style={{ color: '#38bdf8' }}>{cursorPos.lng}</span></div>
<div>ELEV: <span style={{ color: '#4ade80' }}>{cursorPos.elev}m</span></div>
</div>
{authStatus === 'authorized' && (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, borderRight: '1px solid rgba(255,255,255,0.15)', paddingRight: 10 }}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 5,
background: 'rgba(34, 197, 94, 0.15)',
border: '1px solid rgba(34, 197, 94, 0.35)',
color: '#4ade80',
padding: '3px 8px',
borderRadius: 6,
fontSize: '0.7rem',
fontWeight: 700,
fontFamily: 'inherit'
}}>
<ShieldCheck size={13} />
<span>تصريح: {authInfo?.tenantName || 'Enterprise'}</span>
</div>
{/* GPS-Denied Visual Resection Button */}
<button
onClick={() => setShowVisualResection(true)}
title="تحديد الموقع بالتقاطع البصري عند انقطاع الـ GPS"
style={{
background: 'linear-gradient(135deg, rgba(0, 240, 255, 0.2), rgba(0, 113, 227, 0.25))',
border: '1px solid rgba(0, 240, 255, 0.5)',
color: '#38bdf8',
padding: '4px 10px',
borderRadius: 6,
fontSize: '0.72rem',
fontWeight: 800,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 5,
boxShadow: '0 2px 8px rgba(0, 240, 255, 0.2)'
}}
>
<Camera size={13} />
<span>التقاطع البصري (GPS-Denied)</span>
</button>
{/* Flash Drive Live Importer Button */}
<button
onClick={() => setShowFileImporter(true)}
title="استيراد ملفات المركز الجغرافي من الفلاشة (Shapefiles, GeoJSON)"
style={{
background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(79, 70, 229, 0.25))',
border: '1px solid rgba(99, 102, 241, 0.5)',
color: '#a5b4fc',
padding: '4px 10px',
borderRadius: 6,
fontSize: '0.72rem',
fontWeight: 800,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 5,
boxShadow: '0 2px 8px rgba(99, 102, 241, 0.2)'
}}
>
<FolderUp size={13} />
<span>استيراد ملفات الفلاشة</span>
</button>
{/* Digital IPB Overlay Studio Button */}
<button
onClick={() => setShowIPBStudio(true)}
title="منظومة الشفافات التكتيكية الرقمية وإعداد ساحة المعركة (IPB Studio)"
style={{
background: 'linear-gradient(135deg, rgba(16, 185, 129, 0.25), rgba(5, 150, 105, 0.3))',
border: '1px solid rgba(16, 185, 129, 0.6)',
color: '#4ade80',
padding: '4px 10px',
borderRadius: 6,
fontSize: '0.72rem',
fontWeight: 800,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 5,
boxShadow: '0 2px 8px rgba(16, 185, 129, 0.2)'
}}
>
<LayersIcon size={13} />
<span>منظومة الشفافات (IPB Studio)</span>
</button>
<button
onClick={handleRevokeOrLock}
title="قفل المنظومة التكتيكية / خروج"
style={{
background: 'rgba(239, 68, 68, 0.15)',
border: '1px solid rgba(239, 68, 68, 0.3)',
color: '#f87171',
padding: '3px 8px',
borderRadius: 6,
fontSize: '0.7rem',
fontWeight: 700,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 4,
fontFamily: 'inherit',
transition: 'all 0.15s ease'
}}
>
<Lock size={12} />
<span>قفل</span>
</button>
</div>
)}
</div>
</header>
{/* Quick Sector Presets Bar */}
{/* Tier 2: Quick Sector Presets Bar & Real-time Cursor Coordinates */}
<div style={{
height: 38,
background: '#0b1329',
borderBottom: '1px solid rgba(255,255,255,0.08)',
height: 36,
background: '#070b16',
borderBottom: '1px solid rgba(255,255,255,0.06)',
display: 'flex',
alignItems: 'center',
padding: '0 16px',
gap: 8,
fontSize: '0.75rem',
justifyContent: 'space-between',
padding: '0 14px',
fontSize: '0.73rem',
overflowX: 'auto',
flexShrink: 0
}}>
<span style={{ color: '#94a3b8', display: 'flex', alignItems: 'center', gap: 4, fontWeight: 700 }}>
<MapPin size={13} color="#38bdf8" /> الانتقال السريع لقطاعات العمليات:
</span>
{REGIONAL_SECTORS.map((s, idx) => (
<button
key={idx}
onClick={() => flyToSector(s)}
style={{
background: 'rgba(56, 189, 248, 0.1)',
border: '1px solid rgba(56, 189, 248, 0.25)',
color: '#38bdf8',
padding: '3px 10px',
borderRadius: 6,
fontSize: '0.72rem',
fontWeight: 600,
cursor: 'pointer',
whiteSpace: 'nowrap',
transition: 'all 0.15s ease'
}}
>
{s.name}
</button>
))}
{/* Right: Sector Presets */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexShrink: 0 }}>
<span style={{ color: '#64748b', display: 'flex', alignItems: 'center', gap: 4, fontWeight: 700 }}>
<MapPin size={12} color="#38bdf8" /> القطاعات الميدانية:
</span>
{REGIONAL_SECTORS.map((s, idx) => (
<button
key={idx}
onClick={() => flyToSector(s)}
style={{
background: 'rgba(56, 189, 248, 0.08)',
border: '1px solid rgba(56, 189, 248, 0.2)',
color: '#38bdf8',
padding: '2px 8px',
borderRadius: 5,
fontSize: '0.7rem',
fontWeight: 600,
cursor: 'pointer',
whiteSpace: 'nowrap',
transition: 'all 0.15s ease'
}}
>
{s.name}
</button>
))}
</div>
{/* Left: Realtime Cursor Coordinates HUD */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12, fontSize: '0.72rem', fontFamily: 'monospace', color: '#94a3b8', flexShrink: 0 }}>
<div>LAT: <span style={{ color: '#38bdf8' }}>{cursorPos.lat}</span></div>
<div>LNG: <span style={{ color: '#38bdf8' }}>{cursorPos.lng}</span></div>
<div>ELEV: <span style={{ color: '#4ade80' }}>{cursorPos.elev}m</span></div>
{authInfo?.tenantName && (
<span style={{
background: 'rgba(34, 197, 94, 0.15)',
color: '#4ade80',
padding: '1px 6px',
borderRadius: 4,
fontFamily: 'inherit',
fontSize: '0.68rem',
fontWeight: 700
}}>
تصريح: {authInfo.tenantName}
</span>
)}
</div>
</div>
{/* Main Tactical Workspace */}
@@ -2047,25 +2173,53 @@ ${terrainResult.tacticalRecommendations.map((r, i) => `${i + 1}. ${r}`).join('\n
رسم الشفافات التعبوية الـ 10 حياً على الخريطة بقاطع مخصص (4 كم - 30 كم)
</p>
</div>
<button
onClick={() => setShowIPBStudio(true)}
style={{
background: 'rgba(16, 185, 129, 0.2)',
border: '1px solid #10b981',
color: '#4ade80',
padding: '4px 8px',
borderRadius: 6,
fontSize: '0.7rem',
fontWeight: 700,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 4
}}
>
<FileText size={12} />
<span>التقرير الرسمي</span>
</button>
<div style={{ display: 'flex', gap: 6 }}>
<button
onClick={() => {
setIpbStudioTab('ai_advisor');
setShowIPBStudio(true);
}}
style={{
background: 'linear-gradient(135deg, rgba(147, 51, 234, 0.3), rgba(79, 70, 229, 0.35))',
border: '1px solid rgba(192, 132, 252, 0.6)',
color: '#e9d5ff',
padding: '4px 8px',
borderRadius: 6,
fontSize: '0.7rem',
fontWeight: 800,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 4
}}
>
<Zap size={12} color="#fde047" />
<span>المستشار AI</span>
</button>
<button
onClick={() => {
setIpbStudioTab('report');
setShowIPBStudio(true);
}}
style={{
background: 'rgba(16, 185, 129, 0.2)',
border: '1px solid #10b981',
color: '#4ade80',
padding: '4px 8px',
borderRadius: 6,
fontSize: '0.7rem',
fontWeight: 700,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 4
}}
>
<FileText size={12} />
<span>التقرير</span>
</button>
</div>
</div>
{/* AO Center & Radius Customization Card */}
@@ -4216,7 +4370,10 @@ ${terrainResult.tacticalRecommendations.map((r, i) => `${i + 1}. ${r}`).join('\n
{/* Quick Dossier Report Button */}
<button
onClick={() => setShowIPBStudio(true)}
onClick={() => {
setIpbStudioTab('report');
setShowIPBStudio(true);
}}
style={{
background: 'linear-gradient(135deg, #0071e3, #4f46e5)',
border: 'none',
@@ -4235,6 +4392,31 @@ ${terrainResult.tacticalRecommendations.map((r, i) => `${i + 1}. ${r}`).join('\n
<FileText size={14} />
<span>تقرير تقدير موقف الأرض</span>
</button>
{/* AI Strategic Advisor Button (Gemini 3.7 Flash) */}
<button
onClick={() => {
setIpbStudioTab('ai_advisor');
setShowIPBStudio(true);
}}
style={{
background: 'linear-gradient(135deg, #9333ea, #7c3aed, #4f46e5)',
border: '1px solid rgba(216, 180, 254, 0.5)',
color: '#ffffff',
padding: '6px 15px',
borderRadius: 10,
fontSize: '0.78rem',
fontWeight: 800,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 6,
boxShadow: '0 2px 14px rgba(147, 51, 234, 0.45)'
}}
>
<Zap size={14} color="#fde047" />
<span>المستشار الاستراتيجي AI</span>
</button>
</div>
)}
</div>
@@ -4428,6 +4610,10 @@ ${terrainResult.tacticalRecommendations.map((r, i) => `${i + 1}. ${r}`).join('\n
<IPBAnalysisStudio
active={showIPBStudio}
onClose={() => setShowIPBStudio(false)}
ipbData={ipbCalculatedData}
center={ipbCenter}
radiusKm={ipbRadiusKm}
initialTab={ipbStudioTab}
/>
</div>
+164 -135
View File
@@ -1571,45 +1571,43 @@ export async function calculateRealIPBOverlays(
geometry: { type: 'Polygon', coordinates: polyCoords }
});
// 2. Layer 2: Basalt / Rocky Escarpments
if (slopeDeg >= 24 || localDrop > 25) {
// 2. Layer 2: Basalt / Rocky Escarpments & Mountain Crest Cliffs
if (slopeDeg >= 18 || localDrop >= 18) {
layer_2.push({
type: 'Feature',
properties: {
layer: 'layer_2',
color: '#b45309',
strokeColor: '#78350f',
opacity: 0.72,
opacity: 0.78,
drop: localDrop,
label: `■ مقطع صخري وعر (${localDrop}م هبوط)`
label: `■ مقطع صخري وعر (${localDrop}م هبوط / ${slopeDeg}° ميل)`
},
geometry: { type: 'Polygon', coordinates: polyCoords }
});
}
// 6. Layer 6: MCOO - Severely Restricted Red
// 6. Layer 6: MCOO - Severely Restricted Red (Only on steep slopes)
layer_6.push({
type: 'Feature',
properties: {
layer: 'layer_6',
color: '#dc2626',
strokeColor: '#b91c1c',
opacity: 0.65,
label: `MCOO: أرض شديدة الإعاقة (${slopeDeg}°)`
opacity: 0.28
},
geometry: { type: 'Polygon', coordinates: polyCoords }
});
} else if (slopeDeg >= 7) {
} else if (slopeDeg >= 9) {
slowGoCells++;
// 6. Layer 6: MCOO - Restricted Yellow
// 6. Layer 6: MCOO - Restricted Amber (Only on rugged slopes)
layer_6.push({
type: 'Feature',
properties: {
layer: 'layer_6',
color: '#eab308',
strokeColor: '#ca8a04',
opacity: 0.55,
label: `MCOO: أرض مقيدة الحركة (${slopeDeg}°)`
color: '#d97706',
strokeColor: '#b45309',
opacity: 0.20
},
geometry: { type: 'Polygon', coordinates: polyCoords }
});
@@ -1624,18 +1622,7 @@ export async function calculateRealIPBOverlays(
}
} else {
flatCells++;
// 6. Layer 6: MCOO - Unrestricted Green
layer_6.push({
type: 'Feature',
properties: {
layer: 'layer_6',
color: '#16a34a',
strokeColor: '#15803d',
opacity: 0.50,
label: `MCOO: أرض سالكة للدروع (${slopeDeg}°)`
},
geometry: { type: 'Polygon', coordinates: polyCoords }
});
// Unrestricted flat land remains unshaded and clean so the map shows naturally
}
}
@@ -1649,30 +1636,7 @@ export async function calculateRealIPBOverlays(
const restrictedPct = Math.round((slowGoCells / totalCells) * 100);
const severelyRestrictedPct = Math.round((severeCells / totalCells) * 100);
// 3. Layer 3: Urban Built-up Centers & Demographics
// Detect proximity to major settlements or generate realistic urban sector
const urbanRadius = radiusMeters * 0.22;
const urbanLatOffset = (urbanRadius / R_earth) * (180 / Math.PI) * 0.4;
const urbanLngOffset = (urbanRadius / (R_earth * Math.cos((centerLat * Math.PI) / 180))) * (180 / Math.PI) * 0.5;
const urbanPoly = [
[centerLng - urbanLngOffset, centerLat - urbanLatOffset],
[centerLng + urbanLngOffset * 1.2, centerLat - urbanLatOffset * 0.8],
[centerLng + urbanLngOffset * 1.5, centerLat + urbanLatOffset * 0.9],
[centerLng - urbanLngOffset * 0.6, centerLat + urbanLatOffset * 1.1],
[centerLng - urbanLngOffset, centerLat - urbanLatOffset]
];
layer_3.push({
type: 'Feature',
properties: {
layer: 'layer_3',
color: '#7c3aed',
strokeColor: '#5b21b6',
opacity: 0.65,
label: '🏢 تجمع سكاني وعمران (قتال مدن MOUT)'
},
geometry: { type: 'Polygon', coordinates: [urbanPoly] }
});
// 3. Layer 3: Urban Built-up Centers (Rendered directly by OSM/Overture style layers)
// 4. Layer 4: Real Valley Drainage Corridors & Wadis (مجاري السيول والأودية الحقيقية)
if (valleyNodes.length >= 3) {
@@ -1684,61 +1648,57 @@ export async function calculateRealIPBOverlays(
color: '#0284c7',
strokeColor: '#0369a1',
opacity: 0.85,
label: `≈≈ مجرى وادٍ وسيل تضاريسي رئيسي (${lowestPoint.elevation}م)`
label: `≈≈ مجرى وادٍ وسيل رئيسي (${lowestPoint.elevation}م)`
},
geometry: { type: 'LineString', coordinates: wadiLineCoords }
});
}
// 5. Layer 5: Tactical Minefields across real choke points
if (chokePoints.length > 0) {
const cp = chokePoints[Math.floor(chokePoints.length / 2)];
const mLatDelta = (250 / R_earth) * (180 / Math.PI);
const mLngDelta = (450 / (R_earth * Math.cos((cp.lat * Math.PI) / 180))) * (180 / Math.PI);
// 5. Layer 5: Tactical Obstacles & Retaining Barriers (Populated if obstacles exist)
const mineLine = [
[cp.lng - mLngDelta, cp.lat - mLatDelta],
[cp.lng + mLngDelta, cp.lat + mLatDelta]
];
layer_5.push({
type: 'Feature',
properties: {
layer: 'layer_5',
color: '#e11d48',
strokeColor: '#9f1239',
opacity: 0.90,
label: 'XXXX حزام حقول ألغام وسواتر موانع (غلق المخنق الطبيعي)'
},
geometry: { type: 'LineString', coordinates: mineLine }
});
} else {
// Fallback across lowest valley approach
const mLat = (lowestPoint.lat + centerLat) / 2;
const mLng = (lowestPoint.lng + centerLng) / 2;
const mLngDelta = (600 / (R_earth * Math.cos((mLat * Math.PI) / 180))) * (180 / Math.PI);
layer_5.push({
type: 'Feature',
properties: {
layer: 'layer_5',
color: '#e11d48',
strokeColor: '#9f1239',
opacity: 0.90,
label: 'XXXX حزام حقول ألغام هندسية مسجلة'
},
geometry: {
type: 'LineString',
coordinates: [
[mLng - mLngDelta, mLat],
[mLng + mLngDelta, mLat]
]
}
});
}
// 7. Layer 7: Real Approach Corridor & Mobility (المقترب التعبوي الرئيسي AA-1)
let approachLengthKm = Math.round((radiusMeters * 1.4 / 1000) * 10) / 10;
let approachDirection = 'الغرب نحو الشرق';
let approachAzimuth = 85;
let approachWidthM = Math.round(dxMeters * 3);
let approachCapacity = 'كتيبة مدرعة بنسق رتل';
// 7. Layer 7: Real Avenues of Approach (مسالك الاقتراب وممرات الدروع)
// Follows the lowest path from entry boundary to center
if (valleyNodes.length >= 3) {
const approachCoords = valleyNodes.slice(0, Math.floor(valleyNodes.length * 0.8)).map(n => [n.lng, n.lat]);
// Calculate real length
let lenKm = 0;
for (let i = 0; i < approachCoords.length - 1; i++) {
const p1 = approachCoords[i];
const p2 = approachCoords[i + 1];
lenKm += Math.hypot((p2[1] - p1[1]) * 111.139, (p2[0] - p1[0]) * 111.139 * Math.cos(centerLat * Math.PI / 180));
}
if (lenKm > 0.5) approachLengthKm = Math.round(lenKm * 10) / 10;
// Calculate azimuth from start to end
const startP = approachCoords[0];
const endP = approachCoords[approachCoords.length - 1];
const dLngRad = (endP[0] - startP[0]) * (Math.PI / 180);
const lat1Rad = startP[1] * (Math.PI / 180);
const lat2Rad = endP[1] * (Math.PI / 180);
const y = Math.sin(dLngRad) * Math.cos(lat2Rad);
const x = Math.cos(lat1Rad) * Math.sin(lat2Rad) - Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(dLngRad);
const azDeg = Math.round((Math.atan2(y, x) * 180 / Math.PI + 360) % 360);
approachAzimuth = azDeg;
if (azDeg >= 337.5 || azDeg < 22.5) approachDirection = 'الجنوب نحو الشمال';
else if (azDeg >= 22.5 && azDeg < 67.5) approachDirection = 'الجنوب الغربي نحو الشمال الشرقي';
else if (azDeg >= 67.5 && azDeg < 112.5) approachDirection = 'الغرب نحو الشرق';
else if (azDeg >= 112.5 && azDeg < 157.5) approachDirection = 'الشمال الغربي نحو الجنوب الشرقي';
else if (azDeg >= 157.5 && azDeg < 202.5) approachDirection = 'الشمال نحو الجنوب';
else if (azDeg >= 202.5 && azDeg < 247.5) approachDirection = 'الشمال الشرقي نحو الجنوب الغربي';
else if (azDeg >= 247.5 && azDeg < 292.5) approachDirection = 'الشرق نحو الغرب';
else approachDirection = 'الجنوب الشرقي نحو الشمال الغربي';
if (approachWidthM > 1200) approachCapacity = 'لواء مدرع بنسق كتائب';
else if (approachWidthM > 600) approachCapacity = 'كتيبة مدرعة بنسق رتل سرية';
else approachCapacity = 'سرية دبابات بنسق رتل واحد';
layer_7.push({
type: 'Feature',
properties: {
@@ -1746,7 +1706,7 @@ export async function calculateRealIPBOverlays(
color: '#2563eb',
strokeColor: '#1d4ed8',
opacity: 0.90,
label: '➔➔ مسلك اقتراب رئيسي لكتيبة دروع (AA-1 عبر بطن الوادي)'
label: `➔➔ المقترب التعبوي الرئيسي (AA-1: بطول ${approachLengthKm} كم / عرض ${approachWidthM}م / سعة ${approachCapacity})`
},
geometry: { type: 'LineString', coordinates: approachCoords }
});
@@ -1768,47 +1728,76 @@ export async function calculateRealIPBOverlays(
}
});
// 9. Layer 9: Real Dead Ground / LOS from Key Terrain Peak
// Run radial LOS calculation from summit to detect shadowed cells
const deadGroundPolygons: any[] = [];
const numRays = 12;
for (let i = 0; i < numRays; i++) {
const angle = (i / numRays) * 2 * Math.PI;
const rayEndLat = highestPoint.lat + (radiusMeters * 0.8 / R_earth) * (180 / Math.PI) * Math.sin(angle);
const rayEndLng = highestPoint.lng + (radiusMeters * 0.8 / (R_earth * Math.cos((highestPoint.lat * Math.PI) / 180))) * (180 / Math.PI) * Math.cos(angle);
// 9. Layer 9: Real DEM Line of Sight & Viewshed Shadows from Key Terrain Peak
// Soft, continuous terrain shadow masking without harsh black borders
const summitR = Math.min(gridSize - 1, Math.max(0, Math.round(((centerLat + dLatTotal - highestPoint.lat) / (2 * dLatTotal)) * (gridSize - 1))));
const summitC = Math.min(gridSize - 1, Math.max(0, Math.round(((highestPoint.lng - (centerLng - dLngTotal)) / (2 * dLngTotal)) * (gridSize - 1))));
const summitElev = highestPoint.elevation;
// If ray passes near lower valley, create shadowed dead ground polygon behind the ridge
const deadLat = (highestPoint.lat + rayEndLat) / 2 + 0.003 * Math.sin(angle);
const deadLng = (highestPoint.lng + rayEndLng) / 2 + 0.003 * Math.cos(angle);
const dSize = 0.004;
for (let r = 0; r < gridSize; r += 2) {
for (let c = 0; c < gridSize; c += 2) {
if (Math.abs(r - summitR) <= 1 && Math.abs(c - summitC) <= 1) continue;
const distCells = Math.hypot(r - summitR, c - summitC);
if (distCells === 0) continue;
layer_9.push({
type: 'Feature',
properties: {
layer: 'layer_9',
color: '#334155',
strokeColor: '#0f172a',
opacity: 0.68,
label: '👁️🚫 أرض ميتة محجوبة عن الرؤية والرادار'
},
geometry: {
type: 'Polygon',
coordinates: [[
[deadLng - dSize, deadLat - dSize],
[deadLng + dSize, deadLat - dSize],
[deadLng + dSize, deadLat + dSize],
[deadLng - dSize, deadLat + dSize],
[deadLng - dSize, deadLat - dSize]
]]
const targetElev = elevationGrid[r][c];
const angleToTarget = (targetElev - summitElev) / distCells;
let isShadowed = false;
const steps = Math.floor(distCells);
for (let s = 1; s < steps; s++) {
const interR = Math.round(summitR + (r - summitR) * (s / distCells));
const interC = Math.round(summitC + (c - summitC) * (s / distCells));
if (interR >= 0 && interR < gridSize && interC >= 0 && interC < gridSize) {
const interElev = elevationGrid[interR][interC];
const angleToInter = (interElev - summitElev) / s;
if (angleToInter > angleToTarget + 0.8) {
isShadowed = true;
break;
}
}
}
});
if (isShadowed) {
const cellLat = centerLat + dLatTotal - (r / (gridSize - 1)) * (2 * dLatTotal);
const cellLng = centerLng - dLngTotal + (c / (gridSize - 1)) * (2 * dLngTotal);
const distM = Math.hypot(
(cellLat - centerLat) * (R_earth * Math.PI / 180),
(cellLng - centerLng) * (R_earth * Math.cos(centerLat * Math.PI / 180) * Math.PI / 180)
);
if (distM <= radiusMeters * 0.95) {
const hLat = (dLatTotal / (gridSize - 1)) * 1.05;
const hLng = (dLngTotal / (gridSize - 1)) * 1.05;
layer_9.push({
type: 'Feature',
properties: {
layer: 'layer_9',
color: '#475569',
strokeColor: '#334155',
opacity: 0.18
},
geometry: {
type: 'Polygon',
coordinates: [[
[cellLng - hLng, cellLat - hLat],
[cellLng + hLng, cellLat - hLat],
[cellLng + hLng, cellLat + hLat],
[cellLng - hLng, cellLat + hLat],
[cellLng - hLng, cellLat - hLat]
]]
}
});
}
}
}
}
// 10. Layer 10: SITTEMP & Kill Zone
// Placed at the intersection of Avenue of Approach into Line of Sight of Key Terrain
const killZoneLat = (highestPoint.lat + lowestPoint.lat) / 2;
const killZoneLng = (highestPoint.lng + lowestPoint.lng) / 2;
const kzR = (radiusMeters * 0.15 / R_earth) * (180 / Math.PI);
// 10. Layer 10: SITTEMP & Engagement Area (منطقة التقتيل الرئيسية Kill Zone Alpha)
// Placed at the natural choke point near lowest valley within line of sight of key terrain
const killZoneLat = (highestPoint.lat + lowestPoint.lat * 2) / 3;
const killZoneLng = (highestPoint.lng + lowestPoint.lng * 2) / 3;
const kzR = (radiusMeters * 0.14 / R_earth) * (180 / Math.PI);
const kzRLng = kzR / Math.cos((killZoneLat * Math.PI) / 180);
const kzCoords: [number, number][] = [];
@@ -1817,14 +1806,47 @@ export async function calculateRealIPBOverlays(
kzCoords.push([killZoneLng + kzRLng * Math.sin(a), killZoneLat + kzR * Math.cos(a)]);
}
// Direct Fire Line from Key Terrain Peak to Kill Zone Center
const fireLineCoords = [
[highestPoint.lng, highestPoint.lat],
[killZoneLng, killZoneLat]
];
const fireDistKm = Math.round(Math.hypot(
(killZoneLat - highestPoint.lat) * 111.139,
(killZoneLng - highestPoint.lng) * 111.139 * Math.cos(centerLat * Math.PI / 180)
) * 10) / 10;
const fireAzimuth = Math.round((Math.atan2(
(killZoneLng - highestPoint.lng) * Math.cos(centerLat * Math.PI / 180),
killZoneLat - highestPoint.lat
) * 180 / Math.PI + 360) % 360);
// Add Direct Fire Line
layer_10.push({
type: 'Feature',
properties: {
layer: 'layer_10',
color: '#dc2626',
strokeColor: '#991b1b',
opacity: 0.95,
label: `⚡ خط نار ورصد مباشر مسيطر (أزيموث ${fireAzimuth}° / مدى ${fireDistKm} كم)`
},
geometry: {
type: 'LineString',
coordinates: fireLineCoords
}
});
// Add Kill Zone Area
layer_10.push({
type: 'Feature',
properties: {
layer: 'layer_10',
color: '#ef4444',
strokeColor: '#b91c1c',
opacity: 0.82,
label: '⚔️ منطقة القتل المستهدفة (KILL ZONE ALPHA)'
opacity: 0.75,
label: '⚔️ منطقة التقتيل الرئيسية (KILL ZONE ALPHA)'
},
geometry: {
type: 'Polygon',
@@ -1893,7 +1915,14 @@ export async function calculateRealIPBOverlays(
severelyRestrictedPct,
cliffCount,
keyTerrainSummit: highestPoint,
killZoneCenter: { lat: killZoneLat, lng: killZoneLng }
killZoneCenter: { lat: killZoneLat, lng: killZoneLng },
approachLengthKm,
approachWidthM,
approachDirection,
approachAzimuth,
approachCapacity,
fireDistKm,
fireAzimuth
},
allFeatures
};