fix: resolve MapLibre worker rendering regression by configuring custom worker and updating geometry type filters
This commit is contained in:
@@ -11,7 +11,7 @@ export class AdminGuard implements CanActivate {
|
|||||||
throw new UnauthorizedException('Tenant not found in request');
|
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.');
|
throw new ForbiddenException('Admin access required. Your tenant must have an ENTERPRISE plan or ADMIN clearance.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ const MapComponent: React.FC<MapProps> = ({
|
|||||||
id: 'los-line',
|
id: 'los-line',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
source: 'los-source',
|
source: 'los-source',
|
||||||
filter: ['==', '$type', 'LineString'],
|
filter: ['==', ['geometry-type'], 'LineString'],
|
||||||
layout: { 'line-cap': 'round', 'line-join': 'round' },
|
layout: { 'line-cap': 'round', 'line-join': 'round' },
|
||||||
paint: {
|
paint: {
|
||||||
'line-color': '#f59e0b',
|
'line-color': '#f59e0b',
|
||||||
@@ -211,7 +211,7 @@ const MapComponent: React.FC<MapProps> = ({
|
|||||||
id: 'los-points',
|
id: 'los-points',
|
||||||
type: 'circle',
|
type: 'circle',
|
||||||
source: 'los-source',
|
source: 'los-source',
|
||||||
filter: ['==', '$type', 'Point'],
|
filter: ['==', ['geometry-type'], 'Point'],
|
||||||
paint: {
|
paint: {
|
||||||
'circle-radius': 8,
|
'circle-radius': 8,
|
||||||
'circle-color': [
|
'circle-color': [
|
||||||
@@ -230,7 +230,7 @@ const MapComponent: React.FC<MapProps> = ({
|
|||||||
id: 'los-labels',
|
id: 'los-labels',
|
||||||
type: 'symbol',
|
type: 'symbol',
|
||||||
source: 'los-source',
|
source: 'los-source',
|
||||||
filter: ['==', '$type', 'Point'],
|
filter: ['==', ['geometry-type'], 'Point'],
|
||||||
layout: {
|
layout: {
|
||||||
'text-field': ['get', 'label'],
|
'text-field': ['get', 'label'],
|
||||||
'text-size': 12,
|
'text-size': 12,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import maplibregl from 'maplibre-gl'
|
import maplibregl from './utils/maplibreWorker'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
import CompareView from './pages/CompareView'
|
import CompareView from './pages/CompareView'
|
||||||
import IntelligenceDashboard from './pages/IntelligenceDashboard'
|
import IntelligenceDashboard from './pages/IntelligenceDashboard'
|
||||||
|
|||||||
@@ -176,6 +176,8 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const [mode, setMode] = useState<TacticalMode>('terrain');
|
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 [cursorPos, setCursorPos] = useState({ lat: 31.95, lng: 35.93, elev: 850 });
|
||||||
const [activePlacement, setActivePlacement] = useState<string | null>(null);
|
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[]) => {
|
const handlePositionFoundByResection = (lat: number, lng: number, observations: ResectionObservation[]) => {
|
||||||
setResectionPosition([lat, lng]);
|
setResectionPosition([lat, lng]);
|
||||||
if (map.current) {
|
if (map.current) {
|
||||||
@@ -410,11 +444,10 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
id: 'resection-lines',
|
id: 'resection-lines',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
source: 'resection-source',
|
source: 'resection-source',
|
||||||
filter: ['==', '$type', 'LineString'],
|
filter: ['==', ['geometry-type'], 'LineString'],
|
||||||
paint: {
|
paint: {
|
||||||
'line-color': '#00f0ff',
|
'line-color': '#00f0ff',
|
||||||
'line-width': 3,
|
'line-width': 3
|
||||||
'line-dasharray': [3, 2]
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -422,7 +455,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
id: 'resection-points',
|
id: 'resection-points',
|
||||||
type: 'circle',
|
type: 'circle',
|
||||||
source: 'resection-source',
|
source: 'resection-source',
|
||||||
filter: ['==', '$type', 'Point'],
|
filter: ['==', ['geometry-type'], 'Point'],
|
||||||
paint: {
|
paint: {
|
||||||
'circle-radius': 9,
|
'circle-radius': 9,
|
||||||
'circle-color': '#22c55e',
|
'circle-color': '#22c55e',
|
||||||
@@ -450,7 +483,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
id: fillLayerId,
|
id: fillLayerId,
|
||||||
type: 'fill',
|
type: 'fill',
|
||||||
source: sourceId,
|
source: sourceId,
|
||||||
filter: ['==', '$type', 'Polygon'],
|
filter: ['==', ['geometry-type'], 'Polygon'],
|
||||||
paint: {
|
paint: {
|
||||||
'fill-color': '#6366f1',
|
'fill-color': '#6366f1',
|
||||||
'fill-opacity': 0.45
|
'fill-opacity': 0.45
|
||||||
@@ -462,7 +495,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
id: lineLayerId,
|
id: lineLayerId,
|
||||||
type: 'line',
|
type: 'line',
|
||||||
source: sourceId,
|
source: sourceId,
|
||||||
filter: ['any', ['==', '$type', 'LineString'], ['==', '$type', 'Polygon']],
|
filter: ['any', ['==', ['geometry-type'], 'LineString'], ['==', ['geometry-type'], 'Polygon']],
|
||||||
paint: {
|
paint: {
|
||||||
'line-color': '#38bdf8',
|
'line-color': '#38bdf8',
|
||||||
'line-width': 2.5
|
'line-width': 2.5
|
||||||
@@ -474,7 +507,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
id: circleLayerId,
|
id: circleLayerId,
|
||||||
type: 'circle',
|
type: 'circle',
|
||||||
source: sourceId,
|
source: sourceId,
|
||||||
filter: ['==', '$type', 'Point'],
|
filter: ['==', ['geometry-type'], 'Point'],
|
||||||
paint: {
|
paint: {
|
||||||
'circle-radius': 7,
|
'circle-radius': 7,
|
||||||
'circle-color': '#e11d48',
|
'circle-color': '#e11d48',
|
||||||
@@ -679,7 +712,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
|
|
||||||
const initialMap = new maplibregl.Map({
|
const initialMap = new maplibregl.Map({
|
||||||
container: mapContainer.current,
|
container: mapContainer.current,
|
||||||
style: '/tactical-style.json',
|
style: '/style.json',
|
||||||
center: [35.9106, 31.9539],
|
center: [35.9106, 31.9539],
|
||||||
zoom: 11,
|
zoom: 11,
|
||||||
pitch: 35,
|
pitch: 35,
|
||||||
@@ -829,30 +862,46 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
initialMap.on('load', () => {
|
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
|
// 0. Terrain Study Circle & Spatial Layers
|
||||||
initialMap.addSource('tactical-terrain-src', {
|
safeAddSource('tactical-terrain-src', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: { type: 'FeatureCollection', features: [] }
|
data: { type: 'FeatureCollection', features: [] }
|
||||||
});
|
});
|
||||||
|
|
||||||
initialMap.addLayer({
|
safeAddLayer({
|
||||||
id: 'tactical-terrain-fill',
|
id: 'tactical-terrain-fill',
|
||||||
type: 'fill',
|
type: 'fill',
|
||||||
source: 'tactical-terrain-src',
|
source: 'tactical-terrain-src',
|
||||||
paint: {
|
paint: {
|
||||||
'fill-color': '#6366f1',
|
'fill-color': '#6366f1',
|
||||||
'fill-opacity': 0.08
|
'fill-opacity': 0.18
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
initialMap.addLayer({
|
safeAddLayer({
|
||||||
id: 'tactical-terrain-line',
|
id: 'tactical-terrain-line',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
source: 'tactical-terrain-src',
|
source: 'tactical-terrain-src',
|
||||||
paint: {
|
paint: {
|
||||||
'line-color': '#818cf8',
|
'line-color': '#4f46e5',
|
||||||
'line-width': 2.5,
|
'line-width': 3.5,
|
||||||
'line-dasharray': [3, 2]
|
'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)
|
// 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',
|
type: 'geojson',
|
||||||
data: { type: 'FeatureCollection', features: [] }
|
data: { type: 'FeatureCollection', features: [] }
|
||||||
});
|
});
|
||||||
|
|
||||||
initialMap.addLayer({
|
safeAddLayer({
|
||||||
id: 'tactical-terrain-spatial-poly-fill',
|
id: 'tactical-terrain-spatial-poly-fill',
|
||||||
type: 'fill',
|
type: 'fill',
|
||||||
source: 'tactical-terrain-spatial-src',
|
source: 'tactical-terrain-spatial-src',
|
||||||
filter: ['==', '$type', 'Polygon'],
|
filter: ['==', ['geometry-type'], 'Polygon'],
|
||||||
paint: {
|
paint: {
|
||||||
'fill-color': ['get', 'color'],
|
'fill-color': ['get', 'color'],
|
||||||
'fill-opacity': [
|
'fill-opacity': [
|
||||||
'match',
|
'match',
|
||||||
['get', 'layerType'],
|
['get', 'layerType'],
|
||||||
'cliff', 0.38,
|
'cliff', 0.45,
|
||||||
'urban', 0.20,
|
'urban', 0.30,
|
||||||
'hazard', 0.24,
|
'hazard', 0.35,
|
||||||
'slope-sector', 0.14,
|
'slope-sector', 0.22,
|
||||||
0.15
|
0.20
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
initialMap.addLayer({
|
safeAddLayer({
|
||||||
id: 'tactical-terrain-spatial-poly-line',
|
id: 'tactical-terrain-spatial-poly-line',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
source: 'tactical-terrain-spatial-src',
|
source: 'tactical-terrain-spatial-src',
|
||||||
filter: ['==', '$type', 'Polygon'],
|
filter: ['==', ['geometry-type'], 'Polygon'],
|
||||||
paint: {
|
paint: {
|
||||||
'line-color': [
|
'line-color': [
|
||||||
'match',
|
'match',
|
||||||
['get', 'layerType'],
|
['get', 'layerType'],
|
||||||
'cliff', '#ef4444',
|
'cliff', '#ef4444',
|
||||||
'rgba(255, 255, 255, 0.10)'
|
'rgba(255, 255, 255, 0.4)'
|
||||||
],
|
],
|
||||||
'line-width': [
|
'line-width': [
|
||||||
'match',
|
'match',
|
||||||
['get', 'layerType'],
|
['get', 'layerType'],
|
||||||
'cliff', 1.8,
|
'cliff', 2.5,
|
||||||
0.4
|
1.0
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
initialMap.addLayer({
|
safeAddLayer({
|
||||||
id: 'tactical-terrain-spatial-lines',
|
id: 'tactical-terrain-spatial-lines',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
source: 'tactical-terrain-spatial-src',
|
source: 'tactical-terrain-spatial-src',
|
||||||
filter: ['==', '$type', 'LineString'],
|
filter: ['==', ['geometry-type'], 'LineString'],
|
||||||
paint: {
|
paint: {
|
||||||
'line-color': ['get', 'color'],
|
'line-color': ['get', 'color'],
|
||||||
'line-width': [
|
'line-width': [
|
||||||
'match',
|
'match',
|
||||||
['get', 'layerType'],
|
['get', 'layerType'],
|
||||||
'cliff', 3.5,
|
'cliff', 4.0,
|
||||||
'road', 4.0,
|
'road', 4.5,
|
||||||
'wadi', 3.5,
|
'wadi', 4.5,
|
||||||
'ridge', 2.5,
|
'ridge', 3.5,
|
||||||
2.5
|
3.0
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
initialMap.addLayer({
|
safeAddLayer({
|
||||||
id: 'tactical-terrain-spatial-points',
|
id: 'tactical-terrain-spatial-points',
|
||||||
type: 'circle',
|
type: 'circle',
|
||||||
source: 'tactical-terrain-spatial-src',
|
source: 'tactical-terrain-spatial-src',
|
||||||
filter: ['==', '$type', 'Point'],
|
filter: ['==', ['geometry-type'], 'Point'],
|
||||||
paint: {
|
paint: {
|
||||||
'circle-radius': 5,
|
'circle-radius': 7,
|
||||||
'circle-color': ['coalesce', ['get', 'color'], '#38bdf8'],
|
'circle-color': ['coalesce', ['get', 'color'], '#38bdf8'],
|
||||||
'circle-stroke-width': 2,
|
'circle-stroke-width': 2.5,
|
||||||
'circle-stroke-color': '#ffffff'
|
'circle-stroke-color': '#ffffff'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
initialMap.addLayer({
|
safeAddLayer({
|
||||||
id: 'tactical-terrain-spatial-labels',
|
id: 'tactical-terrain-spatial-labels',
|
||||||
type: 'symbol',
|
type: 'symbol',
|
||||||
source: 'tactical-terrain-spatial-src',
|
source: 'tactical-terrain-spatial-src',
|
||||||
filter: ['all', ['==', '$type', 'Point'], ['has', 'title']],
|
filter: ['all', ['==', ['geometry-type'], 'Point'], ['has', 'title']],
|
||||||
layout: {
|
layout: {
|
||||||
'text-field': ['get', 'title'],
|
'text-field': ['get', 'title'],
|
||||||
'text-size': 12,
|
'text-size': 12,
|
||||||
'text-font': ['Noto Sans Bold', 'Open Sans Bold'],
|
'text-font': ['Noto Sans Regular'],
|
||||||
'text-anchor': 'bottom',
|
'text-anchor': 'bottom',
|
||||||
'text-offset': [0, -0.6],
|
'text-offset': [0, -0.8],
|
||||||
'symbol-placement': 'point',
|
'symbol-placement': 'point',
|
||||||
'text-allow-overlap': false
|
'text-allow-overlap': true
|
||||||
},
|
},
|
||||||
paint: {
|
paint: {
|
||||||
'text-color': '#ffffff',
|
'text-color': '#ffffff',
|
||||||
'text-halo-color': '#0f172a',
|
'text-halo-color': '#0f172a',
|
||||||
'text-halo-width': 2.5
|
'text-halo-width': 3
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 1. Line of Sight Layer
|
// 1. Line of Sight Layer
|
||||||
initialMap.addSource('tactical-los-src', {
|
safeAddSource('tactical-los-src', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: { type: 'FeatureCollection', features: [] }
|
data: { type: 'FeatureCollection', features: [] }
|
||||||
});
|
});
|
||||||
|
|
||||||
initialMap.addLayer({
|
safeAddLayer({
|
||||||
id: 'tactical-los-line',
|
id: 'tactical-los-line',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
source: 'tactical-los-src',
|
source: 'tactical-los-src',
|
||||||
filter: ['==', '$type', 'LineString'],
|
filter: ['==', ['geometry-type'], 'LineString'],
|
||||||
paint: {
|
paint: {
|
||||||
'line-color': ['case', ['==', ['get', 'blocked'], true], '#ef4444', '#22c55e'],
|
'line-color': ['case', ['==', ['get', 'blocked'], true], '#ef4444', '#22c55e'],
|
||||||
'line-width': 4,
|
'line-width': 5
|
||||||
'line-dasharray': [2, 1]
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Viewshed 360 Polygon Layer
|
// 2. Viewshed 360 Polygon Layer
|
||||||
initialMap.addSource('tactical-viewshed-src', {
|
safeAddSource('tactical-viewshed-src', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: { type: 'FeatureCollection', features: [] }
|
data: { type: 'FeatureCollection', features: [] }
|
||||||
});
|
});
|
||||||
|
|
||||||
initialMap.addLayer({
|
safeAddLayer({
|
||||||
id: 'tactical-viewshed-fill',
|
id: 'tactical-viewshed-fill',
|
||||||
type: 'fill',
|
type: 'fill',
|
||||||
source: 'tactical-viewshed-src',
|
source: 'tactical-viewshed-src',
|
||||||
paint: {
|
paint: {
|
||||||
'fill-color': '#22c55e',
|
'fill-color': '#ff0000',
|
||||||
'fill-opacity': 0.25
|
'fill-opacity': 0.80
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
initialMap.addLayer({
|
safeAddLayer({
|
||||||
id: 'tactical-viewshed-outline',
|
id: 'tactical-viewshed-outline',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
source: 'tactical-viewshed-src',
|
source: 'tactical-viewshed-src',
|
||||||
paint: {
|
paint: {
|
||||||
'line-color': '#4ade80',
|
'line-color': '#ffff00',
|
||||||
'line-width': 2,
|
'line-width': 8.0,
|
||||||
'line-dasharray': [3, 2]
|
'line-opacity': 1.0
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. Artillery Trajectory Layer
|
// 3. Artillery Trajectory Layer
|
||||||
initialMap.addSource('tactical-artillery-src', {
|
safeAddSource('tactical-artillery-src', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: { type: 'FeatureCollection', features: [] }
|
data: { type: 'FeatureCollection', features: [] }
|
||||||
});
|
});
|
||||||
|
|
||||||
initialMap.addLayer({
|
safeAddLayer({
|
||||||
id: 'tactical-artillery-line',
|
id: 'tactical-artillery-line',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
source: 'tactical-artillery-src',
|
source: 'tactical-artillery-src',
|
||||||
paint: {
|
paint: {
|
||||||
'line-color': '#f97316',
|
'line-color': '#f97316',
|
||||||
'line-width': 4
|
'line-width': 5
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. Minefield Barrier Layer
|
// 4. Minefield Barrier Layer
|
||||||
initialMap.addSource('tactical-minefield-src', {
|
safeAddSource('tactical-minefield-src', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: { type: 'FeatureCollection', features: [] }
|
data: { type: 'FeatureCollection', features: [] }
|
||||||
});
|
});
|
||||||
|
|
||||||
initialMap.addLayer({
|
safeAddLayer({
|
||||||
id: 'tactical-minefield-line',
|
id: 'tactical-minefield-line',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
source: 'tactical-minefield-src',
|
source: 'tactical-minefield-src',
|
||||||
paint: {
|
paint: {
|
||||||
'line-color': '#dc2626',
|
'line-color': '#dc2626',
|
||||||
'line-width': 6,
|
'line-width': 6
|
||||||
'line-dasharray': [1, 1]
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5. Tactical Symbols Layer
|
// 5. Tactical Symbols Layer
|
||||||
initialMap.addSource('tactical-symbols-src', {
|
safeAddSource('tactical-symbols-src', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: {
|
data: {
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
@@ -1199,7 +1246,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
initialMap.addLayer({
|
safeAddLayer({
|
||||||
id: 'tactical-symbols-pts',
|
id: 'tactical-symbols-pts',
|
||||||
type: 'circle',
|
type: 'circle',
|
||||||
source: 'tactical-symbols-src',
|
source: 'tactical-symbols-src',
|
||||||
@@ -1222,7 +1269,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
initialMap.addLayer({
|
safeAddLayer({
|
||||||
id: 'tactical-symbols-lbls',
|
id: 'tactical-symbols-lbls',
|
||||||
type: 'symbol',
|
type: 'symbol',
|
||||||
source: 'tactical-symbols-src',
|
source: 'tactical-symbols-src',
|
||||||
@@ -1241,56 +1288,56 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 6. Isochrone Reachability Polygons Layer (خارطة زمن الاستجابة والوصول)
|
// 6. Isochrone Reachability Polygons Layer (خارطة زمن الاستجابة والوصول)
|
||||||
initialMap.addSource('tactical-isochrone-src', {
|
safeAddSource('tactical-isochrone-src', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: { type: 'FeatureCollection', features: [] }
|
data: { type: 'FeatureCollection', features: [] }
|
||||||
});
|
});
|
||||||
|
|
||||||
initialMap.addLayer({
|
safeAddLayer({
|
||||||
id: 'tactical-isochrone-fill',
|
id: 'tactical-isochrone-fill',
|
||||||
type: 'fill',
|
type: 'fill',
|
||||||
source: 'tactical-isochrone-src',
|
source: 'tactical-isochrone-src',
|
||||||
paint: {
|
paint: {
|
||||||
'fill-color': ['coalesce', ['get', 'color'], '#22c55e'],
|
'fill-color': ['coalesce', ['get', 'color'], '#22c55e'],
|
||||||
'fill-opacity': 0.32
|
'fill-opacity': 0.45
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
initialMap.addLayer({
|
safeAddLayer({
|
||||||
id: 'tactical-isochrone-line',
|
id: 'tactical-isochrone-line',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
source: 'tactical-isochrone-src',
|
source: 'tactical-isochrone-src',
|
||||||
paint: {
|
paint: {
|
||||||
'line-color': ['coalesce', ['get', 'color'], '#22c55e'],
|
'line-color': ['coalesce', ['get', 'color'], '#22c55e'],
|
||||||
'line-width': 2.5,
|
'line-width': 3.5,
|
||||||
'line-opacity': 0.95
|
'line-opacity': 0.95
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 7. Digital IPB Overlays Layer (منظومة الشفافات التكتيكية الرقمية لإعداد ساحة المعركة)
|
// 7. Digital IPB Overlays Layer (منظومة الشفافات التكتيكية الرقمية لإعداد ساحة المعركة)
|
||||||
initialMap.addSource('tactical-ipb-src', {
|
safeAddSource('tactical-ipb-src', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: { type: 'FeatureCollection', features: [] }
|
data: { type: 'FeatureCollection', features: [] }
|
||||||
});
|
});
|
||||||
|
|
||||||
initialMap.addLayer({
|
safeAddLayer({
|
||||||
id: 'tactical-ipb-fill',
|
id: 'tactical-ipb-fill',
|
||||||
type: 'fill',
|
type: 'fill',
|
||||||
source: 'tactical-ipb-src',
|
source: 'tactical-ipb-src',
|
||||||
filter: ['==', '$type', 'Polygon'],
|
filter: ['==', ['geometry-type'], 'Polygon'],
|
||||||
paint: {
|
paint: {
|
||||||
'fill-color': ['coalesce', ['get', 'color'], '#38bdf8'],
|
'fill-color': ['coalesce', ['get', 'color'], '#38bdf8'],
|
||||||
'fill-opacity': ['coalesce', ['get', 'opacity'], 0.65]
|
'fill-opacity': ['coalesce', ['get', 'opacity'], 0.65]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
initialMap.addLayer({
|
safeAddLayer({
|
||||||
id: 'tactical-ipb-line',
|
id: 'tactical-ipb-line',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
source: 'tactical-ipb-src',
|
source: 'tactical-ipb-src',
|
||||||
filter: [
|
filter: [
|
||||||
'any',
|
'any',
|
||||||
['==', '$type', 'LineString'],
|
['==', ['geometry-type'], 'LineString'],
|
||||||
['==', ['get', 'layer'], 'layer_10'],
|
['==', ['get', 'layer'], 'layer_10'],
|
||||||
['==', ['get', 'layer'], 'layer_7'],
|
['==', ['get', 'layer'], 'layer_7'],
|
||||||
['==', ['get', 'layer'], 'layer_4']
|
['==', ['get', 'layer'], 'layer_4']
|
||||||
@@ -1308,7 +1355,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
initialMap.addLayer({
|
safeAddLayer({
|
||||||
id: 'tactical-ipb-symbol',
|
id: 'tactical-ipb-symbol',
|
||||||
type: 'symbol',
|
type: 'symbol',
|
||||||
source: 'tactical-ipb-src',
|
source: 'tactical-ipb-src',
|
||||||
@@ -1334,6 +1381,11 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
'text-halo-width': 2.5
|
'text-halo-width': 2.5
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bringTacticalLayersToFront(initialMap);
|
||||||
|
mapLoadedRef.current = true;
|
||||||
|
setMapLoaded(true);
|
||||||
|
(window as any).tacticalMap = initialMap;
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -1344,41 +1396,47 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 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 = (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',
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
type: 'Feature',
|
||||||
|
properties: {},
|
||||||
|
geometry: {
|
||||||
|
type: 'Polygon',
|
||||||
|
coordinates: [coords]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
bringTacticalLayersToFront(map.current);
|
||||||
|
};
|
||||||
|
|
||||||
// Run Terrain Tactical Study
|
// Run Terrain Tactical Study
|
||||||
const runTerrainStudy = () => {
|
const runTerrainStudy = () => {
|
||||||
if (!terrainCenter) return;
|
if (!terrainCenter) return;
|
||||||
|
// Draw boundary circle immediately so feedback is instant
|
||||||
|
drawSectorCircle(terrainCenter, terrainRadius);
|
||||||
setTerrainLoading(true);
|
setTerrainLoading(true);
|
||||||
calculateTerrainStudy(terrainCenter[0], terrainCenter[1], terrainRadius)
|
calculateTerrainStudy(terrainCenter[0], terrainCenter[1], terrainRadius)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setTerrainResult(res);
|
setTerrainResult(res);
|
||||||
setTerrainLoading(false);
|
setTerrainLoading(false);
|
||||||
|
drawSectorCircle(terrainCenter, terrainRadius);
|
||||||
// 1. Draw sector boundary polygon on map
|
|
||||||
if (map.current && map.current.getSource('tactical-terrain-src')) {
|
|
||||||
const points = 48;
|
|
||||||
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]);
|
|
||||||
}
|
|
||||||
|
|
||||||
(map.current.getSource('tactical-terrain-src') as maplibregl.GeoJSONSource).setData({
|
|
||||||
type: 'FeatureCollection',
|
|
||||||
features: [
|
|
||||||
{
|
|
||||||
type: 'Feature',
|
|
||||||
properties: {},
|
|
||||||
geometry: {
|
|
||||||
type: 'Polygon',
|
|
||||||
coordinates: [coords]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(() => setTerrainLoading(false));
|
.catch(() => setTerrainLoading(false));
|
||||||
};
|
};
|
||||||
@@ -1396,7 +1454,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
// Sync Spatial GeoJSON Features with Active Layer Toggles
|
// Sync Spatial GeoJSON Features with Active Layer Toggles
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map.current || !map.current.getSource('tactical-terrain-spatial-src')) return;
|
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({
|
(map.current.getSource('tactical-terrain-spatial-src') as maplibregl.GeoJSONSource).setData({
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features: []
|
features: []
|
||||||
@@ -1418,7 +1476,8 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features: filtered
|
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
|
// 1. Auto-run real mathematical IPB calculation when center or radius changes or when entering IPB mode
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1453,9 +1512,10 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
// Auto-run initial terrain study on mount & coordinates change
|
// Auto-run initial terrain study on mount & coordinates change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runTerrainStudy();
|
runTerrainStudy();
|
||||||
}, [terrainCenter, terrainRadius]);
|
}, [terrainCenter, terrainRadius, mapLoaded]);
|
||||||
|
|
||||||
const ensureViewshedSourceAndLayer = (m: maplibregl.Map) => {
|
const ensureViewshedSourceAndLayer = (m: maplibregl.Map) => {
|
||||||
|
if (!m) return;
|
||||||
if (!m.getSource('tactical-viewshed-src')) {
|
if (!m.getSource('tactical-viewshed-src')) {
|
||||||
m.addSource('tactical-viewshed-src', {
|
m.addSource('tactical-viewshed-src', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
@@ -1468,8 +1528,8 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
type: 'fill',
|
type: 'fill',
|
||||||
source: 'tactical-viewshed-src',
|
source: 'tactical-viewshed-src',
|
||||||
paint: {
|
paint: {
|
||||||
'fill-color': '#22c55e',
|
'fill-color': '#ff0000',
|
||||||
'fill-opacity': 0.35
|
'fill-opacity': 0.80
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1479,15 +1539,17 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
type: 'line',
|
type: 'line',
|
||||||
source: 'tactical-viewshed-src',
|
source: 'tactical-viewshed-src',
|
||||||
paint: {
|
paint: {
|
||||||
'line-color': '#16a34a',
|
'line-color': '#ffff00',
|
||||||
'line-width': 3,
|
'line-width': 8.0,
|
||||||
'line-dasharray': [3, 2]
|
'line-opacity': 1.0
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
bringTacticalLayersToFront(m);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ensureLosSourceAndLayer = (m: maplibregl.Map) => {
|
const ensureLosSourceAndLayer = (m: maplibregl.Map) => {
|
||||||
|
if (!m) return;
|
||||||
if (!m.getSource('tactical-los-src')) {
|
if (!m.getSource('tactical-los-src')) {
|
||||||
m.addSource('tactical-los-src', {
|
m.addSource('tactical-los-src', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
@@ -1499,17 +1561,18 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
id: 'tactical-los-line',
|
id: 'tactical-los-line',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
source: 'tactical-los-src',
|
source: 'tactical-los-src',
|
||||||
filter: ['==', '$type', 'LineString'],
|
filter: ['==', ['geometry-type'], 'LineString'],
|
||||||
paint: {
|
paint: {
|
||||||
'line-color': ['case', ['==', ['get', 'blocked'], true], '#ef4444', '#22c55e'],
|
'line-color': ['case', ['==', ['get', 'blocked'], true], '#ef4444', '#22c55e'],
|
||||||
'line-width': 4,
|
'line-width': 5
|
||||||
'line-dasharray': [2, 1]
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
bringTacticalLayersToFront(m);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ensureArtillerySourceAndLayer = (m: maplibregl.Map) => {
|
const ensureArtillerySourceAndLayer = (m: maplibregl.Map) => {
|
||||||
|
if (!m) return;
|
||||||
if (!m.getSource('tactical-artillery-src')) {
|
if (!m.getSource('tactical-artillery-src')) {
|
||||||
m.addSource('tactical-artillery-src', {
|
m.addSource('tactical-artillery-src', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
@@ -1523,13 +1586,14 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
source: 'tactical-artillery-src',
|
source: 'tactical-artillery-src',
|
||||||
paint: {
|
paint: {
|
||||||
'line-color': '#f97316',
|
'line-color': '#f97316',
|
||||||
'line-width': 4
|
'line-width': 5
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const ensureMinefieldSourceAndLayer = (m: maplibregl.Map) => {
|
const ensureMinefieldSourceAndLayer = (m: maplibregl.Map) => {
|
||||||
|
if (!m) return;
|
||||||
if (!m.getSource('tactical-minefield-src')) {
|
if (!m.getSource('tactical-minefield-src')) {
|
||||||
m.addSource('tactical-minefield-src', {
|
m.addSource('tactical-minefield-src', {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
@@ -1543,8 +1607,7 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
source: 'tactical-minefield-src',
|
source: 'tactical-minefield-src',
|
||||||
paint: {
|
paint: {
|
||||||
'line-color': '#dc2626',
|
'line-color': '#dc2626',
|
||||||
'line-width': 6,
|
'line-width': 6
|
||||||
'line-dasharray': [1, 1]
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1600,11 +1663,12 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features: feats
|
features: feats
|
||||||
});
|
});
|
||||||
|
bringTacticalLayersToFront(map.current);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => setLosLoading(false));
|
.catch(() => setLosLoading(false));
|
||||||
}, [losPointA, losPointB, losObsHeight, losTgtHeight]);
|
}, [losPointA, losPointB, losObsHeight, losTgtHeight, mapLoaded]);
|
||||||
|
|
||||||
// Update Minefield Analysis & Map Layer
|
// Update Minefield Analysis & Map Layer
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1646,11 +1710,12 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
bringTacticalLayersToFront(map.current);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => setMineLoading(false));
|
.catch(() => setMineLoading(false));
|
||||||
}, [mineStart, mineEnd, mineDensity]);
|
}, [mineStart, mineEnd, mineDensity, mapLoaded]);
|
||||||
|
|
||||||
// Update Artillery Trajectory
|
// Update Artillery Trajectory
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1669,31 +1734,101 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
runArtilleryMission();
|
runArtilleryMission();
|
||||||
}, [gunPos, targetPos, caliber, muzzleVel]);
|
}, [gunPos, targetPos, caliber, muzzleVel, mapLoaded]);
|
||||||
|
|
||||||
// Update 360 Viewshed
|
// Update 360 Viewshed
|
||||||
const runViewshed360 = () => {
|
const runViewshed360 = () => {
|
||||||
if (!viewshedCenter) return;
|
if (!viewshedCenter) return;
|
||||||
setViewshedLoading(true);
|
setViewshedLoading(true);
|
||||||
|
console.log('[VIEWSHED-DEBUG] Starting calculation for center:', viewshedCenter, 'height:', viewshedHeight, 'radius:', viewshedRadius);
|
||||||
calculateRadialViewshed(viewshedCenter[0], viewshedCenter[1], viewshedHeight, viewshedRadius, 36)
|
calculateRadialViewshed(viewshedCenter[0], viewshedCenter[1], viewshedHeight, viewshedRadius, 36)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setViewshedResult(res);
|
setViewshedResult(res);
|
||||||
setViewshedLoading(false);
|
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) {
|
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);
|
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;
|
const src = map.current.getSource('tactical-viewshed-src') as maplibregl.GeoJSONSource;
|
||||||
if (src && res.polygonGeoJson) {
|
if (src && res.polygonGeoJson) {
|
||||||
src.setData({
|
const fc = {
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection' as const,
|
||||||
features: [res.polygonGeoJson]
|
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
|
// Run Artillery Fire Mission
|
||||||
const runArtilleryMission = async () => {
|
const runArtilleryMission = async () => {
|
||||||
if (!gunPos || !targetPos) return;
|
if (!gunPos || !targetPos) return;
|
||||||
@@ -1717,18 +1852,23 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setArtilleryResult(data);
|
setArtilleryResult(data);
|
||||||
|
|
||||||
if (map.current && map.current.getSource('tactical-artillery-src')) {
|
if (map.current) {
|
||||||
const lineCoords = (data.trajectoryPoints || []).map((p: any) => [p.lng, p.lat]);
|
ensureArtillerySourceAndLayer(map.current);
|
||||||
(map.current.getSource('tactical-artillery-src') as maplibregl.GeoJSONSource).setData({
|
const src = map.current.getSource('tactical-artillery-src') as maplibregl.GeoJSONSource;
|
||||||
type: 'FeatureCollection',
|
if (src) {
|
||||||
features: [
|
const lineCoords = (data.trajectoryPoints || []).map((p: any) => [p.lng, p.lat]);
|
||||||
{
|
src.setData({
|
||||||
type: 'Feature',
|
type: 'FeatureCollection',
|
||||||
properties: {},
|
features: [
|
||||||
geometry: { type: 'LineString', coordinates: lineCoords }
|
{
|
||||||
}
|
type: 'Feature',
|
||||||
]
|
properties: {},
|
||||||
});
|
geometry: { type: 'LineString', coordinates: lineCoords }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
bringTacticalLayersToFront(map.current);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Artillery calculation error:', 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 (!map.current || !map.current.getSource('tactical-isochrone-src')) return;
|
||||||
if (mode === 'isochrone' && isochroneData?.featureCollection) {
|
if (mode === 'isochrone' && isochroneData?.featureCollection) {
|
||||||
(map.current.getSource('tactical-isochrone-src') as maplibregl.GeoJSONSource).setData(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({
|
(map.current.getSource('tactical-isochrone-src') as maplibregl.GeoJSONSource).setData({
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features: []
|
features: []
|
||||||
@@ -1824,6 +1982,13 @@ export const TacticalDefenseView: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [mode, isochroneData]);
|
}, [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
|
// Run Isochrone Calculation
|
||||||
const runIsochroneCalculation = async (center = isochroneCenter, profile = isochroneProfile, times = isochroneTimes) => {
|
const runIsochroneCalculation = async (center = isochroneCenter, profile = isochroneProfile, times = isochroneTimes) => {
|
||||||
if (!center) return;
|
if (!center) return;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user