fix: resolve MapLibre worker rendering regression by configuring custom worker and updating geometry type filters

This commit is contained in:
Hamza-Ayed
2026-09-24 18:49:40 +03:00
parent 043cd052b6
commit cb91e5f0de
6 changed files with 497 additions and 274 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ export class AdminGuard implements CanActivate {
throw new UnauthorizedException('Tenant not found in request');
}
if (tenant.plan !== TenantPlan.ENTERPRISE && tenant.role !== TenantRole.ADMIN) {
if (tenant.plan !== TenantPlan.ENTERPRISE && tenant.role !== 'ADMIN') {
throw new ForbiddenException('Admin access required. Your tenant must have an ENTERPRISE plan or ADMIN clearance.');
}
+3 -3
View File
@@ -198,7 +198,7 @@ const MapComponent: React.FC<MapProps> = ({
id: 'los-line',
type: 'line',
source: 'los-source',
filter: ['==', '$type', 'LineString'],
filter: ['==', ['geometry-type'], 'LineString'],
layout: { 'line-cap': 'round', 'line-join': 'round' },
paint: {
'line-color': '#f59e0b',
@@ -211,7 +211,7 @@ const MapComponent: React.FC<MapProps> = ({
id: 'los-points',
type: 'circle',
source: 'los-source',
filter: ['==', '$type', 'Point'],
filter: ['==', ['geometry-type'], 'Point'],
paint: {
'circle-radius': 8,
'circle-color': [
@@ -230,7 +230,7 @@ const MapComponent: React.FC<MapProps> = ({
id: 'los-labels',
type: 'symbol',
source: 'los-source',
filter: ['==', '$type', 'Point'],
filter: ['==', ['geometry-type'], 'Point'],
layout: {
'text-field': ['get', 'label'],
'text-size': 12,
+1 -1
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'
import ReactDOM from 'react-dom/client'
import maplibregl from 'maplibre-gl'
import maplibregl from './utils/maplibreWorker'
import App from './App.tsx'
import CompareView from './pages/CompareView'
import IntelligenceDashboard from './pages/IntelligenceDashboard'
+284 -119
View File
@@ -176,6 +176,8 @@ export const TacticalDefenseView: React.FC = () => {
};
const [mode, setMode] = useState<TacticalMode>('terrain');
const [mapLoaded, setMapLoaded] = useState<boolean>(false);
const mapLoadedRef = useRef<boolean>(false);
const [cursorPos, setCursorPos] = useState({ lat: 31.95, lng: 35.93, elev: 850 });
const [activePlacement, setActivePlacement] = useState<string | null>(null);
@@ -364,6 +366,38 @@ export const TacticalDefenseView: React.FC = () => {
});
};
const bringTacticalLayersToFront = (m: maplibregl.Map | null) => {
if (!m) return;
const tacticalLayerOrder = [
'tactical-terrain-fill',
'tactical-terrain-line',
'tactical-isochrone-fill',
'tactical-isochrone-line',
'tactical-viewshed-fill',
'tactical-viewshed-outline',
'tactical-terrain-spatial-poly-fill',
'tactical-terrain-spatial-poly-line',
'tactical-terrain-spatial-lines',
'tactical-terrain-spatial-points',
'tactical-terrain-spatial-labels',
'tactical-ipb-fill',
'tactical-ipb-line',
'tactical-ipb-symbol',
'tactical-los-line',
'tactical-artillery-line',
'tactical-minefield-line',
'tactical-symbols-pts',
'tactical-symbols-lbls'
];
tacticalLayerOrder.forEach((layerId) => {
if (m.getLayer(layerId)) {
// try {
// m.moveLayer(layerId);
// } catch {}
}
});
};
const handlePositionFoundByResection = (lat: number, lng: number, observations: ResectionObservation[]) => {
setResectionPosition([lat, lng]);
if (map.current) {
@@ -410,11 +444,10 @@ export const TacticalDefenseView: React.FC = () => {
id: 'resection-lines',
type: 'line',
source: 'resection-source',
filter: ['==', '$type', 'LineString'],
filter: ['==', ['geometry-type'], 'LineString'],
paint: {
'line-color': '#00f0ff',
'line-width': 3,
'line-dasharray': [3, 2]
'line-width': 3
}
});
@@ -422,7 +455,7 @@ export const TacticalDefenseView: React.FC = () => {
id: 'resection-points',
type: 'circle',
source: 'resection-source',
filter: ['==', '$type', 'Point'],
filter: ['==', ['geometry-type'], 'Point'],
paint: {
'circle-radius': 9,
'circle-color': '#22c55e',
@@ -450,7 +483,7 @@ export const TacticalDefenseView: React.FC = () => {
id: fillLayerId,
type: 'fill',
source: sourceId,
filter: ['==', '$type', 'Polygon'],
filter: ['==', ['geometry-type'], 'Polygon'],
paint: {
'fill-color': '#6366f1',
'fill-opacity': 0.45
@@ -462,7 +495,7 @@ export const TacticalDefenseView: React.FC = () => {
id: lineLayerId,
type: 'line',
source: sourceId,
filter: ['any', ['==', '$type', 'LineString'], ['==', '$type', 'Polygon']],
filter: ['any', ['==', ['geometry-type'], 'LineString'], ['==', ['geometry-type'], 'Polygon']],
paint: {
'line-color': '#38bdf8',
'line-width': 2.5
@@ -474,7 +507,7 @@ export const TacticalDefenseView: React.FC = () => {
id: circleLayerId,
type: 'circle',
source: sourceId,
filter: ['==', '$type', 'Point'],
filter: ['==', ['geometry-type'], 'Point'],
paint: {
'circle-radius': 7,
'circle-color': '#e11d48',
@@ -679,7 +712,7 @@ export const TacticalDefenseView: React.FC = () => {
const initialMap = new maplibregl.Map({
container: mapContainer.current,
style: '/tactical-style.json',
style: '/style.json',
center: [35.9106, 31.9539],
zoom: 11,
pitch: 35,
@@ -829,30 +862,46 @@ export const TacticalDefenseView: React.FC = () => {
});
initialMap.on('load', () => {
const safeAddSource = (id: string, source: any) => {
if (!initialMap.getSource(id)) {
initialMap.addSource(id, source);
}
};
const safeAddLayer = (layer: any, beforeId?: string) => {
if (!initialMap.getLayer(layer.id)) {
if (beforeId && initialMap.getLayer(beforeId)) {
initialMap.addLayer(layer, beforeId);
} else {
initialMap.addLayer(layer);
}
}
};
// 0. Terrain Study Circle & Spatial Layers
initialMap.addSource('tactical-terrain-src', {
safeAddSource('tactical-terrain-src', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] }
});
initialMap.addLayer({
safeAddLayer({
id: 'tactical-terrain-fill',
type: 'fill',
source: 'tactical-terrain-src',
paint: {
'fill-color': '#6366f1',
'fill-opacity': 0.08
'fill-opacity': 0.18
}
});
initialMap.addLayer({
safeAddLayer({
id: 'tactical-terrain-line',
type: 'line',
source: 'tactical-terrain-src',
paint: {
'line-color': '#818cf8',
'line-width': 2.5,
'line-dasharray': [3, 2]
'line-color': '#4f46e5',
'line-width': 3.5,
'line-opacity': 0.95
}
});
@@ -1010,184 +1059,182 @@ export const TacticalDefenseView: React.FC = () => {
}
// Rich Spatial Feature Layers for Terrain Study (Slope, Wadis, Roads, Urban, Hazards)
initialMap.addSource('tactical-terrain-spatial-src', {
safeAddSource('tactical-terrain-spatial-src', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] }
});
initialMap.addLayer({
safeAddLayer({
id: 'tactical-terrain-spatial-poly-fill',
type: 'fill',
source: 'tactical-terrain-spatial-src',
filter: ['==', '$type', 'Polygon'],
filter: ['==', ['geometry-type'], 'Polygon'],
paint: {
'fill-color': ['get', 'color'],
'fill-opacity': [
'match',
['get', 'layerType'],
'cliff', 0.38,
'urban', 0.20,
'hazard', 0.24,
'slope-sector', 0.14,
0.15
'cliff', 0.45,
'urban', 0.30,
'hazard', 0.35,
'slope-sector', 0.22,
0.20
]
}
});
initialMap.addLayer({
safeAddLayer({
id: 'tactical-terrain-spatial-poly-line',
type: 'line',
source: 'tactical-terrain-spatial-src',
filter: ['==', '$type', 'Polygon'],
filter: ['==', ['geometry-type'], 'Polygon'],
paint: {
'line-color': [
'match',
['get', 'layerType'],
'cliff', '#ef4444',
'rgba(255, 255, 255, 0.10)'
'rgba(255, 255, 255, 0.4)'
],
'line-width': [
'match',
['get', 'layerType'],
'cliff', 1.8,
0.4
'cliff', 2.5,
1.0
]
}
});
initialMap.addLayer({
safeAddLayer({
id: 'tactical-terrain-spatial-lines',
type: 'line',
source: 'tactical-terrain-spatial-src',
filter: ['==', '$type', 'LineString'],
filter: ['==', ['geometry-type'], 'LineString'],
paint: {
'line-color': ['get', 'color'],
'line-width': [
'match',
['get', 'layerType'],
'cliff', 3.5,
'road', 4.0,
'wadi', 3.5,
'ridge', 2.5,
2.5
'cliff', 4.0,
'road', 4.5,
'wadi', 4.5,
'ridge', 3.5,
3.0
]
}
});
initialMap.addLayer({
safeAddLayer({
id: 'tactical-terrain-spatial-points',
type: 'circle',
source: 'tactical-terrain-spatial-src',
filter: ['==', '$type', 'Point'],
filter: ['==', ['geometry-type'], 'Point'],
paint: {
'circle-radius': 5,
'circle-radius': 7,
'circle-color': ['coalesce', ['get', 'color'], '#38bdf8'],
'circle-stroke-width': 2,
'circle-stroke-width': 2.5,
'circle-stroke-color': '#ffffff'
}
});
initialMap.addLayer({
safeAddLayer({
id: 'tactical-terrain-spatial-labels',
type: 'symbol',
source: 'tactical-terrain-spatial-src',
filter: ['all', ['==', '$type', 'Point'], ['has', 'title']],
filter: ['all', ['==', ['geometry-type'], 'Point'], ['has', 'title']],
layout: {
'text-field': ['get', 'title'],
'text-size': 12,
'text-font': ['Noto Sans Bold', 'Open Sans Bold'],
'text-font': ['Noto Sans Regular'],
'text-anchor': 'bottom',
'text-offset': [0, -0.6],
'text-offset': [0, -0.8],
'symbol-placement': 'point',
'text-allow-overlap': false
'text-allow-overlap': true
},
paint: {
'text-color': '#ffffff',
'text-halo-color': '#0f172a',
'text-halo-width': 2.5
'text-halo-width': 3
}
});
// 1. Line of Sight Layer
initialMap.addSource('tactical-los-src', {
safeAddSource('tactical-los-src', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] }
});
initialMap.addLayer({
safeAddLayer({
id: 'tactical-los-line',
type: 'line',
source: 'tactical-los-src',
filter: ['==', '$type', 'LineString'],
filter: ['==', ['geometry-type'], 'LineString'],
paint: {
'line-color': ['case', ['==', ['get', 'blocked'], true], '#ef4444', '#22c55e'],
'line-width': 4,
'line-dasharray': [2, 1]
'line-width': 5
}
});
// 2. Viewshed 360 Polygon Layer
initialMap.addSource('tactical-viewshed-src', {
safeAddSource('tactical-viewshed-src', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] }
});
initialMap.addLayer({
safeAddLayer({
id: 'tactical-viewshed-fill',
type: 'fill',
source: 'tactical-viewshed-src',
paint: {
'fill-color': '#22c55e',
'fill-opacity': 0.25
'fill-color': '#ff0000',
'fill-opacity': 0.80
}
});
initialMap.addLayer({
safeAddLayer({
id: 'tactical-viewshed-outline',
type: 'line',
source: 'tactical-viewshed-src',
paint: {
'line-color': '#4ade80',
'line-width': 2,
'line-dasharray': [3, 2]
'line-color': '#ffff00',
'line-width': 8.0,
'line-opacity': 1.0
}
});
// 3. Artillery Trajectory Layer
initialMap.addSource('tactical-artillery-src', {
safeAddSource('tactical-artillery-src', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] }
});
initialMap.addLayer({
safeAddLayer({
id: 'tactical-artillery-line',
type: 'line',
source: 'tactical-artillery-src',
paint: {
'line-color': '#f97316',
'line-width': 4
'line-width': 5
}
});
// 4. Minefield Barrier Layer
initialMap.addSource('tactical-minefield-src', {
safeAddSource('tactical-minefield-src', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] }
});
initialMap.addLayer({
safeAddLayer({
id: 'tactical-minefield-line',
type: 'line',
source: 'tactical-minefield-src',
paint: {
'line-color': '#dc2626',
'line-width': 6,
'line-dasharray': [1, 1]
'line-width': 6
}
});
// 5. Tactical Symbols Layer
initialMap.addSource('tactical-symbols-src', {
safeAddSource('tactical-symbols-src', {
type: 'geojson',
data: {
type: 'FeatureCollection',
@@ -1199,7 +1246,7 @@ export const TacticalDefenseView: React.FC = () => {
}
});
initialMap.addLayer({
safeAddLayer({
id: 'tactical-symbols-pts',
type: 'circle',
source: 'tactical-symbols-src',
@@ -1222,7 +1269,7 @@ export const TacticalDefenseView: React.FC = () => {
}
});
initialMap.addLayer({
safeAddLayer({
id: 'tactical-symbols-lbls',
type: 'symbol',
source: 'tactical-symbols-src',
@@ -1241,56 +1288,56 @@ export const TacticalDefenseView: React.FC = () => {
});
// 6. Isochrone Reachability Polygons Layer (خارطة زمن الاستجابة والوصول)
initialMap.addSource('tactical-isochrone-src', {
safeAddSource('tactical-isochrone-src', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] }
});
initialMap.addLayer({
safeAddLayer({
id: 'tactical-isochrone-fill',
type: 'fill',
source: 'tactical-isochrone-src',
paint: {
'fill-color': ['coalesce', ['get', 'color'], '#22c55e'],
'fill-opacity': 0.32
'fill-opacity': 0.45
}
});
initialMap.addLayer({
safeAddLayer({
id: 'tactical-isochrone-line',
type: 'line',
source: 'tactical-isochrone-src',
paint: {
'line-color': ['coalesce', ['get', 'color'], '#22c55e'],
'line-width': 2.5,
'line-width': 3.5,
'line-opacity': 0.95
}
});
// 7. Digital IPB Overlays Layer (منظومة الشفافات التكتيكية الرقمية لإعداد ساحة المعركة)
initialMap.addSource('tactical-ipb-src', {
safeAddSource('tactical-ipb-src', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] }
});
initialMap.addLayer({
safeAddLayer({
id: 'tactical-ipb-fill',
type: 'fill',
source: 'tactical-ipb-src',
filter: ['==', '$type', 'Polygon'],
filter: ['==', ['geometry-type'], 'Polygon'],
paint: {
'fill-color': ['coalesce', ['get', 'color'], '#38bdf8'],
'fill-opacity': ['coalesce', ['get', 'opacity'], 0.65]
}
});
initialMap.addLayer({
safeAddLayer({
id: 'tactical-ipb-line',
type: 'line',
source: 'tactical-ipb-src',
filter: [
'any',
['==', '$type', 'LineString'],
['==', ['geometry-type'], 'LineString'],
['==', ['get', 'layer'], 'layer_10'],
['==', ['get', 'layer'], 'layer_7'],
['==', ['get', 'layer'], 'layer_4']
@@ -1308,7 +1355,7 @@ export const TacticalDefenseView: React.FC = () => {
}
});
initialMap.addLayer({
safeAddLayer({
id: 'tactical-ipb-symbol',
type: 'symbol',
source: 'tactical-ipb-src',
@@ -1334,6 +1381,11 @@ export const TacticalDefenseView: React.FC = () => {
'text-halo-width': 2.5
}
});
bringTacticalLayersToFront(initialMap);
mapLoadedRef.current = true;
setMapLoaded(true);
(window as any).tacticalMap = initialMap;
});
return () => {
@@ -1344,26 +1396,19 @@ export const TacticalDefenseView: React.FC = () => {
};
}, []);
// Run Terrain Tactical Study
const runTerrainStudy = () => {
if (!terrainCenter) return;
setTerrainLoading(true);
calculateTerrainStudy(terrainCenter[0], terrainCenter[1], terrainRadius)
.then((res) => {
setTerrainResult(res);
setTerrainLoading(false);
// 1. Draw sector boundary polygon on map
if (map.current && map.current.getSource('tactical-terrain-src')) {
const points = 48;
// Draw sector boundary circle immediately on map
const drawSectorCircle = (center: [number, number], radius: number) => {
if (!map.current || !map.current.getSource('tactical-terrain-src')) return;
const points = 64;
const coords: [number, number][] = [];
const R_earth = 6371000;
for (let i = 0; i <= points; i++) {
const angle = (i / points) * 2 * Math.PI;
const dLat = (terrainRadius / R_earth) * (180 / Math.PI) * Math.cos(angle);
const dLng = (terrainRadius / (R_earth * Math.cos((terrainCenter[0] * Math.PI) / 180))) * (180 / Math.PI) * Math.sin(angle);
coords.push([terrainCenter[1] + dLng, terrainCenter[0] + dLat]);
const dLat = (radius / R_earth) * (180 / Math.PI) * Math.cos(angle);
const dLng = (radius / (R_earth * Math.cos((center[0] * Math.PI) / 180))) * (180 / Math.PI) * Math.sin(angle);
coords.push([Number((center[1] + dLng).toFixed(6)), Number((center[0] + dLat).toFixed(6))]);
}
coords.push(coords[0]);
(map.current.getSource('tactical-terrain-src') as maplibregl.GeoJSONSource).setData({
type: 'FeatureCollection',
@@ -1378,7 +1423,20 @@ export const TacticalDefenseView: React.FC = () => {
}
]
});
}
bringTacticalLayersToFront(map.current);
};
// Run Terrain Tactical Study
const runTerrainStudy = () => {
if (!terrainCenter) return;
// Draw boundary circle immediately so feedback is instant
drawSectorCircle(terrainCenter, terrainRadius);
setTerrainLoading(true);
calculateTerrainStudy(terrainCenter[0], terrainCenter[1], terrainRadius)
.then((res) => {
setTerrainResult(res);
setTerrainLoading(false);
drawSectorCircle(terrainCenter, terrainRadius);
})
.catch(() => setTerrainLoading(false));
};
@@ -1396,7 +1454,7 @@ export const TacticalDefenseView: React.FC = () => {
// Sync Spatial GeoJSON Features with Active Layer Toggles
useEffect(() => {
if (!map.current || !map.current.getSource('tactical-terrain-spatial-src')) return;
if (!terrainResult?.spatialGeoJson || (mode !== 'terrain' && mode !== 'ipb')) {
if (!terrainResult?.spatialGeoJson) {
(map.current.getSource('tactical-terrain-spatial-src') as maplibregl.GeoJSONSource).setData({
type: 'FeatureCollection',
features: []
@@ -1418,7 +1476,8 @@ export const TacticalDefenseView: React.FC = () => {
type: 'FeatureCollection',
features: filtered
});
}, [terrainResult, showPeaksAndValleys, showSlopeSectors, showNaturalObstacles, showRoadCorridors, showUrbanZones, showHazardousGround, mode]);
bringTacticalLayersToFront(map.current);
}, [terrainResult, showPeaksAndValleys, showSlopeSectors, showNaturalObstacles, showRoadCorridors, showUrbanZones, showHazardousGround]);
// 1. Auto-run real mathematical IPB calculation when center or radius changes or when entering IPB mode
useEffect(() => {
@@ -1453,9 +1512,10 @@ export const TacticalDefenseView: React.FC = () => {
// Auto-run initial terrain study on mount & coordinates change
useEffect(() => {
runTerrainStudy();
}, [terrainCenter, terrainRadius]);
}, [terrainCenter, terrainRadius, mapLoaded]);
const ensureViewshedSourceAndLayer = (m: maplibregl.Map) => {
if (!m) return;
if (!m.getSource('tactical-viewshed-src')) {
m.addSource('tactical-viewshed-src', {
type: 'geojson',
@@ -1468,8 +1528,8 @@ export const TacticalDefenseView: React.FC = () => {
type: 'fill',
source: 'tactical-viewshed-src',
paint: {
'fill-color': '#22c55e',
'fill-opacity': 0.35
'fill-color': '#ff0000',
'fill-opacity': 0.80
}
});
}
@@ -1479,15 +1539,17 @@ export const TacticalDefenseView: React.FC = () => {
type: 'line',
source: 'tactical-viewshed-src',
paint: {
'line-color': '#16a34a',
'line-width': 3,
'line-dasharray': [3, 2]
'line-color': '#ffff00',
'line-width': 8.0,
'line-opacity': 1.0
}
});
}
bringTacticalLayersToFront(m);
};
const ensureLosSourceAndLayer = (m: maplibregl.Map) => {
if (!m) return;
if (!m.getSource('tactical-los-src')) {
m.addSource('tactical-los-src', {
type: 'geojson',
@@ -1499,17 +1561,18 @@ export const TacticalDefenseView: React.FC = () => {
id: 'tactical-los-line',
type: 'line',
source: 'tactical-los-src',
filter: ['==', '$type', 'LineString'],
filter: ['==', ['geometry-type'], 'LineString'],
paint: {
'line-color': ['case', ['==', ['get', 'blocked'], true], '#ef4444', '#22c55e'],
'line-width': 4,
'line-dasharray': [2, 1]
'line-width': 5
}
});
}
bringTacticalLayersToFront(m);
};
const ensureArtillerySourceAndLayer = (m: maplibregl.Map) => {
if (!m) return;
if (!m.getSource('tactical-artillery-src')) {
m.addSource('tactical-artillery-src', {
type: 'geojson',
@@ -1523,13 +1586,14 @@ export const TacticalDefenseView: React.FC = () => {
source: 'tactical-artillery-src',
paint: {
'line-color': '#f97316',
'line-width': 4
'line-width': 5
}
});
}
};
const ensureMinefieldSourceAndLayer = (m: maplibregl.Map) => {
if (!m) return;
if (!m.getSource('tactical-minefield-src')) {
m.addSource('tactical-minefield-src', {
type: 'geojson',
@@ -1543,8 +1607,7 @@ export const TacticalDefenseView: React.FC = () => {
source: 'tactical-minefield-src',
paint: {
'line-color': '#dc2626',
'line-width': 6,
'line-dasharray': [1, 1]
'line-width': 6
}
});
}
@@ -1600,11 +1663,12 @@ export const TacticalDefenseView: React.FC = () => {
type: 'FeatureCollection',
features: feats
});
bringTacticalLayersToFront(map.current);
}
}
})
.catch(() => setLosLoading(false));
}, [losPointA, losPointB, losObsHeight, losTgtHeight]);
}, [losPointA, losPointB, losObsHeight, losTgtHeight, mapLoaded]);
// Update Minefield Analysis & Map Layer
useEffect(() => {
@@ -1646,11 +1710,12 @@ export const TacticalDefenseView: React.FC = () => {
}
]
});
bringTacticalLayersToFront(map.current);
}
}
})
.catch(() => setMineLoading(false));
}, [mineStart, mineEnd, mineDensity]);
}, [mineStart, mineEnd, mineDensity, mapLoaded]);
// Update Artillery Trajectory
useEffect(() => {
@@ -1669,31 +1734,101 @@ export const TacticalDefenseView: React.FC = () => {
return;
}
runArtilleryMission();
}, [gunPos, targetPos, caliber, muzzleVel]);
}, [gunPos, targetPos, caliber, muzzleVel, mapLoaded]);
// Update 360 Viewshed
const runViewshed360 = () => {
if (!viewshedCenter) return;
setViewshedLoading(true);
console.log('[VIEWSHED-DEBUG] Starting calculation for center:', viewshedCenter, 'height:', viewshedHeight, 'radius:', viewshedRadius);
calculateRadialViewshed(viewshedCenter[0], viewshedCenter[1], viewshedHeight, viewshedRadius, 36)
.then((res) => {
setViewshedResult(res);
setViewshedLoading(false);
console.log('[VIEWSHED-DEBUG] API response received. polygonGeoJson:', !!res.polygonGeoJson, 'visibleAreaKm2:', res.visibleAreaKm2, 'visiblePercentage:', res.visiblePercentage);
if (res.polygonGeoJson) {
const geom = (res.polygonGeoJson as any).geometry;
console.log('[VIEWSHED-DEBUG] Polygon geometry type:', geom?.type, 'coordinates rings:', geom?.coordinates?.length, 'ring[0] points:', geom?.coordinates?.[0]?.length);
if (geom?.coordinates?.[0]?.[0]) {
console.log('[VIEWSHED-DEBUG] First coord:', JSON.stringify(geom.coordinates[0][0]), 'Last coord:', JSON.stringify(geom.coordinates[0][geom.coordinates[0].length - 1]));
console.log('[VIEWSHED-DEBUG] Full polygon JSON (first 500 chars):', JSON.stringify(res.polygonGeoJson).substring(0, 500));
}
// Store on window for console inspection
(window as any).__viewshedPolygon = res.polygonGeoJson;
}
if (map.current) {
console.log('[VIEWSHED-DEBUG] map.current exists. isStyleLoaded:', map.current.isStyleLoaded(), 'loaded:', map.current.loaded());
console.log('[VIEWSHED-DEBUG] Before ensure - src exists:', !!map.current.getSource('tactical-viewshed-src'), 'fill exists:', !!map.current.getLayer('tactical-viewshed-fill'), 'outline exists:', !!map.current.getLayer('tactical-viewshed-outline'));
ensureViewshedSourceAndLayer(map.current);
console.log('[VIEWSHED-DEBUG] After ensure - src exists:', !!map.current.getSource('tactical-viewshed-src'), 'fill exists:', !!map.current.getLayer('tactical-viewshed-fill'), 'outline exists:', !!map.current.getLayer('tactical-viewshed-outline'));
const src = map.current.getSource('tactical-viewshed-src') as maplibregl.GeoJSONSource;
if (src && res.polygonGeoJson) {
src.setData({
type: 'FeatureCollection',
const fc = {
type: 'FeatureCollection' as const,
features: [res.polygonGeoJson]
});
};
console.log('[VIEWSHED-DEBUG] Calling src.setData with FeatureCollection, features count:', fc.features.length);
src.setData(fc);
if (map.current.getLayer('tactical-viewshed-fill')) {
map.current.setLayoutProperty('tactical-viewshed-fill', 'visibility', 'visible');
map.current.setPaintProperty('tactical-viewshed-fill', 'fill-color', '#ff0000');
map.current.setPaintProperty('tactical-viewshed-fill', 'fill-opacity', 0.80);
console.log('[VIEWSHED-DEBUG] Set tactical-viewshed-fill visible, RED, opacity 0.80');
}
if (map.current.getLayer('tactical-viewshed-outline')) {
map.current.setLayoutProperty('tactical-viewshed-outline', 'visibility', 'visible');
console.log('[VIEWSHED-DEBUG] Set tactical-viewshed-outline visible');
}
bringTacticalLayersToFront(map.current);
// Log full layer order for debugging
const allLayerIds = map.current.getStyle().layers.map((l: any) => l.id);
const vsIdx = allLayerIds.indexOf('tactical-viewshed-fill');
console.log('[VIEWSHED-DEBUG] Layer order - viewshed-fill at index:', vsIdx, '/', allLayerIds.length, 'last 10 layers:', allLayerIds.slice(-10).join(', '));
try {
const ring = (res.polygonGeoJson.geometry as any)?.coordinates?.[0];
if (ring && ring.length > 2) {
const lngs = ring.map((c: number[]) => c[0]);
const lats = ring.map((c: number[]) => c[1]);
const bounds: [[number, number], [number, number]] = [
[Math.min(...lngs), Math.min(...lats)],
[Math.max(...lngs), Math.max(...lats)]
];
console.log('[VIEWSHED-DEBUG] fitBounds SW:', JSON.stringify(bounds[0]), 'NE:', JSON.stringify(bounds[1]));
map.current.fitBounds(bounds, { padding: 80, maxZoom: 14 });
} else {
console.warn('[VIEWSHED-DEBUG] Ring too short for fitBounds:', ring?.length);
}
} catch (e) {
console.error('[VIEWSHED-DEBUG] fitBounds error:', e);
}
} else {
console.warn('[VIEWSHED-DEBUG] Missing src or polygonGeoJson. src:', !!src, 'polygonGeoJson:', !!res.polygonGeoJson);
}
} else {
console.warn('[VIEWSHED-DEBUG] map.current is null');
}
})
.catch(() => setViewshedLoading(false));
.catch((err) => {
console.error('[VIEWSHED-DEBUG] Calculation error:', err);
setViewshedLoading(false);
});
};
// Auto-run Viewshed when entering viewshed mode or center changes
useEffect(() => {
if (mode === 'viewshed' && viewshedCenter && mapLoaded) {
runViewshed360();
}
}, [mode, viewshedCenter, viewshedHeight, viewshedRadius, mapLoaded]);
// Run Artillery Fire Mission
const runArtilleryMission = async () => {
if (!gunPos || !targetPos) return;
@@ -1717,9 +1852,12 @@ export const TacticalDefenseView: React.FC = () => {
const data = await res.json();
setArtilleryResult(data);
if (map.current && map.current.getSource('tactical-artillery-src')) {
if (map.current) {
ensureArtillerySourceAndLayer(map.current);
const src = map.current.getSource('tactical-artillery-src') as maplibregl.GeoJSONSource;
if (src) {
const lineCoords = (data.trajectoryPoints || []).map((p: any) => [p.lng, p.lat]);
(map.current.getSource('tactical-artillery-src') as maplibregl.GeoJSONSource).setData({
src.setData({
type: 'FeatureCollection',
features: [
{
@@ -1729,6 +1867,8 @@ export const TacticalDefenseView: React.FC = () => {
}
]
});
bringTacticalLayersToFront(map.current);
}
}
} catch (e) {
console.error('Artillery calculation error:', e);
@@ -1816,7 +1956,25 @@ export const TacticalDefenseView: React.FC = () => {
if (!map.current || !map.current.getSource('tactical-isochrone-src')) return;
if (mode === 'isochrone' && isochroneData?.featureCollection) {
(map.current.getSource('tactical-isochrone-src') as maplibregl.GeoJSONSource).setData(isochroneData.featureCollection);
} else {
bringTacticalLayersToFront(map.current);
try {
const allCoords: [number, number][] = [];
isochroneData.featureCollection.features.forEach((feat: any) => {
if (feat.geometry?.coordinates?.[0]) {
allCoords.push(...feat.geometry.coordinates[0]);
}
});
if (allCoords.length > 0) {
const lngs = allCoords.map((c: [number, number]) => c[0]);
const lats = allCoords.map((c: [number, number]) => c[1]);
map.current.fitBounds(
[[Math.min(...lngs), Math.min(...lats)], [Math.max(...lngs), Math.max(...lats)]],
{ padding: 80, maxZoom: 13.5 }
);
}
} catch {}
} else if (mode !== 'isochrone') {
(map.current.getSource('tactical-isochrone-src') as maplibregl.GeoJSONSource).setData({
type: 'FeatureCollection',
features: []
@@ -1824,6 +1982,13 @@ export const TacticalDefenseView: React.FC = () => {
}
}, [mode, isochroneData]);
// Auto-run Isochrone calculation when entering mode or center/profile/times change
useEffect(() => {
if (mode === 'isochrone' && isochroneCenter && mapLoaded) {
runIsochroneCalculation(isochroneCenter, isochroneProfile, isochroneTimes);
}
}, [mode, isochroneCenter, isochroneProfile, isochroneTimes, mapLoaded]);
// Run Isochrone Calculation
const runIsochroneCalculation = async (center = isochroneCenter, profile = isochroneProfile, times = isochroneTimes) => {
if (!center) return;
+10
View File
@@ -0,0 +1,10 @@
import maplibregl from 'maplibre-gl';
import workerUrl from 'maplibre-gl/dist/maplibre-gl-csp-worker.js?url';
// MapLibre 5 serializes its embedded worker from functions. Vite's class-field
// transform injects __publicField outside those functions, leaving GeoJSON
// workers with an undefined helper. Serve the matching standalone worker as an
// untransformed asset in both development and production, before any map exists.
maplibregl.setWorkerUrl(workerUrl);
export default maplibregl;
+48
View File
@@ -0,0 +1,48 @@
# GeoJSON overlay rendering regression
The tactical calculations return geometry, but MapLibre's GeoJSON worker fails
with `__publicField is not defined`. HTML markers and vector basemap tiles still
render, so the UI appears functional while all GeoJSON overlays remain invisible.
## Cause
Commit `0bf1382` changed Vite 8.0.1 to 6.4.3 (and the React plugin to 4.7.0).
The current optimized MapLibre 5.20.2 bundle imports `__publicField` outside the
functions that MapLibre serializes into its embedded worker. The GeoJSON indexing
code references that helper inside the worker, where it does not exist.
Changing geometry filters, opacity, layer order, or camera bounds cannot repair
this worker failure.
## Fix
`src/utils/maplibreWorker.ts` loads the installed MapLibre 5 standalone CSP worker
using Vite's asset URL import and calls `setWorkerUrl` before React creates maps.
`src/main.tsx` imports this configured MapLibre instance. Other components share
the same underlying MapLibre module. No API or geometry calculation change is
required.
The `?url` import is intentional for this self-contained MapLibre 5 worker: it
preserves the vendor file without transpilation. Revisit the worker entry point
if upgrading MapLibre to a new major version.
## Verification (2026-09-24)
- Reproduced the missing overlays on the official tactical site, and confirmed
its Isochrone API returned three valid Polygon features.
- A minimal map with an empty base style and a fixed line/polygon reproduced the
same failure: zero source/rendered features and `__publicField is not defined`.
- With the standalone worker, that same map visibly drew both geometries, with
eight tile-level source/rendered feature entries and no worker error.
- Tested the modified tactical UI locally, proxying API requests to the official
service: terrain overlays, the 10.9 km² / 14% viewshed, and colored Isochrone
regions rendered. Temporary diagnostic geometry was removed afterward.
- `npm run build` succeeds. The emitted standalone worker is byte-identical to
the installed vendor worker.
## Deployment
Deploy `apps/web/src/main.tsx` and `apps/web/src/utils/maplibreWorker.ts` together.
The observed official site serves Vite development modules; restart its web
service and reload the browser after deploying. For a production build, deploy
the complete `dist` output, including the emitted worker asset. The live site
has not been changed by this local fix.