760 lines
25 KiB
TypeScript
760 lines
25 KiB
TypeScript
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<MapProps> = ({
|
|
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<HTMLDivElement>(null);
|
|
const map = useRef<maplibregl.Map | null>(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 (
|
|
<div ref={mapContainer} style={{ width: '100%', height: '100%', position: 'relative' }}>
|
|
{/* Intaleq Premium Branding Watermark */}
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
bottom: '10px',
|
|
left: '10px',
|
|
zIndex: 10,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
background: 'rgba(255, 255, 255, 0.85)',
|
|
backdropFilter: 'blur(12px) saturate(180%)',
|
|
WebkitBackdropFilter: 'blur(12px) saturate(180%)',
|
|
padding: '6px 12px',
|
|
borderRadius: '10px',
|
|
pointerEvents: 'none',
|
|
userSelect: 'none',
|
|
gap: '8px',
|
|
boxShadow: '0 4px 16px rgba(0,0,0,0.08)',
|
|
border: '1px solid rgba(255,255,255,0.35)'
|
|
}}
|
|
>
|
|
<img
|
|
src="/intaleq-logo.png"
|
|
alt="Intaleq"
|
|
style={{ height: '18px', filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.1))' }}
|
|
/>
|
|
<span
|
|
style={{
|
|
fontSize: '10px',
|
|
fontWeight: 800,
|
|
color: '#c0a048',
|
|
letterSpacing: '0.8px',
|
|
textTransform: 'uppercase'
|
|
}}
|
|
>
|
|
Powered by Intaleq
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default MapComponent;
|