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:
@@ -36,6 +36,7 @@
|
||||
"farm": "داشبورد مزرعه",
|
||||
"waterData": "دیتاهای آب",
|
||||
"soilData": "اطلاعات خاک",
|
||||
"cropZoning": "زونبندی کشت",
|
||||
"dataSection": "بخش دادهها",
|
||||
"crm": "مدیریت ارتباط با مشتری",
|
||||
"analytics": "تحلیلها",
|
||||
@@ -523,5 +524,34 @@
|
||||
"userDropdown": {
|
||||
"myProfile": "پروفایل من",
|
||||
"logout": "خروج"
|
||||
},
|
||||
"cropZoning": {
|
||||
"title": "زونبندی پیشنهادی کشت",
|
||||
"drawLand": "زمین خود را روی نقشه با Polygon رسم کنید",
|
||||
"optimizeAgain": "بهینهسازی دوباره",
|
||||
"layers": {
|
||||
"crops": "محصولات پیشنهادی",
|
||||
"waterNeed": "نیاز آبی",
|
||||
"soilQuality": "کیفیت خاک",
|
||||
"cultivationRisk": "ریسک کشت"
|
||||
},
|
||||
"legend": "راهنمای رنگها",
|
||||
"crops": {
|
||||
"wheat": "گندم",
|
||||
"canola": "کلزا",
|
||||
"saffron": "زعفران"
|
||||
},
|
||||
"tooltip": {
|
||||
"crop": "محصول پیشنهادی",
|
||||
"matchPercent": "درصد تطابق",
|
||||
"waterNeed": "نیاز آب",
|
||||
"estimatedProfit": "سود تخمینی"
|
||||
},
|
||||
"panel": {
|
||||
"title": "تحلیل زون",
|
||||
"reason": "دلیل پیشنهاد",
|
||||
"criteriaChart": "نمودار تطابق معیارها",
|
||||
"changeCrop": "تغییر محصول"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+368
@@ -26,6 +26,7 @@
|
||||
"@fullcalendar/react": "6.1.15",
|
||||
"@fullcalendar/timegrid": "6.1.15",
|
||||
"@hookform/resolvers": "3.9.1",
|
||||
"@mapbox/mapbox-gl-draw": "^1.5.1",
|
||||
"@mui/lab": "6.0.0-beta.19",
|
||||
"@mui/material": "6.2.1",
|
||||
"@mui/material-nextjs": "6.2.1",
|
||||
@@ -42,6 +43,10 @@
|
||||
"@tiptap/pm": "^2.10.4",
|
||||
"@tiptap/react": "^2.10.4",
|
||||
"@tiptap/starter-kit": "^2.10.4",
|
||||
"@turf/area": "^7.3.4",
|
||||
"@turf/bbox": "^7.3.4",
|
||||
"@turf/intersect": "^7.3.4",
|
||||
"@turf/square-grid": "^7.3.4",
|
||||
"@types/leaflet": "^1.9.21",
|
||||
"@types/leaflet-draw": "^1.0.13",
|
||||
"apexcharts": "3.49.0",
|
||||
@@ -1702,12 +1707,71 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/geojson-area": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://mirror-npm.runflare.com/@mapbox/geojson-area/-/geojson-area-0.2.2.tgz",
|
||||
"integrity": "sha512-bBqqFn1kIbLBfn7Yq1PzzwVkPYQr9lVUeT8Dhd0NL5n76PBuXzOcuLV7GOSbEB1ia8qWxH4COCvFpziEu/yReA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"wgs84": "0.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/geojson-normalize": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://mirror-npm.runflare.com/@mapbox/geojson-normalize/-/geojson-normalize-0.0.1.tgz",
|
||||
"integrity": "sha512-82V7YHcle8lhgIGqEWwtXYN5cy0QM/OHq3ypGhQTbvHR57DF0vMHMjjVSQKFfVXBe/yWCBZTyOuzvK7DFFnx5Q==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"geojson-normalize": "geojson-normalize"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/jsonlint-lines-primitives": {
|
||||
"version": "2.0.2",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/mapbox-gl-draw": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://mirror-npm.runflare.com/@mapbox/mapbox-gl-draw/-/mapbox-gl-draw-1.5.1.tgz",
|
||||
"integrity": "sha512-DnR/oarZVoIrVHssAn+mtpuGzYH+ebORoPjow46zTBNPod/HQnvIZGtL6hIb5BVWxxH49RC9D20ipxiO9WDRxA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@mapbox/geojson-area": "^0.2.2",
|
||||
"@mapbox/geojson-normalize": "^0.0.1",
|
||||
"@mapbox/point-geometry": "^1.1.0",
|
||||
"@turf/projection": "^7.2.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"nanoid": "^5.0.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/mapbox-gl-draw/node_modules/@mapbox/point-geometry": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://mirror-npm.runflare.com/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz",
|
||||
"integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@mapbox/mapbox-gl-draw/node_modules/nanoid": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://mirror-npm.runflare.com/nanoid/-/nanoid-5.1.6.tgz",
|
||||
"integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/mapbox-gl-supported": {
|
||||
"version": "3.0.0",
|
||||
"license": "BSD-3-Clause"
|
||||
@@ -3117,6 +3181,249 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@turf/area": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://mirror-npm.runflare.com/@turf/area/-/area-7.3.4.tgz",
|
||||
"integrity": "sha512-UEQQFw2XwHpozSBAMEtZI3jDsAad4NnHL/poF7/S6zeDCjEBCkt3MYd6DSGH/cvgcOozxH/ky3/rIVSMZdx4vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@turf/helpers": "7.3.4",
|
||||
"@turf/meta": "7.3.4",
|
||||
"@types/geojson": "^7946.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@turf/bbox": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://mirror-npm.runflare.com/@turf/bbox/-/bbox-7.3.4.tgz",
|
||||
"integrity": "sha512-D5ErVWtfQbEPh11yzI69uxqrcJmbPU/9Y59f1uTapgwAwQHQztDWgsYpnL3ns8r1GmPWLP8sGJLVTIk2TZSiYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@turf/helpers": "7.3.4",
|
||||
"@turf/meta": "7.3.4",
|
||||
"@types/geojson": "^7946.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@turf/boolean-disjoint": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://mirror-npm.runflare.com/@turf/boolean-disjoint/-/boolean-disjoint-7.3.4.tgz",
|
||||
"integrity": "sha512-Dl4O27ygi2NqskGQuvSlDLJYlJ2SPkHb3A9T/v6eAudjlMiKdEY6bMxKUfU5y+Px1WiCZxd+9rXGXJgGC3WiQg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@turf/boolean-point-in-polygon": "7.3.4",
|
||||
"@turf/helpers": "7.3.4",
|
||||
"@turf/line-intersect": "7.3.4",
|
||||
"@turf/meta": "7.3.4",
|
||||
"@turf/polygon-to-line": "7.3.4",
|
||||
"@types/geojson": "^7946.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@turf/boolean-intersects": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://mirror-npm.runflare.com/@turf/boolean-intersects/-/boolean-intersects-7.3.4.tgz",
|
||||
"integrity": "sha512-sxi41NXkb5hrJgOvpm32hyBLhW8fem0vn2XxR4+jyRg1rM/v3ziF10/VqC9KDZuDNZkt9JjL9B0825Cf7AN6Lg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@turf/boolean-disjoint": "7.3.4",
|
||||
"@turf/helpers": "7.3.4",
|
||||
"@turf/meta": "7.3.4",
|
||||
"@types/geojson": "^7946.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@turf/boolean-point-in-polygon": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://mirror-npm.runflare.com/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-7.3.4.tgz",
|
||||
"integrity": "sha512-v/4hfyY90Vz9cDgs2GwjQf+Lft8o7mNCLJOTz/iv8SHAIgMMX0czEoIaNVOJr7tBqPqwin1CGwsncrkf5C9n8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@turf/helpers": "7.3.4",
|
||||
"@turf/invariant": "7.3.4",
|
||||
"@types/geojson": "^7946.0.10",
|
||||
"point-in-polygon-hao": "^1.1.0",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@turf/clone": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://mirror-npm.runflare.com/@turf/clone/-/clone-7.3.4.tgz",
|
||||
"integrity": "sha512-pwQ+RyQw986uu7IulY/18NRAebwZZScb084bvVqVkTrllwLSv4oVBqUxmUMiwtp+PNdiRGRFOvNyZqtRsiD+Jw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@turf/helpers": "7.3.4",
|
||||
"@types/geojson": "^7946.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@turf/distance": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://mirror-npm.runflare.com/@turf/distance/-/distance-7.3.4.tgz",
|
||||
"integrity": "sha512-9drWgd46uHPPyzgrcRQLgSvdS/SjVlQ6ZIBoRQagS5P2kSjUbcOXHIMeOSPwfxwlKhEtobLyr+IiR2ns1TfF8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@turf/helpers": "7.3.4",
|
||||
"@turf/invariant": "7.3.4",
|
||||
"@types/geojson": "^7946.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@turf/helpers": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://mirror-npm.runflare.com/@turf/helpers/-/helpers-7.3.4.tgz",
|
||||
"integrity": "sha512-U/S5qyqgx3WTvg4twaH0WxF3EixoTCfDsmk98g1E3/5e2YKp7JKYZdz0vivsS5/UZLJeZDEElOSFH4pUgp+l7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "^7946.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@turf/intersect": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://mirror-npm.runflare.com/@turf/intersect/-/intersect-7.3.4.tgz",
|
||||
"integrity": "sha512-VsqMEMeRWWs2mjwI7sTlUgH1cEfugTGhQ0nF8ncHG7YKd9HUUTzIKpn9FJeoguPWIYITcy1ar4yJEOU/hteBVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@turf/helpers": "7.3.4",
|
||||
"@turf/meta": "7.3.4",
|
||||
"@types/geojson": "^7946.0.10",
|
||||
"polyclip-ts": "^0.16.8",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@turf/invariant": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://mirror-npm.runflare.com/@turf/invariant/-/invariant-7.3.4.tgz",
|
||||
"integrity": "sha512-88Eo4va4rce9sNZs6XiMJowWkikM3cS2TBhaCKlU+GFHdNf8PFEpiU42VDU8q5tOF6/fu21Rvlke5odgOGW4AQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@turf/helpers": "7.3.4",
|
||||
"@types/geojson": "^7946.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@turf/line-intersect": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://mirror-npm.runflare.com/@turf/line-intersect/-/line-intersect-7.3.4.tgz",
|
||||
"integrity": "sha512-XygbTvHa6A+v6l2ZKYtS8AAWxwmrPxKxfBbdH75uED1JvdytSLWYTKGlcU3soxd9sYb4x/g9sDvRIVyU6Lucrg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@turf/helpers": "7.3.4",
|
||||
"@types/geojson": "^7946.0.10",
|
||||
"sweepline-intersections": "^1.5.0",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@turf/meta": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://mirror-npm.runflare.com/@turf/meta/-/meta-7.3.4.tgz",
|
||||
"integrity": "sha512-tlmw9/Hs1p2n0uoHVm1w3ugw1I6L8jv9YZrcdQa4SH5FX5UY0ATrKeIvfA55FlL//PGuYppJp+eyg/0eb4goqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@turf/helpers": "7.3.4",
|
||||
"@types/geojson": "^7946.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@turf/polygon-to-line": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://mirror-npm.runflare.com/@turf/polygon-to-line/-/polygon-to-line-7.3.4.tgz",
|
||||
"integrity": "sha512-xhmOZ5rHZAKLUDLeYKWMsX84ip8CCGOcGLBHtPPYOjdIDHddMV6Sxt5kVgkmlZpK6NEWEmOD6lYR4obxHcHlGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@turf/helpers": "7.3.4",
|
||||
"@turf/invariant": "7.3.4",
|
||||
"@types/geojson": "^7946.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@turf/projection": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://mirror-npm.runflare.com/@turf/projection/-/projection-7.3.4.tgz",
|
||||
"integrity": "sha512-p91zOaLmzoBHzU/2H6Ot1tOhTmAom85n1P7I4Oo0V9xU8hmJXWfNnomLFf/6rnkKDIFZkncLQIBz4iIecZ61sA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@turf/clone": "7.3.4",
|
||||
"@turf/helpers": "7.3.4",
|
||||
"@turf/meta": "7.3.4",
|
||||
"@types/geojson": "^7946.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@turf/rectangle-grid": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://mirror-npm.runflare.com/@turf/rectangle-grid/-/rectangle-grid-7.3.4.tgz",
|
||||
"integrity": "sha512-qM7vujJ4wndB4MKZlEcnUSawgvs5wXpSEFf4f+LWRIfmGhtv6serzDqFzWcmy8kF8hg5J465PMktRmAFWq/a+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@turf/boolean-intersects": "7.3.4",
|
||||
"@turf/distance": "7.3.4",
|
||||
"@turf/helpers": "7.3.4",
|
||||
"@types/geojson": "^7946.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@turf/square-grid": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://mirror-npm.runflare.com/@turf/square-grid/-/square-grid-7.3.4.tgz",
|
||||
"integrity": "sha512-MgjlVRklQYFfQm9yJNha9kXothLPliVdeycNdmn4lWLH3SOZe1rqJPB5Z9+dhmJELT3BJraDq3W5ik5taEpKyQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@turf/helpers": "7.3.4",
|
||||
"@turf/rectangle-grid": "7.3.4",
|
||||
"@types/geojson": "^7946.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.1",
|
||||
"license": "MIT"
|
||||
@@ -4036,6 +4343,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
"version": "9.3.1",
|
||||
"resolved": "https://mirror-npm.runflare.com/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
||||
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"dev": true,
|
||||
@@ -8089,6 +8405,25 @@
|
||||
"pathe": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/point-in-polygon-hao": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://mirror-npm.runflare.com/point-in-polygon-hao/-/point-in-polygon-hao-1.2.4.tgz",
|
||||
"integrity": "sha512-x2pcvXeqhRHlNRdhLs/tgFapAbSSe86wa/eqmj1G6pWftbEs5aVRJhRGM6FYSUERKu0PjekJzMq0gsI2XyiclQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"robust-predicates": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/polyclip-ts": {
|
||||
"version": "0.16.8",
|
||||
"resolved": "https://mirror-npm.runflare.com/polyclip-ts/-/polyclip-ts-0.16.8.tgz",
|
||||
"integrity": "sha512-JPtKbDRuPEuAjuTdhR62Gph7Is2BS1Szx69CFOO3g71lpJDFo78k4tFyi+qFOMVPePEzdSKkpGU3NBXPHHjvKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bignumber.js": "^9.1.0",
|
||||
"splaytree-ts": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/possible-typed-array-names": {
|
||||
"version": "1.0.0",
|
||||
"dev": true,
|
||||
@@ -9166,6 +9501,12 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/robust-predicates": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://mirror-npm.runflare.com/robust-predicates/-/robust-predicates-3.0.2.tgz",
|
||||
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/rope-sequence": {
|
||||
"version": "1.3.4",
|
||||
"license": "MIT"
|
||||
@@ -9574,6 +9915,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/splaytree-ts": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://mirror-npm.runflare.com/splaytree-ts/-/splaytree-ts-1.0.2.tgz",
|
||||
"integrity": "sha512-0kGecIZNIReCSiznK3uheYB8sbstLjCZLiwcQwbmLhgHJj2gz6OnSPkVzJQCMnmEz1BQ4gPK59ylhBoEWOhGNA==",
|
||||
"license": "BDS-3-Clause"
|
||||
},
|
||||
"node_modules/split-string": {
|
||||
"version": "3.1.0",
|
||||
"license": "MIT",
|
||||
@@ -10245,6 +10592,21 @@
|
||||
"dev": true,
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/sweepline-intersections": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://mirror-npm.runflare.com/sweepline-intersections/-/sweepline-intersections-1.5.0.tgz",
|
||||
"integrity": "sha512-AoVmx72QHpKtItPu72TzFL+kcYjd67BPLDoR0LarIk+xyaRg+pDTMFXndIEvZf9xEKnJv6JdhgRMnocoG0D3AQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyqueue": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sweepline-intersections/node_modules/tinyqueue": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://mirror-npm.runflare.com/tinyqueue/-/tinyqueue-2.0.3.tgz",
|
||||
"integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/tabbable": {
|
||||
"version": "6.2.0",
|
||||
"license": "MIT"
|
||||
@@ -10981,6 +11343,12 @@
|
||||
"version": "2.2.8",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wgs84": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://mirror-npm.runflare.com/wgs84/-/wgs84-0.0.0.tgz",
|
||||
"integrity": "sha512-ANHlY4Rb5kHw40D0NJ6moaVfOCMrp9Gpd1R/AIQYg2ko4/jzcJ+TVXYYF6kXJqQwITvEZP4yEthjM7U6rYlljQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "3.1.1",
|
||||
"dev": true,
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"@fullcalendar/react": "6.1.15",
|
||||
"@fullcalendar/timegrid": "6.1.15",
|
||||
"@hookform/resolvers": "3.9.1",
|
||||
"@mapbox/mapbox-gl-draw": "^1.5.1",
|
||||
"@mui/lab": "6.0.0-beta.19",
|
||||
"@mui/material": "6.2.1",
|
||||
"@mui/material-nextjs": "6.2.1",
|
||||
@@ -47,6 +48,10 @@
|
||||
"@tiptap/pm": "^2.10.4",
|
||||
"@tiptap/react": "^2.10.4",
|
||||
"@tiptap/starter-kit": "^2.10.4",
|
||||
"@turf/area": "^7.3.4",
|
||||
"@turf/bbox": "^7.3.4",
|
||||
"@turf/intersect": "^7.3.4",
|
||||
"@turf/square-grid": "^7.3.4",
|
||||
"@types/leaflet": "^1.9.21",
|
||||
"@types/leaflet-draw": "^1.0.13",
|
||||
"apexcharts": "3.49.0",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Components Imports
|
||||
import CropZoningWrapper from '@views/dashboards/farm/cropZoning/CropZoningWrapper'
|
||||
|
||||
const CropZoningPage = async () => {
|
||||
return <CropZoningWrapper />
|
||||
}
|
||||
|
||||
export default CropZoningPage
|
||||
@@ -62,6 +62,21 @@ ul:not([class]) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Crop zoning map tooltip - glassmorphism */
|
||||
.zone-tooltip {
|
||||
background: rgba(255, 255, 255, 0.9) !important;
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
[data-dark] .zone-tooltip {
|
||||
background: rgba(30, 35, 50, 0.95) !important;
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.ps__rail-y {
|
||||
inset-inline-end: 0 !important;
|
||||
inset-inline-start: auto !important;
|
||||
|
||||
@@ -102,6 +102,9 @@ const VerticalMenu = ({ scrollMenu }: Props) => {
|
||||
<MenuItem href="/dashboard/soil-data" icon={<i className="tabler-seedling" />}>
|
||||
{t('soilData')}
|
||||
</MenuItem>
|
||||
<MenuItem href="/dashboard/crop-zoning" icon={<i className="tabler-map-2" />}>
|
||||
{t('cropZoning')}
|
||||
</MenuItem>
|
||||
</MenuSection>
|
||||
|
||||
</Menu>
|
||||
|
||||
@@ -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}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import Box from '@mui/material/Box'
|
||||
import CircularProgress from '@mui/material/CircularProgress'
|
||||
import Button from '@mui/material/Button'
|
||||
import CropZoningMap from './CropZoningMap'
|
||||
import ZoneLegend from './ZoneLegend'
|
||||
import LayerControl from './LayerControl'
|
||||
import ZoneDetailPanel from './ZoneDetailPanel'
|
||||
import type { LayerType } from './cropZoningTypes'
|
||||
import type { ZoneFeatureProperties } from './cropZoningTypes'
|
||||
import type { MapDrawGeoJSON } from './CropZoningMap'
|
||||
|
||||
const MapComponent = dynamic(() => Promise.resolve(CropZoningMap), {
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<Box className='flex items-center justify-center bs-full min-bs-[400px] rounded-xl bg-action-hover'>
|
||||
<CircularProgress size={48} />
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
|
||||
export default function CropZoningWrapper() {
|
||||
const t = useTranslations('cropZoning')
|
||||
const [areaGeoJson, setAreaGeoJson] = useState<MapDrawGeoJSON>(null)
|
||||
const [activeLayer, setActiveLayer] = useState<LayerType>('crops')
|
||||
const [selectedZone, setSelectedZone] = useState<ZoneFeatureProperties | null>(null)
|
||||
const [panelOpen, setPanelOpen] = useState(false)
|
||||
const [optimizationKey, setOptimizationKey] = useState(0)
|
||||
|
||||
const handleAreaChange = useCallback((geojson: MapDrawGeoJSON) => {
|
||||
setAreaGeoJson(geojson)
|
||||
}, [])
|
||||
|
||||
const handleZoneClick = useCallback((zone: ZoneFeatureProperties) => {
|
||||
setSelectedZone(zone)
|
||||
setPanelOpen(true)
|
||||
}, [])
|
||||
|
||||
const handleOptimize = useCallback(() => {
|
||||
setOptimizationKey(k => k + 1)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Box className='relative is-full min-bs-[calc(100vh-120px)] rounded-xl overflow-hidden'>
|
||||
<Box className='absolute inset-0 z-0'>
|
||||
<MapComponent
|
||||
center={[35.6892, 51.389]}
|
||||
zoom={13}
|
||||
height='100%'
|
||||
activeLayer={activeLayer}
|
||||
onAreaChange={handleAreaChange}
|
||||
onZoneClick={handleZoneClick}
|
||||
optimizationKey={optimizationKey}
|
||||
className='min-bs-[400px]'
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<LayerControl activeLayer={activeLayer} onLayerChange={setActiveLayer} />
|
||||
|
||||
{areaGeoJson && (
|
||||
<>
|
||||
<ZoneLegend />
|
||||
<Box className='absolute top-16 end-4 z-[1000]'>
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
size='medium'
|
||||
startIcon={<i className='tabler-refresh text-xl' />}
|
||||
onClick={handleOptimize}
|
||||
className='rounded-xl shadow-lg'
|
||||
>
|
||||
{t('optimizeAgain')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ZoneDetailPanel
|
||||
open={panelOpen}
|
||||
onClose={() => setPanelOpen(false)}
|
||||
zone={selectedZone}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import type { LayerType } from './cropZoningTypes'
|
||||
|
||||
const LAYER_ICONS: Record<LayerType, string> = {
|
||||
crops: 'tabler-plant-2',
|
||||
waterNeed: 'tabler-droplet',
|
||||
soilQuality: 'tabler-seedling',
|
||||
cultivationRisk: 'tabler-alert-triangle'
|
||||
}
|
||||
|
||||
type LayerControlProps = {
|
||||
activeLayer: LayerType
|
||||
onLayerChange: (layer: LayerType) => void
|
||||
}
|
||||
|
||||
const LAYER_ORDER: LayerType[] = ['crops', 'waterNeed', 'soilQuality', 'cultivationRisk']
|
||||
|
||||
export default function LayerControl({ activeLayer, onLayerChange }: LayerControlProps) {
|
||||
const t = useTranslations('cropZoning')
|
||||
|
||||
return (
|
||||
<div className='absolute top-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 => (
|
||||
<IconButton
|
||||
key={layer}
|
||||
size='small'
|
||||
onClick={() => onLayerChange(layer)}
|
||||
title={t(`layers.${layer}`)}
|
||||
sx={{
|
||||
mx: 0.5,
|
||||
bgcolor: activeLayer === layer ? 'action.selected' : 'transparent',
|
||||
'&:hover': { bgcolor: activeLayer === layer ? 'action.selected' : 'action.hover' }
|
||||
}}
|
||||
>
|
||||
<i className={`${LAYER_ICONS[layer]} text-lg`} />
|
||||
</IconButton>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
import useMediaQuery from '@mui/material/useMediaQuery'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import Drawer from '@mui/material/Drawer'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import Box from '@mui/material/Box'
|
||||
import Button from '@mui/material/Button'
|
||||
|
||||
// Component Imports
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
import Select from '@mui/material/Select'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import FormControl from '@mui/material/FormControl'
|
||||
import InputLabel from '@mui/material/InputLabel'
|
||||
import {
|
||||
Radar,
|
||||
RadarChart,
|
||||
PolarGrid,
|
||||
PolarAngleAxis,
|
||||
PolarRadiusAxis,
|
||||
ResponsiveContainer
|
||||
} from 'recharts'
|
||||
import type { ZoneFeatureProperties } from './cropZoningTypes'
|
||||
import { CROP_COLORS } from './cropZoningTypes'
|
||||
|
||||
type ZoneDetailPanelProps = {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
zone: ZoneFeatureProperties | null
|
||||
onCropChange?: (zoneId: string, crop: string) => void
|
||||
}
|
||||
|
||||
const CROP_LABELS: Record<string, string> = {
|
||||
wheat: 'گندم',
|
||||
canola: 'کلزا',
|
||||
saffron: 'زعفران'
|
||||
}
|
||||
|
||||
export default function ZoneDetailPanel({
|
||||
open,
|
||||
onClose,
|
||||
zone,
|
||||
onCropChange
|
||||
}: ZoneDetailPanelProps) {
|
||||
const t = useTranslations('cropZoning')
|
||||
const tCommon = useTranslations('common')
|
||||
const theme = useTheme()
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
|
||||
|
||||
if (!zone) return null
|
||||
|
||||
const chartData = zone.criteria.map(c => ({ subject: c.name, value: c.value, fullMark: 100 }))
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
anchor='end'
|
||||
variant='temporary'
|
||||
ModalProps={{
|
||||
disablePortal: true,
|
||||
disableAutoFocus: true,
|
||||
keepMounted: true
|
||||
}}
|
||||
PaperProps={{
|
||||
className: isMobile ? 'is-full max-is-[100vw]' : 'is-[360px] max-is-[100vw] rounded-s-xl shadow-xl border-s border-gray-200/50 dark:border-gray-700/50',
|
||||
sx: { bgcolor: 'background.paper' }
|
||||
}}
|
||||
sx={{ zIndex: 1300 }}
|
||||
>
|
||||
<Box className='flex flex-col is-full' sx={{ height: '100%' }}>
|
||||
<Box className='p-6 pb-2'>
|
||||
<Typography variant='h5'>
|
||||
{t('panel.title')}
|
||||
</Typography>
|
||||
</Box>
|
||||
<PerfectScrollbar options={{ wheelPropagation: false }} className='flex-1 px-6 pb-6'>
|
||||
<Box className='flex flex-col gap-4'>
|
||||
<Box className='rounded-xl border border-gray-200/60 dark:border-gray-700/60 p-4 bg-white/50 dark:bg-gray-800/30'>
|
||||
<Typography variant='subtitle2' color='text.secondary' className='mbe-2'>
|
||||
محصول پیشنهادی
|
||||
</Typography>
|
||||
<Typography variant='h6' sx={{ color: CROP_COLORS[zone.crop as keyof typeof CROP_COLORS] }}>
|
||||
{CROP_LABELS[zone.crop] ?? zone.crop}
|
||||
</Typography>
|
||||
<Typography variant='body2' className='mt-2'>
|
||||
درصد تطابق: {zone.matchPercent}%
|
||||
</Typography>
|
||||
<Typography variant='body2'>نیاز آب: {zone.waterNeed}</Typography>
|
||||
<Typography variant='body2'>سود تخمینی: {zone.estimatedProfit}</Typography>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography variant='subtitle2' color='text.secondary' className='mbe-2'>
|
||||
{t('panel.reason')}
|
||||
</Typography>
|
||||
<Typography variant='body2'>{zone.reason}</Typography>
|
||||
</Box>
|
||||
|
||||
<Box className='rounded-xl border border-gray-200/60 dark:border-gray-700/60 p-4 bg-white/50 dark:bg-gray-800/30'>
|
||||
<Typography variant='subtitle2' color='text.secondary' className='mbe-3'>
|
||||
{t('panel.criteriaChart')}
|
||||
</Typography>
|
||||
<div className='bs-[220px]'>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<RadarChart data={chartData}>
|
||||
<PolarGrid />
|
||||
<PolarAngleAxis dataKey='subject' />
|
||||
<PolarRadiusAxis angle={90} domain={[0, 100]} />
|
||||
<Radar
|
||||
name='امتیاز'
|
||||
dataKey='value'
|
||||
stroke='var(--mui-palette-primary-main)'
|
||||
fill='var(--mui-palette-primary-main)'
|
||||
fillOpacity={0.4}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</RadarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<FormControl fullWidth size='small'>
|
||||
<InputLabel>{t('panel.changeCrop')}</InputLabel>
|
||||
<Select
|
||||
value={zone.crop}
|
||||
label={t('panel.changeCrop')}
|
||||
onChange={e => onCropChange?.(zone.zoneId, e.target.value)}
|
||||
>
|
||||
<MenuItem value='wheat'>{CROP_LABELS.wheat}</MenuItem>
|
||||
<MenuItem value='canola'>{CROP_LABELS.canola}</MenuItem>
|
||||
<MenuItem value='saffron'>{CROP_LABELS.saffron}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Button variant='outlined' onClick={onClose} fullWidth>
|
||||
{tCommon('close')}
|
||||
</Button>
|
||||
</Box>
|
||||
</PerfectScrollbar>
|
||||
</Box>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { CROP_COLORS, type CropType } from './cropZoningTypes'
|
||||
|
||||
export default function ZoneLegend() {
|
||||
const t = useTranslations('cropZoning')
|
||||
|
||||
const items: { crop: CropType; label: string }[] = [
|
||||
{ crop: 'wheat', label: t('crops.wheat') },
|
||||
{ crop: 'canola', label: t('crops.canola') },
|
||||
{ crop: 'saffron', label: t('crops.saffron') }
|
||||
]
|
||||
|
||||
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='text-xs font-medium text-gray-600 dark:text-gray-400 mbe-2'>{t('legend')}</div>
|
||||
<div className='flex flex-col gap-1.5'>
|
||||
{items.map(({ crop, label }) => (
|
||||
<div key={crop} className='flex items-center gap-2'>
|
||||
<div
|
||||
className='h-4 w-4 rounded'
|
||||
style={{ backgroundColor: CROP_COLORS[crop], opacity: 0.8 }}
|
||||
/>
|
||||
<span className='text-sm text-gray-700 dark:text-gray-300'>{label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { Feature, Polygon } from 'geojson'
|
||||
|
||||
export type CropType = 'wheat' | 'canola' | 'saffron'
|
||||
|
||||
export const CROP_COLORS: Record<CropType, string> = {
|
||||
wheat: '#6bcb77',
|
||||
canola: '#ffd93d',
|
||||
saffron: '#9b59b6'
|
||||
}
|
||||
|
||||
export type LayerType = 'crops' | 'waterNeed' | 'soilQuality' | 'cultivationRisk'
|
||||
|
||||
export interface ZoneData {
|
||||
id: string
|
||||
crop: CropType
|
||||
matchPercent: number
|
||||
waterNeed: string
|
||||
estimatedProfit: string
|
||||
reason: string
|
||||
criteria: { name: string; value: number }[]
|
||||
}
|
||||
|
||||
export interface ZoneFeatureProperties {
|
||||
zoneId: string
|
||||
crop: CropType
|
||||
matchPercent: number
|
||||
waterNeed: string
|
||||
estimatedProfit: string
|
||||
reason: string
|
||||
criteria: { name: string; value: number }[]
|
||||
}
|
||||
|
||||
export type ZoneFeature = Feature<Polygon, ZoneFeatureProperties>
|
||||
@@ -0,0 +1,80 @@
|
||||
import bbox from '@turf/bbox'
|
||||
import squareGrid from '@turf/square-grid'
|
||||
import type { Feature, FeatureCollection, Polygon } from 'geojson'
|
||||
import type { CropType, ZoneFeatureProperties } from './cropZoningTypes'
|
||||
|
||||
const CROPS: CropType[] = ['wheat', 'canola', 'saffron']
|
||||
|
||||
function ruleBasedCropAssignment(
|
||||
index: number,
|
||||
coords: number[][][]
|
||||
): { crop: CropType; matchPercent: number; waterNeed: string; estimatedProfit: string; reason: string; criteria: { name: string; value: number }[] } {
|
||||
const lat = coords[0]?.[0]?.[1] ?? 35
|
||||
const lng = coords[0]?.[0]?.[0] ?? 51
|
||||
const seed = index * 7 + Math.floor(lat * 100) + Math.floor(lng * 100)
|
||||
const cropIndex = Math.abs(seed) % CROPS.length
|
||||
const crop = CROPS[cropIndex]
|
||||
const matchPercent = 60 + (Math.abs(seed) % 35)
|
||||
const waterNeeds: Record<CropType, string> = {
|
||||
wheat: '۴۵۰۰-۵۵۰۰ m³/ha',
|
||||
canola: '۵۰۰۰-۶۰۰۰ m³/ha',
|
||||
saffron: '۳۰۰۰-۴۰۰۰ m³/ha'
|
||||
}
|
||||
const profits: Record<CropType, string> = {
|
||||
wheat: '۱۵-۲۵ میلیون/هکتار',
|
||||
canola: '۲۰-۳۵ میلیون/هکتار',
|
||||
saffron: '۵۰-۱۵۰ میلیون/هکتار'
|
||||
}
|
||||
const reasons: Record<CropType, string> = {
|
||||
wheat: 'دمای مناسب، خاک حاصلخیز، دسترسی به آب کافی',
|
||||
canola: 'شرایط اقلیمی مساعد، نیاز آبی قابل تأمین',
|
||||
saffron: 'ارتفاع و آب و هوای خشک مناسب، پتانسیل سود بالا'
|
||||
}
|
||||
const criteria = [
|
||||
{ name: 'دما', value: 70 + (Math.abs(seed) % 25) },
|
||||
{ name: 'بارش', value: 60 + (Math.abs(seed + 3) % 30) },
|
||||
{ name: 'خاک', value: 65 + (Math.abs(seed + 5) % 30) },
|
||||
{ name: 'آب', value: 55 + (Math.abs(seed + 7) % 40) }
|
||||
]
|
||||
return {
|
||||
crop,
|
||||
matchPercent,
|
||||
waterNeed: waterNeeds[crop],
|
||||
estimatedProfit: profits[crop],
|
||||
reason: reasons[crop],
|
||||
criteria
|
||||
}
|
||||
}
|
||||
|
||||
export function createZonedGrid(
|
||||
polygonFeature: Feature<Polygon>,
|
||||
cellSideKm = 0.15
|
||||
): FeatureCollection<Polygon, ZoneFeatureProperties> {
|
||||
const bboxArr = bbox(polygonFeature)
|
||||
const grid = squareGrid(bboxArr, cellSideKm, {
|
||||
units: 'kilometers',
|
||||
mask: polygonFeature
|
||||
})
|
||||
|
||||
const features: Feature<Polygon, ZoneFeatureProperties>[] = grid.features.map((f, i) => {
|
||||
const coords = (f.geometry as Polygon).coordinates
|
||||
const assigned = ruleBasedCropAssignment(i, coords)
|
||||
return {
|
||||
...f,
|
||||
properties: {
|
||||
zoneId: `zone-${i}`,
|
||||
crop: assigned.crop,
|
||||
matchPercent: assigned.matchPercent,
|
||||
waterNeed: assigned.waterNeed,
|
||||
estimatedProfit: assigned.estimatedProfit,
|
||||
reason: assigned.reason,
|
||||
criteria: assigned.criteria
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
type: 'FeatureCollection',
|
||||
features
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export { default as CropZoningWrapper } from './CropZoningWrapper'
|
||||
export { default as CropZoningMap } from './CropZoningMap'
|
||||
export { default as ZoneLegend } from './ZoneLegend'
|
||||
export { default as LayerControl } from './LayerControl'
|
||||
export { default as ZoneDetailPanel } from './ZoneDetailPanel'
|
||||
export * from './cropZoningTypes'
|
||||
export * from './cropZoningUtils'
|
||||
Reference in New Issue
Block a user