Files
Frontend/src/views/dashboards/farm/cropZoning/CropZoningMap.tsx
T
sajad-dev cb29828a69 Remove deprecated dashboard pages and update vertical menu links for streamlined navigation
- Deleted unused pages for crop zoning, farm AI assistant, fertilization recommendation, irrigation recommendation, pest detection, plant simulator, soil data, and water data.
- Updated the vertical menu to reflect the removal of these pages, ensuring a cleaner and more efficient user experience.
2026-02-21 22:05:47 +03:30

264 lines
8.8 KiB
TypeScript

'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<string, unknown> | 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<HTMLDivElement>(null)
const mapInstanceRef = useRef<L.Map | null>(null)
const drawnItemsRef = useRef<L.FeatureGroup | null>(null)
const drawControlRef = useRef<L.Control.Draw | null>(null)
const zonesLayerRef = useRef<L.GeoJSON | null>(null)
const renderZones = useCallback(
(map: L.Map, polygonFeature: Feature<Polygon>) => {
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 = `
<div style="font-family: inherit; font-size: 12px; padding: 4px 8px; min-width: 160px;">
<div style="font-weight: 600; margin-bottom: 6px;">${cropLabel}</div>
<div>درصد تطابق: ${props.matchPercent}%</div>
<div>نیاز آب: ${props.waterNeed}</div>
<div>سود تخمینی: ${props.estimatedProfit}</div>
</div>
`
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<Polygon>)
} 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<Polygon>).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<Polygon>
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 (
<div
ref={mapRef}
style={{
height: typeof height === 'number' ? `${height}px` : height,
width: '100%',
borderRadius: 12
}}
className={`overflow-hidden ${className}`}
/>
)
}