|
|
|
@@ -176,6 +176,8 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const [mode, setMode] = useState<TacticalMode>('terrain');
|
|
|
|
|
const [mapLoaded, setMapLoaded] = useState<boolean>(false);
|
|
|
|
|
const mapLoadedRef = useRef<boolean>(false);
|
|
|
|
|
const [cursorPos, setCursorPos] = useState({ lat: 31.95, lng: 35.93, elev: 850 });
|
|
|
|
|
const [activePlacement, setActivePlacement] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
@@ -364,6 +366,38 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const bringTacticalLayersToFront = (m: maplibregl.Map | null) => {
|
|
|
|
|
if (!m) return;
|
|
|
|
|
const tacticalLayerOrder = [
|
|
|
|
|
'tactical-terrain-fill',
|
|
|
|
|
'tactical-terrain-line',
|
|
|
|
|
'tactical-isochrone-fill',
|
|
|
|
|
'tactical-isochrone-line',
|
|
|
|
|
'tactical-viewshed-fill',
|
|
|
|
|
'tactical-viewshed-outline',
|
|
|
|
|
'tactical-terrain-spatial-poly-fill',
|
|
|
|
|
'tactical-terrain-spatial-poly-line',
|
|
|
|
|
'tactical-terrain-spatial-lines',
|
|
|
|
|
'tactical-terrain-spatial-points',
|
|
|
|
|
'tactical-terrain-spatial-labels',
|
|
|
|
|
'tactical-ipb-fill',
|
|
|
|
|
'tactical-ipb-line',
|
|
|
|
|
'tactical-ipb-symbol',
|
|
|
|
|
'tactical-los-line',
|
|
|
|
|
'tactical-artillery-line',
|
|
|
|
|
'tactical-minefield-line',
|
|
|
|
|
'tactical-symbols-pts',
|
|
|
|
|
'tactical-symbols-lbls'
|
|
|
|
|
];
|
|
|
|
|
tacticalLayerOrder.forEach((layerId) => {
|
|
|
|
|
if (m.getLayer(layerId)) {
|
|
|
|
|
// try {
|
|
|
|
|
// m.moveLayer(layerId);
|
|
|
|
|
// } catch {}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handlePositionFoundByResection = (lat: number, lng: number, observations: ResectionObservation[]) => {
|
|
|
|
|
setResectionPosition([lat, lng]);
|
|
|
|
|
if (map.current) {
|
|
|
|
@@ -410,11 +444,10 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
id: 'resection-lines',
|
|
|
|
|
type: 'line',
|
|
|
|
|
source: 'resection-source',
|
|
|
|
|
filter: ['==', '$type', 'LineString'],
|
|
|
|
|
filter: ['==', ['geometry-type'], 'LineString'],
|
|
|
|
|
paint: {
|
|
|
|
|
'line-color': '#00f0ff',
|
|
|
|
|
'line-width': 3,
|
|
|
|
|
'line-dasharray': [3, 2]
|
|
|
|
|
'line-width': 3
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -422,7 +455,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
id: 'resection-points',
|
|
|
|
|
type: 'circle',
|
|
|
|
|
source: 'resection-source',
|
|
|
|
|
filter: ['==', '$type', 'Point'],
|
|
|
|
|
filter: ['==', ['geometry-type'], 'Point'],
|
|
|
|
|
paint: {
|
|
|
|
|
'circle-radius': 9,
|
|
|
|
|
'circle-color': '#22c55e',
|
|
|
|
@@ -450,7 +483,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
id: fillLayerId,
|
|
|
|
|
type: 'fill',
|
|
|
|
|
source: sourceId,
|
|
|
|
|
filter: ['==', '$type', 'Polygon'],
|
|
|
|
|
filter: ['==', ['geometry-type'], 'Polygon'],
|
|
|
|
|
paint: {
|
|
|
|
|
'fill-color': '#6366f1',
|
|
|
|
|
'fill-opacity': 0.45
|
|
|
|
@@ -462,7 +495,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
id: lineLayerId,
|
|
|
|
|
type: 'line',
|
|
|
|
|
source: sourceId,
|
|
|
|
|
filter: ['any', ['==', '$type', 'LineString'], ['==', '$type', 'Polygon']],
|
|
|
|
|
filter: ['any', ['==', ['geometry-type'], 'LineString'], ['==', ['geometry-type'], 'Polygon']],
|
|
|
|
|
paint: {
|
|
|
|
|
'line-color': '#38bdf8',
|
|
|
|
|
'line-width': 2.5
|
|
|
|
@@ -474,7 +507,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
id: circleLayerId,
|
|
|
|
|
type: 'circle',
|
|
|
|
|
source: sourceId,
|
|
|
|
|
filter: ['==', '$type', 'Point'],
|
|
|
|
|
filter: ['==', ['geometry-type'], 'Point'],
|
|
|
|
|
paint: {
|
|
|
|
|
'circle-radius': 7,
|
|
|
|
|
'circle-color': '#e11d48',
|
|
|
|
@@ -679,7 +712,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
|
|
|
|
|
const initialMap = new maplibregl.Map({
|
|
|
|
|
container: mapContainer.current,
|
|
|
|
|
style: '/tactical-style.json',
|
|
|
|
|
style: '/style.json',
|
|
|
|
|
center: [35.9106, 31.9539],
|
|
|
|
|
zoom: 11,
|
|
|
|
|
pitch: 35,
|
|
|
|
@@ -829,30 +862,46 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
initialMap.on('load', () => {
|
|
|
|
|
const safeAddSource = (id: string, source: any) => {
|
|
|
|
|
if (!initialMap.getSource(id)) {
|
|
|
|
|
initialMap.addSource(id, source);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const safeAddLayer = (layer: any, beforeId?: string) => {
|
|
|
|
|
if (!initialMap.getLayer(layer.id)) {
|
|
|
|
|
if (beforeId && initialMap.getLayer(beforeId)) {
|
|
|
|
|
initialMap.addLayer(layer, beforeId);
|
|
|
|
|
} else {
|
|
|
|
|
initialMap.addLayer(layer);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 0. Terrain Study Circle & Spatial Layers
|
|
|
|
|
initialMap.addSource('tactical-terrain-src', {
|
|
|
|
|
safeAddSource('tactical-terrain-src', {
|
|
|
|
|
type: 'geojson',
|
|
|
|
|
data: { type: 'FeatureCollection', features: [] }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
initialMap.addLayer({
|
|
|
|
|
safeAddLayer({
|
|
|
|
|
id: 'tactical-terrain-fill',
|
|
|
|
|
type: 'fill',
|
|
|
|
|
source: 'tactical-terrain-src',
|
|
|
|
|
paint: {
|
|
|
|
|
'fill-color': '#6366f1',
|
|
|
|
|
'fill-opacity': 0.08
|
|
|
|
|
'fill-opacity': 0.18
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
initialMap.addLayer({
|
|
|
|
|
safeAddLayer({
|
|
|
|
|
id: 'tactical-terrain-line',
|
|
|
|
|
type: 'line',
|
|
|
|
|
source: 'tactical-terrain-src',
|
|
|
|
|
paint: {
|
|
|
|
|
'line-color': '#818cf8',
|
|
|
|
|
'line-width': 2.5,
|
|
|
|
|
'line-dasharray': [3, 2]
|
|
|
|
|
'line-color': '#4f46e5',
|
|
|
|
|
'line-width': 3.5,
|
|
|
|
|
'line-opacity': 0.95
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -1010,184 +1059,182 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Rich Spatial Feature Layers for Terrain Study (Slope, Wadis, Roads, Urban, Hazards)
|
|
|
|
|
initialMap.addSource('tactical-terrain-spatial-src', {
|
|
|
|
|
safeAddSource('tactical-terrain-spatial-src', {
|
|
|
|
|
type: 'geojson',
|
|
|
|
|
data: { type: 'FeatureCollection', features: [] }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
initialMap.addLayer({
|
|
|
|
|
safeAddLayer({
|
|
|
|
|
id: 'tactical-terrain-spatial-poly-fill',
|
|
|
|
|
type: 'fill',
|
|
|
|
|
source: 'tactical-terrain-spatial-src',
|
|
|
|
|
filter: ['==', '$type', 'Polygon'],
|
|
|
|
|
filter: ['==', ['geometry-type'], 'Polygon'],
|
|
|
|
|
paint: {
|
|
|
|
|
'fill-color': ['get', 'color'],
|
|
|
|
|
'fill-opacity': [
|
|
|
|
|
'match',
|
|
|
|
|
['get', 'layerType'],
|
|
|
|
|
'cliff', 0.38,
|
|
|
|
|
'urban', 0.20,
|
|
|
|
|
'hazard', 0.24,
|
|
|
|
|
'slope-sector', 0.14,
|
|
|
|
|
0.15
|
|
|
|
|
'cliff', 0.45,
|
|
|
|
|
'urban', 0.30,
|
|
|
|
|
'hazard', 0.35,
|
|
|
|
|
'slope-sector', 0.22,
|
|
|
|
|
0.20
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
initialMap.addLayer({
|
|
|
|
|
safeAddLayer({
|
|
|
|
|
id: 'tactical-terrain-spatial-poly-line',
|
|
|
|
|
type: 'line',
|
|
|
|
|
source: 'tactical-terrain-spatial-src',
|
|
|
|
|
filter: ['==', '$type', 'Polygon'],
|
|
|
|
|
filter: ['==', ['geometry-type'], 'Polygon'],
|
|
|
|
|
paint: {
|
|
|
|
|
'line-color': [
|
|
|
|
|
'match',
|
|
|
|
|
['get', 'layerType'],
|
|
|
|
|
'cliff', '#ef4444',
|
|
|
|
|
'rgba(255, 255, 255, 0.10)'
|
|
|
|
|
'rgba(255, 255, 255, 0.4)'
|
|
|
|
|
],
|
|
|
|
|
'line-width': [
|
|
|
|
|
'match',
|
|
|
|
|
['get', 'layerType'],
|
|
|
|
|
'cliff', 1.8,
|
|
|
|
|
0.4
|
|
|
|
|
'cliff', 2.5,
|
|
|
|
|
1.0
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
initialMap.addLayer({
|
|
|
|
|
safeAddLayer({
|
|
|
|
|
id: 'tactical-terrain-spatial-lines',
|
|
|
|
|
type: 'line',
|
|
|
|
|
source: 'tactical-terrain-spatial-src',
|
|
|
|
|
filter: ['==', '$type', 'LineString'],
|
|
|
|
|
filter: ['==', ['geometry-type'], 'LineString'],
|
|
|
|
|
paint: {
|
|
|
|
|
'line-color': ['get', 'color'],
|
|
|
|
|
'line-width': [
|
|
|
|
|
'match',
|
|
|
|
|
['get', 'layerType'],
|
|
|
|
|
'cliff', 3.5,
|
|
|
|
|
'road', 4.0,
|
|
|
|
|
'wadi', 3.5,
|
|
|
|
|
'ridge', 2.5,
|
|
|
|
|
2.5
|
|
|
|
|
'cliff', 4.0,
|
|
|
|
|
'road', 4.5,
|
|
|
|
|
'wadi', 4.5,
|
|
|
|
|
'ridge', 3.5,
|
|
|
|
|
3.0
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
initialMap.addLayer({
|
|
|
|
|
safeAddLayer({
|
|
|
|
|
id: 'tactical-terrain-spatial-points',
|
|
|
|
|
type: 'circle',
|
|
|
|
|
source: 'tactical-terrain-spatial-src',
|
|
|
|
|
filter: ['==', '$type', 'Point'],
|
|
|
|
|
filter: ['==', ['geometry-type'], 'Point'],
|
|
|
|
|
paint: {
|
|
|
|
|
'circle-radius': 5,
|
|
|
|
|
'circle-radius': 7,
|
|
|
|
|
'circle-color': ['coalesce', ['get', 'color'], '#38bdf8'],
|
|
|
|
|
'circle-stroke-width': 2,
|
|
|
|
|
'circle-stroke-width': 2.5,
|
|
|
|
|
'circle-stroke-color': '#ffffff'
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
initialMap.addLayer({
|
|
|
|
|
safeAddLayer({
|
|
|
|
|
id: 'tactical-terrain-spatial-labels',
|
|
|
|
|
type: 'symbol',
|
|
|
|
|
source: 'tactical-terrain-spatial-src',
|
|
|
|
|
filter: ['all', ['==', '$type', 'Point'], ['has', 'title']],
|
|
|
|
|
filter: ['all', ['==', ['geometry-type'], 'Point'], ['has', 'title']],
|
|
|
|
|
layout: {
|
|
|
|
|
'text-field': ['get', 'title'],
|
|
|
|
|
'text-size': 12,
|
|
|
|
|
'text-font': ['Noto Sans Bold', 'Open Sans Bold'],
|
|
|
|
|
'text-font': ['Noto Sans Regular'],
|
|
|
|
|
'text-anchor': 'bottom',
|
|
|
|
|
'text-offset': [0, -0.6],
|
|
|
|
|
'text-offset': [0, -0.8],
|
|
|
|
|
'symbol-placement': 'point',
|
|
|
|
|
'text-allow-overlap': false
|
|
|
|
|
'text-allow-overlap': true
|
|
|
|
|
},
|
|
|
|
|
paint: {
|
|
|
|
|
'text-color': '#ffffff',
|
|
|
|
|
'text-halo-color': '#0f172a',
|
|
|
|
|
'text-halo-width': 2.5
|
|
|
|
|
'text-halo-width': 3
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 1. Line of Sight Layer
|
|
|
|
|
initialMap.addSource('tactical-los-src', {
|
|
|
|
|
safeAddSource('tactical-los-src', {
|
|
|
|
|
type: 'geojson',
|
|
|
|
|
data: { type: 'FeatureCollection', features: [] }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
initialMap.addLayer({
|
|
|
|
|
safeAddLayer({
|
|
|
|
|
id: 'tactical-los-line',
|
|
|
|
|
type: 'line',
|
|
|
|
|
source: 'tactical-los-src',
|
|
|
|
|
filter: ['==', '$type', 'LineString'],
|
|
|
|
|
filter: ['==', ['geometry-type'], 'LineString'],
|
|
|
|
|
paint: {
|
|
|
|
|
'line-color': ['case', ['==', ['get', 'blocked'], true], '#ef4444', '#22c55e'],
|
|
|
|
|
'line-width': 4,
|
|
|
|
|
'line-dasharray': [2, 1]
|
|
|
|
|
'line-width': 5
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 2. Viewshed 360 Polygon Layer
|
|
|
|
|
initialMap.addSource('tactical-viewshed-src', {
|
|
|
|
|
safeAddSource('tactical-viewshed-src', {
|
|
|
|
|
type: 'geojson',
|
|
|
|
|
data: { type: 'FeatureCollection', features: [] }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
initialMap.addLayer({
|
|
|
|
|
safeAddLayer({
|
|
|
|
|
id: 'tactical-viewshed-fill',
|
|
|
|
|
type: 'fill',
|
|
|
|
|
source: 'tactical-viewshed-src',
|
|
|
|
|
paint: {
|
|
|
|
|
'fill-color': '#22c55e',
|
|
|
|
|
'fill-opacity': 0.25
|
|
|
|
|
'fill-color': '#ff0000',
|
|
|
|
|
'fill-opacity': 0.80
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
initialMap.addLayer({
|
|
|
|
|
safeAddLayer({
|
|
|
|
|
id: 'tactical-viewshed-outline',
|
|
|
|
|
type: 'line',
|
|
|
|
|
source: 'tactical-viewshed-src',
|
|
|
|
|
paint: {
|
|
|
|
|
'line-color': '#4ade80',
|
|
|
|
|
'line-width': 2,
|
|
|
|
|
'line-dasharray': [3, 2]
|
|
|
|
|
'line-color': '#ffff00',
|
|
|
|
|
'line-width': 8.0,
|
|
|
|
|
'line-opacity': 1.0
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 3. Artillery Trajectory Layer
|
|
|
|
|
initialMap.addSource('tactical-artillery-src', {
|
|
|
|
|
safeAddSource('tactical-artillery-src', {
|
|
|
|
|
type: 'geojson',
|
|
|
|
|
data: { type: 'FeatureCollection', features: [] }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
initialMap.addLayer({
|
|
|
|
|
safeAddLayer({
|
|
|
|
|
id: 'tactical-artillery-line',
|
|
|
|
|
type: 'line',
|
|
|
|
|
source: 'tactical-artillery-src',
|
|
|
|
|
paint: {
|
|
|
|
|
'line-color': '#f97316',
|
|
|
|
|
'line-width': 4
|
|
|
|
|
'line-width': 5
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 4. Minefield Barrier Layer
|
|
|
|
|
initialMap.addSource('tactical-minefield-src', {
|
|
|
|
|
safeAddSource('tactical-minefield-src', {
|
|
|
|
|
type: 'geojson',
|
|
|
|
|
data: { type: 'FeatureCollection', features: [] }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
initialMap.addLayer({
|
|
|
|
|
safeAddLayer({
|
|
|
|
|
id: 'tactical-minefield-line',
|
|
|
|
|
type: 'line',
|
|
|
|
|
source: 'tactical-minefield-src',
|
|
|
|
|
paint: {
|
|
|
|
|
'line-color': '#dc2626',
|
|
|
|
|
'line-width': 6,
|
|
|
|
|
'line-dasharray': [1, 1]
|
|
|
|
|
'line-width': 6
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 5. Tactical Symbols Layer
|
|
|
|
|
initialMap.addSource('tactical-symbols-src', {
|
|
|
|
|
safeAddSource('tactical-symbols-src', {
|
|
|
|
|
type: 'geojson',
|
|
|
|
|
data: {
|
|
|
|
|
type: 'FeatureCollection',
|
|
|
|
@@ -1199,7 +1246,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
initialMap.addLayer({
|
|
|
|
|
safeAddLayer({
|
|
|
|
|
id: 'tactical-symbols-pts',
|
|
|
|
|
type: 'circle',
|
|
|
|
|
source: 'tactical-symbols-src',
|
|
|
|
@@ -1222,7 +1269,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
initialMap.addLayer({
|
|
|
|
|
safeAddLayer({
|
|
|
|
|
id: 'tactical-symbols-lbls',
|
|
|
|
|
type: 'symbol',
|
|
|
|
|
source: 'tactical-symbols-src',
|
|
|
|
@@ -1241,56 +1288,56 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 6. Isochrone Reachability Polygons Layer (خارطة زمن الاستجابة والوصول)
|
|
|
|
|
initialMap.addSource('tactical-isochrone-src', {
|
|
|
|
|
safeAddSource('tactical-isochrone-src', {
|
|
|
|
|
type: 'geojson',
|
|
|
|
|
data: { type: 'FeatureCollection', features: [] }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
initialMap.addLayer({
|
|
|
|
|
safeAddLayer({
|
|
|
|
|
id: 'tactical-isochrone-fill',
|
|
|
|
|
type: 'fill',
|
|
|
|
|
source: 'tactical-isochrone-src',
|
|
|
|
|
paint: {
|
|
|
|
|
'fill-color': ['coalesce', ['get', 'color'], '#22c55e'],
|
|
|
|
|
'fill-opacity': 0.32
|
|
|
|
|
'fill-opacity': 0.45
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
initialMap.addLayer({
|
|
|
|
|
safeAddLayer({
|
|
|
|
|
id: 'tactical-isochrone-line',
|
|
|
|
|
type: 'line',
|
|
|
|
|
source: 'tactical-isochrone-src',
|
|
|
|
|
paint: {
|
|
|
|
|
'line-color': ['coalesce', ['get', 'color'], '#22c55e'],
|
|
|
|
|
'line-width': 2.5,
|
|
|
|
|
'line-width': 3.5,
|
|
|
|
|
'line-opacity': 0.95
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 7. Digital IPB Overlays Layer (منظومة الشفافات التكتيكية الرقمية لإعداد ساحة المعركة)
|
|
|
|
|
initialMap.addSource('tactical-ipb-src', {
|
|
|
|
|
safeAddSource('tactical-ipb-src', {
|
|
|
|
|
type: 'geojson',
|
|
|
|
|
data: { type: 'FeatureCollection', features: [] }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
initialMap.addLayer({
|
|
|
|
|
safeAddLayer({
|
|
|
|
|
id: 'tactical-ipb-fill',
|
|
|
|
|
type: 'fill',
|
|
|
|
|
source: 'tactical-ipb-src',
|
|
|
|
|
filter: ['==', '$type', 'Polygon'],
|
|
|
|
|
filter: ['==', ['geometry-type'], 'Polygon'],
|
|
|
|
|
paint: {
|
|
|
|
|
'fill-color': ['coalesce', ['get', 'color'], '#38bdf8'],
|
|
|
|
|
'fill-opacity': ['coalesce', ['get', 'opacity'], 0.65]
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
initialMap.addLayer({
|
|
|
|
|
safeAddLayer({
|
|
|
|
|
id: 'tactical-ipb-line',
|
|
|
|
|
type: 'line',
|
|
|
|
|
source: 'tactical-ipb-src',
|
|
|
|
|
filter: [
|
|
|
|
|
'any',
|
|
|
|
|
['==', '$type', 'LineString'],
|
|
|
|
|
['==', ['geometry-type'], 'LineString'],
|
|
|
|
|
['==', ['get', 'layer'], 'layer_10'],
|
|
|
|
|
['==', ['get', 'layer'], 'layer_7'],
|
|
|
|
|
['==', ['get', 'layer'], 'layer_4']
|
|
|
|
@@ -1308,7 +1355,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
initialMap.addLayer({
|
|
|
|
|
safeAddLayer({
|
|
|
|
|
id: 'tactical-ipb-symbol',
|
|
|
|
|
type: 'symbol',
|
|
|
|
|
source: 'tactical-ipb-src',
|
|
|
|
@@ -1334,6 +1381,11 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
'text-halo-width': 2.5
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
bringTacticalLayersToFront(initialMap);
|
|
|
|
|
mapLoadedRef.current = true;
|
|
|
|
|
setMapLoaded(true);
|
|
|
|
|
(window as any).tacticalMap = initialMap;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
@@ -1344,41 +1396,47 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// Draw sector boundary circle immediately on map
|
|
|
|
|
const drawSectorCircle = (center: [number, number], radius: number) => {
|
|
|
|
|
if (!map.current || !map.current.getSource('tactical-terrain-src')) return;
|
|
|
|
|
const points = 64;
|
|
|
|
|
const coords: [number, number][] = [];
|
|
|
|
|
const R_earth = 6371000;
|
|
|
|
|
for (let i = 0; i <= points; i++) {
|
|
|
|
|
const angle = (i / points) * 2 * Math.PI;
|
|
|
|
|
const dLat = (radius / R_earth) * (180 / Math.PI) * Math.cos(angle);
|
|
|
|
|
const dLng = (radius / (R_earth * Math.cos((center[0] * Math.PI) / 180))) * (180 / Math.PI) * Math.sin(angle);
|
|
|
|
|
coords.push([Number((center[1] + dLng).toFixed(6)), Number((center[0] + dLat).toFixed(6))]);
|
|
|
|
|
}
|
|
|
|
|
coords.push(coords[0]);
|
|
|
|
|
|
|
|
|
|
(map.current.getSource('tactical-terrain-src') as maplibregl.GeoJSONSource).setData({
|
|
|
|
|
type: 'FeatureCollection',
|
|
|
|
|
features: [
|
|
|
|
|
{
|
|
|
|
|
type: 'Feature',
|
|
|
|
|
properties: {},
|
|
|
|
|
geometry: {
|
|
|
|
|
type: 'Polygon',
|
|
|
|
|
coordinates: [coords]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
bringTacticalLayersToFront(map.current);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Run Terrain Tactical Study
|
|
|
|
|
const runTerrainStudy = () => {
|
|
|
|
|
if (!terrainCenter) return;
|
|
|
|
|
// Draw boundary circle immediately so feedback is instant
|
|
|
|
|
drawSectorCircle(terrainCenter, terrainRadius);
|
|
|
|
|
setTerrainLoading(true);
|
|
|
|
|
calculateTerrainStudy(terrainCenter[0], terrainCenter[1], terrainRadius)
|
|
|
|
|
.then((res) => {
|
|
|
|
|
setTerrainResult(res);
|
|
|
|
|
setTerrainLoading(false);
|
|
|
|
|
|
|
|
|
|
// 1. Draw sector boundary polygon on map
|
|
|
|
|
if (map.current && map.current.getSource('tactical-terrain-src')) {
|
|
|
|
|
const points = 48;
|
|
|
|
|
const coords: [number, number][] = [];
|
|
|
|
|
const R_earth = 6371000;
|
|
|
|
|
for (let i = 0; i <= points; i++) {
|
|
|
|
|
const angle = (i / points) * 2 * Math.PI;
|
|
|
|
|
const dLat = (terrainRadius / R_earth) * (180 / Math.PI) * Math.cos(angle);
|
|
|
|
|
const dLng = (terrainRadius / (R_earth * Math.cos((terrainCenter[0] * Math.PI) / 180))) * (180 / Math.PI) * Math.sin(angle);
|
|
|
|
|
coords.push([terrainCenter[1] + dLng, terrainCenter[0] + dLat]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
(map.current.getSource('tactical-terrain-src') as maplibregl.GeoJSONSource).setData({
|
|
|
|
|
type: 'FeatureCollection',
|
|
|
|
|
features: [
|
|
|
|
|
{
|
|
|
|
|
type: 'Feature',
|
|
|
|
|
properties: {},
|
|
|
|
|
geometry: {
|
|
|
|
|
type: 'Polygon',
|
|
|
|
|
coordinates: [coords]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
drawSectorCircle(terrainCenter, terrainRadius);
|
|
|
|
|
})
|
|
|
|
|
.catch(() => setTerrainLoading(false));
|
|
|
|
|
};
|
|
|
|
@@ -1396,7 +1454,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' && mode !== 'ipb')) {
|
|
|
|
|
if (!terrainResult?.spatialGeoJson) {
|
|
|
|
|
(map.current.getSource('tactical-terrain-spatial-src') as maplibregl.GeoJSONSource).setData({
|
|
|
|
|
type: 'FeatureCollection',
|
|
|
|
|
features: []
|
|
|
|
@@ -1418,7 +1476,8 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
type: 'FeatureCollection',
|
|
|
|
|
features: filtered
|
|
|
|
|
});
|
|
|
|
|
}, [terrainResult, showPeaksAndValleys, showSlopeSectors, showNaturalObstacles, showRoadCorridors, showUrbanZones, showHazardousGround, mode]);
|
|
|
|
|
bringTacticalLayersToFront(map.current);
|
|
|
|
|
}, [terrainResult, showPeaksAndValleys, showSlopeSectors, showNaturalObstacles, showRoadCorridors, showUrbanZones, showHazardousGround]);
|
|
|
|
|
|
|
|
|
|
// 1. Auto-run real mathematical IPB calculation when center or radius changes or when entering IPB mode
|
|
|
|
|
useEffect(() => {
|
|
|
|
@@ -1453,9 +1512,10 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
// Auto-run initial terrain study on mount & coordinates change
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
runTerrainStudy();
|
|
|
|
|
}, [terrainCenter, terrainRadius]);
|
|
|
|
|
}, [terrainCenter, terrainRadius, mapLoaded]);
|
|
|
|
|
|
|
|
|
|
const ensureViewshedSourceAndLayer = (m: maplibregl.Map) => {
|
|
|
|
|
if (!m) return;
|
|
|
|
|
if (!m.getSource('tactical-viewshed-src')) {
|
|
|
|
|
m.addSource('tactical-viewshed-src', {
|
|
|
|
|
type: 'geojson',
|
|
|
|
@@ -1468,8 +1528,8 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
type: 'fill',
|
|
|
|
|
source: 'tactical-viewshed-src',
|
|
|
|
|
paint: {
|
|
|
|
|
'fill-color': '#22c55e',
|
|
|
|
|
'fill-opacity': 0.35
|
|
|
|
|
'fill-color': '#ff0000',
|
|
|
|
|
'fill-opacity': 0.80
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
@@ -1479,15 +1539,17 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
type: 'line',
|
|
|
|
|
source: 'tactical-viewshed-src',
|
|
|
|
|
paint: {
|
|
|
|
|
'line-color': '#16a34a',
|
|
|
|
|
'line-width': 3,
|
|
|
|
|
'line-dasharray': [3, 2]
|
|
|
|
|
'line-color': '#ffff00',
|
|
|
|
|
'line-width': 8.0,
|
|
|
|
|
'line-opacity': 1.0
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
bringTacticalLayersToFront(m);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const ensureLosSourceAndLayer = (m: maplibregl.Map) => {
|
|
|
|
|
if (!m) return;
|
|
|
|
|
if (!m.getSource('tactical-los-src')) {
|
|
|
|
|
m.addSource('tactical-los-src', {
|
|
|
|
|
type: 'geojson',
|
|
|
|
@@ -1499,17 +1561,18 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
id: 'tactical-los-line',
|
|
|
|
|
type: 'line',
|
|
|
|
|
source: 'tactical-los-src',
|
|
|
|
|
filter: ['==', '$type', 'LineString'],
|
|
|
|
|
filter: ['==', ['geometry-type'], 'LineString'],
|
|
|
|
|
paint: {
|
|
|
|
|
'line-color': ['case', ['==', ['get', 'blocked'], true], '#ef4444', '#22c55e'],
|
|
|
|
|
'line-width': 4,
|
|
|
|
|
'line-dasharray': [2, 1]
|
|
|
|
|
'line-width': 5
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
bringTacticalLayersToFront(m);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const ensureArtillerySourceAndLayer = (m: maplibregl.Map) => {
|
|
|
|
|
if (!m) return;
|
|
|
|
|
if (!m.getSource('tactical-artillery-src')) {
|
|
|
|
|
m.addSource('tactical-artillery-src', {
|
|
|
|
|
type: 'geojson',
|
|
|
|
@@ -1523,13 +1586,14 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
source: 'tactical-artillery-src',
|
|
|
|
|
paint: {
|
|
|
|
|
'line-color': '#f97316',
|
|
|
|
|
'line-width': 4
|
|
|
|
|
'line-width': 5
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const ensureMinefieldSourceAndLayer = (m: maplibregl.Map) => {
|
|
|
|
|
if (!m) return;
|
|
|
|
|
if (!m.getSource('tactical-minefield-src')) {
|
|
|
|
|
m.addSource('tactical-minefield-src', {
|
|
|
|
|
type: 'geojson',
|
|
|
|
@@ -1543,8 +1607,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
source: 'tactical-minefield-src',
|
|
|
|
|
paint: {
|
|
|
|
|
'line-color': '#dc2626',
|
|
|
|
|
'line-width': 6,
|
|
|
|
|
'line-dasharray': [1, 1]
|
|
|
|
|
'line-width': 6
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
@@ -1600,11 +1663,12 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
type: 'FeatureCollection',
|
|
|
|
|
features: feats
|
|
|
|
|
});
|
|
|
|
|
bringTacticalLayersToFront(map.current);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch(() => setLosLoading(false));
|
|
|
|
|
}, [losPointA, losPointB, losObsHeight, losTgtHeight]);
|
|
|
|
|
}, [losPointA, losPointB, losObsHeight, losTgtHeight, mapLoaded]);
|
|
|
|
|
|
|
|
|
|
// Update Minefield Analysis & Map Layer
|
|
|
|
|
useEffect(() => {
|
|
|
|
@@ -1646,11 +1710,12 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
bringTacticalLayersToFront(map.current);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch(() => setMineLoading(false));
|
|
|
|
|
}, [mineStart, mineEnd, mineDensity]);
|
|
|
|
|
}, [mineStart, mineEnd, mineDensity, mapLoaded]);
|
|
|
|
|
|
|
|
|
|
// Update Artillery Trajectory
|
|
|
|
|
useEffect(() => {
|
|
|
|
@@ -1669,31 +1734,101 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
runArtilleryMission();
|
|
|
|
|
}, [gunPos, targetPos, caliber, muzzleVel]);
|
|
|
|
|
}, [gunPos, targetPos, caliber, muzzleVel, mapLoaded]);
|
|
|
|
|
|
|
|
|
|
// Update 360 Viewshed
|
|
|
|
|
const runViewshed360 = () => {
|
|
|
|
|
if (!viewshedCenter) return;
|
|
|
|
|
setViewshedLoading(true);
|
|
|
|
|
console.log('[VIEWSHED-DEBUG] Starting calculation for center:', viewshedCenter, 'height:', viewshedHeight, 'radius:', viewshedRadius);
|
|
|
|
|
calculateRadialViewshed(viewshedCenter[0], viewshedCenter[1], viewshedHeight, viewshedRadius, 36)
|
|
|
|
|
.then((res) => {
|
|
|
|
|
setViewshedResult(res);
|
|
|
|
|
setViewshedLoading(false);
|
|
|
|
|
console.log('[VIEWSHED-DEBUG] API response received. polygonGeoJson:', !!res.polygonGeoJson, 'visibleAreaKm2:', res.visibleAreaKm2, 'visiblePercentage:', res.visiblePercentage);
|
|
|
|
|
|
|
|
|
|
if (res.polygonGeoJson) {
|
|
|
|
|
const geom = (res.polygonGeoJson as any).geometry;
|
|
|
|
|
console.log('[VIEWSHED-DEBUG] Polygon geometry type:', geom?.type, 'coordinates rings:', geom?.coordinates?.length, 'ring[0] points:', geom?.coordinates?.[0]?.length);
|
|
|
|
|
if (geom?.coordinates?.[0]?.[0]) {
|
|
|
|
|
console.log('[VIEWSHED-DEBUG] First coord:', JSON.stringify(geom.coordinates[0][0]), 'Last coord:', JSON.stringify(geom.coordinates[0][geom.coordinates[0].length - 1]));
|
|
|
|
|
console.log('[VIEWSHED-DEBUG] Full polygon JSON (first 500 chars):', JSON.stringify(res.polygonGeoJson).substring(0, 500));
|
|
|
|
|
}
|
|
|
|
|
// Store on window for console inspection
|
|
|
|
|
(window as any).__viewshedPolygon = res.polygonGeoJson;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (map.current) {
|
|
|
|
|
console.log('[VIEWSHED-DEBUG] map.current exists. isStyleLoaded:', map.current.isStyleLoaded(), 'loaded:', map.current.loaded());
|
|
|
|
|
console.log('[VIEWSHED-DEBUG] Before ensure - src exists:', !!map.current.getSource('tactical-viewshed-src'), 'fill exists:', !!map.current.getLayer('tactical-viewshed-fill'), 'outline exists:', !!map.current.getLayer('tactical-viewshed-outline'));
|
|
|
|
|
|
|
|
|
|
ensureViewshedSourceAndLayer(map.current);
|
|
|
|
|
|
|
|
|
|
console.log('[VIEWSHED-DEBUG] After ensure - src exists:', !!map.current.getSource('tactical-viewshed-src'), 'fill exists:', !!map.current.getLayer('tactical-viewshed-fill'), 'outline exists:', !!map.current.getLayer('tactical-viewshed-outline'));
|
|
|
|
|
|
|
|
|
|
const src = map.current.getSource('tactical-viewshed-src') as maplibregl.GeoJSONSource;
|
|
|
|
|
if (src && res.polygonGeoJson) {
|
|
|
|
|
src.setData({
|
|
|
|
|
type: 'FeatureCollection',
|
|
|
|
|
const fc = {
|
|
|
|
|
type: 'FeatureCollection' as const,
|
|
|
|
|
features: [res.polygonGeoJson]
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
console.log('[VIEWSHED-DEBUG] Calling src.setData with FeatureCollection, features count:', fc.features.length);
|
|
|
|
|
src.setData(fc);
|
|
|
|
|
|
|
|
|
|
if (map.current.getLayer('tactical-viewshed-fill')) {
|
|
|
|
|
map.current.setLayoutProperty('tactical-viewshed-fill', 'visibility', 'visible');
|
|
|
|
|
map.current.setPaintProperty('tactical-viewshed-fill', 'fill-color', '#ff0000');
|
|
|
|
|
map.current.setPaintProperty('tactical-viewshed-fill', 'fill-opacity', 0.80);
|
|
|
|
|
console.log('[VIEWSHED-DEBUG] Set tactical-viewshed-fill visible, RED, opacity 0.80');
|
|
|
|
|
}
|
|
|
|
|
if (map.current.getLayer('tactical-viewshed-outline')) {
|
|
|
|
|
map.current.setLayoutProperty('tactical-viewshed-outline', 'visibility', 'visible');
|
|
|
|
|
console.log('[VIEWSHED-DEBUG] Set tactical-viewshed-outline visible');
|
|
|
|
|
}
|
|
|
|
|
bringTacticalLayersToFront(map.current);
|
|
|
|
|
|
|
|
|
|
// Log full layer order for debugging
|
|
|
|
|
const allLayerIds = map.current.getStyle().layers.map((l: any) => l.id);
|
|
|
|
|
const vsIdx = allLayerIds.indexOf('tactical-viewshed-fill');
|
|
|
|
|
console.log('[VIEWSHED-DEBUG] Layer order - viewshed-fill at index:', vsIdx, '/', allLayerIds.length, 'last 10 layers:', allLayerIds.slice(-10).join(', '));
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const ring = (res.polygonGeoJson.geometry as any)?.coordinates?.[0];
|
|
|
|
|
if (ring && ring.length > 2) {
|
|
|
|
|
const lngs = ring.map((c: number[]) => c[0]);
|
|
|
|
|
const lats = ring.map((c: number[]) => c[1]);
|
|
|
|
|
const bounds: [[number, number], [number, number]] = [
|
|
|
|
|
[Math.min(...lngs), Math.min(...lats)],
|
|
|
|
|
[Math.max(...lngs), Math.max(...lats)]
|
|
|
|
|
];
|
|
|
|
|
console.log('[VIEWSHED-DEBUG] fitBounds SW:', JSON.stringify(bounds[0]), 'NE:', JSON.stringify(bounds[1]));
|
|
|
|
|
map.current.fitBounds(bounds, { padding: 80, maxZoom: 14 });
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('[VIEWSHED-DEBUG] Ring too short for fitBounds:', ring?.length);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('[VIEWSHED-DEBUG] fitBounds error:', e);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('[VIEWSHED-DEBUG] Missing src or polygonGeoJson. src:', !!src, 'polygonGeoJson:', !!res.polygonGeoJson);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('[VIEWSHED-DEBUG] map.current is null');
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch(() => setViewshedLoading(false));
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
console.error('[VIEWSHED-DEBUG] Calculation error:', err);
|
|
|
|
|
setViewshedLoading(false);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Auto-run Viewshed when entering viewshed mode or center changes
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (mode === 'viewshed' && viewshedCenter && mapLoaded) {
|
|
|
|
|
runViewshed360();
|
|
|
|
|
}
|
|
|
|
|
}, [mode, viewshedCenter, viewshedHeight, viewshedRadius, mapLoaded]);
|
|
|
|
|
|
|
|
|
|
// Run Artillery Fire Mission
|
|
|
|
|
const runArtilleryMission = async () => {
|
|
|
|
|
if (!gunPos || !targetPos) return;
|
|
|
|
@@ -1717,18 +1852,23 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
setArtilleryResult(data);
|
|
|
|
|
|
|
|
|
|
if (map.current && map.current.getSource('tactical-artillery-src')) {
|
|
|
|
|
const lineCoords = (data.trajectoryPoints || []).map((p: any) => [p.lng, p.lat]);
|
|
|
|
|
(map.current.getSource('tactical-artillery-src') as maplibregl.GeoJSONSource).setData({
|
|
|
|
|
type: 'FeatureCollection',
|
|
|
|
|
features: [
|
|
|
|
|
{
|
|
|
|
|
type: 'Feature',
|
|
|
|
|
properties: {},
|
|
|
|
|
geometry: { type: 'LineString', coordinates: lineCoords }
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
if (map.current) {
|
|
|
|
|
ensureArtillerySourceAndLayer(map.current);
|
|
|
|
|
const src = map.current.getSource('tactical-artillery-src') as maplibregl.GeoJSONSource;
|
|
|
|
|
if (src) {
|
|
|
|
|
const lineCoords = (data.trajectoryPoints || []).map((p: any) => [p.lng, p.lat]);
|
|
|
|
|
src.setData({
|
|
|
|
|
type: 'FeatureCollection',
|
|
|
|
|
features: [
|
|
|
|
|
{
|
|
|
|
|
type: 'Feature',
|
|
|
|
|
properties: {},
|
|
|
|
|
geometry: { type: 'LineString', coordinates: lineCoords }
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
bringTacticalLayersToFront(map.current);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Artillery calculation error:', e);
|
|
|
|
@@ -1816,7 +1956,25 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
if (!map.current || !map.current.getSource('tactical-isochrone-src')) return;
|
|
|
|
|
if (mode === 'isochrone' && isochroneData?.featureCollection) {
|
|
|
|
|
(map.current.getSource('tactical-isochrone-src') as maplibregl.GeoJSONSource).setData(isochroneData.featureCollection);
|
|
|
|
|
} else {
|
|
|
|
|
bringTacticalLayersToFront(map.current);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const allCoords: [number, number][] = [];
|
|
|
|
|
isochroneData.featureCollection.features.forEach((feat: any) => {
|
|
|
|
|
if (feat.geometry?.coordinates?.[0]) {
|
|
|
|
|
allCoords.push(...feat.geometry.coordinates[0]);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
if (allCoords.length > 0) {
|
|
|
|
|
const lngs = allCoords.map((c: [number, number]) => c[0]);
|
|
|
|
|
const lats = allCoords.map((c: [number, number]) => c[1]);
|
|
|
|
|
map.current.fitBounds(
|
|
|
|
|
[[Math.min(...lngs), Math.min(...lats)], [Math.max(...lngs), Math.max(...lats)]],
|
|
|
|
|
{ padding: 80, maxZoom: 13.5 }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} catch {}
|
|
|
|
|
} else if (mode !== 'isochrone') {
|
|
|
|
|
(map.current.getSource('tactical-isochrone-src') as maplibregl.GeoJSONSource).setData({
|
|
|
|
|
type: 'FeatureCollection',
|
|
|
|
|
features: []
|
|
|
|
@@ -1824,6 +1982,13 @@ export const TacticalDefenseView: React.FC = () => {
|
|
|
|
|
}
|
|
|
|
|
}, [mode, isochroneData]);
|
|
|
|
|
|
|
|
|
|
// Auto-run Isochrone calculation when entering mode or center/profile/times change
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (mode === 'isochrone' && isochroneCenter && mapLoaded) {
|
|
|
|
|
runIsochroneCalculation(isochroneCenter, isochroneProfile, isochroneTimes);
|
|
|
|
|
}
|
|
|
|
|
}, [mode, isochroneCenter, isochroneProfile, isochroneTimes, mapLoaded]);
|
|
|
|
|
|
|
|
|
|
// Run Isochrone Calculation
|
|
|
|
|
const runIsochroneCalculation = async (center = isochroneCenter, profile = isochroneProfile, times = isochroneTimes) => {
|
|
|
|
|
if (!center) return;
|
|
|
|
|