From fedd3630758133b563fbe0d71c8a61bd4b5bec0b Mon Sep 17 00:00:00 2001 From: Pepijn van Oort Date: Wed, 4 Oct 2023 17:21:05 +0200 Subject: [PATCH] in KPIApplication possible to (1) add an operation, e.g. applying fertilizer and (2) add a cropfield characteristic containing cropyield. --- FarmmapsApi/Constants.cs | 8 +- FarmmapsApi/Services/GeneralService.cs | 76 ++++++++++++- FarmmapsKPI/KPIApplication.cs | 144 ++++++++++++++++--------- FarmmapsKPI/KPIInput.json | 50 ++++++--- FarmmapsKPI/Models/KPIInput.cs | 14 ++- FarmmapsKPI/Models/Settings.cs | 6 +- 6 files changed, 221 insertions(+), 77 deletions(-) diff --git a/FarmmapsApi/Constants.cs b/FarmmapsApi/Constants.cs index 613dfde..6b37e24 100644 --- a/FarmmapsApi/Constants.cs +++ b/FarmmapsApi/Constants.cs @@ -12,9 +12,12 @@ namespace FarmmapsApiSamples public const string SHAPE_ITEMTYPE = "vnd.farmmaps.itemtype.shape"; public const string GEOJSON_ITEMTYPE = "vnd.farmmaps.itemtype.geojson"; public const string BLIGHT_ITEMTYPE = "vnd.farmmaps.itemtype.blight"; - public const string CROPREC_ITEMTYPE = "vnd.farmmaps.itemtype.crprec.operation"; + public const string CROPREC_ITEMTYPE = "vnd.farmmaps.itemtype.crprec"; + public const string CROPOP_ITEMTYPE = "vnd.farmmaps.itemtype.crprec.operation"; + public const string CROPCHAR_ITEMTYPE = "vnd.farmmaps.itemtype.edicrop.characteristic"; public const string CROPSCHEME_ITEMTYPE = "vnd.farmmaps.itemtype.croppingscheme"; // deze toegevoegd, misschien is het type van de KPI task wel een croppingscheme - public const string KPI_ITEM = "vnd.farmmaps.itemtype.kpi.data"; + //public const string KPI_ITEM = "vnd.farmmaps.itemtype.kpi.data"; //PO20231004: originally with .data + public const string KPI_ITEM = "vnd.farmmaps.itemtype.kpi.data.container"; //PO20231004: originally without .container public const string VRANBS_TASK = "vnd.farmmaps.task.vranbs"; public const string VRAHERBICIDE_TASK = "vnd.farmmaps.task.vraherbicide"; @@ -26,6 +29,7 @@ namespace FarmmapsApiSamples public const string TASKMAP_TASK = "vnd.farmmaps.task.taskmap"; public const string WORKFLOW_TASK = "vnd.farmmaps.task.workflow"; public const string BOFEK_TASK = "vnd.farmmaps.task.bofek"; + public const string CROPREC_TASK = "vnd.farmmaps.task.crprec"; public const string SHADOW_TASK = "vnd.farmmaps.task.shadow"; public const string AHN_TASK = "vnd.farmmaps.task.ahn"; public const string WATBAL_TASK = "vnd.farmmaps.task.watbal"; diff --git a/FarmmapsApi/Services/GeneralService.cs b/FarmmapsApi/Services/GeneralService.cs index c292508..a5184e8 100644 --- a/FarmmapsApi/Services/GeneralService.cs +++ b/FarmmapsApi/Services/GeneralService.cs @@ -37,6 +37,35 @@ namespace FarmmapsApi.Services return await _farmmapsApiService.CreateItemAsync(cropfieldItemRequest); } + public async Task CreateOperationItemAsync(string cropRecordingItemCode, string data = "{}") + { + JObject jdata = JObject.Parse(data); + string name = string.Format($"CrpRec Operation, {jdata.GetValue("name")}"); + + ItemRequest operationItemRequest = new ItemRequest() + { + ParentCode = cropRecordingItemCode, + ItemType = CROPOP_ITEMTYPE, + Name = name, + Data = jdata, + }; + + return await _farmmapsApiService.CreateItemAsync(operationItemRequest); + } + public async Task CreateCropfieldCharacteristicItemAsync(string cropfieldItemCode, string data = "{}") + { + string name = "Cropfield characteristic"; + + ItemRequest cropfieldCharactericsticItemRequest = new ItemRequest() + { + ParentCode = cropfieldItemCode, + ItemType = CROPCHAR_ITEMTYPE, + Name = name, + Data = JObject.Parse(data), + }; + + return await _farmmapsApiService.CreateItemAsync(cropfieldCharactericsticItemRequest); + } public async Task UploadDataAsync(UserRoot root, string itemType, string filePath, string itemName, string geoJsonString = null) { var startUpload = DateTime.UtcNow.AddSeconds(-3); @@ -247,6 +276,39 @@ namespace FarmmapsApi.Services return dataItem; } + public async Task RunCropRecordingTask(Item cropfieldItem) + { + var cropRecordingRequest = new TaskRequest { TaskType = CROPREC_TASK }; + + string itemTaskCode = await _farmmapsApiService.QueueTaskAsync(cropfieldItem.Code, cropRecordingRequest); + + await PollTask(TimeSpan.FromSeconds(5), async (tokenSource) => { + var itemTaskStatus = await _farmmapsApiService.GetTaskStatusAsync(cropfieldItem.Code, itemTaskCode); + _logger.LogInformation($"Waiting on RunCropRecordingTask; status: {itemTaskStatus.State}"); + if (itemTaskStatus.IsFinished) + tokenSource.Cancel(); + }); + + var itemTask = await _farmmapsApiService.GetTaskStatusAsync(cropfieldItem.Code, itemTaskCode); + if (itemTask.State == ItemTaskState.Error) + { + _logger.LogError($"Something went wrong with task execution: {itemTask.Message}"); + return null; + } + + //the CropRecording data is a child of the cropfield + var itemName = "Crprec"; + var cropRecordingItem = await FindChildItemAsync(cropfieldItem.Code, + CROPREC_ITEMTYPE, itemName); + if (cropRecordingItem == null) + { + _logger.LogError("Could not find the CropRecording data as a child item under the cropfield"); + return null; + } + + return cropRecordingItem; + } + public async Task RunBofekTask(Item cropfieldItem) { var taskmapRequest = new TaskRequest { TaskType = BOFEK_TASK }; @@ -279,11 +341,12 @@ namespace FarmmapsApi.Services public async Task> GetKpiItemsForCropField(Item cropfieldItem) // dit is dus nieuw om de KPI task te runnen { - var taskmapRequest = new TaskRequest { TaskType = KPI_TASK }; //hier KPI request van maken - taskmapRequest.attributes["processAggregateKpi"] = "false"; // zo de properties meegeven?, moet dit niet bij de KPIinput? - taskmapRequest.attributes["year"] = "2022"; + var kpiRequest = new TaskRequest { TaskType = KPI_TASK }; + kpiRequest.attributes["processAggregateKpi"] = "false"; + int year = cropfieldItem.DataDate.Value.Year; + kpiRequest.attributes["year"] = year.ToString(); - string itemTaskCode = await _farmmapsApiService.QueueTaskAsync(cropfieldItem.Code, taskmapRequest); + string itemTaskCode = await _farmmapsApiService.QueueTaskAsync(cropfieldItem.Code, kpiRequest); await PollTask(TimeSpan.FromSeconds(5), async (tokenSource) => { var itemTaskStatus = await _farmmapsApiService.GetTaskStatusAsync(cropfieldItem.Code, itemTaskCode); @@ -299,10 +362,13 @@ namespace FarmmapsApi.Services return null; } await Task.Delay(60000); //wacht hier een minuut tot de KPIs berekend zijn + //PO20230627 Je zou hier ook om de 10 sec eens kunnen kijken of we al zo ver zijn? Iets in trant van while KPI_ITEM is null? //hier nog definieren waar in de hierarchie een KPI item is? + var allChildren = await _farmmapsApiService.GetItemChildrenAsync(cropfieldItem.Code); + var kpiItems = await _farmmapsApiService.GetItemChildrenAsync(cropfieldItem.Code, KPI_ITEM); - return await _farmmapsApiService.GetItemChildrenAsync(cropfieldItem.Code, KPI_ITEM); + return kpiItems; } diff --git a/FarmmapsKPI/KPIApplication.cs b/FarmmapsKPI/KPIApplication.cs index 546e6a6..301a3e3 100644 --- a/FarmmapsKPI/KPIApplication.cs +++ b/FarmmapsKPI/KPIApplication.cs @@ -22,18 +22,18 @@ namespace FarmmapsKPI private readonly ILogger _logger; private readonly FarmmapsApiService _farmmapsApiService; - private readonly KPIService _dataDownloadService; + private readonly KPIService _kpiService; private readonly GeneralService _generalService; private Settings _settings; public KPIApplication(ILogger logger, FarmmapsApiService farmmapsApiService, - GeneralService generalService, KPIService dataDownloadService) + GeneralService generalService, KPIService kpiService) { _logger = logger; _farmmapsApiService = farmmapsApiService; _generalService = generalService; - _dataDownloadService = dataDownloadService; + _kpiService = kpiService; } public async Task RunAsync() @@ -70,6 +70,9 @@ namespace FarmmapsKPI // !!specify if you are using an already created cropfield: bool useCreatedCropfield = input.UseCreatedCropfield; + bool useCreatedCropRecording = input.UseCreatedCropRecording; + bool useCreatedOperations = input.UseCreatedOperation; + bool useCreatedCropfieldCharacteristic = input.UseCreatedCropfieldCharacteristic; var cropYear = input.CropYear; var fieldName = input.fieldName; //bool storeSatelliteStatistics = input.StoreSatelliteStatisticsSingleImage; @@ -98,63 +101,119 @@ namespace FarmmapsKPI // Use already created cropfield or create new one, added a Data input, with field specific data for the KPI calculation Item cropfieldItem; - //1 useCreatedCropfield = false -> get new + //1 useCreatedCropfield = false -> create new //2 useCreatedCropfield = true && CropfieldItemCode = "" or absent -> read from settings json - //2 useCreatedCropfield = true && CropfieldItemCode like "deb48a74c5b54299bb852f17288010e9" in KPIinput -> use this one - + //3 useCreatedCropfield = true && CropfieldItemCode like "deb48a74c5b54299bb852f17288010e9" in KPIinput -> use this one if (useCreatedCropfield == false || string.IsNullOrEmpty(_settings.CropfieldItemCode)) { _logger.LogInformation("Creating cropfield, writting to settings file"); cropfieldItem = await _generalService.CreateCropfieldItemAsync(myDriveRoot.Code, - $"DataCropfield {input.OutputFileName}", cropYear, input.GeometryJson.ToString(Formatting.None), input.Data.ToString(Formatting.None)); + $"{input.OutputFileName}", cropYear, input.GeometryJson.ToString(Formatting.None), input.DataCropfield.ToString(Formatting.None)); _settings.CropfieldItemCode = cropfieldItem.Code; SaveSettings(settingsfile); } else if (string.IsNullOrEmpty(input.CropfieldItemCode)) { - _logger.LogInformation("CropfieldItemCode not in json input, reading from settings json"); + _logger.LogInformation("reading CropfieldItemCode from settings file"); cropfieldItem = await _farmmapsApiService.GetItemAsync(_settings.CropfieldItemCode); } else { - _logger.LogInformation("CropfieldItemCode not in json input, reading from settings json"); + _logger.LogInformation("reading CropfieldItemCode from KPIinput.json"); cropfieldItem = await _farmmapsApiService.GetItemAsync(input.CropfieldItemCode); } - //Get croprecordings - if (input.GetCropRecordings) + + // Use already created croprecording or create new one, added a Data input, with field specific data for the KPI calculation + Item crprecItem; + //1 useCreatedCropRecording = false -> create new + //2 useCreatedCropRecording = true && CropRecordingItemCode = "" or absent -> read from settings json + //3 useCreatedCropRecording = true && CropRecordingItemCode like "deb48a74c5b54299bb852f17288010e9" in KPIinput -> use this one + if (useCreatedCropRecording == false || string.IsNullOrEmpty(_settings.CropRecordingItemCode)) { - var crprecItem = input.CrprecItem; - _logger.LogInformation($"Trying to get crop recordings of croprecording: {crprecItem}"); - - var cropRec = await _farmmapsApiService.GetItemChildrenAsync(crprecItem, CROPREC_ITEMTYPE); - if (cropRec == null) - { - _logger.LogError("Something went wrong while obtaining the croprecordings"); - return; - } - - var cropRecPath = Path.Combine(downloadFolder, $"croprecordings_{crprecItem}.json"); - _logger.LogInformation($"Found {cropRec.Count} crop recordings"); - var count1 = 0; - await Task.Delay(500); - foreach (var item in cropRec) - { - Console.WriteLine($"Crop recording #{count1}: {item.Name}"); - File.AppendAllText(cropRecPath, item.Data +Environment.NewLine); - count1++; - } - _logger.LogInformation($"Downloaded file {cropRecPath}"); + _logger.LogInformation("RunCropRecordingTask ..."); + crprecItem = await _generalService.RunCropRecordingTask(cropfieldItem); + _settings.CropRecordingItemCode = crprecItem.Code; + SaveSettings(settingsfile); + } + else if (string.IsNullOrEmpty(input.CropfieldItemCode)) + { + _logger.LogInformation("reading CropRecordingItemCode from settings file"); + crprecItem = await _farmmapsApiService.GetItemAsync(_settings.CropRecordingItemCode); } else { - //todo set croprecordings from file + _logger.LogInformation("reading CropRecordingItemCode from KPIinput.json"); + crprecItem = await _farmmapsApiService.GetItemAsync(input.CropRecordingItemCode); } + // Use already created operation or create new one, added a Data input, with field specific data for the KPI calculation + Item crpOperationItem; + //1 useCreatedOperation = false -> create new + //2 useCreatedOperation = true && OperationItemCode = "" or absent -> read from settings json + //3 useCreatedOperation = true && OperationItemCode like "deb48a74c5b54299bb852f17288010e9" in KPIinput -> use this one + if (useCreatedOperations == false || string.IsNullOrEmpty(_settings.OperationItemCode)) + { + _logger.LogInformation("CreateOperationItemAsync ..."); + crpOperationItem = await _generalService.CreateOperationItemAsync(crprecItem.Code,input.DataOperation.ToString(Formatting.None)); + _settings.OperationItemCode = crpOperationItem.Code; + SaveSettings(settingsfile); + } + else if (string.IsNullOrEmpty(input.CropfieldItemCode)) + { + _logger.LogInformation("reading OperationItemCode from settings file"); + crpOperationItem = await _farmmapsApiService.GetItemAsync(_settings.OperationItemCode); + } + else + { + _logger.LogInformation("reading OperationItemCode from KPIinput.json"); + crpOperationItem = await _farmmapsApiService.GetItemAsync(input.OperationItemCode); + } + + // The cropfieldCharacteristicItem is used to enter crop yields + // So once we have added an operation for fertilizer application and a crop yield, then KPIapp can calculate + // Nutrient balance. + // Use already created cropfieldCharacteristicItem or create new one, added a Data input, with field specific data for the KPI calculation + Item cropfieldCharacteristicItem; + //1 useCreatedCropfieldCharacteristic = false -> create new + //2 useCreatedCropfieldCharacteristic = true && CropfieldCharacteristicItemCode = "" or absent -> read from settings json + //3 useCreatedCropfieldCharacteristic = true && CropfieldCharacteristicItemCode like "deb48a74c5b54299bb852f17288010e9" in KPIinput -> use this one + if (useCreatedCropfieldCharacteristic == false) + { + _logger.LogInformation("CreateCropfieldCharacteristicItemAsync ..."); + cropfieldCharacteristicItem = await _generalService.CreateCropfieldCharacteristicItemAsync(cropfieldItem.Code, input.DataCropfieldCharacteristic.ToString(Formatting.None)); + _settings.CropfieldCharacteristicItemCode = cropfieldCharacteristicItem.Code; + SaveSettings(settingsfile); + } + else if (string.IsNullOrEmpty(input.CropfieldCharacteristicItemCode)) + { + _logger.LogInformation("reading OperationItemCode from settings file"); + cropfieldCharacteristicItem = await _farmmapsApiService.GetItemAsync(_settings.CropfieldCharacteristicItemCode); + } + else + { + _logger.LogInformation("reading CropfieldCharacteristicItemCode from KPIinput.json"); + cropfieldCharacteristicItem = await _farmmapsApiService.GetItemAsync(input.CropfieldCharacteristicItemCode); + } + + // Inspect the children. If all is well, cropfield will have one crprec and one edicrop.characteristic + // And the crprec will have 0-many operations as children + // And the Data of an operation will have specification of how much fertilizer was applied + // And crprec can have multiple operations + // Note existing cropfields and croprecordings keep existing properties you added in previous runs + // (unless you deleted them) + // So ech time you run with "UseCreatedCropfield": true & "UseCreatedCropRecording": false -> a new recording will be added to the existing cropfield + // So ech time you run with "UseCreatedCropfield": true & "useCreatedCropfieldCharacteristic": false -> a new CropfieldCharacteristic will be added to the existing cropfield + // So ech time you run with "UseCreatedCropRecording": true & "UseCreatedOperation": false -> a new operation will be added to the existing crop recording + var cropfieldChildren = await _farmmapsApiService.GetItemChildrenAsync(cropfieldItem.Code); + var crprecChildren = await _farmmapsApiService.GetItemChildrenAsync(crprecItem.Code); + + //Now get the KPIs for this cropfield, mounted with operations & cropyield // Get KPI data for saving it in a file, here the generalsedrvice is called to get the KPI data - _logger.LogInformation($"Trying to get the cropfielditem: {cropfieldItem.Code}"); + _logger.LogInformation($"GetKpiItemsForCropField({cropfieldItem.Code})"); var KPIItem = await _generalService.GetKpiItemsForCropField(cropfieldItem); + //Download KPI's into a json output file for this specific cropfield (with unique cropfieldItem.Code) var KPIItemPath = Path.Combine(downloadFolder, $"KPIItems_{cropfieldItem.Code}.json"); _logger.LogInformation($"Found {KPIItem.Count} KPI items"); var count = 0; @@ -167,23 +226,6 @@ namespace FarmmapsKPI } _logger.LogInformation($"Downloaded file {KPIItemPath}"); - //////////// - //_logger.LogInformation("Calculate KPI map for field"); - //var KPIItem = await _generalService.GetKpiItemsForCropField(cropfieldItem);// hier dus verwijzen naar de KPI task - //if ((object)null == null) - //{ - // _logger.LogError("Something went wrong while obtaining the KPI map"); - // return; - // } - - //_logger.LogInformation("Downloading KPI map"); - //await _farmmapsApiService.DownloadItemAsync(KPIItem.Code, - // Path.Combine(downloadFolder, $"{input.OutputFileName}_KPI.zip")); - - - - - } // Functions to save previously created cropfields diff --git a/FarmmapsKPI/KPIInput.json b/FarmmapsKPI/KPIInput.json index d9aeb63..d76e98e 100644 --- a/FarmmapsKPI/KPIInput.json +++ b/FarmmapsKPI/KPIInput.json @@ -1,16 +1,8 @@ [ { "UseCreatedCropfield": true, - "CropfieldItemCode": "38870d5ff9b54b12877b5b01018b6fec", - "outputFileName": "TestData", - //"fieldName": "aardappelveld_test", - //"DownloadFolder": "Downloads", //"C:\\workdir\\groenmonitor\\", // "Downloads", -> if you just put "Downloads" the program will download to somewhere in ..\FarmMapsApiClient_WURtest\FarmmapsDataDownload\bin\Debug\netcoreapp3.1\Downloads\ - "GetCropRecordings": false, - //cropfield code. toevoegen - "CropYear": 2022, - "dataDate": "2022-04-15T00:00:00Z", - "dataEndDate": "2022-09-15T00:00:00Z", - "data": { + "CropfieldItemCode": "", + "dataCropfield": { //"area": 4.22, "final": true, //"soilCode": "5", @@ -22,6 +14,39 @@ "productionPurposeCode": "003" //"productionPurposeName": "consumption" }, + "UseCreatedCropRecording": true, + "CropRecordingItemCode": "", + "dataCropRecording": {}, //not yet used + "UseCreatedOperation": true, + "OperationItemCode": "", + "dataOperation": { + "n": "92", + "to": "2022-05-23T12:34:00", + "area": "0.08", //?! + "from": "2022-05-23T11:34:00", + "name": "Kunstmest strooien", + "unit": "kg/ha", + "method": "70400", + "status": "3", + "product": "7360", + "quantity": "200", + "unitCode": "KGMHAR", //?! + "contractor": false, + "designator": "Kunstmest strooien", + "operationCode": "7" + }, + "UseCreatedCropfieldCharacteristic": false, + "CropfieldCharacteristicItemCode": "", + "DataCropfieldCharacteristic": { + "code": "860619", //PO20231004: so what does this code mean? Can we see the code list somewhere? + "label": "cropyield", + "value": "48.01" + }, + //"DownloadFolder": "Downloads", //"C:\\workdir\\groenmonitor\\", // "Downloads", -> if you just put "Downloads" the program will download to somewhere in ..\FarmMapsApiClient_WURtest\FarmmapsDataDownload\bin\Debug\netcoreapp3.1\Downloads\ + "CropYear": 2022, + "dataDate": "2022-04-15T00:00:00Z", + "dataEndDate": "2022-09-15T00:00:00Z", + "fieldName": "aardappelveld_test", //"ProcessAggegrateKpi": "false", // toegevoegd, want alleen veld level KPIs voor nu. "geometryJson": { "type": "Polygon", @@ -49,8 +74,7 @@ ] ] ] - } + }, + "outputFileName": "TestData" } - - ] \ No newline at end of file diff --git a/FarmmapsKPI/Models/KPIInput.cs b/FarmmapsKPI/Models/KPIInput.cs index c48a808..fb832fb 100644 --- a/FarmmapsKPI/Models/KPIInput.cs +++ b/FarmmapsKPI/Models/KPIInput.cs @@ -7,18 +7,26 @@ namespace FarmmapsKPI.Models { public bool UseCreatedCropfield { get; set; } public string CropfieldItemCode { get; set; } + public JObject DataCropfield { get; set; } + public bool UseCreatedCropRecording { get; set; } + public string CropRecordingItemCode { get; set; } + public JObject DataCropRecording { get; set; } + public bool UseCreatedOperation { get; set; } + public string OperationItemCode { get; set; } + public JObject DataOperation { get; set; } + public bool UseCreatedCropfieldCharacteristic { get; set; } + public string CropfieldCharacteristicItemCode { get; set; } + public JObject DataCropfieldCharacteristic { get; set; } + public string File { get; set; } public string InputVariable { get; set; } public string OutputFileName { get; set; } public string DownloadFolder { get; set; } public int CropYear { get; set; } public JObject GeometryJson { get; set; } - public JObject Data { get; set; } public string InputLayerName { get; set; } public string fieldName { get; set; } public bool GetShadowData { get; set; } - public bool GetCropRecordings { get; set; } - public string CrprecItem { get; set; } } } \ No newline at end of file diff --git a/FarmmapsKPI/Models/Settings.cs b/FarmmapsKPI/Models/Settings.cs index bd7ff29..f6ed630 100644 --- a/FarmmapsKPI/Models/Settings.cs +++ b/FarmmapsKPI/Models/Settings.cs @@ -3,9 +3,9 @@ public class Settings { public string CropfieldItemCode { get; set; } - //public string SatelliteTaskCode { get; set; } - //public string VanDerSatTaskCode { get; set; } - //public string WatBalTaskCode { get; set; } + public string CropRecordingItemCode { get; set; } + public string OperationItemCode { get; set; } + public string CropfieldCharacteristicItemCode { get; set; } } } \ No newline at end of file