from django.db import models class SoilLocation(models.Model): """ مرکز زمین برای داده‌های خاک و مزرعه. هر مختصات سه سطر در SoilDepthData دارد (۰–۵، ۵–۱۵، ۱۵–۳۰ سانتی‌متر). """ latitude = models.DecimalField( max_digits=9, decimal_places=6, db_index=True, help_text="عرض جغرافیایی مرکز زمین (lat)", ) longitude = models.DecimalField( max_digits=9, decimal_places=6, db_index=True, help_text="طول جغرافیایی مرکز زمین (lon)", ) task_id = models.CharField( max_length=255, blank=True, help_text="شناسه تسک Celery در حال پردازش", ) farm_boundary = models.JSONField( default=dict, blank=True, help_text=( "مرز مزرعه برای درخواست‌های سنجش‌ازدور. " 'می‌تواند GeoJSON polygon یا bbox مثل {"type": "Polygon", "coordinates": [...]} باشد.' ), ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: constraints = [ models.UniqueConstraint( fields=["latitude", "longitude"], name="soil_location_unique_lat_lon", ) ] ordering = ["-updated_at"] verbose_name = "مرکز زمین" verbose_name_plural = "مراکز زمین" def __str__(self): return f"SoilLocation({self.latitude}, {self.longitude})" @property def center_latitude(self): return self.latitude @property def center_longitude(self): return self.longitude @property def is_complete(self): """آیا هر سه عمق ذخیره شده‌اند؟""" return self.depths.count() == 3 class SoilDepthData(models.Model): """ داده‌های خاک برای یک عمق مشخص، مرتبط با یک SoilLocation. مقادیر خام از API SoilGrids (قبل از اعمال d_factor). """ DEPTH_0_5 = "0-5cm" DEPTH_5_15 = "5-15cm" DEPTH_15_30 = "15-30cm" DEPTH_CHOICES = [ (DEPTH_0_5, "۰–۵ سانتی‌متر"), (DEPTH_5_15, "۵–۱۵ سانتی‌متر"), (DEPTH_15_30, "۱۵–۳۰ سانتی‌متر"), ] soil_location = models.ForeignKey( SoilLocation, on_delete=models.CASCADE, related_name="depths", ) depth_label = models.CharField( max_length=10, choices=DEPTH_CHOICES, db_index=True, ) # خواص خاک — مقادیر mean از API (raw) bdod = models.FloatField(null=True, blank=True) cec = models.FloatField(null=True, blank=True) cfvo = models.FloatField(null=True, blank=True) clay = models.FloatField(null=True, blank=True) nitrogen = models.FloatField(null=True, blank=True) ocd = models.FloatField(null=True, blank=True) ocs = models.FloatField(null=True, blank=True) phh2o = models.FloatField(null=True, blank=True) sand = models.FloatField(null=True, blank=True) silt = models.FloatField(null=True, blank=True) soc = models.FloatField(null=True, blank=True) wv0010 = models.FloatField(null=True, blank=True) wv0033 = models.FloatField(null=True, blank=True) wv1500 = models.FloatField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: constraints = [ models.UniqueConstraint( fields=["soil_location", "depth_label"], name="soil_depth_unique_location_depth", ) ] ordering = ["soil_location", "depth_label"] def __str__(self): return f"SoilDepthData({self.soil_location_id}, {self.depth_label})" class NdviObservation(models.Model): location = models.ForeignKey( SoilLocation, on_delete=models.CASCADE, related_name="ndvi_observations", ) observation_date = models.DateField(db_index=True) mean_ndvi = models.FloatField() ndvi_map = models.JSONField(default=dict, blank=True) vegetation_health_class = models.CharField(max_length=64) satellite_source = models.CharField(max_length=64, default="sentinel-2") cloud_cover = models.FloatField(null=True, blank=True) metadata = models.JSONField(default=dict, blank=True) created_at = models.DateTimeField(auto_now_add=True, db_index=True) class Meta: db_table = "dashboard_data_ndviobservation" ordering = ["-observation_date", "-created_at"] constraints = [ models.UniqueConstraint( fields=["location", "observation_date", "satellite_source"], name="ndvi_unique_location_date_source", ) ] verbose_name = "NDVI Observation" verbose_name_plural = "NDVI Observations" def __str__(self): return f"NDVI {self.location_id} {self.observation_date} {self.satellite_source}"