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,
'ridge', 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();
}, [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
useEffect(() => {
if (!losPointA || !losPointB) {
if (map.current && map.current.getSource('tactical-los-src')) {
(map.current.getSource('tactical-los-src') as maplibregl.GeoJSONSource).setData({
type: 'FeatureCollection',
features: []
});
if (map.current) {
ensureLosSourceAndLayer(map.current);
const src = map.current.getSource('tactical-los-src') as maplibregl.GeoJSONSource;
if (src) {
src.setData({
type: 'FeatureCollection',
features: []
});
}
}
setLosResult(null);
return;
@@ -1483,7 +1573,8 @@ export const TacticalDefenseView: React.FC = () => {
setLosResult(res);
setLosLoading(false);
if (map.current && map.current.getSource('tactical-los-src')) {
if (map.current) {
ensureLosSourceAndLayer(map.current);
const feats: any[] = [
{
type: 'Feature',
@@ -1503,10 +1594,13 @@ export const TacticalDefenseView: React.FC = () => {
});
}
(map.current.getSource('tactical-los-src') as maplibregl.GeoJSONSource).setData({
type: 'FeatureCollection',
features: feats
});
const src = map.current.getSource('tactical-los-src') as maplibregl.GeoJSONSource;
if (src) {
src.setData({
type: 'FeatureCollection',
features: feats
});
}
}
})
.catch(() => setLosLoading(false));
@@ -1515,11 +1609,15 @@ export const TacticalDefenseView: React.FC = () => {
// Update Minefield Analysis & Map Layer
useEffect(() => {
if (!mineStart || !mineEnd) {
if (map.current && map.current.getSource('tactical-minefield-src')) {
(map.current.getSource('tactical-minefield-src') as maplibregl.GeoJSONSource).setData({
type: 'FeatureCollection',
features: []
});
if (map.current) {
ensureMinefieldSourceAndLayer(map.current);
const src = map.current.getSource('tactical-minefield-src') as maplibregl.GeoJSONSource;
if (src) {
src.setData({
type: 'FeatureCollection',
features: []
});
}
}
setMineResult(null);
return;
@@ -1531,20 +1629,24 @@ export const TacticalDefenseView: React.FC = () => {
setMineResult(res);
setMineLoading(false);
if (map.current && map.current.getSource('tactical-minefield-src')) {
(map.current.getSource('tactical-minefield-src') as maplibregl.GeoJSONSource).setData({
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates: [[mineStart[1], mineStart[0]], [mineEnd[1], mineEnd[0]]]
if (map.current) {
ensureMinefieldSourceAndLayer(map.current);
const src = map.current.getSource('tactical-minefield-src') as maplibregl.GeoJSONSource;
if (src) {
src.setData({
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates: [[mineStart[1], mineStart[0]], [mineEnd[1], mineEnd[0]]]
}
}
}
]
});
]
});
}
}
})
.catch(() => setMineLoading(false));
@@ -1553,11 +1655,15 @@ export const TacticalDefenseView: React.FC = () => {
// Update Artillery Trajectory
useEffect(() => {
if (!gunPos || !targetPos) {
if (map.current && map.current.getSource('tactical-artillery-src')) {
(map.current.getSource('tactical-artillery-src') as maplibregl.GeoJSONSource).setData({
type: 'FeatureCollection',
features: []
});
if (map.current) {
ensureArtillerySourceAndLayer(map.current);
const src = map.current.getSource('tactical-artillery-src') as maplibregl.GeoJSONSource;
if (src) {
src.setData({
type: 'FeatureCollection',
features: []
});
}
}
setArtilleryResult(null);
return;
@@ -1574,11 +1680,15 @@ export const TacticalDefenseView: React.FC = () => {
setViewshedResult(res);
setViewshedLoading(false);
if (map.current && map.current.getSource('tactical-viewshed-src')) {
(map.current.getSource('tactical-viewshed-src') as maplibregl.GeoJSONSource).setData({
type: 'FeatureCollection',
features: [res.polygonGeoJson]
});
if (map.current) {
ensureViewshedSourceAndLayer(map.current);
const src = map.current.getSource('tactical-viewshed-src') as maplibregl.GeoJSONSource;
if (src && res.polygonGeoJson) {
src.setData({
type: 'FeatureCollection',
features: [res.polygonGeoJson]
});
}
}
})
.catch(() => setViewshedLoading(false));
+9 -6
View File
@@ -167,9 +167,10 @@ export async function sampleElevationsBatch(
const controller = new AbortController();
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',
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
body: JSON.stringify({ coordinates: coords }),
signal: controller.signal
});
@@ -303,13 +304,13 @@ export async function calculateLineOfSight(
// Try fetching high-precision result from Backend Tactical API
try {
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 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}`, {
headers: apiKey ? { 'x-api-key': apiKey } : {},
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: { 'x-api-key': apiKey },
signal: controller.signal
});
clearTimeout(timeoutId);
@@ -490,7 +491,9 @@ export async function calculateRadialViewshed(
const controller = new AbortController();
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
});
clearTimeout(timeoutId);