'use client' import { useEffect, useRef, useCallback, useState } from 'react' import type L from 'leaflet' import 'leaflet/dist/leaflet.css' import 'leaflet-draw/dist/leaflet.draw.css' import type { Feature, Polygon } from 'geojson' import type { LayerType } from './cropZoningTypes' import type { ZoneMapData, ZoneInitialData } from '@/libs/api/services/cropZoningService' export type MapDrawGeoJSON = Record | null type CropZoningMapProps = { center?: [number, number] zoom?: number height?: string | number activeLayer: LayerType onAreaChange?: (geojson: MapDrawGeoJSON) => void onZoneClick?: (zone: ZoneInitialData) => void optimizationKey?: number className?: string /** منطقهٔ اولیه؛ وقتی مقدار دارد و readOnly باشد نقشه فقط نمایشی است */ initialAreaGeoJson?: MapDrawGeoJSON | null /** دیتای زون‌ها برای نقشه — از APIهای zones/initial یا water-need یا soil-quality یا cultivation-risk */ zonesData?: ZoneMapData[] | null /** لیبل محصولات از API (id -> label) — فقط برای لایهٔ crops */ productLabels?: Record /** غیرفعال کردن رسم و ویرایش منطقه توسط کاربر */ readOnly?: boolean } export default function CropZoningMap({ center = [35.6892, 51.389], zoom = 13, height = '100%', activeLayer, onAreaChange, onZoneClick, optimizationKey = 0, className = '', initialAreaGeoJson = null, zonesData = null, productLabels = {}, readOnly = false }: CropZoningMapProps) { const mapRef = useRef(null) const mapInstanceRef = useRef(null) const drawnItemsRef = useRef(null) const drawControlRef = useRef(null) const zonesLayerRef = useRef(null) const [isMapReady, setIsMapReady] = useState(false) const renderZonesFromApi = useCallback( (map: L.Map, zones: ZoneMapData[]) => { if (zonesLayerRef.current) { map.removeLayer(zonesLayerRef.current) zonesLayerRef.current = null } if (zones.length === 0) return const grid = { type: 'FeatureCollection' as const, features: zones.map(z => ({ type: 'Feature' as const, geometry: z.geometry, properties: { zoneId: z.zoneId, color: z.color, tooltipContent: z.tooltipContent, cultivable: z.cultivable, zoneInitialData: z.zoneInitialData } })) } const L = (window as unknown as { L: typeof import('leaflet') }).L if (!L) return const geoJsonLayer = L.geoJSON(grid as never, { style: (feature?: { properties?: { color?: string } }) => { const color = feature?.properties?.color ?? '#94a3b8' return { fillColor: color, fillOpacity: 0.5, weight: 1, color: '#fff' } }, onEachFeature: (feat: unknown, leafLayer: L.Layer) => { const f = feat as { geometry?: Polygon; properties?: Record } const props = f?.properties const geom = f?.geometry if (!props || !geom) return const layer = leafLayer as L.Polygon const tooltipContent = (props.tooltipContent as string) ?? '' const cultivable = props.cultivable === true const zoneInitialData = props.zoneInitialData as ZoneInitialData | undefined layer.bindTooltip(tooltipContent, { sticky: true, className: 'zone-tooltip', direction: 'top', offset: [0, -8] }) layer.on('click', () => cultivable && zoneInitialData && onZoneClick?.(zoneInitialData)) } }) geoJsonLayer.addTo(map) zonesLayerRef.current = geoJsonLayer const bounds = geoJsonLayer.getBounds() if (bounds.isValid()) { map.fitBounds(bounds, { padding: [24, 24] }) } let idx = 0 geoJsonLayer.eachLayer((layer: L.Layer) => { const leafLayer = layer as L.Polygon leafLayer.setStyle({ fillOpacity: 0 }) const index = idx++ const targetOpacity = 0.5 const delay = Math.min(index * 30, 600) setTimeout(() => { leafLayer.setStyle({ fillOpacity: targetOpacity }) }, delay) }) }, [onZoneClick] ) useEffect(() => { if (typeof window === 'undefined' || !mapRef.current) return let isMounted = true let cleanupFn: (() => void) | null = null const initMap = async () => { const L = (await import('leaflet')).default await import('leaflet-draw') ;(window as unknown as { L: typeof L }).L = L if (!isMounted || !mapRef.current || mapInstanceRef.current) return const map = L.map(mapRef.current).setView(center, zoom) L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap' }).addTo(map) const drawnItems = L.featureGroup().addTo(map) drawnItemsRef.current = drawnItems if (!readOnly) { const drawControl = new L.Control.Draw({ position: 'topright', draw: { polygon: { shapeOptions: { color: '#3388ff' } }, rectangle: { shapeOptions: { color: '#3388ff' } }, circle: false, circlemarker: false, marker: false, polyline: false }, edit: { featureGroup: drawnItems, remove: true } }) map.addControl(drawControl) drawControlRef.current = drawControl } const getGeoJsonFromDrawn = (): MapDrawGeoJSON => { const geojson = drawnItems.toGeoJSON() if (geojson.type === 'FeatureCollection' && geojson.features.length > 0) { return geojson.features[0] as unknown as MapDrawGeoJSON } return null } const emitAreaChange = () => { const geojson = getGeoJsonFromDrawn() onAreaChange?.(geojson) if (!geojson && zonesLayerRef.current) { map.removeLayer(zonesLayerRef.current) zonesLayerRef.current = null } } if (readOnly && initialAreaGeoJson && initialAreaGeoJson.geometry && (initialAreaGeoJson.geometry as { type: string }).type === 'Polygon') { drawnItems.clearLayers() L.geoJSON(initialAreaGeoJson as unknown as Feature).eachLayer((layer) => drawnItems.addLayer(layer)) emitAreaChange() const bounds = drawnItems.getBounds() if (bounds.isValid()) { map.fitBounds(bounds, { padding: [24, 24] }) } } const onCreated = (e: L.LeafletEvent) => { const event = e as L.DrawEvents.Created drawnItems.clearLayers() drawnItems.addLayer(event.layer) emitAreaChange() } const onEdited = () => emitAreaChange() const onDeleted = () => { onAreaChange?.(null) if (zonesLayerRef.current) { map.removeLayer(zonesLayerRef.current) zonesLayerRef.current = null } } if (!readOnly) { map.on(L.Draw.Event.CREATED, onCreated) map.on(L.Draw.Event.EDITED, onEdited) map.on(L.Draw.Event.DELETED, onDeleted) } mapInstanceRef.current = map setIsMapReady(true) cleanupFn = () => { setIsMapReady(false) if (!readOnly) { map.off(L.Draw.Event.CREATED, onCreated) map.off(L.Draw.Event.EDITED, onEdited) map.off(L.Draw.Event.DELETED, onDeleted) if (drawControlRef.current) { map.removeControl(drawControlRef.current) } } if (zonesLayerRef.current) map.removeLayer(zonesLayerRef.current) map.remove() mapInstanceRef.current = null drawnItemsRef.current = null drawControlRef.current = null zonesLayerRef.current = null } } initMap() return () => { isMounted = false if (cleanupFn) cleanupFn() } }, []) useEffect(() => { if (!isMapReady || !mapInstanceRef.current) return mapInstanceRef.current.invalidateSize() if (zonesData && zonesData.length > 0) { renderZonesFromApi(mapInstanceRef.current, zonesData) } else if (zonesLayerRef.current) { mapInstanceRef.current.removeLayer(zonesLayerRef.current) zonesLayerRef.current = null } }, [isMapReady, zonesData, optimizationKey, renderZonesFromApi]) return (
) }