fix(tactical): resolve MapLibre line-dasharray crash and add self-healing layers for viewshed, LOS, and artillery

This commit is contained in:
Hamza-Ayed
2026-09-24 13:23:38 +03:00
parent 8dbb336e0a
commit 043cd052b6
2 changed files with 166 additions and 53 deletions
+131 -21
View File
@@ -1070,15 +1070,6 @@ export const TacticalDefenseView: React.FC = () => {
'wadi', 3.5, 'wadi', 3.5,
'ridge', 2.5, 'ridge', 2.5,
2.5 2.5
],
'line-dasharray': [
'match',
['get', 'layerType'],
'cliff', ['literal', [2, 1]],
'ridge', ['literal', [3, 1.5]],
'road', ['literal', [1, 0]],
'wadi', ['literal', [1, 0]],
['literal', [1, 0]]
] ]
} }
}); });
@@ -1464,15 +1455,114 @@ export const TacticalDefenseView: React.FC = () => {
runTerrainStudy(); runTerrainStudy();
}, [terrainCenter, terrainRadius]); }, [terrainCenter, terrainRadius]);
const ensureViewshedSourceAndLayer = (m: maplibregl.Map) => {
if (!m.getSource('tactical-viewshed-src')) {
m.addSource('tactical-viewshed-src', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] }
});
}
if (!m.getLayer('tactical-viewshed-fill')) {
m.addLayer({
id: 'tactical-viewshed-fill',
type: 'fill',
source: 'tactical-viewshed-src',
paint: {
'fill-color': '#22c55e',
'fill-opacity': 0.35
}
});
}
if (!m.getLayer('tactical-viewshed-outline')) {
m.addLayer({
id: 'tactical-viewshed-outline',
type: 'line',
source: 'tactical-viewshed-src',
paint: {
'line-color': '#16a34a',
'line-width': 3,
'line-dasharray': [3, 2]
}
});
}
};
const ensureLosSourceAndLayer = (m: maplibregl.Map) => {
if (!m.getSource('tactical-los-src')) {
m.addSource('tactical-los-src', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] }
});
}
if (!m.getLayer('tactical-los-line')) {
m.addLayer({
id: 'tactical-los-line',
type: 'line',
source: 'tactical-los-src',
filter: ['==', '$type', 'LineString'],
paint: {
'line-color': ['case', ['==', ['get', 'blocked'], true], '#ef4444', '#22c55e'],
'line-width': 4,
'line-dasharray': [2, 1]
}
});
}
};
const ensureArtillerySourceAndLayer = (m: maplibregl.Map) => {
if (!m.getSource('tactical-artillery-src')) {
m.addSource('tactical-artillery-src', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] }
});
}
if (!m.getLayer('tactical-artillery-line')) {
m.addLayer({
id: 'tactical-artillery-line',
type: 'line',
source: 'tactical-artillery-src',
paint: {
'line-color': '#f97316',
'line-width': 4
}
});
}
};
const ensureMinefieldSourceAndLayer = (m: maplibregl.Map) => {
if (!m.getSource('tactical-minefield-src')) {
m.addSource('tactical-minefield-src', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] }
});
}
if (!m.getLayer('tactical-minefield-line')) {
m.addLayer({
id: 'tactical-minefield-line',
type: 'line',
source: 'tactical-minefield-src',
paint: {
'line-color': '#dc2626',
'line-width': 6,
'line-dasharray': [1, 1]
}
});
}
};
// Update Line of Sight Calculations & Map Layer // Update Line of Sight Calculations & Map Layer
useEffect(() => { useEffect(() => {
if (!losPointA || !losPointB) { if (!losPointA || !losPointB) {
if (map.current && map.current.getSource('tactical-los-src')) { if (map.current) {
(map.current.getSource('tactical-los-src') as maplibregl.GeoJSONSource).setData({ ensureLosSourceAndLayer(map.current);
const src = map.current.getSource('tactical-los-src') as maplibregl.GeoJSONSource;
if (src) {
src.setData({
type: 'FeatureCollection', type: 'FeatureCollection',
features: [] features: []
}); });
} }
}
setLosResult(null); setLosResult(null);
return; return;
} }
@@ -1483,7 +1573,8 @@ export const TacticalDefenseView: React.FC = () => {
setLosResult(res); setLosResult(res);
setLosLoading(false); setLosLoading(false);
if (map.current && map.current.getSource('tactical-los-src')) { if (map.current) {
ensureLosSourceAndLayer(map.current);
const feats: any[] = [ const feats: any[] = [
{ {
type: 'Feature', type: 'Feature',
@@ -1503,11 +1594,14 @@ export const TacticalDefenseView: React.FC = () => {
}); });
} }
(map.current.getSource('tactical-los-src') as maplibregl.GeoJSONSource).setData({ const src = map.current.getSource('tactical-los-src') as maplibregl.GeoJSONSource;
if (src) {
src.setData({
type: 'FeatureCollection', type: 'FeatureCollection',
features: feats features: feats
}); });
} }
}
}) })
.catch(() => setLosLoading(false)); .catch(() => setLosLoading(false));
}, [losPointA, losPointB, losObsHeight, losTgtHeight]); }, [losPointA, losPointB, losObsHeight, losTgtHeight]);
@@ -1515,12 +1609,16 @@ export const TacticalDefenseView: React.FC = () => {
// Update Minefield Analysis & Map Layer // Update Minefield Analysis & Map Layer
useEffect(() => { useEffect(() => {
if (!mineStart || !mineEnd) { if (!mineStart || !mineEnd) {
if (map.current && map.current.getSource('tactical-minefield-src')) { if (map.current) {
(map.current.getSource('tactical-minefield-src') as maplibregl.GeoJSONSource).setData({ ensureMinefieldSourceAndLayer(map.current);
const src = map.current.getSource('tactical-minefield-src') as maplibregl.GeoJSONSource;
if (src) {
src.setData({
type: 'FeatureCollection', type: 'FeatureCollection',
features: [] features: []
}); });
} }
}
setMineResult(null); setMineResult(null);
return; return;
} }
@@ -1531,8 +1629,11 @@ export const TacticalDefenseView: React.FC = () => {
setMineResult(res); setMineResult(res);
setMineLoading(false); setMineLoading(false);
if (map.current && map.current.getSource('tactical-minefield-src')) { if (map.current) {
(map.current.getSource('tactical-minefield-src') as maplibregl.GeoJSONSource).setData({ ensureMinefieldSourceAndLayer(map.current);
const src = map.current.getSource('tactical-minefield-src') as maplibregl.GeoJSONSource;
if (src) {
src.setData({
type: 'FeatureCollection', type: 'FeatureCollection',
features: [ features: [
{ {
@@ -1546,6 +1647,7 @@ export const TacticalDefenseView: React.FC = () => {
] ]
}); });
} }
}
}) })
.catch(() => setMineLoading(false)); .catch(() => setMineLoading(false));
}, [mineStart, mineEnd, mineDensity]); }, [mineStart, mineEnd, mineDensity]);
@@ -1553,12 +1655,16 @@ export const TacticalDefenseView: React.FC = () => {
// Update Artillery Trajectory // Update Artillery Trajectory
useEffect(() => { useEffect(() => {
if (!gunPos || !targetPos) { if (!gunPos || !targetPos) {
if (map.current && map.current.getSource('tactical-artillery-src')) { if (map.current) {
(map.current.getSource('tactical-artillery-src') as maplibregl.GeoJSONSource).setData({ ensureArtillerySourceAndLayer(map.current);
const src = map.current.getSource('tactical-artillery-src') as maplibregl.GeoJSONSource;
if (src) {
src.setData({
type: 'FeatureCollection', type: 'FeatureCollection',
features: [] features: []
}); });
} }
}
setArtilleryResult(null); setArtilleryResult(null);
return; return;
} }
@@ -1574,12 +1680,16 @@ export const TacticalDefenseView: React.FC = () => {
setViewshedResult(res); setViewshedResult(res);
setViewshedLoading(false); setViewshedLoading(false);
if (map.current && map.current.getSource('tactical-viewshed-src')) { if (map.current) {
(map.current.getSource('tactical-viewshed-src') as maplibregl.GeoJSONSource).setData({ ensureViewshedSourceAndLayer(map.current);
const src = map.current.getSource('tactical-viewshed-src') as maplibregl.GeoJSONSource;
if (src && res.polygonGeoJson) {
src.setData({
type: 'FeatureCollection', type: 'FeatureCollection',
features: [res.polygonGeoJson] features: [res.polygonGeoJson]
}); });
} }
}
}) })
.catch(() => setViewshedLoading(false)); .catch(() => setViewshedLoading(false));
}; };
+9 -6
View File
@@ -167,9 +167,10 @@ export async function sampleElevationsBatch(
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 2500); const timeoutId = setTimeout(() => controller.abort(), 2500);
const res = await fetch(`${apiUrl}/tactical/elevations`, { const apiKey = localStorage.getItem('map_admin_key') || localStorage.getItem('intaleq_api_key') || (import.meta as any).env.VITE_ADMIN_API_KEY || (import.meta as any).env.VITE_API_KEY || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX';
const res = await fetch(`${apiUrl}/tactical/elevations?key=${encodeURIComponent(apiKey)}`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
body: JSON.stringify({ coordinates: coords }), body: JSON.stringify({ coordinates: coords }),
signal: controller.signal signal: controller.signal
}); });
@@ -303,13 +304,13 @@ export async function calculateLineOfSight(
// Try fetching high-precision result from Backend Tactical API // Try fetching high-precision result from Backend Tactical API
try { try {
const apiUrl = (import.meta as any).env.VITE_API_URL || '/api'; const apiUrl = (import.meta as any).env.VITE_API_URL || '/api';
const apiKey = (import.meta as any).env.VITE_API_KEY; const apiKey = (import.meta as any).env.VITE_ADMIN_API_KEY || (import.meta as any).env.VITE_API_KEY || localStorage.getItem('map_admin_key') || localStorage.getItem('intaleq_api_key') || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX';
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 8000); const timeoutId = setTimeout(() => controller.abort(), 8000);
const res = await fetch(`${apiUrl}/tactical/line-of-sight?observerLat=${startLat}&observerLng=${startLng}&targetLat=${endLat}&targetLng=${endLng}&observerHeight=${obsHeight}&targetHeight=${tgtHeight}&samples=${samples}`, { const res = await fetch(`${apiUrl}/tactical/line-of-sight?observerLat=${startLat}&observerLng=${startLng}&targetLat=${endLat}&targetLng=${endLng}&observerHeight=${obsHeight}&targetHeight=${tgtHeight}&samples=${samples}&key=${encodeURIComponent(apiKey)}`, {
headers: apiKey ? { 'x-api-key': apiKey } : {}, headers: { 'x-api-key': apiKey },
signal: controller.signal signal: controller.signal
}); });
clearTimeout(timeoutId); clearTimeout(timeoutId);
@@ -490,7 +491,9 @@ export async function calculateRadialViewshed(
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3500); const timeoutId = setTimeout(() => controller.abort(), 3500);
const res = await fetch(`${apiUrl}/tactical/viewshed?lat=${centerLat}&lng=${centerLng}&height=${obsHeight}&radius=${radiusMeters}&rays=${numRays}`, { const apiKey = (import.meta as any).env.VITE_ADMIN_API_KEY || (import.meta as any).env.VITE_API_KEY || localStorage.getItem('map_admin_key') || localStorage.getItem('intaleq_api_key') || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX';
const res = await fetch(`${apiUrl}/tactical/viewshed?lat=${centerLat}&lng=${centerLng}&height=${obsHeight}&radius=${radiusMeters}&rays=${numRays}&key=${encodeURIComponent(apiKey)}`, {
headers: { 'x-api-key': apiKey },
signal: controller.signal signal: controller.signal
}); });
clearTimeout(timeoutId); clearTimeout(timeoutId);