fix(web): add shared DemSource singleton, add ErrorBoundary, and conditional view rendering to prevent WebGL crash

This commit is contained in:
Hamza-Ayed
2026-08-18 12:35:17 +03:00
parent ba4ae15eab
commit c9b3249aa5
5 changed files with 119 additions and 70 deletions
+2 -25
View File
@@ -1,32 +1,8 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import maplibregl from 'maplibre-gl'; import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css'; import 'maplibre-gl/dist/maplibre-gl.css';
import mlcontour from 'maplibre-contour';
import { attachIconLoader } from '../utils/mapIcons'; import { attachIconLoader } from '../utils/mapIcons';
import { getSharedDemSource } from '../utils/demSource';
interface MapProps {
onMapLoad: (map: maplibregl.Map) => void;
onMapClick?: (lat: number, lng: number) => void;
show3D?: boolean;
showPOIs?: boolean;
showTerrain?: boolean;
showContours?: boolean;
showAdminBoundaries?: boolean;
showWeather?: boolean;
currentRegion?: string;
onCityClick?: (city: any) => void;
losActive?: boolean;
losPointA?: [number, number] | null;
losPointB?: [number, number] | null;
}
const demSource = new mlcontour.DemSource({
url: 'https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png',
encoding: 'terrarium',
maxzoom: 14,
worker: true,
});
demSource.setupMaplibre(maplibregl);
const MapComponent: React.FC<MapProps> = ({ const MapComponent: React.FC<MapProps> = ({
onMapLoad, onMapLoad,
@@ -468,6 +444,7 @@ const MapComponent: React.FC<MapProps> = ({
}); });
} }
const demSource = getSharedDemSource();
const contourUrl = demSource.contourProtocolUrl({ const contourUrl = demSource.contourProtocolUrl({
thresholds: { thresholds: {
10: [100, 500], 10: [100, 500],
+86 -25
View File
@@ -73,14 +73,70 @@ function ViewNav({ hash }: { hash: string }) {
) )
} }
class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean; error: any }> {
constructor(props: any) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: any) {
return { hasError: true, error };
}
componentDidCatch(error: any, errorInfo: any) {
console.error('App UI Error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
background: '#0f172a',
color: '#fff',
fontFamily: 'system-ui, sans-serif',
direction: 'rtl',
padding: 24,
textAlign: 'center'
}}>
<div style={{ fontSize: '3rem', marginBottom: 16 }}>⚠️</div>
<h2 style={{ fontSize: '1.4rem', fontWeight: 700, marginBottom: 8 }}>حدث خطأ أثناء تحميل الواجهة</h2>
<p style={{ color: '#94a3b8', fontSize: '0.9rem', maxWidth: 450, marginBottom: 20 }}>
{this.state.error?.message || 'تعذر تحميل أحد مكونات الخريطة'}
</p>
<button
onClick={() => { window.location.hash = '#map'; window.location.reload(); }}
style={{
padding: '10px 24px',
background: '#6366f1',
color: '#fff',
border: 'none',
borderRadius: 8,
fontWeight: 700,
cursor: 'pointer',
fontSize: '0.95rem'
}}
>
إعادة تحميل الخريطة الأساسية
</button>
</div>
);
}
return this.props.children;
}
}
function Root() { function Root() {
const [hash, setHash] = useState(window.location.hash || '#tactical') const [hash, setHash] = useState(window.location.hash || '#tactical');
useEffect(() => { useEffect(() => {
const on = () => { const on = () => {
const h = window.location.hash || '#tactical'; const h = window.location.hash || '#tactical';
setHash(h); setHash(h);
// Trigger a resize event to ensure MapLibre maps adapt to visibility changes instantly
setTimeout(() => { setTimeout(() => {
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
}, 50); }, 50);
@@ -96,35 +152,40 @@ function Root() {
const isMap = !isTactical && !isExecutive && !isCompare && !isReview; const isMap = !isTactical && !isExecutive && !isCompare && !isReview;
return ( return (
<> <ErrorBoundary>
<ViewNav hash={hash} /> <ViewNav hash={hash} />
{/* Keep-Alive Persistent Views: Preserves Map Instances and GPU Tile Caches across Tab switches */} {isTactical && (
<div style={{ display: isTactical ? 'block' : 'none', width: '100vw', height: '100vh', position: 'absolute', top: 0, left: 0 }}> <div style={{ width: '100vw', height: '100vh', position: 'absolute', top: 0, left: 0 }}>
<TacticalDefenseView /> <TacticalDefenseView />
</div> </div>
)}
<div style={{ display: isExecutive ? 'block' : 'none', width: '100vw', height: '100vh', position: 'absolute', top: 0, left: 0 }}> {isExecutive && (
<ExecutiveShowcase /> <div style={{ width: '100vw', height: '100vh', position: 'absolute', top: 0, left: 0 }}>
</div> <ExecutiveShowcase />
</div>
)}
<div style={{ display: isCompare ? 'block' : 'none', width: '100vw', height: '100vh', position: 'absolute', top: 0, left: 0 }}> {isCompare && (
<CompareView /> <div style={{ width: '100vw', height: '100vh', position: 'absolute', top: 0, left: 0 }}>
</div> <CompareView />
</div>
)}
<div style={{ display: isReview ? 'block' : 'none', width: '100vw', height: '100vh', position: 'absolute', top: 0, left: 0 }}> {isReview && (
<IntelligenceDashboard /> <div style={{ width: '100vw', height: '100vh', position: 'absolute', top: 0, left: 0 }}>
</div> <IntelligenceDashboard />
</div>
)}
<div style={{ display: isMap ? 'block' : 'none', width: '100vw', height: '100vh', position: 'absolute', top: 0, left: 0 }}> {isMap && (
<App /> <div style={{ width: '100vw', height: '100vh', position: 'absolute', top: 0, left: 0 }}>
</div> <App />
</> </div>
)}
</ErrorBoundary>
); );
} }
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(<Root />);
<React.StrictMode>
<Root />
</React.StrictMode>,
)
+2 -10
View File
@@ -1,17 +1,8 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import maplibregl from 'maplibre-gl'; import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css'; import 'maplibre-gl/dist/maplibre-gl.css';
import mlcontour from 'maplibre-contour';
import { attachIconLoader } from '../utils/mapIcons'; import { attachIconLoader } from '../utils/mapIcons';
import { getSharedDemSource } from '../utils/demSource';
// Initialize DEM source for on-the-fly global contours (Jordan, Syria, Iraq, Egypt, etc.)
const demSource = new mlcontour.DemSource({
url: 'https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png',
encoding: 'terrarium',
maxzoom: 14,
worker: true,
});
demSource.setupMaplibre(maplibregl);
/** /**
* CompareView — side-by-side, pan/zoom-synced map comparison. * CompareView — side-by-side, pan/zoom-synced map comparison.
@@ -102,6 +93,7 @@ const CompareView: React.FC = () => {
lMap.on('load', () => { lMap.on('load', () => {
// Dynamic On-the-Fly Contours from Elevation DEM // Dynamic On-the-Fly Contours from Elevation DEM
const demSource = getSharedDemSource();
const contourUrl = demSource.contourProtocolUrl({ const contourUrl = demSource.contourProtocolUrl({
thresholds: { thresholds: {
10: [100, 500], 10: [100, 500],
+2 -10
View File
@@ -1,16 +1,7 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import maplibregl from 'maplibre-gl'; import maplibregl from 'maplibre-gl';
import mlcontour from 'maplibre-contour';
const demSource = new mlcontour.DemSource({
url: 'https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png',
encoding: 'terrarium',
maxzoom: 15,
worker: true,
cacheSize: 100,
});
demSource.setupMaplibre(maplibregl);
import 'maplibre-gl/dist/maplibre-gl.css'; import 'maplibre-gl/dist/maplibre-gl.css';
import { getSharedDemSource } from '../utils/demSource';
import { import {
Shield, Shield,
Crosshair, Crosshair,
@@ -456,6 +447,7 @@ export const TacticalDefenseView: React.FC = () => {
}); });
// Dynamic On-the-Fly Contours from Elevation DEM // Dynamic On-the-Fly Contours from Elevation DEM
const demSource = getSharedDemSource();
const contourUrl = demSource.contourProtocolUrl({ const contourUrl = demSource.contourProtocolUrl({
thresholds: { thresholds: {
10: [100, 500], 10: [100, 500],
+27
View File
@@ -0,0 +1,27 @@
import maplibregl from 'maplibre-gl';
import mlcontour from 'maplibre-contour';
let sharedDemSource: any = null;
let isConfigured = false;
export function getSharedDemSource() {
if (!sharedDemSource) {
sharedDemSource = new mlcontour.DemSource({
url: 'https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png',
encoding: 'terrarium',
maxzoom: 15,
worker: true,
cacheSize: 100,
});
if (!isConfigured) {
try {
sharedDemSource.setupMaplibre(maplibregl);
isConfigured = true;
} catch (e) {
console.warn('DemSource setup warning:', e);
}
}
}
return sharedDemSource;
}