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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user