fix(tactical): resolve MapLibre line-dasharray crash and add self-healing layers for viewshed, LOS, and artillery
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user