281 lines
8.1 KiB
TypeScript
281 lines
8.1 KiB
TypeScript
import React, { useEffect, useRef } from 'react';
|
|
import maplibregl from 'maplibre-gl';
|
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
import mlcontour from 'maplibre-contour';
|
|
import { attachIconLoader } from '../utils/mapIcons';
|
|
|
|
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);
|
|
|
|
interface MapComponentProps {
|
|
onMapLoad: (map: maplibregl.Map) => void;
|
|
onMapClick?: (lat: number, lng: number) => void;
|
|
show3D?: boolean;
|
|
showPOIs?: boolean;
|
|
showTerrain?: boolean;
|
|
showContours?: boolean;
|
|
showAdminBoundaries?: boolean;
|
|
}
|
|
|
|
const MapComponent: React.FC<MapComponentProps> = ({
|
|
onMapLoad,
|
|
onMapClick,
|
|
show3D = false,
|
|
showPOIs = true,
|
|
showTerrain = true,
|
|
showContours = false,
|
|
showAdminBoundaries = true
|
|
}) => {
|
|
const mapContainer = useRef<HTMLDivElement>(null);
|
|
const map = useRef<maplibregl.Map | null>(null);
|
|
|
|
// Sync toggles with map layers
|
|
useEffect(() => {
|
|
if (!map.current || !map.current.isStyleLoaded()) return;
|
|
|
|
// Toggle 3D Buildings
|
|
['building-3d', 'building-3d-osm'].forEach(layerId => {
|
|
if (map.current!.getLayer(layerId)) {
|
|
map.current!.setLayoutProperty(layerId, 'visibility', show3D ? 'visible' : 'none');
|
|
}
|
|
});
|
|
if (map.current.getLayer('overture-building-footprint')) {
|
|
map.current.setLayoutProperty('overture-building-footprint', 'visibility', show3D ? 'none' : 'visible');
|
|
}
|
|
map.current.easeTo({ pitch: show3D ? 55 : 0, duration: 600 });
|
|
|
|
// Toggle POI Layers
|
|
const poiLayers = ['poi-icons', 'place-labels', 'overture-building-names', 'places-jordan-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');
|
|
}
|
|
|
|
// 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');
|
|
}
|
|
});
|
|
}, [show3D, showPOIs, showTerrain, showContours, showAdminBoundaries]);
|
|
|
|
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");
|
|
|
|
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,
|
|
},
|
|
});
|
|
}
|
|
|
|
onMapLoad(map.current!);
|
|
});
|
|
|
|
map.current.on('click', (e) => {
|
|
if (onMapClick) {
|
|
onMapClick(e.lngLat.lat, e.lngLat.lng);
|
|
}
|
|
});
|
|
|
|
map.current.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;
|