'use client' import { useEffect, useRef, useCallback } 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 { createZonedGrid } from './cropZoningUtils' import { CROP_COLORS, type CropType, type LayerType, type ZoneFeatureProperties } from './cropZoningTypes' export type MapDrawGeoJSON = Record | null type CropZoningMapProps = { center?: [number, number] zoom?: number height?: string | number activeLayer: LayerType onAreaChange?: (geojson: MapDrawGeoJSON) => void onZoneClick?: (zone: ZoneFeatureProperties) => void optimizationKey?: number className?: string /** منطقهٔ اولیه از دیتای ماک؛ وقتی مقدار دارد نقشه فقط نمایشی است و کاربر نمیتواند منطقه را تغییر دهد */ initialAreaGeoJson?: MapDrawGeoJSON | null /** غیرفعال کردن رسم و ویرایش منطقه توسط کاربر */ readOnly?: boolean } export default function CropZoningMap({ center = [35.6892, 51.389], zoom = 13, height = '100%', activeLayer, onAreaChange, onZoneClick, optimizationKey = 0, className = '', initialAreaGeoJson = null, readOnly = false }: CropZoningMapProps) { const mapRef = useRef(null) const mapInstanceRef = useRef(null) const drawnItemsRef = useRef(null) const drawControlRef = useRef(null) const zonesLayerRef = useRef(null) const renderZones = useCallback( (map: L.Map, polygonFeature: Feature) => { if (zonesLayerRef.current) { map.removeLayer(zonesLayerRef.current) zonesLayerRef.current = null } const grid = createZonedGrid(polygonFeature) const L = (window as unknown as { L: typeof import('leaflet') }).L if (!L) return const geoJsonLayer = L.geoJSON(grid as never, { style: (feature?: { properties?: ZoneFeatureProperties }) => { const props = feature?.properties if (!props || activeLayer !== 'crops') { return { fillColor: '#94a3b8', fillOpacity: 0.5, weight: 1, color: '#fff' } } return { fillColor: CROP_COLORS[props.crop as CropType], fillOpacity: 0.5, weight: 1, color: '#fff' } }, onEachFeature: (feature: { properties?: ZoneFeatureProperties }, leafLayer: L.Layer) => { const props = feature?.properties if (!props) return const layer = leafLayer as L.Polygon const cropLabel = props.crop === 'wheat' ? 'گندم' : props.crop === 'canola' ? 'کلزا' : 'زعفران' const tooltipContent = `
${cropLabel}
درصد تطابق: ${props.matchPercent}%
نیاز آب: ${props.waterNeed}
سود تخمینی: ${props.estimatedProfit}
` layer.bindTooltip(tooltipContent, { sticky: true, className: 'zone-tooltip', direction: 'top', offset: [0, -8] }) layer.on('click', () => onZoneClick?.(props)) } }) geoJsonLayer.addTo(map) zonesLayerRef.current = geoJsonLayer 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) }) }, [activeLayer, 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 emitAndRender = () => { const geojson = getGeoJsonFromDrawn() onAreaChange?.(geojson) if (geojson && geojson.geometry && (geojson.geometry as { type: string }).type === 'Polygon') { renderZones(map, geojson as Feature) } else if (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 Feature).eachLayer((layer) => drawnItems.addLayer(layer)) emitAndRender() } const onCreated = (e: L.LeafletEvent) => { const event = e as L.DrawEvents.Created drawnItems.clearLayers() drawnItems.addLayer(event.layer) emitAndRender() } const onEdited = () => emitAndRender() 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 cleanupFn = () => { 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 (!mapInstanceRef.current || !drawnItemsRef.current || optimizationKey === 0) return const geojson = drawnItemsRef.current.toGeoJSON() if (geojson.type === 'FeatureCollection' && geojson.features.length > 0) { const feature = geojson.features[0] as Feature renderZones(mapInstanceRef.current, feature) } }, [optimizationKey, activeLayer, renderZones]) useEffect(() => { const zonesLayer = zonesLayerRef.current if (!zonesLayer || !drawnItemsRef.current) return const geojson = drawnItemsRef.current.toGeoJSON() if (geojson.type !== 'FeatureCollection' || geojson.features.length === 0) return zonesLayer.eachLayer((layer: L.Layer) => { const leafLayer = layer as L.Polygon & { feature?: { properties: ZoneFeatureProperties } } const props = leafLayer.feature?.properties if (!props) return leafLayer.setStyle({ fillColor: activeLayer === 'crops' ? CROP_COLORS[props.crop as CropType] : '#94a3b8', fillOpacity: 0.5, weight: 1, color: '#fff' }) }) }, [activeLayer]) return (
) }