import React, { useEffect, useRef } from 'react'; import maplibregl from 'maplibre-gl'; import 'maplibre-gl/dist/maplibre-gl.css'; import { attachIconLoader } from '../utils/mapIcons'; import { getSharedDemSource } from '../utils/demSource'; const MapComponent: React.FC = ({ onMapLoad, onMapClick, show3D = false, showPOIs = true, showTerrain = false, showContours = false, showAdminBoundaries = false, showWeather = false, currentRegion = 'Jordan', onCityClick, losActive = false, losPointA = null, losPointB = null, }) => { const mapContainer = useRef(null); const map = useRef(null); // Synchronize Line of Sight (LOS) Tactical Layer useEffect(() => { if (!map.current || !map.current.isStyleLoaded()) return; const features: any[] = []; if (losActive) { if (losPointA) { features.push({ type: 'Feature', properties: { role: 'obs', label: 'A (الراصد)' }, geometry: { type: 'Point', coordinates: [losPointA[1], losPointA[0]] } }); } if (losPointB) { features.push({ type: 'Feature', properties: { role: 'tgt', label: 'B (الهدف)' }, geometry: { type: 'Point', coordinates: [losPointB[1], losPointB[0]] } }); } if (losPointA && losPointB) { features.push({ type: 'Feature', properties: { role: 'ray' }, geometry: { type: 'LineString', coordinates: [ [losPointA[1], losPointA[0]], [losPointB[1], losPointB[0]] ] } }); } } const losGeoJson = { type: 'FeatureCollection', features }; if (!map.current.getSource('los-source')) { map.current.addSource('los-source', { type: 'geojson', data: losGeoJson as any }); map.current.addLayer({ id: 'los-line', type: 'line', source: 'los-source', filter: ['==', '$type', 'LineString'], layout: { 'line-cap': 'round', 'line-join': 'round' }, paint: { 'line-color': '#f59e0b', 'line-width': 3.5, 'line-dasharray': [2, 2] } }); map.current.addLayer({ id: 'los-points', type: 'circle', source: 'los-source', filter: ['==', '$type', 'Point'], paint: { 'circle-radius': 8, 'circle-color': [ 'match', ['get', 'role'], 'obs', '#3b82f6', 'tgt', '#ef4444', '#ffffff' ], 'circle-stroke-color': '#ffffff', 'circle-stroke-width': 2.5 } }); map.current.addLayer({ id: 'los-labels', type: 'symbol', source: 'los-source', filter: ['==', '$type', 'Point'], layout: { 'text-field': ['get', 'label'], 'text-size': 12, 'text-font': ['Noto Sans Bold', 'Open Sans Bold'], 'text-offset': [0, -1.6], 'text-anchor': 'bottom' }, paint: { 'text-color': '#ffffff', 'text-halo-color': '#0f172a', 'text-halo-width': 2 } }); } else { (map.current.getSource('los-source') as maplibregl.GeoJSONSource).setData(losGeoJson as any); map.current.setLayoutProperty('los-line', 'visibility', losActive ? 'visible' : 'none'); map.current.setLayoutProperty('los-points', 'visibility', losActive ? 'visible' : 'none'); map.current.setLayoutProperty('los-labels', 'visibility', losActive ? 'visible' : 'none'); } }, [losActive, losPointA, losPointB]); // Synchronize layers visibility useEffect(() => { if (!map.current || !map.current.isStyleLoaded()) return; // Toggle 3D Buildings if (map.current.getLayer('3d-buildings')) { map.current.setLayoutProperty('3d-buildings', 'visibility', show3D ? 'visible' : 'none'); } // Toggle POIs const poiLayers = ['poi-level-1', 'poi-level-2', 'poi-level-3', 'poi-labels']; poiLayers.forEach(layerId => { if (map.current!.getLayer(layerId)) { map.current!.setLayoutProperty(layerId, 'visibility', showPOIs ? 'visible' : 'none'); } }); // Toggle Hillshading / Terrain if (map.current.getLayer('hillshading')) { map.current.setLayoutProperty('hillshading', 'visibility', showTerrain ? 'visible' : 'none'); } if (map.current.getLayer('terrain-hillshade')) { map.current.setLayoutProperty('terrain-hillshade', 'visibility', showTerrain ? 'visible' : 'none'); } // Toggle Contours const contourLayers = ['contour-lines-minor', 'contour-lines-major', 'contour-labels']; contourLayers.forEach(layerId => { if (map.current!.getLayer(layerId)) { map.current!.setLayoutProperty(layerId, 'visibility', showContours ? 'visible' : 'none'); } }); // Toggle Admin Boundaries const adminLayers = [ 'admin-boundary-national', 'admin-boundary-governorate-poly', 'admin-boundary-governorate', 'admin-boundary-district-poly', 'admin-boundary-district' ]; adminLayers.forEach(layerId => { if (map.current!.getLayer(layerId)) { map.current!.setLayoutProperty(layerId, 'visibility', showAdminBoundaries ? 'visible' : 'none'); } }); const weatherLayers = ['weather-city-circles', 'weather-city-icons', 'weather-wind-arrows', 'weather-wind-speed']; weatherLayers.forEach(layerId => { if (map.current!.getLayer(layerId)) { map.current!.setLayoutProperty(layerId, 'visibility', showWeather ? 'visible' : 'none'); } }); }, [show3D, showPOIs, showTerrain, showContours, showAdminBoundaries, showWeather]); // Weather data fetching and vector rendering useEffect(() => { if (!map.current || !map.current.isStyleLoaded()) return; if (showWeather) { const fetchWeather = async () => { try { const apiUrl = (import.meta as any).env.VITE_API_URL || '/api'; const apiKey = (import.meta as any).env.VITE_API_KEY || ''; const regionParam = currentRegion ? `?region=${encodeURIComponent(currentRegion)}` : ''; const citiesRes = await fetch(`${apiUrl}/weather/cities${regionParam}`, { headers: { 'x-api-key': apiKey } }); const rawCitiesData = await citiesRes.json(); // Convert cities array to GeoJSON FeatureCollection const citiesGeoJson = { type: 'FeatureCollection', features: (Array.isArray(rawCitiesData) ? rawCitiesData : []).map((city: any) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [city.lng, city.lat] }, properties: { name: city.name, name_ar: city.name_ar, temperature: Number(city.current?.temp ?? 20), feelsLike: Number(city.current?.feelsLike ?? 20), humidity: Number(city.current?.humidity ?? 50), weatherCode: Number(city.current?.weatherCode ?? 0), windSpeed: Number(city.current?.windSpeed ?? 10), windDirection: Number(city.current?.windDirection ?? 0), cloudCover: Number(city.current?.cloudCover ?? 0), precipitation: Number(city.current?.precipitation ?? 0), isDay: Number(city.current?.isDay ?? 1), forecast: JSON.stringify(city.daily || []) } })) }; if (!map.current!.getSource('weather-cities-source')) { map.current!.addSource('weather-cities-source', { type: 'geojson', data: citiesGeoJson as any }); // 1. Soft temperature indicator circle badge map.current!.addLayer({ id: 'weather-city-circles', type: 'circle', source: 'weather-cities-source', paint: { 'circle-radius': 24, 'circle-color': [ 'step', ['to-number', ['get', 'temperature'], 20], '#3b82f6', 15, '#10b981', 25, '#f59e0b', 35, '#ef4444' ], 'circle-opacity': 0.9, 'circle-stroke-width': 2.5, 'circle-stroke-color': '#ffffff' } }); // 2. City name and temperature text badge map.current!.addLayer({ id: 'weather-city-icons', type: 'symbol', source: 'weather-cities-source', layout: { 'text-field': ['concat', ['coalesce', ['get', 'name_ar'], ''], '\n', ['to-string', ['round', ['to-number', ['get', 'temperature'], 20]]], '°C'], 'text-size': 11, 'text-font': ['Noto Sans Bold', 'Open Sans Bold'], 'text-justify': 'center', 'text-anchor': 'center' }, paint: { 'text-color': '#ffffff', 'text-halo-color': 'rgba(0, 0, 0, 0.6)', 'text-halo-width': 1.5 } }); // 3. Wind indicator arrow (rotated by wind direction angle) map.current!.addLayer({ id: 'weather-wind-arrows', type: 'symbol', source: 'weather-cities-source', minzoom: 4, layout: { 'text-field': '➤', 'text-rotation-alignment': 'map', 'text-rotate': ['to-number', ['get', 'windDirection'], 0], 'text-size': 14, 'text-offset': [2.2, 0], 'text-allow-overlap': true }, paint: { 'text-color': '#38bdf8', 'text-halo-color': '#0f172a', 'text-halo-width': 2 } }); // 4. Wind speed badge (e.g. 15 km/h) map.current!.addLayer({ id: 'weather-wind-speed', type: 'symbol', source: 'weather-cities-source', minzoom: 4, layout: { 'text-field': ['concat', ['to-string', ['round', ['to-number', ['get', 'windSpeed'], 10]]], ' km/h'], 'text-size': 9, 'text-font': ['Noto Sans Regular', 'Open Sans Regular'], 'text-offset': [0, 2.6], 'text-anchor': 'top', 'text-allow-overlap': false }, paint: { 'text-color': '#bae6fd', 'text-halo-color': 'rgba(15, 23, 42, 0.85)', 'text-halo-width': 2 } }); const handleCityClick = (e: any) => { if (e.originalEvent) { e.originalEvent.stopPropagation(); } if (onCityClick && e.features && e.features.length > 0) { onCityClick(e.features[0].properties); } }; map.current!.on('click', 'weather-city-circles', handleCityClick); map.current!.on('click', 'weather-city-icons', handleCityClick); map.current!.on('mouseenter', 'weather-city-circles', () => { if (map.current) map.current.getCanvas().style.cursor = 'pointer'; }); map.current!.on('mouseleave', 'weather-city-circles', () => { if (map.current) map.current.getCanvas().style.cursor = ''; }); } else { (map.current!.getSource('weather-cities-source') as maplibregl.GeoJSONSource).setData(citiesGeoJson as any); map.current!.setLayoutProperty('weather-city-circles', 'visibility', 'visible'); map.current!.setLayoutProperty('weather-city-icons', 'visibility', 'visible'); map.current!.setLayoutProperty('weather-wind-arrows', 'visibility', 'visible'); map.current!.setLayoutProperty('weather-wind-speed', 'visibility', 'visible'); } } catch (e) { console.warn('Weather fetch failed', e); } }; fetchWeather(); // 2. Fetch RainViewer Animated Cloud & Rain Radar let radarTimer: any = null; const fetchRadar = async () => { try { const res = await fetch('https://api.rainviewer.com/public/weather-maps.json'); const data = await res.json(); const host = data.host || 'https://tilecache.rainviewer.com'; const frames = data.radar?.past || []; if (frames.length > 0 && map.current && map.current.isStyleLoaded()) { let currentIdx = frames.length - 1; const getTileUrl = (i: number) => `${host}${frames[i].path}`; if (!map.current.getSource('weather-radar-source')) { map.current.addSource('weather-radar-source', { type: 'raster', tiles: [getTileUrl(currentIdx)], tileSize: 256 }); map.current.addLayer({ id: 'weather-radar-layer', type: 'raster', source: 'weather-radar-source', paint: { 'raster-opacity': 0.72, 'raster-fade-duration': 250 } }, map.current.getLayer('weather-city-circles') ? 'weather-city-circles' : undefined); } else { map.current.setLayoutProperty('weather-radar-layer', 'visibility', 'visible'); } // Animate through radar frames every 1200ms if (frames.length > 1) { radarTimer = setInterval(() => { if (!map.current || !map.current.getSource('weather-radar-source')) return; currentIdx = (currentIdx + 1) % frames.length; const src = map.current.getSource('weather-radar-source') as any; if (src && src.setTiles) { src.setTiles([getTileUrl(currentIdx)]); } }, 1200); } } } catch (err) { console.warn('Radar fetch failed', err); } }; fetchRadar(); return () => { if (radarTimer) clearInterval(radarTimer); }; } else { const weatherLayers = ['weather-city-circles', 'weather-city-icons', 'weather-wind-arrows', 'weather-wind-speed', 'weather-radar-layer']; weatherLayers.forEach(layerId => { if (map.current && map.current.getLayer(layerId)) { map.current.setLayoutProperty(layerId, 'visibility', 'none'); } }); } }, [showWeather, currentRegion, onCityClick]); const onMapClickRef = useRef(onMapClick); useEffect(() => { onMapClickRef.current = onMapClick; }, [onMapClick]); // Update canvas cursor when LOS is active useEffect(() => { if (map.current) { map.current.getCanvas().style.cursor = losActive ? 'crosshair' : ''; } }, [losActive]); useEffect(() => { if (map.current) return; if (!mapContainer.current) return; // Load RTL Text Plugin for correct Arabic rendering if (maplibregl.getRTLTextPluginStatus() === 'unavailable') { maplibregl.setRTLTextPlugin( '/rtl-plugin.js', true ); } try { const initialMap = new maplibregl.Map({ container: mapContainer.current, style: '/style.json', center: [35.9106, 31.9539], zoom: 12, attributionControl: false }); map.current = initialMap; attachIconLoader(initialMap); // Request User Location if ('geolocation' in navigator) { navigator.geolocation.getCurrentPosition( (position) => { if (initialMap) { initialMap.flyTo({ center: [position.coords.longitude, position.coords.latitude], zoom: 14, essential: true }); } }, (error) => console.warn('Geolocation denied or failed', error), { timeout: 5000 } ); } initialMap.on('load', () => { console.log("MapComponent: Map Loaded Successfully"); // Initialize Route Source and Layers if (!initialMap.getSource('route')) { initialMap.addSource('route', { type: 'geojson', data: { type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: [] } } }); // Route outer shadow/glow initialMap.addLayer({ id: 'route-casing', type: 'line', source: 'route', layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-color': '#0369a1', 'line-width': 9, 'line-opacity': 0.8 } }); // Route inner bright line initialMap.addLayer({ id: 'route-line', type: 'line', source: 'route', layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-color': '#38bdf8', 'line-width': 5, 'line-opacity': 1.0 } }); } // 3D Elevation Hillshade Relief (315° North-West Illumination) if (!initialMap.getSource('terrain-dem')) { initialMap.addSource('terrain-dem', { type: 'raster-dem', tiles: ['https://tiles.intaleqapp.com/raster_dem/{z}/{x}/{y}.png'], tileSize: 256, maxzoom: 14, }); initialMap.addLayer({ id: 'terrain-hillshade', type: 'hillshade', source: 'terrain-dem', layout: { visibility: showTerrain ? 'visible' : 'none', }, paint: { 'hillshade-illumination-direction': 315, 'hillshade-illumination-anchor': 'viewport', 'hillshade-shadow-color': '#3b1c06', 'hillshade-highlight-color': '#ffffff', 'hillshade-accent-color': '#9a3412', 'hillshade-exaggeration': 0.65, }, }); } const demSource = getSharedDemSource(); const contourUrl = demSource.contourProtocolUrl({ thresholds: { 10: [100, 500], 11: [50, 250], 12: [25, 100], 13: [20, 100], 14: [10, 50], 15: [10, 50], 16: [10, 50], 17: [10, 50], 18: [10, 50], }, elevationKey: 'ele', levelKey: 'level', contourLayer: 'contours', }); if (!initialMap.getSource('contour-source')) { initialMap.addSource('contour-source', { type: 'vector', tiles: [contourUrl], maxzoom: 18, }); initialMap.addLayer({ id: 'contour-lines-minor', type: 'line', source: 'contour-source', 'source-layer': 'contours', minzoom: 10, layout: { visibility: showContours ? 'visible' : 'none', }, filter: ['==', ['get', 'level'], 0], paint: { 'line-color': '#a86324', 'line-width': 1.1, 'line-opacity': 0.9, }, }); initialMap.addLayer({ id: 'contour-lines-major', type: 'line', source: 'contour-source', 'source-layer': 'contours', minzoom: 9, layout: { visibility: showContours ? 'visible' : 'none', }, filter: ['>', ['get', 'level'], 0], paint: { 'line-color': '#703800', 'line-width': 2.0, 'line-opacity': 1.0, }, }); initialMap.addLayer({ id: 'contour-labels', type: 'symbol', source: 'contour-source', 'source-layer': 'contours', minzoom: 12, layout: { visibility: showContours ? 'visible' : 'none', 'symbol-placement': 'line', 'text-field': ['concat', ['to-string', ['get', 'ele']], ' m'], 'text-size': 10, 'text-font': ['Noto Sans Bold', 'Open Sans Bold'], }, paint: { 'text-color': '#5c2d00', 'text-halo-color': 'rgba(255, 255, 255, 0.95)', 'text-halo-width': 2, }, }); // Hydro-Flattening Water Mask over Contour Layers if (!initialMap.getLayer('water-mask-hydroflat-map')) { initialMap.addLayer({ id: 'water-mask-hydroflat-map', type: 'fill', source: 'local-osm-polygons', 'source-layer': 'planet_osm_polygon', filter: [ 'any', ['==', 'natural', 'water'], ['==', 'water', 'reservoir'], ['==', 'water', 'dam'], ['==', 'water', 'lake'], ['==', 'water', 'pond'], ['==', 'waterway', 'riverbank'], ['==', 'landuse', 'reservoir'], ['==', 'landuse', 'basin'] ], paint: { 'fill-color': '#90C3D4', 'fill-opacity': 1.0 } }); } // Military Mountain Peak Spot Heights (✕ [ele]m) if (!initialMap.getLayer('tactical-peaks-spot-heights')) { initialMap.addLayer({ id: 'tactical-peaks-spot-heights', type: 'symbol', source: 'local-osm-points', 'source-layer': 'planet_osm_point', minzoom: 9, filter: [ 'any', ['in', 'natural', 'peak', 'volcano', 'ridge', 'hill', 'cliff'], ['in', 'place', 'isolated_dwelling', 'locality'] ], layout: { 'text-field': [ 'case', ['has', 'ele'], [ 'concat', '✕ ', ['to-string', ['get', 'ele']], 'م\n', ['coalesce', ['get', 'name:ar'], ['get', 'name'], ''] ], [ 'concat', '✕\n', ['coalesce', ['get', 'name:ar'], ['get', 'name'], 'مرتفع'] ] ], 'text-font': ['Noto Sans Bold', 'Open Sans Bold'], 'text-size': [ 'interpolate', ['linear'], ['zoom'], 9, 9, 11, 10, 13, 11.5, 15, 13, 17, 15 ], 'text-anchor': 'center', 'text-justify': 'center', 'text-padding': 8, 'text-allow-overlap': false }, paint: { 'text-color': '#7F1D1D', 'text-halo-color': 'rgba(255,255,255,0.95)', 'text-halo-width': 2.2 } }); } } onMapLoad(map.current!); }); initialMap.on('click', (e) => { if (onMapClickRef.current) { onMapClickRef.current(e.lngLat.lat, e.lngLat.lng); } }); initialMap.on('error', (e) => { console.error("MapComponent: Map Error:", e); }); } catch (err) { console.error("MapComponent: Fatal Initialization Error:", err); } return () => { if (map.current) { map.current.remove(); map.current = null; } }; }, []); return (
{/* Intaleq Premium Branding Watermark */}
Intaleq Powered by Intaleq
); }; export default MapComponent;