fix(web): add shared DemSource singleton, add ErrorBoundary, and conditional view rendering to prevent WebGL crash
This commit is contained in:
@@ -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
@@ -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>,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user