Add weather information section and update dependencies
- Introduced a new CropZoningWeatherSection component to display weather data in the crop zoning dashboard. - Updated package.json and package-lock.json to include chart.js and react-chartjs-2 for enhanced data visualization. - Added Persian translations for weather-related UI elements to support localization.
This commit is contained in:
@@ -0,0 +1,457 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState, useCallback } from 'react'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
LineElement,
|
||||
PointElement,
|
||||
LinearScale,
|
||||
CategoryScale,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler
|
||||
} from 'chart.js'
|
||||
import { Line } from 'react-chartjs-2'
|
||||
|
||||
ChartJS.register(LineElement, PointElement, LinearScale, CategoryScale, Title, Tooltip, Legend, Filler)
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
interface Leaf {
|
||||
id: number
|
||||
side: 'left' | 'right'
|
||||
heightFraction: number // 0..1 relative to stem
|
||||
angle: number // base rotation angle in degrees
|
||||
scale: number // 0..1 grow-in scale
|
||||
swayOffset: number // random sway phase offset
|
||||
}
|
||||
|
||||
interface PlantState {
|
||||
height: number // current stem height px (0 → MAX_HEIGHT)
|
||||
leaves: Leaf[]
|
||||
tick: number
|
||||
}
|
||||
|
||||
interface EnvironmentSettings {
|
||||
light: number // 0..100
|
||||
water: number // 0..100
|
||||
}
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
const MAX_HEIGHT = 260
|
||||
const SVG_WIDTH = 200
|
||||
const SVG_HEIGHT = 320
|
||||
const STEM_X = SVG_WIDTH / 2
|
||||
const BASE_Y = SVG_HEIGHT - 10
|
||||
const LEAF_INTERVAL_PX = 36 // stem height growth between new leaves
|
||||
const MAX_LEAVES = 12
|
||||
|
||||
// ─── Helper: compute growth speed multiplier from env settings ────────────────
|
||||
|
||||
function growthRate(env: EnvironmentSettings, speed: number): number {
|
||||
const lightFactor = 0.3 + (env.light / 100) * 0.7
|
||||
const waterFactor = 0.3 + (env.water / 100) * 0.7
|
||||
return speed * lightFactor * waterFactor
|
||||
}
|
||||
|
||||
// ─── Plant SVG Component ──────────────────────────────────────────────────────
|
||||
|
||||
function PlantSVG({ plant, tick }: { plant: PlantState; tick: number }) {
|
||||
const stemTop = BASE_Y - plant.height
|
||||
const stemHeight = plant.height
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={SVG_WIDTH}
|
||||
height={SVG_HEIGHT}
|
||||
viewBox={`0 0 ${SVG_WIDTH} ${SVG_HEIGHT}`}
|
||||
className='drop-shadow-lg'
|
||||
>
|
||||
{/* Soil */}
|
||||
<ellipse cx={STEM_X} cy={BASE_Y + 4} rx={40} ry={8} fill='#8B6914' opacity={0.6} />
|
||||
|
||||
{/* Stem */}
|
||||
{stemHeight > 0 && (
|
||||
<rect
|
||||
x={STEM_X - 4}
|
||||
y={stemTop}
|
||||
width={8}
|
||||
height={stemHeight}
|
||||
rx={4}
|
||||
fill='#4a7c59'
|
||||
className='transition-all duration-300'
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Leaves */}
|
||||
{plant.leaves.map(leaf => {
|
||||
const leafY = BASE_Y - leaf.heightFraction * plant.height
|
||||
const swayAngle = Math.sin((tick / 18 + leaf.swayOffset) * 1) * 4
|
||||
const baseAngle = leaf.side === 'left' ? -30 - swayAngle : 30 + swayAngle
|
||||
|
||||
return (
|
||||
<g
|
||||
key={leaf.id}
|
||||
transform={`translate(${STEM_X}, ${leafY})`}
|
||||
style={{ transformOrigin: `${STEM_X}px ${leafY}px` }}
|
||||
>
|
||||
<ellipse
|
||||
cx={leaf.side === 'left' ? -22 * leaf.scale : 22 * leaf.scale}
|
||||
cy={-6 * leaf.scale}
|
||||
rx={22 * leaf.scale}
|
||||
ry={10 * leaf.scale}
|
||||
fill={`hsl(${115 + leaf.id * 3}, 60%, ${38 + leaf.id * 2}%)`}
|
||||
transform={`rotate(${baseAngle})`}
|
||||
opacity={leaf.scale}
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Bud / flower at the top */}
|
||||
{stemHeight > MAX_HEIGHT * 0.85 && (
|
||||
<g>
|
||||
<circle cx={STEM_X} cy={stemTop - 8} r={8} fill='#f9c74f' opacity={0.9} />
|
||||
<circle cx={STEM_X} cy={stemTop - 8} r={4} fill='#f3722c' />
|
||||
</g>
|
||||
)}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Growth Chart ─────────────────────────────────────────────────────────────
|
||||
|
||||
function GrowthChart({
|
||||
heightHistory,
|
||||
leafHistory
|
||||
}: {
|
||||
heightHistory: number[]
|
||||
leafHistory: number[]
|
||||
}) {
|
||||
const labels = heightHistory.map((_, i) => `${i}s`)
|
||||
|
||||
const data = {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'ارتفاع (px)',
|
||||
data: heightHistory,
|
||||
borderColor: '#4a7c59',
|
||||
backgroundColor: 'rgba(74,124,89,0.15)',
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 0,
|
||||
yAxisID: 'yHeight'
|
||||
},
|
||||
{
|
||||
label: 'تعداد برگ',
|
||||
data: leafHistory,
|
||||
borderColor: '#f9c74f',
|
||||
backgroundColor: 'rgba(249,199,79,0.15)',
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 0,
|
||||
yAxisID: 'yLeaf'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const options = {
|
||||
responsive: true,
|
||||
animation: { duration: 0 },
|
||||
plugins: {
|
||||
legend: { labels: { color: '#e2e8f0', font: { size: 12 } } },
|
||||
title: {
|
||||
display: true,
|
||||
text: 'نمودار رشد گیاه',
|
||||
color: '#e2e8f0',
|
||||
font: { size: 14 }
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: { color: '#94a3b8', maxTicksLimit: 8 },
|
||||
grid: { color: 'rgba(148,163,184,0.1)' }
|
||||
},
|
||||
yHeight: {
|
||||
type: 'linear' as const,
|
||||
position: 'left' as const,
|
||||
min: 0,
|
||||
max: MAX_HEIGHT,
|
||||
ticks: { color: '#4a7c59' },
|
||||
grid: { color: 'rgba(148,163,184,0.1)' },
|
||||
title: { display: true, text: 'ارتفاع', color: '#4a7c59' }
|
||||
},
|
||||
yLeaf: {
|
||||
type: 'linear' as const,
|
||||
position: 'right' as const,
|
||||
min: 0,
|
||||
max: MAX_LEAVES,
|
||||
ticks: { color: '#f9c74f' },
|
||||
grid: { display: false },
|
||||
title: { display: true, text: 'برگ', color: '#f9c74f' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <Line data={data} options={options} />
|
||||
}
|
||||
|
||||
// ─── Main Component ───────────────────────────────────────────────────────────
|
||||
|
||||
export default function PlantSimulator() {
|
||||
const [running, setRunning] = useState(false)
|
||||
const [speed, setSpeed] = useState(1.5) // px per tick base
|
||||
const [env, setEnv] = useState<EnvironmentSettings>({ light: 75, water: 65 })
|
||||
|
||||
const [plant, setPlant] = useState<PlantState>({ height: 0, leaves: [], tick: 0 })
|
||||
const [heightHistory, setHeightHistory] = useState<number[]>([0])
|
||||
const [leafHistory, setLeafHistory] = useState<number[]>([0])
|
||||
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
const tickRef = useRef(0)
|
||||
const historyIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setPlant({ height: 0, leaves: [], tick: 0 })
|
||||
setHeightHistory([0])
|
||||
setLeafHistory([0])
|
||||
tickRef.current = 0
|
||||
}, [])
|
||||
|
||||
// Main simulation tick at ~30fps
|
||||
useEffect(() => {
|
||||
if (!running) {
|
||||
if (intervalRef.current) clearInterval(intervalRef.current)
|
||||
return
|
||||
}
|
||||
|
||||
intervalRef.current = setInterval(() => {
|
||||
tickRef.current += 1
|
||||
const t = tickRef.current
|
||||
|
||||
setPlant(prev => {
|
||||
const rate = growthRate(env, speed)
|
||||
const newHeight = Math.min(prev.height + rate * 0.4, MAX_HEIGHT)
|
||||
const newLeaves = [...prev.leaves]
|
||||
|
||||
// Add a new leaf every LEAF_INTERVAL_PX of stem growth
|
||||
const expectedLeaves = Math.min(Math.floor(newHeight / LEAF_INTERVAL_PX), MAX_LEAVES)
|
||||
|
||||
while (newLeaves.length < expectedLeaves) {
|
||||
const id = newLeaves.length
|
||||
newLeaves.push({
|
||||
id,
|
||||
side: id % 2 === 0 ? 'left' : 'right',
|
||||
heightFraction: (id + 1) / (expectedLeaves + 1),
|
||||
angle: 0,
|
||||
scale: 0,
|
||||
swayOffset: Math.random() * Math.PI * 2
|
||||
})
|
||||
}
|
||||
|
||||
// Grow existing leaves (scale 0 → 1 over ~40 ticks)
|
||||
const grownLeaves = newLeaves.map(l => ({
|
||||
...l,
|
||||
scale: Math.min(l.scale + 0.025, 1)
|
||||
}))
|
||||
|
||||
return { height: newHeight, leaves: grownLeaves, tick: t }
|
||||
})
|
||||
}, 33)
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) clearInterval(intervalRef.current)
|
||||
}
|
||||
}, [running, speed, env])
|
||||
|
||||
// History logging every 1 second
|
||||
useEffect(() => {
|
||||
if (!running) {
|
||||
if (historyIntervalRef.current) clearInterval(historyIntervalRef.current)
|
||||
return
|
||||
}
|
||||
|
||||
historyIntervalRef.current = setInterval(() => {
|
||||
setPlant(prev => {
|
||||
setHeightHistory(h => [...h.slice(-59), Math.round(prev.height)])
|
||||
setLeafHistory(l => [...l.slice(-59), prev.leaves.length])
|
||||
return prev
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
return () => {
|
||||
if (historyIntervalRef.current) clearInterval(historyIntervalRef.current)
|
||||
}
|
||||
}, [running])
|
||||
|
||||
const isFinished = plant.height >= MAX_HEIGHT
|
||||
|
||||
return (
|
||||
<div className='min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-slate-100 p-6'>
|
||||
<h1 className='text-3xl font-bold text-center mb-8 tracking-tight'>
|
||||
🌱 شبیهساز رشد گیاه
|
||||
</h1>
|
||||
|
||||
<div className='max-w-6xl mx-auto grid grid-cols-1 lg:grid-cols-3 gap-6'>
|
||||
|
||||
{/* ── Left: Plant visualization ── */}
|
||||
<div className='lg:col-span-1 flex flex-col items-center gap-4'>
|
||||
<div className='bg-slate-800 border border-slate-700 rounded-2xl p-6 w-full flex flex-col items-center shadow-xl'>
|
||||
<PlantSVG plant={plant} tick={plant.tick} />
|
||||
|
||||
{/* Stats */}
|
||||
<div className='mt-4 grid grid-cols-2 gap-3 w-full text-sm'>
|
||||
<div className='bg-green-900/40 border border-green-700/40 rounded-xl p-3 text-center'>
|
||||
<div className='text-green-400 font-semibold text-lg'>{Math.round(plant.height)}</div>
|
||||
<div className='text-slate-400'>ارتفاع (px)</div>
|
||||
</div>
|
||||
<div className='bg-yellow-900/40 border border-yellow-700/40 rounded-xl p-3 text-center'>
|
||||
<div className='text-yellow-400 font-semibold text-lg'>{plant.leaves.length}</div>
|
||||
<div className='text-slate-400'>تعداد برگ</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isFinished && (
|
||||
<div className='mt-3 text-yellow-300 text-sm font-medium animate-pulse'>
|
||||
🌼 گیاه به حداکثر رشد رسید!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className='bg-slate-800 border border-slate-700 rounded-2xl p-5 w-full shadow-xl space-y-4'>
|
||||
<h2 className='font-semibold text-slate-300 text-base mb-1'>کنترلها</h2>
|
||||
|
||||
{/* Start / Stop / Reset */}
|
||||
<div className='flex gap-2'>
|
||||
<button
|
||||
onClick={() => setRunning(r => !r)}
|
||||
disabled={isFinished}
|
||||
className={`flex-1 py-2 rounded-xl font-semibold transition-all text-sm
|
||||
${running
|
||||
? 'bg-red-600 hover:bg-red-500'
|
||||
: 'bg-green-600 hover:bg-green-500'}
|
||||
disabled:opacity-40 disabled:cursor-not-allowed`}
|
||||
>
|
||||
{running ? '⏸ توقف' : '▶ شروع'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setRunning(false); reset() }}
|
||||
className='px-4 py-2 rounded-xl bg-slate-600 hover:bg-slate-500 text-sm font-semibold transition-all'
|
||||
>
|
||||
↺ ریست
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Speed slider */}
|
||||
<div>
|
||||
<label className='flex justify-between text-sm text-slate-400 mb-1'>
|
||||
<span>سرعت رشد</span>
|
||||
<span className='text-white font-medium'>{speed.toFixed(1)}×</span>
|
||||
</label>
|
||||
<input
|
||||
type='range' min={0.5} max={5} step={0.5}
|
||||
value={speed}
|
||||
onChange={e => setSpeed(Number(e.target.value))}
|
||||
className='w-full accent-green-500'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Light */}
|
||||
<div>
|
||||
<label className='flex justify-between text-sm text-slate-400 mb-1'>
|
||||
<span>☀️ نور</span>
|
||||
<span className='text-yellow-400 font-medium'>{env.light}%</span>
|
||||
</label>
|
||||
<input
|
||||
type='range' min={0} max={100} step={5}
|
||||
value={env.light}
|
||||
onChange={e => setEnv(prev => ({ ...prev, light: Number(e.target.value) }))}
|
||||
className='w-full accent-yellow-400'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Water */}
|
||||
<div>
|
||||
<label className='flex justify-between text-sm text-slate-400 mb-1'>
|
||||
<span>💧 آب</span>
|
||||
<span className='text-blue-400 font-medium'>{env.water}%</span>
|
||||
</label>
|
||||
<input
|
||||
type='range' min={0} max={100} step={5}
|
||||
value={env.water}
|
||||
onChange={e => setEnv(prev => ({ ...prev, water: Number(e.target.value) }))}
|
||||
className='w-full accent-blue-400'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Effective rate indicator */}
|
||||
<div className='bg-slate-700/50 rounded-xl p-3 text-xs text-slate-400'>
|
||||
نرخ رشد مؤثر:{' '}
|
||||
<span className='text-green-400 font-semibold'>
|
||||
{growthRate(env, speed).toFixed(2)}×
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Right: Chart ── */}
|
||||
<div className='lg:col-span-2 bg-slate-800 border border-slate-700 rounded-2xl p-6 shadow-xl'>
|
||||
<GrowthChart heightHistory={heightHistory} leafHistory={leafHistory} />
|
||||
|
||||
{/* Info cards */}
|
||||
<div className='mt-6 grid grid-cols-3 gap-4 text-sm'>
|
||||
<div className='bg-slate-700/60 rounded-xl p-4 border border-slate-600'>
|
||||
<div className='text-slate-400 mb-1'>پیشرفت رشد</div>
|
||||
<div className='w-full bg-slate-600 rounded-full h-2 mb-1'>
|
||||
<div
|
||||
className='bg-green-500 h-2 rounded-full transition-all'
|
||||
style={{ width: `${(plant.height / MAX_HEIGHT) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className='text-green-400 font-semibold'>
|
||||
{Math.round((plant.height / MAX_HEIGHT) * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='bg-slate-700/60 rounded-xl p-4 border border-slate-600'>
|
||||
<div className='text-slate-400 mb-1'>وضعیت نور</div>
|
||||
<div className='w-full bg-slate-600 rounded-full h-2 mb-1'>
|
||||
<div
|
||||
className='bg-yellow-400 h-2 rounded-full transition-all'
|
||||
style={{ width: `${env.light}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className='text-yellow-400 font-semibold'>{env.light}%</div>
|
||||
</div>
|
||||
|
||||
<div className='bg-slate-700/60 rounded-xl p-4 border border-slate-600'>
|
||||
<div className='text-slate-400 mb-1'>وضعیت آب</div>
|
||||
<div className='w-full bg-slate-600 rounded-full h-2 mb-1'>
|
||||
<div
|
||||
className='bg-blue-400 h-2 rounded-full transition-all'
|
||||
style={{ width: `${env.water}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className='text-blue-400 font-semibold'>{env.water}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className='mt-6 bg-slate-700/30 border border-slate-600/50 rounded-xl p-4 text-xs text-slate-400 leading-6'>
|
||||
<p>
|
||||
این شبیهساز رشد گیاه را بر اساس سرعت پایه، میزان نور خورشید و آب دریافتی
|
||||
محاسبه میکند. هر برگ به صورت تدریجی روی ساقه ظاهر شده و با حرکت طبیعی
|
||||
در باد نمایش داده میشود. نمودار تغییرات ارتفاع و تعداد برگها را در طول
|
||||
زمان ثبت میکند.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user