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:
@@ -544,6 +544,11 @@
|
|||||||
"cultivationRisk": "ریسک کشت"
|
"cultivationRisk": "ریسک کشت"
|
||||||
},
|
},
|
||||||
"legend": "راهنمای رنگها",
|
"legend": "راهنمای رنگها",
|
||||||
|
"legendLevels": {
|
||||||
|
"low": "کم",
|
||||||
|
"medium": "متوسط",
|
||||||
|
"high": "زیاد"
|
||||||
|
},
|
||||||
"crops": {
|
"crops": {
|
||||||
"wheat": "گندم",
|
"wheat": "گندم",
|
||||||
"canola": "کلزا",
|
"canola": "کلزا",
|
||||||
|
|||||||
@@ -12,16 +12,22 @@
|
|||||||
```
|
```
|
||||||
۱. GET area → منطقهٔ ثابت (کاربر امکان رسم ندارد)
|
۱. GET area → منطقهٔ ثابت (کاربر امکان رسم ندارد)
|
||||||
۲. GET products → لیست محصولات و رنگها
|
۲. GET products → لیست محصولات و رنگها
|
||||||
۳. POST zones/initial → ارسال محدودهٔ مربعها → دریافت دیتای اولیه (نقشه + هاور/tooltip)
|
۳. POST zones/initial → ارسال محدودهٔ مربعها → دیتای محصولات پیشنهادی (نقشه + tooltip)
|
||||||
۴. GET zone/:zoneId → کلیک روی مربع → دریافت دیتای تکمیلی (پنل جزئیات: reason, criteria, ...)
|
۴. POST zones/water-need → ارسال محدودهٔ مربعها → نیاز آبی هر منطقه
|
||||||
|
۵. POST zones/soil-quality → ارسال محدودهٔ مربعها → کیفیت خاک هر منطقه
|
||||||
|
۶. POST zones/cultivation-risk → ارسال محدودهٔ مربعها → ریسک کشت هر منطقه
|
||||||
|
۷. GET zone/:zoneId → کلیک روی مربع → دیتای تکمیلی (پنل جزئیات: reason, criteria, ...)
|
||||||
```
|
```
|
||||||
|
|
||||||
| ردیف | API | هدف |
|
| ردیف | API | هدف |
|
||||||
|------|-----|------|
|
|------|-----|------|
|
||||||
| ۱ | **منطقهٔ اولیه** | دریافت منطقهٔ زمین به صورت GeoJSON؛ کاربر نمیتواند چیزی رسم کند |
|
| ۱ | **منطقهٔ اولیه** | دریافت منطقهٔ زمین به صورت GeoJSON؛ کاربر نمیتواند چیزی رسم کند |
|
||||||
| ۲ | **لیست محصولات و رنگها** | دریافت محصولات قابل کشت به همراه رنگ نمایش و لیبل فارسی |
|
| ۲ | **لیست محصولات و رنگها** | دریافت محصولات قابل کشت به همراه رنگ نمایش و لیبل فارسی |
|
||||||
| ۳ | **دیتای اولیه زونها** | ارسال محدودهٔ مربعها، دریافت دیتا برای نقشه و **هاور/tooltip** |
|
| ۳ | **دیتای اولیه زونها (محصولات)** | ارسال محدودهٔ مربعها، دریافت محصول پیشنهادی برای نقشه و tooltip |
|
||||||
| ۴ | **دیتای تکمیلی زون** | با کلیک روی هر مربع، دریافت دیتای جزئیات (دلیل، معیارها، نمودار) |
|
| ۴ | **نیاز آبی** | ارسال محدودهٔ مربعها، دریافت نیاز آبی هر منطقه برای لایهٔ نیاز آبی |
|
||||||
|
| ۵ | **کیفیت خاک** | ارسال محدودهٔ مربعها، دریافت کیفیت خاک هر منطقه برای لایهٔ کیفیت خاک |
|
||||||
|
| ۶ | **ریسک کشت** | ارسال محدودهٔ مربعها، دریافت ریسک کشت هر منطقه برای لایهٔ ریسک کشت |
|
||||||
|
| ۷ | **دیتای تکمیلی زون** | با کلیک روی هر مربع، دریافت دیتای جزئیات (دلیل، معیارها، نمودار) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -241,15 +247,164 @@
|
|||||||
|------|-----|--------|--------|
|
|------|-----|--------|--------|
|
||||||
| `zoneId` | string | بله | شناسهٔ یکتا برای درخواست دیتای تکمیلی |
|
| `zoneId` | string | بله | شناسهٔ یکتا برای درخواست دیتای تکمیلی |
|
||||||
| `geometry` | Polygon | بله | هندسهٔ همان مربع ارسالی |
|
| `geometry` | Polygon | بله | هندسهٔ همان مربع ارسالی |
|
||||||
| `crop` | string | بله | محصول پیشنهادی (رنگ نقشه + tooltip) |
|
| `crop` | string \| null | خیر | محصول پیشنهادی؛ اگر `null`/خالی/`uncultivable` باشد → زون **غیرقابل کشت** و رنگ خاکستری |
|
||||||
| `matchPercent` | number | بله | درصد تطابق (هاور/tooltip) |
|
| `matchPercent` | number | خیر | درصد تطابق (هاور/tooltip) |
|
||||||
| `waterNeed` | string | بله | نیاز آبی (هاور/tooltip) |
|
| `waterNeed` | string | خیر | نیاز آبی (هاور/tooltip) |
|
||||||
| `estimatedProfit` | string | بله | سود تخمینی (هاور/tooltip) |
|
| `estimatedProfit` | string | خیر | سود تخمینی (هاور/tooltip) |
|
||||||
|
|
||||||
|
**زون غیرقابل کشت:** اگر برای مربعی اطلاعاتی نیاید یا `crop` خالی/`null`/`uncultivable` باشد، آن مربع خاکستری نمایش داده شده و در tooltip «غیر قابل کشت» نشان داده میشود. کلیک روی آن پنل جزئیات باز نمیشود.
|
||||||
|
|
||||||
**نکته:** این فیلدها هنگام **هاور** روی مربع در tooltip نمایش داده میشوند؛ نیازی به درخواست جداگانه برای tooltip نیست.
|
**نکته:** این فیلدها هنگام **هاور** روی مربع در tooltip نمایش داده میشوند؛ نیازی به درخواست جداگانه برای tooltip نیست.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## ۲.۱ API نیاز آبی (Water Need)
|
||||||
|
|
||||||
|
نیاز آبی هر منطقه را بر اساس محدودهٔ مربعها برمیگرداند. با تغییر لایه به «نیاز آبی» در LayerControl، فرانت این API را صدا میزند و نقشه و Legend را بهروزرسانی میکند.
|
||||||
|
|
||||||
|
### مشخصات
|
||||||
|
|
||||||
|
- **متد:** `POST`
|
||||||
|
- **آدرس پیشنهادی:** `POST /api/crop-zoning/zones/water-need/`
|
||||||
|
- **هدف:** دریافت نیاز آبی هر منطقه برای نمایش روی نقشه در لایهٔ نیاز آبی.
|
||||||
|
|
||||||
|
### ورودی (Request Body)
|
||||||
|
|
||||||
|
همان ساختار `POST zones/initial/`:
|
||||||
|
|
||||||
|
| فیلد | نوع | اجباری | توضیح |
|
||||||
|
|------|-----|--------|--------|
|
||||||
|
| `zones` | GeoJSON FeatureCollection | بله | محدودهٔ هر مربع به صورت Polygon |
|
||||||
|
|
||||||
|
### خروجی (Response Body)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"zones": [
|
||||||
|
{
|
||||||
|
"zoneId": "zone-0",
|
||||||
|
"geometry": { "type": "Polygon", "coordinates": [...] },
|
||||||
|
"level": "low",
|
||||||
|
"value": "۳۰۰۰-۴۰۰۰ m³/ha",
|
||||||
|
"color": "#7dd3fc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"zoneId": "zone-1",
|
||||||
|
"geometry": { "type": "Polygon", "coordinates": [...] },
|
||||||
|
"level": "medium",
|
||||||
|
"value": "۵۰۰۰-۶۰۰۰ m³/ha",
|
||||||
|
"color": "#0ea5e9"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| فیلد | نوع | توضیح |
|
||||||
|
|------|-----|--------|
|
||||||
|
| `zoneId` | string | شناسهٔ زون |
|
||||||
|
| `geometry` | Polygon | هندسهٔ مربع |
|
||||||
|
| `level` | string | سطح: `low`, `medium`, `high` |
|
||||||
|
| `value` | string | مقدار نیاز آبی (مثلاً m³/ha) |
|
||||||
|
| `color` | string | رنگ hex برای نمایش |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۲.۲ API کیفیت خاک (Soil Quality)
|
||||||
|
|
||||||
|
کیفیت خاک هر منطقه را برمیگرداند. با تغییر لایه به «کیفیت خاک»، فرانت این API را صدا میزند.
|
||||||
|
|
||||||
|
### مشخصات
|
||||||
|
|
||||||
|
- **متد:** `POST`
|
||||||
|
- **آدرس پیشنهادی:** `POST /api/crop-zoning/zones/soil-quality/`
|
||||||
|
|
||||||
|
### ورودی (Request Body)
|
||||||
|
|
||||||
|
همان `zones` (FeatureCollection).
|
||||||
|
|
||||||
|
### خروجی (Response Body)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"zones": [
|
||||||
|
{
|
||||||
|
"zoneId": "zone-0",
|
||||||
|
"geometry": { "type": "Polygon", "coordinates": [...] },
|
||||||
|
"level": "low",
|
||||||
|
"score": 35,
|
||||||
|
"color": "#f87171"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"zoneId": "zone-1",
|
||||||
|
"geometry": { "type": "Polygon", "coordinates": [...] },
|
||||||
|
"level": "high",
|
||||||
|
"score": 85,
|
||||||
|
"color": "#22c55e"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| فیلد | نوع | توضیح |
|
||||||
|
|------|-----|--------|
|
||||||
|
| `level` | string | سطح: `low`, `medium`, `high` |
|
||||||
|
| `score` | number | امتیاز ۰–۱۰۰ |
|
||||||
|
| `color` | string | رنگ hex |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۲.۳ API ریسک کشت (Cultivation Risk)
|
||||||
|
|
||||||
|
ریسک کشت هر منطقه را برمیگرداند. با تغییر لایه به «ریسک کشت»، فرانت این API را صدا میزند.
|
||||||
|
|
||||||
|
### مشخصات
|
||||||
|
|
||||||
|
- **متد:** `POST`
|
||||||
|
- **آدرس پیشنهادی:** `POST /api/crop-zoning/zones/cultivation-risk/`
|
||||||
|
|
||||||
|
### ورودی و خروجی
|
||||||
|
|
||||||
|
ورودی: همان `zones` (FeatureCollection).
|
||||||
|
|
||||||
|
خروجی نمونه:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"zones": [
|
||||||
|
{
|
||||||
|
"zoneId": "zone-0",
|
||||||
|
"geometry": { "type": "Polygon", "coordinates": [...] },
|
||||||
|
"level": "low",
|
||||||
|
"color": "#22c55e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"zoneId": "zone-1",
|
||||||
|
"geometry": { "type": "Polygon", "coordinates": [...] },
|
||||||
|
"level": "high",
|
||||||
|
"color": "#ef4444"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| فیلد | نوع | توضیح |
|
||||||
|
|------|-----|--------|
|
||||||
|
| `level` | string | سطح: `low`, `medium`, `high` |
|
||||||
|
| `color` | string | رنگ hex |
|
||||||
|
|
||||||
|
**نکته:** برای هر لایه (نیاز آبی، کیفیت خاک، ریسک کشت) فرانت یک **درخواست جداگانه** ارسال میکند و نقشه و Legend متناسب با همان لایه بهروزرسانی میشوند.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ۳. API دیتای تکمیلی زون (با کلیک روی مربع)
|
## ۳. API دیتای تکمیلی زون (با کلیک روی مربع)
|
||||||
|
|
||||||
وقتی کاربر روی یک مربع کلیک میکند، فرانت با `zoneId` دیتای **تکمیلی** را درخواست میکند — برای نمایش پنل جزئیات: دلیل پیشنهاد، معیارها، نمودار راداری.
|
وقتی کاربر روی یک مربع کلیک میکند، فرانت با `zoneId` دیتای **تکمیلی** را درخواست میکند — برای نمایش پنل جزئیات: دلیل پیشنهاد، معیارها، نمودار راداری.
|
||||||
@@ -369,8 +524,13 @@ const CROP_COLORS: Record<CropType, string> = {
|
|||||||
## ۶. جریان فرانت با APIها
|
## ۶. جریان فرانت با APIها
|
||||||
|
|
||||||
1. **لود صفحه:** `GET /api/crop-zoning/products/` → لیست محصولات و رنگها.
|
1. **لود صفحه:** `GET /api/crop-zoning/products/` → لیست محصولات و رنگها.
|
||||||
2. **رسم منطقه / بهینهسازی:** فرانت با Turf از polygon منطقه گرید میسازد → `POST /api/crop-zoning/zones/initial/` با `zones` (FeatureCollection) → نقشه و هاور/tooltip با دیتای اولیه رسم میشود.
|
2. **رسم منطقه / بهینهسازی:** فرانت با Turf از polygon منطقه گرید میسازد → `POST /api/crop-zoning/zones/initial/` با `zones` (FeatureCollection) → نقشه و tooltip با دیتای محصولات رسم میشود.
|
||||||
3. **کلیک روی مربع:** `GET /api/crop-zoning/zones/{zoneId}/details/` → دیتای تکمیلی → پنل جزئیات باز میشود (reason, criteria, نمودار راداری).
|
3. **تغییر لایه در LayerControl:** برای هر لایه یک درخواست جداگانه ارسال میشود:
|
||||||
|
- محصولات پیشنهادی: `POST zones/initial/` (در مرحلهٔ ۲)
|
||||||
|
- نیاز آبی: `POST zones/water-need/` → نقشه و Legend بهروزرسانی میشوند
|
||||||
|
- کیفیت خاک: `POST zones/soil-quality/` → نقشه و Legend بهروزرسانی میشوند
|
||||||
|
- ریسک کشت: `POST zones/cultivation-risk/` → نقشه و Legend بهروزرسانی میشوند
|
||||||
|
4. **کلیک روی مربع:** `GET /api/crop-zoning/zones/{zoneId}/details/` → دیتای تکمیلی → پنل جزئیات باز میشود (reason, criteria, نمودار راداری).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ export interface Product {
|
|||||||
export interface ZoneInitialData {
|
export interface ZoneInitialData {
|
||||||
zoneId: string
|
zoneId: string
|
||||||
geometry: Polygon
|
geometry: Polygon
|
||||||
crop: string
|
/** اگر null/خالی/uncultivable باشد، زون غیرقابل کشت و خاکستری نمایش داده میشود */
|
||||||
matchPercent: number
|
crop?: string | null
|
||||||
waterNeed: string
|
matchPercent?: number | null
|
||||||
estimatedProfit: string
|
waterNeed?: string | null
|
||||||
|
estimatedProfit?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ZonesInitialResponse {
|
export interface ZonesInitialResponse {
|
||||||
@@ -45,6 +46,42 @@ export interface ZoneDetailData {
|
|||||||
area_hectares?: number
|
area_hectares?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** دیتای نیاز آبی هر زون — لایهٔ نیاز آبی */
|
||||||
|
export interface ZoneWaterNeedData {
|
||||||
|
zoneId: string
|
||||||
|
geometry: Polygon
|
||||||
|
level: 'low' | 'medium' | 'high'
|
||||||
|
value?: string
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** دیتای کیفیت خاک هر زون — لایهٔ کیفیت خاک */
|
||||||
|
export interface ZoneSoilQualityData {
|
||||||
|
zoneId: string
|
||||||
|
geometry: Polygon
|
||||||
|
level: 'low' | 'medium' | 'high'
|
||||||
|
score?: number
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** دیتای ریسک کشت هر زون — لایهٔ ریسک کشت */
|
||||||
|
export interface ZoneCultivationRiskData {
|
||||||
|
zoneId: string
|
||||||
|
geometry: Polygon
|
||||||
|
level: 'low' | 'medium' | 'high'
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** دادهٔ نمایشی هر زون روی نقشه — خروجی تبدیل از تمام لایهها */
|
||||||
|
export interface ZoneMapData {
|
||||||
|
zoneId: string
|
||||||
|
geometry: Polygon
|
||||||
|
color: string
|
||||||
|
tooltipContent: string
|
||||||
|
cultivable: boolean
|
||||||
|
zoneInitialData?: ZoneInitialData
|
||||||
|
}
|
||||||
|
|
||||||
interface ApiResponse<T> {
|
interface ApiResponse<T> {
|
||||||
status: string
|
status: string
|
||||||
data: T
|
data: T
|
||||||
@@ -73,5 +110,31 @@ export const cropZoningService = {
|
|||||||
|
|
||||||
getArea(): Promise<AreaResponse> {
|
getArea(): Promise<AreaResponse> {
|
||||||
return unwrap(apiClient.get<ApiResponse<AreaResponse>>(`${PREFIX}/area/`))
|
return unwrap(apiClient.get<ApiResponse<AreaResponse>>(`${PREFIX}/area/`))
|
||||||
|
},
|
||||||
|
|
||||||
|
/** نیاز آبی هر منطقه — برای لایهٔ نیاز آبی */
|
||||||
|
getZonesWaterNeed(body: { zones: FeatureCollection<Polygon> }): Promise<{ zones: ZoneWaterNeedData[] }> {
|
||||||
|
return unwrap(
|
||||||
|
apiClient.post<ApiResponse<{ zones: ZoneWaterNeedData[] }>>(`${PREFIX}/zones/water-need/`, body)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
/** کیفیت خاک هر منطقه — برای لایهٔ کیفیت خاک */
|
||||||
|
getZonesSoilQuality(body: { zones: FeatureCollection<Polygon> }): Promise<{ zones: ZoneSoilQualityData[] }> {
|
||||||
|
return unwrap(
|
||||||
|
apiClient.post<ApiResponse<{ zones: ZoneSoilQualityData[] }>>(`${PREFIX}/zones/soil-quality/`, body)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
/** ریسک کشت هر منطقه — برای لایهٔ ریسک کشت */
|
||||||
|
getZonesCultivationRisk(body: {
|
||||||
|
zones: FeatureCollection<Polygon>
|
||||||
|
}): Promise<{ zones: ZoneCultivationRiskData[] }> {
|
||||||
|
return unwrap(
|
||||||
|
apiClient.post<ApiResponse<{ zones: ZoneCultivationRiskData[] }>>(
|
||||||
|
`${PREFIX}/zones/cultivation-risk/`,
|
||||||
|
body
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import type L from 'leaflet'
|
|||||||
import 'leaflet/dist/leaflet.css'
|
import 'leaflet/dist/leaflet.css'
|
||||||
import 'leaflet-draw/dist/leaflet.draw.css'
|
import 'leaflet-draw/dist/leaflet.draw.css'
|
||||||
import type { Feature, Polygon } from 'geojson'
|
import type { Feature, Polygon } from 'geojson'
|
||||||
import { CROP_COLORS, type CropType, type LayerType, type ZoneFeatureProperties } from './cropZoningTypes'
|
import type { LayerType } from './cropZoningTypes'
|
||||||
import type { ZoneInitialData } from '@/libs/api/services/cropZoningService'
|
import type { ZoneMapData, ZoneInitialData } from '@/libs/api/services/cropZoningService'
|
||||||
|
|
||||||
export type MapDrawGeoJSON = Record<string, unknown> | null
|
export type MapDrawGeoJSON = Record<string, unknown> | null
|
||||||
|
|
||||||
@@ -21,19 +21,14 @@ type CropZoningMapProps = {
|
|||||||
className?: string
|
className?: string
|
||||||
/** منطقهٔ اولیه؛ وقتی مقدار دارد و readOnly باشد نقشه فقط نمایشی است */
|
/** منطقهٔ اولیه؛ وقتی مقدار دارد و readOnly باشد نقشه فقط نمایشی است */
|
||||||
initialAreaGeoJson?: MapDrawGeoJSON | null
|
initialAreaGeoJson?: MapDrawGeoJSON | null
|
||||||
/** دیتای زونها از API (POST zones/initial) */
|
/** دیتای زونها برای نقشه — از APIهای zones/initial یا water-need یا soil-quality یا cultivation-risk */
|
||||||
zonesData?: ZoneInitialData[] | null
|
zonesData?: ZoneMapData[] | null
|
||||||
/** لیبل محصولات از API (id -> label) برای tooltip */
|
/** لیبل محصولات از API (id -> label) — فقط برای لایهٔ crops */
|
||||||
productLabels?: Record<string, string>
|
productLabels?: Record<string, string>
|
||||||
/** غیرفعال کردن رسم و ویرایش منطقه توسط کاربر */
|
/** غیرفعال کردن رسم و ویرایش منطقه توسط کاربر */
|
||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_PRODUCT_LABELS: Record<string, string> = {
|
|
||||||
wheat: 'گندم',
|
|
||||||
canola: 'کلزا',
|
|
||||||
saffron: 'زعفران'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CropZoningMap({
|
export default function CropZoningMap({
|
||||||
center = [35.6892, 51.389],
|
center = [35.6892, 51.389],
|
||||||
@@ -46,7 +41,7 @@ export default function CropZoningMap({
|
|||||||
className = '',
|
className = '',
|
||||||
initialAreaGeoJson = null,
|
initialAreaGeoJson = null,
|
||||||
zonesData = null,
|
zonesData = null,
|
||||||
productLabels = DEFAULT_PRODUCT_LABELS,
|
productLabels = {},
|
||||||
readOnly = false
|
readOnly = false
|
||||||
}: CropZoningMapProps) {
|
}: CropZoningMapProps) {
|
||||||
const mapRef = useRef<HTMLDivElement>(null)
|
const mapRef = useRef<HTMLDivElement>(null)
|
||||||
@@ -56,14 +51,13 @@ export default function CropZoningMap({
|
|||||||
const zonesLayerRef = useRef<L.GeoJSON | null>(null)
|
const zonesLayerRef = useRef<L.GeoJSON | null>(null)
|
||||||
|
|
||||||
const renderZonesFromApi = useCallback(
|
const renderZonesFromApi = useCallback(
|
||||||
(map: L.Map, zones: ZoneInitialData[]) => {
|
(map: L.Map, zones: ZoneMapData[]) => {
|
||||||
if (zonesLayerRef.current) {
|
if (zonesLayerRef.current) {
|
||||||
map.removeLayer(zonesLayerRef.current)
|
map.removeLayer(zonesLayerRef.current)
|
||||||
zonesLayerRef.current = null
|
zonesLayerRef.current = null
|
||||||
}
|
}
|
||||||
if (zones.length === 0) return
|
if (zones.length === 0) return
|
||||||
|
|
||||||
const labels = { ...DEFAULT_PRODUCT_LABELS, ...productLabels }
|
|
||||||
const grid = {
|
const grid = {
|
||||||
type: 'FeatureCollection' as const,
|
type: 'FeatureCollection' as const,
|
||||||
features: zones.map(z => ({
|
features: zones.map(z => ({
|
||||||
@@ -71,10 +65,10 @@ export default function CropZoningMap({
|
|||||||
geometry: z.geometry,
|
geometry: z.geometry,
|
||||||
properties: {
|
properties: {
|
||||||
zoneId: z.zoneId,
|
zoneId: z.zoneId,
|
||||||
crop: z.crop,
|
color: z.color,
|
||||||
matchPercent: z.matchPercent,
|
tooltipContent: z.tooltipContent,
|
||||||
waterNeed: z.waterNeed,
|
cultivable: z.cultivable,
|
||||||
estimatedProfit: z.estimatedProfit
|
zoneInitialData: z.zoneInitialData
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -83,48 +77,31 @@ export default function CropZoningMap({
|
|||||||
if (!L) return
|
if (!L) return
|
||||||
|
|
||||||
const geoJsonLayer = L.geoJSON(grid as never, {
|
const geoJsonLayer = L.geoJSON(grid as never, {
|
||||||
style: (feature?: { properties?: ZoneFeatureProperties }) => {
|
style: (feature?: { properties?: { color?: string } }) => {
|
||||||
const props = feature?.properties
|
const color = feature?.properties?.color ?? '#94a3b8'
|
||||||
if (!props || activeLayer !== 'crops') {
|
|
||||||
return { fillColor: '#94a3b8', fillOpacity: 0.5, weight: 1, color: '#fff' }
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
fillColor: (CROP_COLORS[props.crop as CropType] ?? '#94a3b8'),
|
fillColor: color,
|
||||||
fillOpacity: 0.5,
|
fillOpacity: 0.5,
|
||||||
weight: 1,
|
weight: 1,
|
||||||
color: '#fff'
|
color: '#fff'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onEachFeature: (feat: { geometry?: unknown; properties?: ZoneFeatureProperties }, leafLayer: L.Layer) => {
|
onEachFeature: (feat: unknown, leafLayer: L.Layer) => {
|
||||||
const feature = feat as { geometry: Polygon; properties?: ZoneFeatureProperties }
|
const f = feat as { geometry?: Polygon; properties?: Record<string, unknown> }
|
||||||
const props = feature?.properties
|
const props = f?.properties
|
||||||
const geom = feature?.geometry
|
const geom = f?.geometry
|
||||||
if (!props || !geom) return
|
if (!props || !geom) return
|
||||||
const layer = leafLayer as L.Polygon
|
const layer = leafLayer as L.Polygon
|
||||||
const cropLabel = labels[props.crop] ?? props.crop
|
const tooltipContent = (props.tooltipContent as string) ?? ''
|
||||||
const tooltipContent = `
|
const cultivable = props.cultivable === true
|
||||||
<div style="font-family: inherit; font-size: 12px; padding: 4px 8px; min-width: 160px;">
|
const zoneInitialData = props.zoneInitialData as ZoneInitialData | undefined
|
||||||
<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, {
|
layer.bindTooltip(tooltipContent, {
|
||||||
sticky: true,
|
sticky: true,
|
||||||
className: 'zone-tooltip',
|
className: 'zone-tooltip',
|
||||||
direction: 'top',
|
direction: 'top',
|
||||||
offset: [0, -8]
|
offset: [0, -8]
|
||||||
})
|
})
|
||||||
const zoneData: ZoneInitialData = {
|
layer.on('click', () => cultivable && zoneInitialData && onZoneClick?.(zoneInitialData))
|
||||||
zoneId: props.zoneId,
|
|
||||||
geometry: geom,
|
|
||||||
crop: props.crop,
|
|
||||||
matchPercent: props.matchPercent,
|
|
||||||
waterNeed: props.waterNeed,
|
|
||||||
estimatedProfit: props.estimatedProfit
|
|
||||||
}
|
|
||||||
layer.on('click', () => onZoneClick?.(zoneData))
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -143,7 +120,7 @@ export default function CropZoningMap({
|
|||||||
}, delay)
|
}, delay)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[activeLayer, onZoneClick, productLabels]
|
[onZoneClick]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -268,23 +245,6 @@ export default function CropZoningMap({
|
|||||||
}
|
}
|
||||||
}, [zonesData, optimizationKey, renderZonesFromApi])
|
}, [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 (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useCallback, useEffect } from 'react'
|
import { useState, useCallback, useEffect, useMemo } from 'react'
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
@@ -12,7 +12,17 @@ import LayerControl from './LayerControl'
|
|||||||
import ZoneDetailPanel from './ZoneDetailPanel'
|
import ZoneDetailPanel from './ZoneDetailPanel'
|
||||||
import CropZoningWeatherSection from './CropZoningWeatherSection'
|
import CropZoningWeatherSection from './CropZoningWeatherSection'
|
||||||
import { createGridFromPolygon } from './cropZoningUtils'
|
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 { LayerType } from './cropZoningTypes'
|
||||||
import type { MapDrawGeoJSON } from './CropZoningMap'
|
import type { MapDrawGeoJSON } from './CropZoningMap'
|
||||||
|
|
||||||
@@ -34,9 +44,13 @@ export default function CropZoningWrapper() {
|
|||||||
const [areaGeoJson, setAreaGeoJson] = useState<MapDrawGeoJSON>(null)
|
const [areaGeoJson, setAreaGeoJson] = useState<MapDrawGeoJSON>(null)
|
||||||
const [areaLoading, setAreaLoading] = useState(true)
|
const [areaLoading, setAreaLoading] = useState(true)
|
||||||
const [zonesData, setZonesData] = useState<ZoneInitialData[] | null>(null)
|
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 [products, setProducts] = useState<Product[]>([])
|
||||||
const [productsLoading, setProductsLoading] = useState(true)
|
const [productsLoading, setProductsLoading] = useState(true)
|
||||||
const [zonesLoading, setZonesLoading] = useState(false)
|
const [zonesLoading, setZonesLoading] = useState(false)
|
||||||
|
const [layerDataLoading, setLayerDataLoading] = useState(false)
|
||||||
const [activeLayer, setActiveLayer] = useState<LayerType>('crops')
|
const [activeLayer, setActiveLayer] = useState<LayerType>('crops')
|
||||||
const [selectedZone, setSelectedZone] = useState<ZoneDetailData | null>(null)
|
const [selectedZone, setSelectedZone] = useState<ZoneDetailData | null>(null)
|
||||||
const [panelOpen, setPanelOpen] = useState(false)
|
const [panelOpen, setPanelOpen] = useState(false)
|
||||||
@@ -65,8 +79,14 @@ export default function CropZoningWrapper() {
|
|||||||
const fetchZones = useCallback((geojson: MapDrawGeoJSON) => {
|
const fetchZones = useCallback((geojson: MapDrawGeoJSON) => {
|
||||||
if (!isPolygon(geojson)) {
|
if (!isPolygon(geojson)) {
|
||||||
setZonesData(null)
|
setZonesData(null)
|
||||||
|
setZonesWaterNeed(null)
|
||||||
|
setZonesSoilQuality(null)
|
||||||
|
setZonesCultivationRisk(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
setZonesWaterNeed(null)
|
||||||
|
setZonesSoilQuality(null)
|
||||||
|
setZonesCultivationRisk(null)
|
||||||
setZonesLoading(true)
|
setZonesLoading(true)
|
||||||
const grid = createGridFromPolygon(geojson as unknown as import('geojson').Feature<import('geojson').Polygon>)
|
const grid = createGridFromPolygon(geojson as unknown as import('geojson').Feature<import('geojson').Polygon>)
|
||||||
cropZoningService
|
cropZoningService
|
||||||
@@ -81,9 +101,113 @@ export default function CropZoningWrapper() {
|
|||||||
fetchZones(areaGeoJson)
|
fetchZones(areaGeoJson)
|
||||||
} else {
|
} else {
|
||||||
setZonesData(null)
|
setZonesData(null)
|
||||||
|
setZonesWaterNeed(null)
|
||||||
|
setZonesSoilQuality(null)
|
||||||
|
setZonesCultivationRisk(null)
|
||||||
}
|
}
|
||||||
}, [areaGeoJson, optimizationKey, fetchZones])
|
}, [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) => {
|
const handleAreaChange = useCallback((geojson: MapDrawGeoJSON) => {
|
||||||
setAreaGeoJson(geojson)
|
setAreaGeoJson(geojson)
|
||||||
}, [])
|
}, [])
|
||||||
@@ -119,7 +243,7 @@ export default function CropZoningWrapper() {
|
|||||||
optimizationKey={optimizationKey}
|
optimizationKey={optimizationKey}
|
||||||
className='min-bs-[400px]'
|
className='min-bs-[400px]'
|
||||||
initialAreaGeoJson={areaGeoJson}
|
initialAreaGeoJson={areaGeoJson}
|
||||||
zonesData={zonesData}
|
zonesData={mapZonesData}
|
||||||
productLabels={productLabels}
|
productLabels={productLabels}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
@@ -128,7 +252,7 @@ export default function CropZoningWrapper() {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{(areaLoading || zonesLoading) && (
|
{(areaLoading || zonesLoading || (activeLayer !== 'crops' && layerDataLoading)) && (
|
||||||
<Box
|
<Box
|
||||||
className='absolute inset-0 z-[500] flex items-center justify-center bg-white/60 dark:bg-gray-900/60'
|
className='absolute inset-0 z-[500] flex items-center justify-center bg-white/60 dark:bg-gray-900/60'
|
||||||
sx={{ borderRadius: 12 }}
|
sx={{ borderRadius: 12 }}
|
||||||
@@ -139,7 +263,11 @@ export default function CropZoningWrapper() {
|
|||||||
|
|
||||||
<LayerControl activeLayer={activeLayer} onLayerChange={setActiveLayer} />
|
<LayerControl activeLayer={activeLayer} onLayerChange={setActiveLayer} />
|
||||||
|
|
||||||
<ZoneLegend products={products} loading={productsLoading} />
|
<ZoneLegend
|
||||||
|
activeLayer={activeLayer}
|
||||||
|
products={products}
|
||||||
|
loading={productsLoading || (activeLayer !== 'crops' && layerDataLoading)}
|
||||||
|
/>
|
||||||
|
|
||||||
{areaGeoJson && (
|
{areaGeoJson && (
|
||||||
<Box className='absolute top-16 end-4 z-[1000]'>
|
<Box className='absolute top-16 end-4 z-[1000]'>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
import IconButton from '@mui/material/IconButton'
|
import IconButton from '@mui/material/IconButton'
|
||||||
|
import { useTheme, alpha } from '@mui/material/styles'
|
||||||
import type { LayerType } from './cropZoningTypes'
|
import type { LayerType } from './cropZoningTypes'
|
||||||
|
|
||||||
const LAYER_ICONS: Record<LayerType, string> = {
|
const LAYER_ICONS: Record<LayerType, string> = {
|
||||||
@@ -20,10 +21,14 @@ const LAYER_ORDER: LayerType[] = ['crops', 'waterNeed', 'soilQuality', 'cultivat
|
|||||||
|
|
||||||
export default function LayerControl({ activeLayer, onLayerChange }: LayerControlProps) {
|
export default function LayerControl({ activeLayer, onLayerChange }: LayerControlProps) {
|
||||||
const t = useTranslations('cropZoning')
|
const t = useTranslations('cropZoning')
|
||||||
|
const theme = useTheme()
|
||||||
|
const primaryMain = theme.palette.primary.main
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='absolute bottom-4 end-4 z-[1000] flex flex-col gap-1 rounded-xl border border-white/20 bg-white/70 py-2 shadow-lg backdrop-blur-md dark:bg-gray-900/70 dark:border-gray-700/50'>
|
<div className='absolute bottom-4 end-4 z-[1000] flex flex-col gap-1 rounded-xl border border-white/20 bg-white/70 py-2 shadow-lg backdrop-blur-md dark:bg-gray-900/70 dark:border-gray-700/50'>
|
||||||
{LAYER_ORDER.map(layer => (
|
{LAYER_ORDER.map(layer => {
|
||||||
|
const isActive = activeLayer === layer
|
||||||
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
key={layer}
|
key={layer}
|
||||||
size='small'
|
size='small'
|
||||||
@@ -31,13 +36,19 @@ export default function LayerControl({ activeLayer, onLayerChange }: LayerContro
|
|||||||
title={t(`layers.${layer}`)}
|
title={t(`layers.${layer}`)}
|
||||||
sx={{
|
sx={{
|
||||||
mx: 0.5,
|
mx: 0.5,
|
||||||
bgcolor: activeLayer === layer ? 'action.selected' : 'transparent',
|
bgcolor: isActive ? alpha(primaryMain, 0.2) : 'transparent',
|
||||||
'&:hover': { bgcolor: activeLayer === layer ? 'action.selected' : 'action.hover' }
|
color: isActive ? primaryMain : 'text.secondary',
|
||||||
|
border: isActive ? `1px solid ${alpha(primaryMain, 0.5)}` : '1px solid transparent',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: isActive ? alpha(primaryMain, 0.25) : 'action.hover',
|
||||||
|
color: isActive ? primaryMain : undefined
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<i className={`${LAYER_ICONS[layer]} text-lg`} />
|
<i className={`${LAYER_ICONS[layer]} text-lg`} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
import { CROP_COLORS, type CropType } from './cropZoningTypes'
|
import { CROP_COLORS, type CropType } from './cropZoningTypes'
|
||||||
import type { Product } from '@/libs/api/services/cropZoningService'
|
import type { Product } from '@/libs/api/services/cropZoningService'
|
||||||
|
import type { LayerType } from './cropZoningTypes'
|
||||||
|
|
||||||
const FALLBACK_ITEMS: { crop: CropType; label: string }[] = [
|
const FALLBACK_ITEMS: { crop: CropType; label: string }[] = [
|
||||||
{ crop: 'wheat', label: 'گندم' },
|
{ crop: 'wheat', label: 'گندم' },
|
||||||
@@ -10,16 +11,66 @@ const FALLBACK_ITEMS: { crop: CropType; label: string }[] = [
|
|||||||
{ crop: 'saffron', label: 'زعفران' }
|
{ crop: 'saffron', label: 'زعفران' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const WATER_NEED_ITEMS = [
|
||||||
|
{ level: 'low' as const, color: '#7dd3fc', labelKey: 'low' },
|
||||||
|
{ level: 'medium' as const, color: '#0ea5e9', labelKey: 'medium' },
|
||||||
|
{ level: 'high' as const, color: '#0369a1', labelKey: 'high' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const SOIL_QUALITY_ITEMS = [
|
||||||
|
{ level: 'low' as const, color: '#f87171', labelKey: 'low' },
|
||||||
|
{ level: 'medium' as const, color: '#fbbf24', labelKey: 'medium' },
|
||||||
|
{ level: 'high' as const, color: '#22c55e', labelKey: 'high' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const CULTIVATION_RISK_ITEMS = [
|
||||||
|
{ level: 'low' as const, color: '#22c55e', labelKey: 'low' },
|
||||||
|
{ level: 'medium' as const, color: '#f97316', labelKey: 'medium' },
|
||||||
|
{ level: 'high' as const, color: '#ef4444', labelKey: 'high' }
|
||||||
|
]
|
||||||
|
|
||||||
type ZoneLegendProps = {
|
type ZoneLegendProps = {
|
||||||
|
activeLayer?: LayerType
|
||||||
products?: Product[]
|
products?: Product[]
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ZoneLegend({ products = [], loading = false }: ZoneLegendProps) {
|
export default function ZoneLegend({ activeLayer = 'crops', products = [], loading = false }: ZoneLegendProps) {
|
||||||
const t = useTranslations('cropZoning')
|
const t = useTranslations('cropZoning')
|
||||||
const items = products.length > 0
|
|
||||||
? products.map(p => ({ crop: p.id as CropType, label: p.label, color: p.color }))
|
const items =
|
||||||
: FALLBACK_ITEMS.map(({ crop }) => ({ crop, label: t(`crops.${crop}`), color: CROP_COLORS[crop] }))
|
activeLayer === 'waterNeed'
|
||||||
|
? WATER_NEED_ITEMS.map(({ level, color, labelKey }) => ({
|
||||||
|
key: `water-${level}`,
|
||||||
|
label: t(`legendLevels.${labelKey}`),
|
||||||
|
color
|
||||||
|
}))
|
||||||
|
: activeLayer === 'soilQuality'
|
||||||
|
? SOIL_QUALITY_ITEMS.map(({ level, color, labelKey }) => ({
|
||||||
|
key: `soil-${level}`,
|
||||||
|
label: t(`legendLevels.${labelKey}`),
|
||||||
|
color
|
||||||
|
}))
|
||||||
|
: activeLayer === 'cultivationRisk'
|
||||||
|
? CULTIVATION_RISK_ITEMS.map(({ level, color, labelKey }) => ({
|
||||||
|
key: `risk-${level}`,
|
||||||
|
label: t(`legendLevels.${labelKey}`),
|
||||||
|
color
|
||||||
|
}))
|
||||||
|
: (() => {
|
||||||
|
const productItems =
|
||||||
|
products.length > 0
|
||||||
|
? products.map(p => ({ key: p.id, label: p.label, color: p.color }))
|
||||||
|
: FALLBACK_ITEMS.map(({ crop }) => ({
|
||||||
|
key: crop,
|
||||||
|
label: t(`crops.${crop}`),
|
||||||
|
color: CROP_COLORS[crop]
|
||||||
|
}))
|
||||||
|
return [
|
||||||
|
...productItems,
|
||||||
|
{ key: 'uncultivable', label: 'غیر قابل کشت', color: '#94a3b8' }
|
||||||
|
]
|
||||||
|
})()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='absolute bottom-4 start-4 z-[1000] rounded-xl border border-white/20 bg-white/70 px-4 py-3 shadow-lg backdrop-blur-md dark:bg-gray-900/70 dark:border-gray-700/50'>
|
<div className='absolute bottom-4 start-4 z-[1000] rounded-xl border border-white/20 bg-white/70 px-4 py-3 shadow-lg backdrop-blur-md dark:bg-gray-900/70 dark:border-gray-700/50'>
|
||||||
@@ -28,12 +79,9 @@ export default function ZoneLegend({ products = [], loading = false }: ZoneLegen
|
|||||||
<div className='text-sm text-gray-500'>...</div>
|
<div className='text-sm text-gray-500'>...</div>
|
||||||
) : (
|
) : (
|
||||||
<div className='flex flex-col gap-1.5'>
|
<div className='flex flex-col gap-1.5'>
|
||||||
{items.map(({ crop, label, color }) => (
|
{items.map(({ key, label, color }) => (
|
||||||
<div key={crop} className='flex items-center gap-2'>
|
<div key={key} className='flex items-center gap-2'>
|
||||||
<div
|
<div className='h-4 w-4 rounded' style={{ backgroundColor: color, opacity: 0.8 }} />
|
||||||
className='h-4 w-4 rounded'
|
|
||||||
style={{ backgroundColor: color ?? CROP_COLORS[crop as CropType] ?? '#94a3b8', opacity: 0.8 }}
|
|
||||||
/>
|
|
||||||
<span className='text-sm text-gray-700 dark:text-gray-300'>{label}</span>
|
<span className='text-sm text-gray-700 dark:text-gray-300'>{label}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user