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
+157 -47
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,14 +1455,113 @@ 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);
type: 'FeatureCollection', const src = map.current.getSource('tactical-los-src') as maplibregl.GeoJSONSource;
features: [] if (src) {
}); src.setData({
type: 'FeatureCollection',
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,10 +1594,13 @@ 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;
type: 'FeatureCollection', if (src) {
features: feats src.setData({
}); type: 'FeatureCollection',
features: feats
});
}
} }
}) })
.catch(() => setLosLoading(false)); .catch(() => setLosLoading(false));
@@ -1515,11 +1609,15 @@ 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);
type: 'FeatureCollection', const src = map.current.getSource('tactical-minefield-src') as maplibregl.GeoJSONSource;
features: [] if (src) {
}); src.setData({
type: 'FeatureCollection',
features: []
});
}
} }
setMineResult(null); setMineResult(null);
return; return;
@@ -1531,20 +1629,24 @@ 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);
type: 'FeatureCollection', const src = map.current.getSource('tactical-minefield-src') as maplibregl.GeoJSONSource;
features: [ if (src) {
{ src.setData({
type: 'Feature', type: 'FeatureCollection',
properties: {}, features: [
geometry: { {
type: 'LineString', type: 'Feature',
coordinates: [[mineStart[1], mineStart[0]], [mineEnd[1], mineEnd[0]]] properties: {},
geometry: {
type: 'LineString',
coordinates: [[mineStart[1], mineStart[0]], [mineEnd[1], mineEnd[0]]]
}
} }
} ]
] });
}); }
} }
}) })
.catch(() => setMineLoading(false)); .catch(() => setMineLoading(false));
@@ -1553,11 +1655,15 @@ 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);
type: 'FeatureCollection', const src = map.current.getSource('tactical-artillery-src') as maplibregl.GeoJSONSource;
features: [] if (src) {
}); src.setData({
type: 'FeatureCollection',
features: []
});
}
} }
setArtilleryResult(null); setArtilleryResult(null);
return; return;
@@ -1574,11 +1680,15 @@ 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);
type: 'FeatureCollection', const src = map.current.getSource('tactical-viewshed-src') as maplibregl.GeoJSONSource;
features: [res.polygonGeoJson] if (src && res.polygonGeoJson) {
}); src.setData({
type: 'FeatureCollection',
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);