chore: add build artifacts, map assets, and routing data
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user