UPDATE
This commit is contained in:
+336
-1
@@ -1,4 +1,5 @@
|
||||
from datetime import timedelta
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
from django.apps import apps
|
||||
@@ -26,13 +27,16 @@ from .models import (
|
||||
AnalysisGridCell,
|
||||
AnalysisGridObservation,
|
||||
BlockSubdivision,
|
||||
RemoteSensingClusterBlock,
|
||||
RemoteSensingRun,
|
||||
RemoteSensingSubdivisionResult,
|
||||
RemoteSensingSubdivisionOption,
|
||||
SoilLocation,
|
||||
)
|
||||
from farm_data.models import SensorData
|
||||
|
||||
from .data_driven_subdivision import DEFAULT_CLUSTER_FEATURES
|
||||
from .data_driven_subdivision import activate_subdivision_option
|
||||
from .serializers import (
|
||||
BlockSubdivisionSerializer,
|
||||
NdviHealthRequestSerializer,
|
||||
@@ -42,12 +46,25 @@ from .serializers import (
|
||||
RemoteSensingFarmRequestSerializer,
|
||||
RemoteSensingRunSerializer,
|
||||
RemoteSensingRunStatusResponseSerializer,
|
||||
RemoteSensingClusterBlockLiveRequestSerializer,
|
||||
RemoteSensingClusterBlockLiveResponseSerializer,
|
||||
RemoteSensingClusterBlockSerializer,
|
||||
RemoteSensingSubdivisionOptionActivateResponseSerializer,
|
||||
RemoteSensingSubdivisionOptionActivateSerializer,
|
||||
RemoteSensingSubdivisionOptionListResponseSerializer,
|
||||
RemoteSensingSubdivisionOptionSerializer,
|
||||
RemoteSensingSummarySerializer,
|
||||
RemoteSensingSubdivisionResultSerializer,
|
||||
SoilDataRequestSerializer,
|
||||
SoilLocationResponseSerializer,
|
||||
)
|
||||
from .tasks import run_remote_sensing_analysis_task
|
||||
from .openeo_service import (
|
||||
OpenEOAuthenticationError,
|
||||
OpenEOExecutionError,
|
||||
OpenEOServiceError,
|
||||
compute_remote_sensing_metrics,
|
||||
)
|
||||
|
||||
MAX_REMOTE_SENSING_PAGE_SIZE = 200
|
||||
REMOTE_SENSING_RUN_STAGE_ORDER = (
|
||||
@@ -119,6 +136,18 @@ RemoteSensingRunStatusEnvelopeSerializer = build_envelope_serializer(
|
||||
"RemoteSensingRunStatusEnvelopeSerializer",
|
||||
RemoteSensingRunStatusResponseSerializer,
|
||||
)
|
||||
RemoteSensingClusterBlockLiveEnvelopeSerializer = build_envelope_serializer(
|
||||
"RemoteSensingClusterBlockLiveEnvelopeSerializer",
|
||||
RemoteSensingClusterBlockLiveResponseSerializer,
|
||||
)
|
||||
RemoteSensingSubdivisionOptionListEnvelopeSerializer = build_envelope_serializer(
|
||||
"RemoteSensingSubdivisionOptionListEnvelopeSerializer",
|
||||
RemoteSensingSubdivisionOptionListResponseSerializer,
|
||||
)
|
||||
RemoteSensingSubdivisionOptionActivateEnvelopeSerializer = build_envelope_serializer(
|
||||
"RemoteSensingSubdivisionOptionActivateEnvelopeSerializer",
|
||||
RemoteSensingSubdivisionOptionActivateResponseSerializer,
|
||||
)
|
||||
class SoilDataView(APIView):
|
||||
"""
|
||||
ثبت مختصات گوشههای مزرعه و بلوکهای تعریفشده توسط کشاورز.
|
||||
@@ -679,6 +708,256 @@ class RemoteSensingRunStatusView(APIView):
|
||||
)
|
||||
|
||||
|
||||
class RemoteSensingClusterBlockLiveView(APIView):
|
||||
@extend_schema(
|
||||
tags=["Location Data"],
|
||||
summary="دریافت زنده remote sensing برای زیربلاک KMeans",
|
||||
description="با دریافت UUID زیربلاک ساختهشده توسط KMeans، هندسه همان زیربلاک از دیتابیس خوانده میشود و داده تازه ماهوارهای از openEO برگردانده میشود.",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="cluster_uuid",
|
||||
type={"type": "string", "format": "uuid"},
|
||||
location=OpenApiParameter.PATH,
|
||||
required=True,
|
||||
description="شناسه UUID زیربلاک KMeans.",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="temporal_start",
|
||||
type={"type": "string", "format": "date"},
|
||||
location=OpenApiParameter.QUERY,
|
||||
required=False,
|
||||
description="شروع بازه سفارشی. اگر ست شود، temporal_end هم باید ارسال شود.",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="temporal_end",
|
||||
type={"type": "string", "format": "date"},
|
||||
location=OpenApiParameter.QUERY,
|
||||
required=False,
|
||||
description="پایان بازه سفارشی. اگر ست شود، temporal_start هم باید ارسال شود.",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="days",
|
||||
type=int,
|
||||
location=OpenApiParameter.QUERY,
|
||||
required=False,
|
||||
default=30,
|
||||
description="اگر بازه سفارشی ارسال نشود، از yesterday backfill با این تعداد روز استفاده میشود.",
|
||||
),
|
||||
],
|
||||
responses={
|
||||
200: build_response(
|
||||
RemoteSensingClusterBlockLiveEnvelopeSerializer,
|
||||
"داده زنده openEO برای زیربلاک KMeans بازگردانده شد.",
|
||||
),
|
||||
400: build_response(
|
||||
SoilErrorResponseSerializer,
|
||||
"پارامترهای ورودی یا هندسه زیربلاک نامعتبر است.",
|
||||
),
|
||||
404: build_response(
|
||||
SoilErrorResponseSerializer,
|
||||
"زیربلاک KMeans پیدا نشد.",
|
||||
),
|
||||
502: build_response(
|
||||
SoilErrorResponseSerializer,
|
||||
"خواندن داده از openEO ناموفق بود.",
|
||||
),
|
||||
},
|
||||
)
|
||||
def get(self, request, cluster_uuid):
|
||||
serializer = RemoteSensingClusterBlockLiveRequestSerializer(data=request.query_params)
|
||||
if not serializer.is_valid():
|
||||
return Response(
|
||||
{"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
cluster_block = (
|
||||
RemoteSensingClusterBlock.objects.select_related("soil_location", "block_subdivision", "result")
|
||||
.filter(uuid=cluster_uuid)
|
||||
.first()
|
||||
)
|
||||
if cluster_block is None:
|
||||
return Response(
|
||||
{"code": 404, "msg": "زیربلاک KMeans پیدا نشد.", "data": None},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
geometry = _resolve_cluster_block_geometry(cluster_block)
|
||||
if not geometry:
|
||||
return Response(
|
||||
{
|
||||
"code": 400,
|
||||
"msg": "هندسه زیربلاک KMeans نامعتبر است.",
|
||||
"data": {"cluster_uuid": [str(cluster_block.uuid)]},
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
temporal_start, temporal_end = _resolve_live_remote_sensing_window(serializer.validated_data)
|
||||
virtual_cell = _build_virtual_cluster_block_cell(cluster_block=cluster_block, geometry=geometry)
|
||||
try:
|
||||
remote_payload = compute_remote_sensing_metrics(
|
||||
[virtual_cell],
|
||||
temporal_start=temporal_start,
|
||||
temporal_end=temporal_end,
|
||||
selected_features=list(DEFAULT_CLUSTER_FEATURES),
|
||||
)
|
||||
except (OpenEOAuthenticationError, OpenEOExecutionError, OpenEOServiceError) as exc:
|
||||
return Response(
|
||||
{"code": 502, "msg": "خواندن داده از openEO ناموفق بود.", "data": {"detail": str(exc)}},
|
||||
status=status.HTTP_502_BAD_GATEWAY,
|
||||
)
|
||||
|
||||
metrics = dict(remote_payload.get("results", {}).get(virtual_cell.cell_code, {}) or {})
|
||||
response_payload = {
|
||||
"status": "success",
|
||||
"source": "openeo",
|
||||
"cluster_block": RemoteSensingClusterBlockSerializer(cluster_block).data,
|
||||
"temporal_extent": {
|
||||
"start_date": temporal_start.isoformat(),
|
||||
"end_date": temporal_end.isoformat(),
|
||||
},
|
||||
"selected_features": list(DEFAULT_CLUSTER_FEATURES),
|
||||
"summary": {
|
||||
"cell_count": int(cluster_block.cell_count or 0),
|
||||
"ndvi_mean": _round_or_none(metrics.get("ndvi")),
|
||||
"ndwi_mean": _round_or_none(metrics.get("ndwi")),
|
||||
"soil_vv_db_mean": _round_or_none(metrics.get("soil_vv_db")),
|
||||
},
|
||||
"metrics": {
|
||||
"ndvi": _round_or_none(metrics.get("ndvi")),
|
||||
"ndwi": _round_or_none(metrics.get("ndwi")),
|
||||
"soil_vv": _round_or_none(metrics.get("soil_vv")),
|
||||
"soil_vv_db": _round_or_none(metrics.get("soil_vv_db")),
|
||||
},
|
||||
"metadata": {
|
||||
**dict(remote_payload.get("metadata") or {}),
|
||||
"requested_cluster_uuid": str(cluster_block.uuid),
|
||||
"block_code": cluster_block.block_code,
|
||||
"sub_block_code": cluster_block.sub_block_code,
|
||||
},
|
||||
}
|
||||
return Response(
|
||||
{"code": 200, "msg": "success", "data": response_payload},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
class RemoteSensingSubdivisionOptionListView(APIView):
|
||||
@extend_schema(
|
||||
tags=["Location Data"],
|
||||
summary="فهرست همه گزینههای K ذخیرهشده برای یک subdivision result",
|
||||
description="همه ترکیبهای K که برای این run/result محاسبه و ذخیره شدهاند را برمیگرداند و مشخص میکند کدامیک active و کدامیک recommended است.",
|
||||
responses={
|
||||
200: build_response(
|
||||
RemoteSensingSubdivisionOptionListEnvelopeSerializer,
|
||||
"فهرست گزینههای K بازگردانده شد.",
|
||||
),
|
||||
404: build_response(
|
||||
SoilErrorResponseSerializer,
|
||||
"subdivision result موردنظر پیدا نشد.",
|
||||
),
|
||||
},
|
||||
)
|
||||
def get(self, request, result_id):
|
||||
result = (
|
||||
RemoteSensingSubdivisionResult.objects.filter(pk=result_id)
|
||||
.prefetch_related("options__cluster_blocks")
|
||||
.first()
|
||||
)
|
||||
if result is None:
|
||||
return Response(
|
||||
{"code": 404, "msg": "subdivision result پیدا نشد.", "data": None},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
options = list(result.options.all().order_by("requested_k"))
|
||||
response_payload = {
|
||||
"result_id": result.id,
|
||||
"active_requested_k": next((option.requested_k for option in options if option.is_active), None),
|
||||
"recommended_requested_k": next((option.requested_k for option in options if option.is_recommended), None),
|
||||
"options": RemoteSensingSubdivisionOptionSerializer(options, many=True).data,
|
||||
}
|
||||
return Response(
|
||||
{"code": 200, "msg": "success", "data": response_payload},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
class RemoteSensingSubdivisionOptionActivateView(APIView):
|
||||
@extend_schema(
|
||||
tags=["Location Data"],
|
||||
summary="فعالسازی یک K ذخیرهشده برای subdivision result",
|
||||
description="کاربر میتواند یکی از Kهای از قبل محاسبهشده و ذخیرهشده را انتخاب کند تا active شود و خروجی اصلی subdivision بر همان مبنا sync شود.",
|
||||
request=RemoteSensingSubdivisionOptionActivateSerializer,
|
||||
responses={
|
||||
200: build_response(
|
||||
RemoteSensingSubdivisionOptionActivateEnvelopeSerializer,
|
||||
"K انتخابی فعال شد.",
|
||||
),
|
||||
400: build_response(
|
||||
SoilErrorResponseSerializer,
|
||||
"درخواست نامعتبر است یا K انتخابی موجود نیست.",
|
||||
),
|
||||
404: build_response(
|
||||
SoilErrorResponseSerializer,
|
||||
"subdivision result موردنظر پیدا نشد.",
|
||||
),
|
||||
},
|
||||
)
|
||||
def post(self, request, result_id):
|
||||
serializer = RemoteSensingSubdivisionOptionActivateSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response(
|
||||
{"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
result = (
|
||||
RemoteSensingSubdivisionResult.objects.filter(pk=result_id)
|
||||
.select_related("soil_location", "block_subdivision")
|
||||
.prefetch_related("options__cluster_blocks", "options__assignments__cell")
|
||||
.first()
|
||||
)
|
||||
if result is None:
|
||||
return Response(
|
||||
{"code": 404, "msg": "subdivision result پیدا نشد.", "data": None},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
requested_k = serializer.validated_data["requested_k"]
|
||||
option = next(
|
||||
(candidate for candidate in result.options.all() if candidate.requested_k == requested_k),
|
||||
None,
|
||||
)
|
||||
if option is None:
|
||||
return Response(
|
||||
{
|
||||
"code": 400,
|
||||
"msg": "K انتخابی برای این subdivision result موجود نیست.",
|
||||
"data": {"requested_k": [requested_k]},
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
activate_subdivision_option(option=option, selection_source="user")
|
||||
result.refresh_from_db()
|
||||
result = (
|
||||
RemoteSensingSubdivisionResult.objects.filter(pk=result.pk)
|
||||
.prefetch_related("assignments__cell", "cluster_blocks", "options__cluster_blocks")
|
||||
.first()
|
||||
)
|
||||
response_payload = {
|
||||
"result_id": result.id,
|
||||
"activated_requested_k": requested_k,
|
||||
"subdivision_result": RemoteSensingSubdivisionResultSerializer(result).data,
|
||||
}
|
||||
return Response(
|
||||
{"code": 200, "msg": "success", "data": response_payload},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
def _build_remote_sensing_run_status_payload(run: RemoteSensingRun, *, page: int, page_size: int) -> dict:
|
||||
run_data = RemoteSensingRunSerializer(run).data
|
||||
task_id = (run.metadata or {}).get("task_id")
|
||||
@@ -1069,6 +1348,62 @@ def _resolve_chunk_size_for_location(location: SoilLocation, block_code: str) ->
|
||||
return 900
|
||||
|
||||
|
||||
def _resolve_live_remote_sensing_window(payload: dict[str, Any]):
|
||||
temporal_start = payload.get("temporal_start")
|
||||
temporal_end = payload.get("temporal_end")
|
||||
if temporal_start and temporal_end:
|
||||
return temporal_start, temporal_end
|
||||
days = int(payload.get("days") or 30)
|
||||
end_date = timezone.localdate() - timedelta(days=1)
|
||||
start_date = end_date - timedelta(days=days - 1)
|
||||
return start_date, end_date
|
||||
|
||||
|
||||
def _resolve_cluster_block_geometry(cluster_block: RemoteSensingClusterBlock) -> dict[str, Any]:
|
||||
geometry = dict(cluster_block.geometry or {})
|
||||
if geometry.get("type") and geometry.get("coordinates"):
|
||||
return geometry
|
||||
|
||||
cell_codes = list(cluster_block.cell_codes or [])
|
||||
if not cell_codes:
|
||||
return {}
|
||||
cell_geometries = list(
|
||||
AnalysisGridCell.objects.filter(
|
||||
soil_location=cluster_block.soil_location,
|
||||
cell_code__in=cell_codes,
|
||||
)
|
||||
.order_by("cell_code")
|
||||
.values_list("geometry", flat=True)
|
||||
)
|
||||
polygon_coordinates: list[list[list[list[float]]]] = []
|
||||
for cell_geometry in cell_geometries:
|
||||
cell_geometry = dict(cell_geometry or {})
|
||||
coordinates = cell_geometry.get("coordinates") or []
|
||||
if cell_geometry.get("type") == "Polygon" and coordinates:
|
||||
polygon_coordinates.append(coordinates)
|
||||
elif cell_geometry.get("type") == "MultiPolygon" and coordinates:
|
||||
polygon_coordinates.extend(coordinates)
|
||||
if not polygon_coordinates:
|
||||
return {}
|
||||
if len(polygon_coordinates) == 1:
|
||||
return {"type": "Polygon", "coordinates": polygon_coordinates[0]}
|
||||
return {"type": "MultiPolygon", "coordinates": polygon_coordinates}
|
||||
|
||||
|
||||
def _build_virtual_cluster_block_cell(
|
||||
*,
|
||||
cluster_block: RemoteSensingClusterBlock,
|
||||
geometry: dict[str, Any],
|
||||
):
|
||||
return SimpleNamespace(
|
||||
cell_code=f"cluster-{cluster_block.uuid}",
|
||||
block_code=cluster_block.block_code,
|
||||
soil_location_id=cluster_block.soil_location_id,
|
||||
chunk_size_sqm=cluster_block.chunk_size_sqm,
|
||||
geometry=geometry,
|
||||
)
|
||||
|
||||
|
||||
def _get_remote_sensing_observations(*, location, block_code: str, start_date, end_date):
|
||||
queryset = (
|
||||
AnalysisGridObservation.objects.select_related("cell", "run")
|
||||
@@ -1104,7 +1439,7 @@ def _get_remote_sensing_subdivision_result(*, location, block_code: str, start_d
|
||||
temporal_end=end_date,
|
||||
)
|
||||
.select_related("run")
|
||||
.prefetch_related("assignments__cell")
|
||||
.prefetch_related("assignments__cell", "cluster_blocks")
|
||||
.order_by("-created_at", "-id")
|
||||
.first()
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user