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:
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import { useState, useCallback, useEffect, useMemo } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import Box from '@mui/material/Box'
|
||||
@@ -12,7 +12,17 @@ import LayerControl from './LayerControl'
|
||||
import ZoneDetailPanel from './ZoneDetailPanel'
|
||||
import CropZoningWeatherSection from './CropZoningWeatherSection'
|
||||
import { createGridFromPolygon } from './cropZoningUtils'
|
||||
import { cropZoningService, type Product, type ZoneInitialData, type ZoneDetailData } from '@/libs/api/services/cropZoningService'
|
||||
import {
|
||||
cropZoningService,
|
||||
type Product,
|
||||
type ZoneInitialData,
|
||||
type ZoneDetailData,
|
||||
type ZoneMapData,
|
||||
type ZoneWaterNeedData,
|
||||
type ZoneSoilQualityData,
|
||||
type ZoneCultivationRiskData
|
||||
} from '@/libs/api/services/cropZoningService'
|
||||
import { CROP_COLORS, type CropType } from './cropZoningTypes'
|
||||
import type { LayerType } from './cropZoningTypes'
|
||||
import type { MapDrawGeoJSON } from './CropZoningMap'
|
||||
|
||||
@@ -34,9 +44,13 @@ export default function CropZoningWrapper() {
|
||||
const [areaGeoJson, setAreaGeoJson] = useState<MapDrawGeoJSON>(null)
|
||||
const [areaLoading, setAreaLoading] = useState(true)
|
||||
const [zonesData, setZonesData] = useState<ZoneInitialData[] | null>(null)
|
||||
const [zonesWaterNeed, setZonesWaterNeed] = useState<ZoneWaterNeedData[] | null>(null)
|
||||
const [zonesSoilQuality, setZonesSoilQuality] = useState<ZoneSoilQualityData[] | null>(null)
|
||||
const [zonesCultivationRisk, setZonesCultivationRisk] = useState<ZoneCultivationRiskData[] | null>(null)
|
||||
const [products, setProducts] = useState<Product[]>([])
|
||||
const [productsLoading, setProductsLoading] = useState(true)
|
||||
const [zonesLoading, setZonesLoading] = useState(false)
|
||||
const [layerDataLoading, setLayerDataLoading] = useState(false)
|
||||
const [activeLayer, setActiveLayer] = useState<LayerType>('crops')
|
||||
const [selectedZone, setSelectedZone] = useState<ZoneDetailData | null>(null)
|
||||
const [panelOpen, setPanelOpen] = useState(false)
|
||||
@@ -65,8 +79,14 @@ export default function CropZoningWrapper() {
|
||||
const fetchZones = useCallback((geojson: MapDrawGeoJSON) => {
|
||||
if (!isPolygon(geojson)) {
|
||||
setZonesData(null)
|
||||
setZonesWaterNeed(null)
|
||||
setZonesSoilQuality(null)
|
||||
setZonesCultivationRisk(null)
|
||||
return
|
||||
}
|
||||
setZonesWaterNeed(null)
|
||||
setZonesSoilQuality(null)
|
||||
setZonesCultivationRisk(null)
|
||||
setZonesLoading(true)
|
||||
const grid = createGridFromPolygon(geojson as unknown as import('geojson').Feature<import('geojson').Polygon>)
|
||||
cropZoningService
|
||||
@@ -81,9 +101,113 @@ export default function CropZoningWrapper() {
|
||||
fetchZones(areaGeoJson)
|
||||
} else {
|
||||
setZonesData(null)
|
||||
setZonesWaterNeed(null)
|
||||
setZonesSoilQuality(null)
|
||||
setZonesCultivationRisk(null)
|
||||
}
|
||||
}, [areaGeoJson, optimizationKey, fetchZones])
|
||||
|
||||
const gridForLayers = isPolygon(areaGeoJson)
|
||||
? createGridFromPolygon(areaGeoJson as unknown as import('geojson').Feature<import('geojson').Polygon>)
|
||||
: null
|
||||
|
||||
useEffect(() => {
|
||||
if (!gridForLayers || zonesLoading) return
|
||||
if (activeLayer === 'waterNeed' && zonesWaterNeed === null) {
|
||||
setLayerDataLoading(true)
|
||||
cropZoningService
|
||||
.getZonesWaterNeed({ zones: gridForLayers })
|
||||
.then(res => setZonesWaterNeed(res.zones))
|
||||
.catch(() => setZonesWaterNeed(null))
|
||||
.finally(() => setLayerDataLoading(false))
|
||||
} else if (activeLayer === 'soilQuality' && zonesSoilQuality === null) {
|
||||
setLayerDataLoading(true)
|
||||
cropZoningService
|
||||
.getZonesSoilQuality({ zones: gridForLayers })
|
||||
.then(res => setZonesSoilQuality(res.zones))
|
||||
.catch(() => setZonesSoilQuality(null))
|
||||
.finally(() => setLayerDataLoading(false))
|
||||
} else if (activeLayer === 'cultivationRisk' && zonesCultivationRisk === null) {
|
||||
setLayerDataLoading(true)
|
||||
cropZoningService
|
||||
.getZonesCultivationRisk({ zones: gridForLayers })
|
||||
.then(res => setZonesCultivationRisk(res.zones))
|
||||
.catch(() => setZonesCultivationRisk(null))
|
||||
.finally(() => setLayerDataLoading(false))
|
||||
}
|
||||
}, [activeLayer, gridForLayers, zonesLoading, zonesWaterNeed, zonesSoilQuality, zonesCultivationRisk])
|
||||
|
||||
const mapZonesData = useMemo((): ZoneMapData[] | null => {
|
||||
const labels = Object.fromEntries(products.map(p => [p.id, p.label]))
|
||||
if (activeLayer === 'crops' && zonesData) {
|
||||
const isCultivable = (crop: string | null | undefined) =>
|
||||
!!crop && crop !== 'uncultivable' && crop.toLowerCase() !== 'uncultivable'
|
||||
return zonesData.map(z => {
|
||||
const cultivable = isCultivable(z.crop)
|
||||
const cropLabel = cultivable ? (labels[z.crop!] ?? z.crop) : 'غیر قابل کشت'
|
||||
const tooltipContent = cultivable
|
||||
? `<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>درصد تطابق: ${z.matchPercent ?? '-'}%</div>
|
||||
<div>نیاز آب: ${z.waterNeed ?? '-'}</div>
|
||||
<div>سود تخمینی: ${z.estimatedProfit ?? '-'}</div>
|
||||
</div>`
|
||||
: `<div style="font-family: inherit; font-size: 12px; padding: 4px 8px; min-width: 160px;">
|
||||
<div style="font-weight: 600; margin-bottom: 6px; color: #64748b;">غیر قابل کشت</div>
|
||||
<div style="color: #94a3b8;">این بخش برای کشت مناسب تشخیص داده نشده است.</div>
|
||||
</div>`
|
||||
const color = cultivable ? (CROP_COLORS[z.crop as CropType] ?? '#94a3b8') : '#94a3b8'
|
||||
return {
|
||||
zoneId: z.zoneId,
|
||||
geometry: z.geometry,
|
||||
color,
|
||||
tooltipContent,
|
||||
cultivable,
|
||||
zoneInitialData: z
|
||||
}
|
||||
})
|
||||
}
|
||||
if (activeLayer === 'waterNeed' && zonesWaterNeed) {
|
||||
const levelLabels = { low: 'کم', medium: 'متوسط', high: 'زیاد' }
|
||||
return zonesWaterNeed.map(z => ({
|
||||
zoneId: z.zoneId,
|
||||
geometry: z.geometry,
|
||||
color: z.color,
|
||||
tooltipContent: `<div style="font-family: inherit; font-size: 12px; padding: 4px 8px; min-width: 160px;">
|
||||
<div style="font-weight: 600; margin-bottom: 6px;">نیاز آبی: ${levelLabels[z.level]}</div>
|
||||
<div>${z.value ?? '-'}</div>
|
||||
</div>`,
|
||||
cultivable: false
|
||||
}))
|
||||
}
|
||||
if (activeLayer === 'soilQuality' && zonesSoilQuality) {
|
||||
const levelLabels = { low: 'کم', medium: 'متوسط', high: 'زیاد' }
|
||||
return zonesSoilQuality.map(z => ({
|
||||
zoneId: z.zoneId,
|
||||
geometry: z.geometry,
|
||||
color: z.color,
|
||||
tooltipContent: `<div style="font-family: inherit; font-size: 12px; padding: 4px 8px; min-width: 160px;">
|
||||
<div style="font-weight: 600; margin-bottom: 6px;">کیفیت خاک: ${levelLabels[z.level]}</div>
|
||||
<div>امتیاز: ${z.score ?? '-'}</div>
|
||||
</div>`,
|
||||
cultivable: false
|
||||
}))
|
||||
}
|
||||
if (activeLayer === 'cultivationRisk' && zonesCultivationRisk) {
|
||||
const levelLabels = { low: 'کم', medium: 'متوسط', high: 'زیاد' }
|
||||
return zonesCultivationRisk.map(z => ({
|
||||
zoneId: z.zoneId,
|
||||
geometry: z.geometry,
|
||||
color: z.color,
|
||||
tooltipContent: `<div style="font-family: inherit; font-size: 12px; padding: 4px 8px; min-width: 160px;">
|
||||
<div style="font-weight: 600; margin-bottom: 6px;">ریسک کشت: ${levelLabels[z.level]}</div>
|
||||
</div>`,
|
||||
cultivable: false
|
||||
}))
|
||||
}
|
||||
return null
|
||||
}, [activeLayer, zonesData, zonesWaterNeed, zonesSoilQuality, zonesCultivationRisk, products])
|
||||
|
||||
const handleAreaChange = useCallback((geojson: MapDrawGeoJSON) => {
|
||||
setAreaGeoJson(geojson)
|
||||
}, [])
|
||||
@@ -119,7 +243,7 @@ export default function CropZoningWrapper() {
|
||||
optimizationKey={optimizationKey}
|
||||
className='min-bs-[400px]'
|
||||
initialAreaGeoJson={areaGeoJson}
|
||||
zonesData={zonesData}
|
||||
zonesData={mapZonesData}
|
||||
productLabels={productLabels}
|
||||
readOnly
|
||||
/>
|
||||
@@ -128,7 +252,7 @@ export default function CropZoningWrapper() {
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{(areaLoading || zonesLoading) && (
|
||||
{(areaLoading || zonesLoading || (activeLayer !== 'crops' && layerDataLoading)) && (
|
||||
<Box
|
||||
className='absolute inset-0 z-[500] flex items-center justify-center bg-white/60 dark:bg-gray-900/60'
|
||||
sx={{ borderRadius: 12 }}
|
||||
@@ -139,7 +263,11 @@ export default function CropZoningWrapper() {
|
||||
|
||||
<LayerControl activeLayer={activeLayer} onLayerChange={setActiveLayer} />
|
||||
|
||||
<ZoneLegend products={products} loading={productsLoading} />
|
||||
<ZoneLegend
|
||||
activeLayer={activeLayer}
|
||||
products={products}
|
||||
loading={productsLoading || (activeLayer !== 'crops' && layerDataLoading)}
|
||||
/>
|
||||
|
||||
{areaGeoJson && (
|
||||
<Box className='absolute top-16 end-4 z-[1000]'>
|
||||
|
||||
Reference in New Issue
Block a user