164 lines
3.9 KiB
TypeScript
164 lines
3.9 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import L from 'leaflet';
|
|
|
|
export interface MapMarker {
|
|
id?: string;
|
|
lat: number;
|
|
lng: number;
|
|
label?: string;
|
|
type?: 'pickup' | 'destination' | 'driver' | 'default';
|
|
}
|
|
|
|
export interface MapViewProps {
|
|
center: [number, number];
|
|
zoom?: number;
|
|
markers?: MapMarker[];
|
|
polyline?: [number, number][];
|
|
className?: string;
|
|
onMapClick?: (lat: number, lng: number) => void;
|
|
}
|
|
|
|
const createCustomIcon = (type: MapMarker['type'] = 'default') => {
|
|
let bgColor = '#0f172a';
|
|
let ringColor = 'rgba(15, 23, 42, 0.2)';
|
|
let symbol = '•';
|
|
|
|
if (type === 'pickup') {
|
|
bgColor = '#15803d';
|
|
ringColor = 'rgba(21, 128, 61, 0.3)';
|
|
symbol = 'A';
|
|
} else if (type === 'destination') {
|
|
bgColor = '#dc2626';
|
|
ringColor = 'rgba(220, 38, 38, 0.3)';
|
|
symbol = 'B';
|
|
} else if (type === 'driver') {
|
|
bgColor = '#0284c7';
|
|
ringColor = 'rgba(2, 132, 199, 0.35)';
|
|
symbol = '🚕';
|
|
}
|
|
|
|
const html = `
|
|
<div style="
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 32px;
|
|
height: 32px;
|
|
background-color: ${bgColor};
|
|
color: #ffffff;
|
|
font-weight: 700;
|
|
font-size: 13px;
|
|
border-radius: 50%;
|
|
border: 2px solid #ffffff;
|
|
box-shadow: 0 0 0 4px ${ringColor}, 0 4px 10px rgba(0,0,0,0.2);
|
|
">
|
|
${symbol}
|
|
</div>
|
|
`;
|
|
|
|
return L.divIcon({
|
|
html,
|
|
className: 'custom-leaflet-marker-icon',
|
|
iconSize: [32, 32],
|
|
iconAnchor: [16, 16],
|
|
popupAnchor: [0, -16],
|
|
});
|
|
};
|
|
|
|
export default function MapView({
|
|
center,
|
|
zoom = 13,
|
|
markers = [],
|
|
polyline = [],
|
|
className,
|
|
onMapClick,
|
|
}: MapViewProps) {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const mapRef = useRef<L.Map | null>(null);
|
|
const markerLayerRef = useRef<L.LayerGroup | null>(null);
|
|
const polylineLayerRef = useRef<L.Polyline | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!containerRef.current || mapRef.current) return;
|
|
|
|
const map = L.map(containerRef.current, {
|
|
zoomControl: true,
|
|
attributionControl: false,
|
|
}).setView(center, zoom);
|
|
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
maxZoom: 19,
|
|
attribution: '© OpenStreetMap contributors',
|
|
}).addTo(map);
|
|
|
|
markerLayerRef.current = L.layerGroup().addTo(map);
|
|
mapRef.current = map;
|
|
|
|
if (onMapClick) {
|
|
map.on('click', (e: L.LeafletMouseEvent) => {
|
|
onMapClick(e.latlng.lat, e.latlng.lng);
|
|
});
|
|
}
|
|
|
|
const timer = setTimeout(() => {
|
|
map.invalidateSize();
|
|
}, 150);
|
|
|
|
return () => {
|
|
clearTimeout(timer);
|
|
map.remove();
|
|
mapRef.current = null;
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (mapRef.current) {
|
|
mapRef.current.setView(center, zoom);
|
|
}
|
|
}, [center[0], center[1], zoom]);
|
|
|
|
useEffect(() => {
|
|
const layer = markerLayerRef.current;
|
|
if (!layer) return;
|
|
layer.clearLayers();
|
|
|
|
markers.forEach((m) => {
|
|
const icon = createCustomIcon(m.type);
|
|
const marker = L.marker([m.lat, m.lng], { icon });
|
|
if (m.label) {
|
|
marker.bindPopup(
|
|
`<div style="font-family: sans-serif; font-weight: 600; font-size: 13px; color: #0f172a;">${m.label}</div>`
|
|
);
|
|
}
|
|
layer.addLayer(marker);
|
|
});
|
|
}, [markers]);
|
|
|
|
useEffect(() => {
|
|
if (!mapRef.current) return;
|
|
|
|
if (polylineLayerRef.current) {
|
|
polylineLayerRef.current.remove();
|
|
polylineLayerRef.current = null;
|
|
}
|
|
|
|
if (polyline && polyline.length > 0) {
|
|
polylineLayerRef.current = L.polyline(polyline, {
|
|
color: '#0284c7',
|
|
weight: 5,
|
|
opacity: 0.85,
|
|
lineCap: 'round',
|
|
lineJoin: 'round',
|
|
}).addTo(mapRef.current);
|
|
}
|
|
}, [polyline]);
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className={className || 'h-[450px] w-full rounded-xl border border-slate-300 shadow-sm bg-slate-100 overflow-hidden'}
|
|
/>
|
|
);
|
|
}
|