UPDATE
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
from location_data.models import SoilLocation
|
||||
from location_data.serializers import SoilDepthDataSerializer
|
||||
from plant.serializers import PlantSerializer
|
||||
from weather.models import WeatherForecast
|
||||
|
||||
from .models import SensorData
|
||||
from .serializers import WeatherForecastDetailSerializer
|
||||
|
||||
|
||||
DEPTH_PRIORITY = ["0-5cm", "5-15cm", "15-30cm"]
|
||||
DECIMAL_PRECISION = Decimal("0.000001")
|
||||
|
||||
|
||||
def get_farm_details(farm_uuid: str):
|
||||
farm = (
|
||||
SensorData.objects.select_related("center_location", "weather_forecast")
|
||||
.prefetch_related("plants", "center_location__depths")
|
||||
.filter(farm_uuid=farm_uuid)
|
||||
.first()
|
||||
)
|
||||
if farm is None:
|
||||
return None
|
||||
|
||||
center_location = farm.center_location
|
||||
weather = farm.weather_forecast
|
||||
if weather is None:
|
||||
weather = (
|
||||
center_location.weather_forecasts.order_by("-forecast_date", "-id").first()
|
||||
)
|
||||
|
||||
depths = list(center_location.depths.all())
|
||||
depths.sort(key=lambda item: DEPTH_PRIORITY.index(item.depth_label) if item.depth_label in DEPTH_PRIORITY else 99)
|
||||
|
||||
soil_metrics = _surface_soil_metrics(depths)
|
||||
sensor_metrics = _flatten_sensor_metrics(farm.sensor_payload)
|
||||
|
||||
resolved_metrics = dict(soil_metrics)
|
||||
metric_sources = {key: "soil" for key in soil_metrics}
|
||||
for key, value in sensor_metrics.items():
|
||||
resolved_metrics[key] = value
|
||||
metric_sources[key] = "sensor"
|
||||
|
||||
return {
|
||||
"farm_uuid": farm.farm_uuid,
|
||||
"center_location": {
|
||||
"id": center_location.id,
|
||||
"lat": center_location.latitude,
|
||||
"lon": center_location.longitude,
|
||||
"farm_boundary": center_location.farm_boundary,
|
||||
},
|
||||
"weather": WeatherForecastDetailSerializer(weather).data if weather else None,
|
||||
"sensor_payload": farm.sensor_payload or {},
|
||||
"soil": {
|
||||
"resolved_metrics": resolved_metrics,
|
||||
"metric_sources": metric_sources,
|
||||
"depths": SoilDepthDataSerializer(depths, many=True).data,
|
||||
},
|
||||
"plant_ids": list(farm.plants.values_list("id", flat=True)),
|
||||
"plants": PlantSerializer(farm.plants.all(), many=True).data,
|
||||
"created_at": farm.created_at,
|
||||
"updated_at": farm.updated_at,
|
||||
}
|
||||
|
||||
|
||||
def resolve_center_location_from_boundary(farm_boundary: dict | list) -> SoilLocation:
|
||||
"""
|
||||
مرز مزرعه را میگیرد، مرکز را محاسبه میکند و رکورد SoilLocation را
|
||||
ایجاد/بهروزرسانی میکند.
|
||||
"""
|
||||
points = _extract_boundary_points(farm_boundary)
|
||||
if not points:
|
||||
raise ValueError("farm_boundary باید حداقل 3 گوشه معتبر داشته باشد.")
|
||||
|
||||
normalized_points = _normalize_points(points)
|
||||
if len(normalized_points) < 3:
|
||||
raise ValueError("farm_boundary باید حداقل 3 گوشه معتبر داشته باشد.")
|
||||
|
||||
lat_sum = sum(lat for lat, _ in normalized_points)
|
||||
lon_sum = sum(lon for _, lon in normalized_points)
|
||||
count = Decimal(len(normalized_points))
|
||||
center_lat = (lat_sum / count).quantize(DECIMAL_PRECISION, rounding=ROUND_HALF_UP)
|
||||
center_lon = (lon_sum / count).quantize(DECIMAL_PRECISION, rounding=ROUND_HALF_UP)
|
||||
|
||||
with transaction.atomic():
|
||||
location, _ = SoilLocation.objects.update_or_create(
|
||||
latitude=center_lat,
|
||||
longitude=center_lon,
|
||||
defaults={"farm_boundary": _serialize_boundary(farm_boundary)},
|
||||
)
|
||||
return location
|
||||
|
||||
|
||||
def resolve_weather_for_location(location: SoilLocation) -> WeatherForecast | None:
|
||||
return (
|
||||
WeatherForecast.objects.filter(location=location)
|
||||
.order_by("-forecast_date", "-id")
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
def _flatten_sensor_metrics(sensor_payload: dict | None) -> dict:
|
||||
if not isinstance(sensor_payload, dict):
|
||||
return {}
|
||||
|
||||
flattened = {}
|
||||
for sensor_values in sensor_payload.values():
|
||||
if not isinstance(sensor_values, dict):
|
||||
continue
|
||||
flattened.update(sensor_values)
|
||||
return flattened
|
||||
|
||||
|
||||
def _surface_soil_metrics(depths) -> dict:
|
||||
if not depths:
|
||||
return {}
|
||||
|
||||
primary_depth = depths[0]
|
||||
fields = [
|
||||
"bdod",
|
||||
"cec",
|
||||
"cfvo",
|
||||
"clay",
|
||||
"nitrogen",
|
||||
"ocd",
|
||||
"ocs",
|
||||
"phh2o",
|
||||
"sand",
|
||||
"silt",
|
||||
"soc",
|
||||
"wv0010",
|
||||
"wv0033",
|
||||
"wv1500",
|
||||
]
|
||||
return {
|
||||
field: getattr(primary_depth, field)
|
||||
for field in fields
|
||||
if getattr(primary_depth, field) is not None
|
||||
}
|
||||
|
||||
|
||||
def _extract_boundary_points(boundary: dict | list) -> list:
|
||||
if isinstance(boundary, dict):
|
||||
if boundary.get("type") == "Polygon":
|
||||
coordinates = boundary.get("coordinates") or []
|
||||
if coordinates and isinstance(coordinates[0], list):
|
||||
return coordinates[0]
|
||||
return []
|
||||
if "corners" in boundary:
|
||||
return boundary.get("corners") or []
|
||||
if isinstance(boundary, list):
|
||||
return boundary
|
||||
return []
|
||||
|
||||
|
||||
def _normalize_points(points: list) -> list[tuple[Decimal, Decimal]]:
|
||||
normalized: list[tuple[Decimal, Decimal]] = []
|
||||
for point in points:
|
||||
lat = lon = None
|
||||
if isinstance(point, dict):
|
||||
lat = point.get("lat", point.get("latitude"))
|
||||
lon = point.get("lon", point.get("longitude"))
|
||||
elif isinstance(point, (list, tuple)) and len(point) >= 2:
|
||||
lon, lat = point[0], point[1]
|
||||
|
||||
if lat is None or lon is None:
|
||||
continue
|
||||
|
||||
lat_decimal = Decimal(str(lat))
|
||||
lon_decimal = Decimal(str(lon))
|
||||
normalized.append((lat_decimal, lon_decimal))
|
||||
|
||||
if len(normalized) > 1 and normalized[0] == normalized[-1]:
|
||||
normalized = normalized[:-1]
|
||||
return normalized
|
||||
|
||||
|
||||
def _serialize_boundary(boundary: dict | list) -> dict:
|
||||
if isinstance(boundary, dict) and boundary.get("type") == "Polygon":
|
||||
return boundary
|
||||
raw_points = boundary.get("corners") if isinstance(boundary, dict) else boundary
|
||||
normalized = _normalize_points(raw_points or [])
|
||||
coordinates = [[float(lon), float(lat)] for lat, lon in normalized]
|
||||
if coordinates and coordinates[0] != coordinates[-1]:
|
||||
coordinates.append(coordinates[0])
|
||||
return {
|
||||
"type": "Polygon",
|
||||
"coordinates": [coordinates],
|
||||
}
|
||||
Reference in New Issue
Block a user