diff --git a/messages/fa.json b/messages/fa.json index b1ec7fd..102ce83 100644 --- a/messages/fa.json +++ b/messages/fa.json @@ -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": "تغییر محصول" + } } } diff --git a/package-lock.json b/package-lock.json index f15db1e..6fd05fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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, diff --git a/package.json b/package.json index 40c3459..19faa84 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/(dashboard)/(private)/dashboard/crop-zoning/page.tsx b/src/app/(dashboard)/(private)/dashboard/crop-zoning/page.tsx new file mode 100644 index 0000000..f2aa05e --- /dev/null +++ b/src/app/(dashboard)/(private)/dashboard/crop-zoning/page.tsx @@ -0,0 +1,8 @@ +// Components Imports +import CropZoningWrapper from '@views/dashboards/farm/cropZoning/CropZoningWrapper' + +const CropZoningPage = async () => { + return +} + +export default CropZoningPage diff --git a/src/app/globals.css b/src/app/globals.css index 110c590..31c6d3c 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -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; diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index d33b2ef..d7f73fa 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -102,6 +102,9 @@ const VerticalMenu = ({ scrollMenu }: Props) => { }> {t('soilData')} + }> + {t('cropZoning')} + diff --git a/src/views/dashboards/farm/cropZoning/CropZoningMap.tsx b/src/views/dashboards/farm/cropZoning/CropZoningMap.tsx new file mode 100644 index 0000000..313fb12 --- /dev/null +++ b/src/views/dashboards/farm/cropZoning/CropZoningMap.tsx @@ -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 | 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(null) + const mapInstanceRef = useRef(null) + const drawnItemsRef = useRef(null) + const drawControlRef = useRef(null) + const zonesLayerRef = useRef(null) + + const renderZones = useCallback( + (map: L.Map, polygonFeature: Feature) => { + 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 = ` +
+
${cropLabel}
+
درصد تطابق: ${props.matchPercent}%
+
نیاز آب: ${props.waterNeed}
+
سود تخمینی: ${props.estimatedProfit}
+
+ ` + 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) + } 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 + 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 ( +
+ ) +} diff --git a/src/views/dashboards/farm/cropZoning/CropZoningWrapper.tsx b/src/views/dashboards/farm/cropZoning/CropZoningWrapper.tsx new file mode 100644 index 0000000..26ba89f --- /dev/null +++ b/src/views/dashboards/farm/cropZoning/CropZoningWrapper.tsx @@ -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: () => ( + + + + ) +}) + +export default function CropZoningWrapper() { + const t = useTranslations('cropZoning') + const [areaGeoJson, setAreaGeoJson] = useState(null) + const [activeLayer, setActiveLayer] = useState('crops') + const [selectedZone, setSelectedZone] = useState(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 ( + + + + + + + + {areaGeoJson && ( + <> + + + + + + )} + + setPanelOpen(false)} + zone={selectedZone} + /> + + ) +} diff --git a/src/views/dashboards/farm/cropZoning/LayerControl.tsx b/src/views/dashboards/farm/cropZoning/LayerControl.tsx new file mode 100644 index 0000000..81b3217 --- /dev/null +++ b/src/views/dashboards/farm/cropZoning/LayerControl.tsx @@ -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 = { + 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 ( +
+ {LAYER_ORDER.map(layer => ( + onLayerChange(layer)} + title={t(`layers.${layer}`)} + sx={{ + mx: 0.5, + bgcolor: activeLayer === layer ? 'action.selected' : 'transparent', + '&:hover': { bgcolor: activeLayer === layer ? 'action.selected' : 'action.hover' } + }} + > + + + ))} +
+ ) +} diff --git a/src/views/dashboards/farm/cropZoning/ZoneDetailPanel.tsx b/src/views/dashboards/farm/cropZoning/ZoneDetailPanel.tsx new file mode 100644 index 0000000..1ffdef6 --- /dev/null +++ b/src/views/dashboards/farm/cropZoning/ZoneDetailPanel.tsx @@ -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 = { + 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 ( + + + + + {t('panel.title')} + + + + + + + محصول پیشنهادی + + + {CROP_LABELS[zone.crop] ?? zone.crop} + + + درصد تطابق: {zone.matchPercent}% + + نیاز آب: {zone.waterNeed} + سود تخمینی: {zone.estimatedProfit} + + + + + {t('panel.reason')} + + {zone.reason} + + + + + {t('panel.criteriaChart')} + +
+ + + + + + + + +
+
+ + + {t('panel.changeCrop')} + + + + +
+
+
+
+ ) +} diff --git a/src/views/dashboards/farm/cropZoning/ZoneLegend.tsx b/src/views/dashboards/farm/cropZoning/ZoneLegend.tsx new file mode 100644 index 0000000..239de9f --- /dev/null +++ b/src/views/dashboards/farm/cropZoning/ZoneLegend.tsx @@ -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 ( +
+
{t('legend')}
+
+ {items.map(({ crop, label }) => ( +
+
+ {label} +
+ ))} +
+
+ ) +} diff --git a/src/views/dashboards/farm/cropZoning/cropZoningTypes.ts b/src/views/dashboards/farm/cropZoning/cropZoningTypes.ts new file mode 100644 index 0000000..a4ca440 --- /dev/null +++ b/src/views/dashboards/farm/cropZoning/cropZoningTypes.ts @@ -0,0 +1,33 @@ +import type { Feature, Polygon } from 'geojson' + +export type CropType = 'wheat' | 'canola' | 'saffron' + +export const CROP_COLORS: Record = { + 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 diff --git a/src/views/dashboards/farm/cropZoning/cropZoningUtils.ts b/src/views/dashboards/farm/cropZoning/cropZoningUtils.ts new file mode 100644 index 0000000..3243121 --- /dev/null +++ b/src/views/dashboards/farm/cropZoning/cropZoningUtils.ts @@ -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 = { + wheat: '۴۵۰۰-۵۵۰۰ m³/ha', + canola: '۵۰۰۰-۶۰۰۰ m³/ha', + saffron: '۳۰۰۰-۴۰۰۰ m³/ha' + } + const profits: Record = { + wheat: '۱۵-۲۵ میلیون/هکتار', + canola: '۲۰-۳۵ میلیون/هکتار', + saffron: '۵۰-۱۵۰ میلیون/هکتار' + } + const reasons: Record = { + 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, + cellSideKm = 0.15 +): FeatureCollection { + const bboxArr = bbox(polygonFeature) + const grid = squareGrid(bboxArr, cellSideKm, { + units: 'kilometers', + mask: polygonFeature + }) + + const features: Feature[] = 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 + } +} diff --git a/src/views/dashboards/farm/cropZoning/index.ts b/src/views/dashboards/farm/cropZoning/index.ts new file mode 100644 index 0000000..52dd641 --- /dev/null +++ b/src/views/dashboards/farm/cropZoning/index.ts @@ -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'