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,
|
'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));
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user