This commit is contained in:
2026-04-25 17:22:41 +03:30
parent 569d520a5c
commit aa24fc22b0
124 changed files with 8491 additions and 2582 deletions
+114 -13
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
from decimal import Decimal, ROUND_HALF_UP
from numbers import Number
from django.db import transaction
@@ -49,13 +50,13 @@ def get_farm_details(farm_uuid: str):
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)
sensor_metrics, sensor_metric_sources = _resolve_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"
metric_sources[key] = sensor_metric_sources[key]
return {
"center_location": {
@@ -97,11 +98,7 @@ def resolve_center_location_from_boundary(farm_boundary: dict | list) -> SoilLoc
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)
center_lat, center_lon = _compute_polygon_centroid(normalized_points)
with transaction.atomic():
location, _ = SoilLocation.objects.update_or_create(
@@ -152,16 +149,83 @@ def ensure_location_and_weather_data(location: SoilLocation) -> tuple[SoilLocati
return location, weather_forecast
def _flatten_sensor_metrics(sensor_payload: dict | None) -> dict:
def _resolve_sensor_metrics(sensor_payload: dict | None) -> tuple[dict, dict]:
if not isinstance(sensor_payload, dict):
return {}
return {}, {}
flattened = {}
for sensor_values in sensor_payload.values():
readings_by_metric: dict[str, list[tuple[str, object]]] = {}
for sensor_key, sensor_values in sorted(sensor_payload.items()):
if not isinstance(sensor_values, dict):
continue
flattened.update(sensor_values)
return flattened
for metric_key, metric_value in sensor_values.items():
readings_by_metric.setdefault(metric_key, []).append((sensor_key, metric_value))
resolved_metrics = {}
metric_sources = {}
for metric_key, readings in readings_by_metric.items():
resolved_value, source = _resolve_metric_readings(readings)
resolved_metrics[metric_key] = resolved_value
metric_sources[metric_key] = source
return resolved_metrics, metric_sources
def _resolve_metric_readings(readings: list[tuple[str, object]]) -> tuple[object, dict[str, object]]:
if not readings:
return None, {"type": "sensor", "strategy": "empty", "sensor_keys": []}
sensor_keys = [sensor_key for sensor_key, _value in readings]
distinct_values: list[object] = []
for _sensor_key, value in readings:
if value not in distinct_values:
distinct_values.append(value)
if len(distinct_values) == 1:
return distinct_values[0], {
"type": "sensor",
"strategy": "single_value",
"sensor_keys": sensor_keys,
"sensor_count": len(sensor_keys),
}
numeric_values = [_coerce_numeric(value) for value in distinct_values]
if all(value is not None for value in numeric_values):
average = sum(numeric_values) / len(numeric_values)
resolved_value = _normalize_numeric_result(average, distinct_values)
return resolved_value, {
"type": "sensor",
"strategy": "average",
"sensor_keys": sensor_keys,
"sensor_count": len(sensor_keys),
"conflict": True,
"distinct_values": distinct_values,
}
return distinct_values, {
"type": "sensor",
"strategy": "distinct_values",
"sensor_keys": sensor_keys,
"sensor_count": len(sensor_keys),
"conflict": True,
"distinct_values": distinct_values,
}
def _coerce_numeric(value: object) -> float | None:
if isinstance(value, bool):
return None
if isinstance(value, Number):
return float(value)
try:
return float(value)
except (TypeError, ValueError):
return None
def _normalize_numeric_result(value: float, source_values: list[object]) -> int | float:
if all(isinstance(item, int) and not isinstance(item, bool) for item in source_values):
if value.is_integer():
return int(value)
return float(Decimal(str(value)).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP))
def _surface_soil_metrics(depths) -> dict:
@@ -240,3 +304,40 @@ def _serialize_boundary(boundary: dict | list) -> dict:
"type": "Polygon",
"coordinates": [coordinates],
}
def _compute_polygon_centroid(points: list[tuple[Decimal, Decimal]]) -> tuple[Decimal, Decimal]:
polygon = list(points)
if polygon[0] != polygon[-1]:
polygon.append(polygon[0])
twice_area = Decimal("0")
centroid_lon = Decimal("0")
centroid_lat = Decimal("0")
for index in range(len(polygon) - 1):
lat1, lon1 = polygon[index]
lat2, lon2 = polygon[index + 1]
cross = (lon1 * lat2) - (lon2 * lat1)
twice_area += cross
centroid_lon += (lon1 + lon2) * cross
centroid_lat += (lat1 + lat2) * cross
if twice_area == 0:
return _compute_average_center(points)
factor = Decimal("3") * twice_area
return (
(centroid_lat / factor).quantize(DECIMAL_PRECISION, rounding=ROUND_HALF_UP),
(centroid_lon / factor).quantize(DECIMAL_PRECISION, rounding=ROUND_HALF_UP),
)
def _compute_average_center(points: list[tuple[Decimal, Decimal]]) -> tuple[Decimal, Decimal]:
lat_sum = sum(lat for lat, _ in points)
lon_sum = sum(lon for _, lon in points)
count = Decimal(len(points))
return (
(lat_sum / count).quantize(DECIMAL_PRECISION, rounding=ROUND_HALF_UP),
(lon_sum / count).quantize(DECIMAL_PRECISION, rounding=ROUND_HALF_UP),
)