Enhance crop zoning features with new API integrations and UI updates

- Added Persian translations for legend levels in fa.json to improve localization.
- Updated CROP_ZONING_APIS.md to include new API endpoints for water need, soil quality, and cultivation risk, enhancing data retrieval capabilities.
- Refactored crop zoning components to support new data structures and improve rendering logic for different layers.
- Enhanced LayerControl and ZoneLegend components to dynamically display information based on the active layer, improving user experience.
- Implemented loading states and error handling in CropZoningWrapper for better data management during asynchronous operations.
This commit is contained in:
2026-02-26 00:37:00 +03:30
parent 3db9a86cbf
commit 5aea10a756
7 changed files with 484 additions and 109 deletions
@@ -5,8 +5,8 @@ import type L from 'leaflet'
import 'leaflet/dist/leaflet.css'
import 'leaflet-draw/dist/leaflet.draw.css'
import type { Feature, Polygon } from 'geojson'
import { CROP_COLORS, type CropType, type LayerType, type ZoneFeatureProperties } from './cropZoningTypes'
import type { ZoneInitialData } from '@/libs/api/services/cropZoningService'
import type { LayerType } from './cropZoningTypes'
import type { ZoneMapData, ZoneInitialData } from '@/libs/api/services/cropZoningService'
export type MapDrawGeoJSON = Record<string, unknown> | null
@@ -21,19 +21,14 @@ type CropZoningMapProps = {
className?: string
/** منطقهٔ اولیه؛ وقتی مقدار دارد و readOnly باشد نقشه فقط نمایشی است */
initialAreaGeoJson?: MapDrawGeoJSON | null
/** دیتای زون‌ها از API (POST zones/initial) */
zonesData?: ZoneInitialData[] | null
/** لیبل محصولات از API (id -> label) برای tooltip */
/** دیتای زون‌ها برای نقشه — از APIهای zones/initial یا water-need یا soil-quality یا cultivation-risk */
zonesData?: ZoneMapData[] | null
/** لیبل محصولات از API (id -> label) — فقط برای لایهٔ crops */
productLabels?: Record<string, string>
/** غیرفعال کردن رسم و ویرایش منطقه توسط کاربر */
readOnly?: boolean
}
const DEFAULT_PRODUCT_LABELS: Record<string, string> = {
wheat: 'گندم',
canola: 'کلزا',
saffron: 'زعفران'
}
export default function CropZoningMap({
center = [35.6892, 51.389],
@@ -46,7 +41,7 @@ export default function CropZoningMap({
className = '',
initialAreaGeoJson = null,
zonesData = null,
productLabels = DEFAULT_PRODUCT_LABELS,
productLabels = {},
readOnly = false
}: CropZoningMapProps) {
const mapRef = useRef<HTMLDivElement>(null)
@@ -56,14 +51,13 @@ export default function CropZoningMap({
const zonesLayerRef = useRef<L.GeoJSON | null>(null)
const renderZonesFromApi = useCallback(
(map: L.Map, zones: ZoneInitialData[]) => {
(map: L.Map, zones: ZoneMapData[]) => {
if (zonesLayerRef.current) {
map.removeLayer(zonesLayerRef.current)
zonesLayerRef.current = null
}
if (zones.length === 0) return
const labels = { ...DEFAULT_PRODUCT_LABELS, ...productLabels }
const grid = {
type: 'FeatureCollection' as const,
features: zones.map(z => ({
@@ -71,10 +65,10 @@ export default function CropZoningMap({
geometry: z.geometry,
properties: {
zoneId: z.zoneId,
crop: z.crop,
matchPercent: z.matchPercent,
waterNeed: z.waterNeed,
estimatedProfit: z.estimatedProfit
color: z.color,
tooltipContent: z.tooltipContent,
cultivable: z.cultivable,
zoneInitialData: z.zoneInitialData
}
}))
}
@@ -83,48 +77,31 @@ export default function CropZoningMap({
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' }
}
style: (feature?: { properties?: { color?: string } }) => {
const color = feature?.properties?.color ?? '#94a3b8'
return {
fillColor: (CROP_COLORS[props.crop as CropType] ?? '#94a3b8'),
fillColor: color,
fillOpacity: 0.5,
weight: 1,
color: '#fff'
}
},
onEachFeature: (feat: { geometry?: unknown; properties?: ZoneFeatureProperties }, leafLayer: L.Layer) => {
const feature = feat as { geometry: Polygon; properties?: ZoneFeatureProperties }
const props = feature?.properties
const geom = feature?.geometry
onEachFeature: (feat: unknown, leafLayer: L.Layer) => {
const f = feat as { geometry?: Polygon; properties?: Record<string, unknown> }
const props = f?.properties
const geom = f?.geometry
if (!props || !geom) return
const layer = leafLayer as L.Polygon
const cropLabel = labels[props.crop] ?? props.crop
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>
`
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]
})
const zoneData: ZoneInitialData = {
zoneId: props.zoneId,
geometry: geom,
crop: props.crop,
matchPercent: props.matchPercent,
waterNeed: props.waterNeed,
estimatedProfit: props.estimatedProfit
}
layer.on('click', () => onZoneClick?.(zoneData))
layer.on('click', () => cultivable && zoneInitialData && onZoneClick?.(zoneInitialData))
}
})
@@ -143,7 +120,7 @@ export default function CropZoningMap({
}, delay)
})
},
[activeLayer, onZoneClick, productLabels]
[onZoneClick]
)
useEffect(() => {
@@ -268,23 +245,6 @@ export default function CropZoningMap({
}
}, [zonesData, optimizationKey, renderZonesFromApi])
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