Add crop zoning feature with localization support and new dependencies
- Introduced crop zoning functionality, including a new section in the navigation menu. - Added Persian translations for crop zoning-related UI elements. - Updated package.json and package-lock.json to include new dependencies: @mapbox/mapbox-gl-draw and various @turf packages for geospatial calculations. - Enhanced global styles for crop zoning tooltips to improve user experience.
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
'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
|
||||
}
|
||||
|
||||
export default function CropZoningMap({
|
||||
center = [35.6892, 51.389],
|
||||
zoom = 13,
|
||||
height = '100%',
|
||||
activeLayer,
|
||||
onAreaChange,
|
||||
onZoneClick,
|
||||
optimizationKey = 0,
|
||||
className = ''
|
||||
}: 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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
map.off(L.Draw.Event.CREATED, onCreated)
|
||||
map.off(L.Draw.Event.EDITED, onEdited)
|
||||
map.off(L.Draw.Event.DELETED, onDeleted)
|
||||
map.removeControl(drawControl)
|
||||
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}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user