From d546edaa0d3f7eb1687ae204f66da6cd517611d9 Mon Sep 17 00:00:00 2001 From: Mark van der Wal Date: Wed, 25 Mar 2020 17:31:42 +0100 Subject: [PATCH] Initial resumable file upload. Other changes. --- FarmmapsApi/FarmmapsApi.csproj | 2 + FarmmapsApi/Models/Configuration.cs | 1 + FarmmapsApi/Models/FileRequest.cs | 45 +++++ FarmmapsApi/Models/FileResponse.cs | 17 ++ FarmmapsApi/Services/FarmmapsApiService.cs | 195 ++++++++++++++----- FarmmapsApi/Services/FarmmapsUploader.cs | 160 +++++++++++++++ FarmmapsApiSamples/Data/Belgie.zip | Bin 0 -> 1722 bytes FarmmapsApiSamples/Data/Scan_1_20190605.zip | Bin 0 -> 43368 bytes FarmmapsApiSamples/FarmmapsApiSamples.csproj | 3 + FarmmapsApiSamples/NbsApp.cs | 104 ++++++++-- FarmmapsApiSamples/appsettings.json | 3 +- 11 files changed, 462 insertions(+), 68 deletions(-) create mode 100644 FarmmapsApi/Models/FileRequest.cs create mode 100644 FarmmapsApi/Models/FileResponse.cs create mode 100644 FarmmapsApi/Services/FarmmapsUploader.cs create mode 100644 FarmmapsApiSamples/Data/Belgie.zip create mode 100644 FarmmapsApiSamples/Data/Scan_1_20190605.zip diff --git a/FarmmapsApi/FarmmapsApi.csproj b/FarmmapsApi/FarmmapsApi.csproj index 8719220..8f133f8 100644 --- a/FarmmapsApi/FarmmapsApi.csproj +++ b/FarmmapsApi/FarmmapsApi.csproj @@ -5,6 +5,7 @@ + @@ -12,6 +13,7 @@ + diff --git a/FarmmapsApi/Models/Configuration.cs b/FarmmapsApi/Models/Configuration.cs index 72ec057..ee29b7e 100644 --- a/FarmmapsApi/Models/Configuration.cs +++ b/FarmmapsApi/Models/Configuration.cs @@ -4,6 +4,7 @@ { public string Authority { get; set; } public string Endpoint { get; set; } + public string BasePath { get; set; } public string DiscoveryEndpointUrl { get; set; } public string RedirectUri { get; set; } public string ClientId { get; set; } diff --git a/FarmmapsApi/Models/FileRequest.cs b/FarmmapsApi/Models/FileRequest.cs new file mode 100644 index 0000000..0e04088 --- /dev/null +++ b/FarmmapsApi/Models/FileRequest.cs @@ -0,0 +1,45 @@ +using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json.Linq; + +namespace FarmmapsApi.Models +{ + public class FileRequest + { + /// + /// Code of the parent + /// + /// 41971e7ea8a446069a817e66b608dcae + public string ParentCode { get; set; } + + /// + /// Geometry meta data + /// + /// {"type": "Point","coordinates": [5.27, 52.10]} + public JObject Geometry { get; set; } + + /// + /// Name of the file to upload + /// + /// MyFile.tiff + [Required] + public string Name { get; set; } + + /// + /// Size of the file to upload + /// + /// 67351 + [Required] + public long Size { get; set; } + + /// + /// If a chunked upload, the size of a single chunk + /// + /// 1048576 + public long ChunkSize { get; set; } + + /// + /// Optional data for the item coupled with the file + /// + public JObject Data { get; set; } + } +} \ No newline at end of file diff --git a/FarmmapsApi/Models/FileResponse.cs b/FarmmapsApi/Models/FileResponse.cs new file mode 100644 index 0000000..4bd7cc5 --- /dev/null +++ b/FarmmapsApi/Models/FileResponse.cs @@ -0,0 +1,17 @@ +namespace FarmmapsApi.Models +{ + public class FileResponse : FileRequest + { + /// + /// Code created for the registered file, use this in subsequent calls + /// + /// a00fbd18320742c787f99f952aef0dbb + public string Code { get; set; } + + /// + /// If a chunked upload, the number of chunks to upload + /// + /// 1 + public long Chunks { get; set; } + } +} \ No newline at end of file diff --git a/FarmmapsApi/Services/FarmmapsApiService.cs b/FarmmapsApi/Services/FarmmapsApiService.cs index b997c26..6d33957 100644 --- a/FarmmapsApi/Services/FarmmapsApiService.cs +++ b/FarmmapsApi/Services/FarmmapsApiService.cs @@ -10,30 +10,31 @@ using System.Text; using System.Threading.Tasks; using System.Web; using FarmmapsApi.Models; +using Google.Apis.Http; +using Google.Apis.Upload; using IdentityModel; using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Winista.Mime; namespace FarmmapsApi.Services { public class FarmmapsApiService { - private readonly ILogger _logger; private readonly HttpClient _httpClient; private readonly OpenIdConnectService _openIdConnectService; private readonly Configuration _configuration; - + private HubConnection _hubConnection; public event Action EventCallback; - public FarmmapsApiService(HttpClient httpClient, ILogger logger, + public FarmmapsApiService(HttpClient httpClient, OpenIdConnectService openIdConnectService, Configuration configuration) { - _logger = logger; _httpClient = httpClient; _openIdConnectService = openIdConnectService; _configuration = configuration; @@ -65,10 +66,9 @@ namespace FarmmapsApi.Services private async Task StartEventHub(string accessToken) { - var uri = new Uri(_configuration.Endpoint); - var eventEndpoint = $"{uri.Scheme}://{uri.Host}:{uri.Port}/EventHub"; + var eventEndpoint = $"{_configuration.Endpoint}/EventHub"; _hubConnection = new HubConnectionBuilder() - .WithUrl(eventEndpoint, HttpTransportType.WebSockets, + .WithUrl(eventEndpoint, HttpTransportType.WebSockets, options => options.SkipNegotiation = true) .ConfigureLogging(log => log.AddConsole()) .WithAutomaticReconnect() @@ -76,9 +76,9 @@ namespace FarmmapsApi.Services await _hubConnection.StartAsync(); await AuthenticateEventHub(accessToken); - + _hubConnection.Reconnected += async s => await AuthenticateEventHub(accessToken); - _hubConnection.On("event", (Object @event) => _logger.LogInformation(@event.ToString())); + _hubConnection.On("event", (EventMessage eventMessage) => EventCallback?.Invoke(eventMessage)); } private async Task AuthenticateEventHub(string accessToken) @@ -88,7 +88,8 @@ namespace FarmmapsApi.Services public async Task GetCurrentUserCodeAsync() { - var response = await _httpClient.GetAsync(ResourceEndpoints.CURRENTUSER_RESOURCE); + var url = $"{_configuration.BasePath}/{ResourceEndpoints.CURRENTUSER_RESOURCE}"; + var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) throw new Exception(response.ReasonPhrase); @@ -100,7 +101,8 @@ namespace FarmmapsApi.Services public async Task> GetCurrentUserRootsAsync() { - var response = await _httpClient.GetAsync(ResourceEndpoints.MYROOTS_RESOURCE); + var url = $"{_configuration.BasePath}/{ResourceEndpoints.MYROOTS_RESOURCE}"; + var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) throw new Exception(response.ReasonPhrase); @@ -109,7 +111,7 @@ namespace FarmmapsApi.Services return JsonConvert.DeserializeObject>(jsonString); } - + public async Task GetItemAsync(string itemCode, string itemType = null, JObject dataFilter = null) { if (dataFilter == null) @@ -118,28 +120,29 @@ namespace FarmmapsApi.Services var items = await GetItemsAsync(itemCode, itemType, dataFilter); return items[0]; } - + public async Task> GetItemsAsync(string itemCode, string itemType = null, JObject dataFilter = null) { - var resourceUri = string.Format(ResourceEndpoints.ITEMS_RESOURCE, itemCode); + var url = $"{_configuration.BasePath}/{ResourceEndpoints.ITEMS_RESOURCE}"; + var resourceUri = string.Format(url, itemCode); var queryString = HttpUtility.ParseQueryString(string.Empty); - - if(itemType != null) + + if (itemType != null) queryString["it"] = itemType; - - if(dataFilter != null) - queryString["df"] = dataFilter.ToString(Formatting.None); + + if (dataFilter != null) + queryString["df"] = dataFilter.ToString(Formatting.None); resourceUri = (queryString.Count > 0 ? $"{resourceUri}?" : resourceUri) + queryString; - + var response = await _httpClient.GetAsync(resourceUri); if (!response.IsSuccessStatusCode) { if (response.StatusCode == HttpStatusCode.NotFound) return null; - + throw new Exception(response.ReasonPhrase); } @@ -149,36 +152,39 @@ namespace FarmmapsApi.Services private async Task GetItemSingleAsync(string itemCode, string itemType = null) { - var resourceUri = string.Format(ResourceEndpoints.ITEMS_RESOURCE, itemCode); + var url = $"{_configuration.BasePath}/{ResourceEndpoints.ITEMS_RESOURCE}"; + var resourceUri = string.Format(url, itemCode); if (!string.IsNullOrEmpty(itemType)) resourceUri = $"{resourceUri}/{itemType}"; - + var response = await _httpClient.GetAsync(resourceUri); - + if (!response.IsSuccessStatusCode) { if (response.StatusCode == HttpStatusCode.NotFound) return null; - + throw new Exception(response.ReasonPhrase); } var jsonString = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(jsonString); } - - public async Task> GetItemChildrenAsync(string itemCode, string itemType = null, JObject dataFilter = null) + + public async Task> GetItemChildrenAsync(string itemCode, string itemType = null, + JObject dataFilter = null) { - var resourceUri = string.Format(ResourceEndpoints.ITEMS_CHILDREN_RESOURCE, itemCode); - + var url = $"{_configuration.BasePath}/{ResourceEndpoints.ITEMS_CHILDREN_RESOURCE}"; + var resourceUri = string.Format(url, itemCode); + var queryString = HttpUtility.ParseQueryString(string.Empty); - - if(itemType != null) + + if (itemType != null) queryString["it"] = itemType; - - if(dataFilter != null) - queryString["df"] = dataFilter.ToString(Formatting.None); + + if (dataFilter != null) + queryString["df"] = dataFilter.ToString(Formatting.None); resourceUri = (queryString.Count > 0 ? $"{resourceUri}?" : resourceUri) + queryString; @@ -195,31 +201,34 @@ namespace FarmmapsApi.Services { var jsonString = JsonConvert.SerializeObject(itemRequest); - var content = new StringContent(jsonString, Encoding.UTF8,MediaTypeNames.Application.Json); - var response = await _httpClient.PostAsync(ResourceEndpoints.ITEMS_CREATE_RESOURCE, + var url = $"{_configuration.BasePath}/{ResourceEndpoints.ITEMS_CREATE_RESOURCE}"; + var content = new StringContent(jsonString, Encoding.UTF8, MediaTypeNames.Application.Json); + var response = await _httpClient.PostAsync(url, content); if (!response.IsSuccessStatusCode) throw new Exception(response.ReasonPhrase); - + var jsonStringResponse = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(jsonStringResponse); } public async Task DeleteItemAsync(string itemCode) { - var resourceUri = string.Format(ResourceEndpoints.ITEMS_RESOURCE, itemCode); + var url = $"{_configuration.BasePath}/{ResourceEndpoints.ITEMS_RESOURCE}"; + var resourceUri = string.Format(url, itemCode); var response = await _httpClient.DeleteAsync(resourceUri); if (!response.IsSuccessStatusCode) throw new Exception(response.ReasonPhrase); } - + public async Task DeleteItemsAsync(IList itemCodes) { var jsonString = JsonConvert.SerializeObject(itemCodes); - var content = new StringContent(jsonString); - var response = await _httpClient.PostAsync(ResourceEndpoints.ITEMS_DELETE_RESOURCE, content); + var content = new StringContent(jsonString); + var url = $"{_configuration.BasePath}/{ResourceEndpoints.ITEMS_DELETE_RESOURCE}"; + var response = await _httpClient.PostAsync(url, content); if (!response.IsSuccessStatusCode) throw new Exception(response.ReasonPhrase); @@ -227,10 +236,11 @@ namespace FarmmapsApi.Services public async Task DownloadItemAsync(string itemCode, string filePath) { - var resourceUri = string.Format(ResourceEndpoints.ITEMS_DOWNLOAD_RESOURCE, itemCode); + var url = $"{_configuration.BasePath}/{ResourceEndpoints.ITEMS_DOWNLOAD_RESOURCE}"; + var resourceUri = string.Format(url, itemCode); var response = await _httpClient.GetAsync(resourceUri, HttpCompletionOption.ResponseHeadersRead); - - if(response.IsSuccessStatusCode) + + if (response.IsSuccessStatusCode) { await using Stream streamToReadFrom = await response.Content.ReadAsStreamAsync(); await using Stream streamToWriteTo = File.Open(filePath, FileMode.Create); @@ -238,30 +248,32 @@ namespace FarmmapsApi.Services } else { - throw new FileNotFoundException(response.ReasonPhrase); + throw new FileNotFoundException(response.ReasonPhrase); } } public async Task QueueTaskAsync(string itemCode, TaskRequest taskRequest) { - var resourceUri = string.Format(ResourceEndpoints.ITEMTASK_REQUEST_RESOURCE, itemCode); + var url = $"{_configuration.BasePath}/{ResourceEndpoints.ITEMTASK_REQUEST_RESOURCE}"; + var resourceUri = string.Format(url, itemCode); var jsonString = JsonConvert.SerializeObject(taskRequest); - var content = new StringContent(jsonString, Encoding.UTF8,MediaTypeNames.Application.Json); + var content = new StringContent(jsonString, Encoding.UTF8, MediaTypeNames.Application.Json); var response = await _httpClient.PostAsync(resourceUri, content); if (!response.IsSuccessStatusCode) throw new Exception(response.ReasonPhrase); - + var jsonStringResponse = await response.Content.ReadAsStringAsync(); var json = JsonConvert.DeserializeObject(jsonStringResponse); - + return json["code"].Value(); } public async Task> GetTasksStatusAsync(string itemCode) { - var resourceUri = string.Format(ResourceEndpoints.ITEMTASKS_RESOURCE, itemCode); + var url = $"{_configuration.BasePath}/{ResourceEndpoints.ITEMTASKS_RESOURCE}"; + var resourceUri = string.Format(url, itemCode); var response = await _httpClient.GetAsync(resourceUri); if (!response.IsSuccessStatusCode) @@ -273,7 +285,8 @@ namespace FarmmapsApi.Services public async Task GetTaskStatusAsync(string itemCode, string itemTaskCode) { - var resourceUri = string.Format(ResourceEndpoints.ITEMTASK_RESOURCE, itemCode, itemTaskCode); + var url = $"{_configuration.BasePath}/{ResourceEndpoints.ITEMTASK_RESOURCE}"; + var resourceUri = string.Format(url, itemCode, itemTaskCode); var response = await _httpClient.GetAsync(resourceUri); if (!response.IsSuccessStatusCode) @@ -282,5 +295,85 @@ namespace FarmmapsApi.Services var jsonString = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(jsonString); } + + /// + /// Experimental + /// + /// + /// + /// + /// + /// + public async Task UploadFile(string filePath, string parentItemCode, Action uploadCallback) + { + if (!File.Exists(filePath)) + throw new FileNotFoundException($"File not found {filePath}"); + + var mimeTypes = new MimeTypes(); + var mimeType = mimeTypes.GetMimeTypeFromFile(filePath); + + await using var uploadStream = new FileStream(filePath, FileMode.OpenOrCreate); + + var request = new FileRequest() + { + Name = Path.GetFileName(filePath), + ParentCode = parentItemCode, + Size = uploadStream.Length + }; + + Uri uploadIdentifierUri = null; + using var httpClient = CreateConfigurableHttpClient(_httpClient); + var farmmapsUploader = new FarmmapsUploader(httpClient, uploadStream, request, mimeType.ToString()); + + farmmapsUploader.ProgressChanged += uploadCallback; + farmmapsUploader.UploadSessionData += data => uploadIdentifierUri = data.UploadUri; + + await farmmapsUploader.UploadAsync(); + + return uploadIdentifierUri; + } + + /// + /// Experimental + /// + /// + /// + /// + /// + /// + private async Task ResumeUploadFile(string filePath, Uri location, Action uploadCallback) + { + if (!File.Exists(filePath)) + throw new FileNotFoundException($"File not found {filePath}"); + + await using var uploadStream = new FileStream(filePath, FileMode.OpenOrCreate); + + var request = new FileRequest() + { + Name = Path.GetFileName(filePath), + ParentCode = string.Empty, + Size = uploadStream.Length + }; + + using var httpClient = CreateConfigurableHttpClient(_httpClient); + var farmmapsUploader = new FarmmapsUploader(httpClient, uploadStream, request, string.Empty); + farmmapsUploader.ProgressChanged += uploadCallback; + + await farmmapsUploader.ResumeAsync(location); + + return location; + } + + private ConfigurableHttpClient CreateConfigurableHttpClient(HttpClient parent) + { + var googleHttpClient = new HttpClientFactory().CreateHttpClient(new CreateHttpClientArgs {GZipEnabled = true, ApplicationName = "FarmMaps"}); + googleHttpClient.BaseAddress = parent.BaseAddress; + googleHttpClient.DefaultRequestHeaders.Add("Accept", MediaTypeNames.Application.Json); + + var authHeader = parent.DefaultRequestHeaders.Authorization; + googleHttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authHeader.Scheme, authHeader.Parameter); + + return googleHttpClient; + } } } \ No newline at end of file diff --git a/FarmmapsApi/Services/FarmmapsUploader.cs b/FarmmapsApi/Services/FarmmapsUploader.cs new file mode 100644 index 0000000..0bd2945 --- /dev/null +++ b/FarmmapsApi/Services/FarmmapsUploader.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections; +using System.IO; +using System.Net.Http; +using System.Net.Mime; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using FarmmapsApi.Models; +using Google.Apis.Http; +using Google.Apis.Json; +using Google.Apis.Requests; +using Google.Apis.Upload; +using Google.Apis.Util; +using Newtonsoft.Json; + +namespace FarmmapsApi.Services +{ + public class FarmmapsUploader : ResumableUpload + { + /// Payload description headers, describing the content itself. + private const string PayloadContentTypeHeader = "X-Upload-Content-Type"; + + /// Payload description headers, describing the content itself. + private const string PayloadContentLengthHeader = "X-Upload-Content-Length"; + + private const int UnknownSize = -1; + + /// Gets or sets the service. + private HttpClient HttpClient { get; } + + /// + /// Gets or sets the path of the method (combined with + /// ) to produce + /// absolute Uri. + /// + private string Path { get; } + + /// Gets or sets the HTTP method of this upload (used to initialize the upload). + private string HttpMethod { get; } + + /// Gets or sets the stream's Content-Type. + private string ContentType { get; } + + /// Gets or sets the body of this request. + private FileRequest Body { get; } + + private long _streamLength; + + /// + /// Create a resumable upload instance with the required parameters. + /// + /// The stream containing the content to upload. + /// + /// Caller is responsible for maintaining the open until the upload is + /// completed. + /// Caller is responsible for closing the . + /// + public FarmmapsUploader(ConfigurableHttpClient httpClient, Stream contentStream, FileRequest body, string contentType) + : base(contentStream, + new ResumableUploadOptions + { + HttpClient = httpClient, + Serializer = new NewtonsoftJsonSerializer(), + ServiceName = "FarmMaps" + }) + { + httpClient.ThrowIfNull(nameof(httpClient)); + contentStream.ThrowIfNull(nameof(contentStream)); + + _streamLength = ContentStream.CanSeek ? ContentStream.Length : UnknownSize; + Body = body; + Path = "api/v1/file"; + HttpClient = httpClient; + HttpMethod = HttpConsts.Post; + ContentType = contentType; + } + + /// + public override async Task InitiateSessionAsync(CancellationToken cancellationToken = default) + { + HttpRequestMessage request = CreateInitializeRequest(); + Options?.ModifySessionInitiationRequest?.Invoke(request); + var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + + if (!response.IsSuccessStatusCode) + { + throw await ExceptionForResponseAsync(response).ConfigureAwait(false); + } + return response.Headers.Location; + } + + /// Creates a request to initialize a request. + private HttpRequestMessage CreateInitializeRequest() + { + var baseAddres = HttpClient.BaseAddress; + var uri = new Uri($"{baseAddres.Scheme}://{baseAddres.Host}:{baseAddres.Port}"); + var builder = new RequestBuilder() + { + BaseUri = uri, + Path = Path, + Method = HttpMethod, + }; + + SetAllPropertyValues(builder); + + HttpRequestMessage request = builder.CreateRequest(); + if (ContentType != null) + { + request.Headers.Add(PayloadContentTypeHeader, ContentType); + } + + // if the length is unknown at the time of this request, omit "X-Upload-Content-Length" header + if (ContentStream.CanSeek) + { + request.Headers.Add(PayloadContentLengthHeader, _streamLength.ToString()); + } + + var jsonText = JsonConvert.SerializeObject(Body); + request.Content = new StringContent(jsonText, Encoding.UTF8, MediaTypeNames.Application.Json); + + return request; + } + + /// + /// Reflectively enumerate the properties of this object looking for all properties containing the + /// RequestParameterAttribute and copy their values into the request builder. + /// + private void SetAllPropertyValues(RequestBuilder requestBuilder) + { + Type myType = this.GetType(); + var properties = myType.GetProperties(); + + foreach (var property in properties) + { + var attribute = property.GetCustomAttribute(); + if (attribute != null) + { + string name = attribute.Name ?? property.Name.ToLower(); + object value = property.GetValue(this, null); + if (value != null) + { + if (!(value is string) && value is IEnumerable valueAsEnumerable) + { + foreach (var elem in valueAsEnumerable) + { + requestBuilder.AddParameter(attribute.Type, name, Utilities.ConvertToString(elem)); + } + } + else + { + // Otherwise just convert it to a string. + requestBuilder.AddParameter(attribute.Type, name, Utilities.ConvertToString(value)); + } + } + } + } + } + } +} \ No newline at end of file diff --git a/FarmmapsApiSamples/Data/Belgie.zip b/FarmmapsApiSamples/Data/Belgie.zip new file mode 100644 index 0000000000000000000000000000000000000000..864aa27412e6a5ce9bb1da9f0d976f7464ca59d5 GIT binary patch literal 1722 zcmZ{kc{CJS9LHzO*ml!CbG<1;ZP9qGF8c+eRC>DQ)H`qQOuG=WcJS-6^N>Tzj;+Ns&9V?hTir{ zk9uCcB87-dW48I$d_1G?kcNii(lWofzx01b6L(H!R^(I0udj8|Kt@mPQkdjWiZ+YZ zvl`dp*&!I&$m08w$sU8zO06V%e~H{^ad#;>H^o+xw;IS3%(B*j1r#?DNZvhC*?^Mt z#F)8bS-5W9`a6fCkCX@7H-@3M@W5!u=W4ZEZ&>;55|`c#2s)+c=Af=Jh^w+fP zq}eK1L{K!e&4t995Q z-_A@*YOW^4B$9XiJ#$i+U^s9TeO_0Vr+8kd@~Ga`=P0mhTX_pjHYWup%YODH2JxyL z+tr=vXQZaO66^+chHgpch3wCf$D8}rsAUc+3MDd{rxZ|4DKB~VFA%mXj{l= zP=mxEU8l=Y_kBH^jZWq}*WPb%HxE=1o*FMiUg*b6x6v}#P!o+pss-f^ApMlN+} z1&LO_A+4EVP?3BZ6)2ZMGR{fUElhBPcu;>z(x@di(Cou<>+%K0tf*@-Y-XREAfrX8 zh7D`*SL_nIA!Si8A(QbCJjSd!-P~paJCWy~%-k?YmehB&oR^obB1HCDJcTQq*s`x$ zt&JKznb&uB^*}3-7c0-qS^~DN4S!LO=(~&+I(8q&dY9S~-w}OFPR)*)d znu$TzT7@;LuPIY*f@(vz6AEZVpBI7Y^-#p(A-Fj?Ie13^NzD!xw6y~A7z6&#HRU-K zd|%KX=WY)b`k4a)4gmlIWKuh97+ANHYQWn+3^aW(aVwy282Dc{x2^1E8*rxWr$2K{ zW-m6$>E#c$;x+o7Z0@<;%{~tLm(7{tz1a8qCn;ZshJpMb+MaA~J$ADj!MoVFz1SIS ammhUEvHoH6@_lb6n6t5*SL^V8zx@p(OyJ-E literal 0 HcmV?d00001 diff --git a/FarmmapsApiSamples/Data/Scan_1_20190605.zip b/FarmmapsApiSamples/Data/Scan_1_20190605.zip new file mode 100644 index 0000000000000000000000000000000000000000..ab00afd49e34726db805e889ce841d3ecc0577c8 GIT binary patch literal 43368 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 Always + + Always + diff --git a/FarmmapsApiSamples/NbsApp.cs b/FarmmapsApiSamples/NbsApp.cs index b344698..499e24b 100644 --- a/FarmmapsApiSamples/NbsApp.cs +++ b/FarmmapsApiSamples/NbsApp.cs @@ -1,8 +1,11 @@ using System; +using System.Globalization; +using System.IO; using System.Linq; using System.Threading.Tasks; using FarmmapsApi.Models; using FarmmapsApi.Services; +using Google.Apis.Upload; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; using static FarmmapsApi.Extensions; @@ -12,6 +15,8 @@ namespace FarmmapsApiSamples public class NbsApp : IApp { private const string USERINPUT_ITEMTYPE = "vnd.farmmaps.itemtype.user.input"; + private const string GEOTIFF_ITEMTYPE = "vnd.farmmaps.itemtype.geotiff"; + private const string CROPFIELD_ITEMTYPE = "vnd.farmmaps.itemtype.cropfield"; private const string VRANBS_TASK = "vnd.farmmaps.task.vranbs"; private readonly ILogger _logger; @@ -41,14 +46,33 @@ namespace FarmmapsApiSamples _logger.LogInformation("Authenticated client credentials"); var roots = await _farmmapsApiService.GetCurrentUserRootsAsync(); - var myDriveRoot = roots.SingleOrDefault(r => r.Name == "My drive"); - - if (myDriveRoot != null) + + // upload data to Uploaded + var uploadedRoot = roots.SingleOrDefault(r => r.Name == "Uploaded"); + if (uploadedRoot != null) { - var cropfieldItem = await GetOrCreateCropfieldItem(myDriveRoot.Code); - var targetN = await CalculateTargetN(cropfieldItem, 60); + await _farmmapsApiService.UploadFile(Path.Combine("Data", "Scan_1_20190605.zip"), uploadedRoot.Code, + progress => + { + _logger.LogInformation($"Status: {progress.Status} - BytesSent: {progress.BytesSent}"); + if(progress.Status == UploadStatus.Failed) + _logger.LogError($"Uploading failed {progress.Exception.Message}"); + }); - _logger.LogInformation($"TargetN: {targetN}"); + // need to transform shape data to geotiff + + var myDriveRoot = roots.SingleOrDefault(r => r.Name == "My drive"); + if (myDriveRoot != null) + { + var cropfieldItem = await GetOrCreateCropfieldItem(myDriveRoot.Code); + + _logger.LogInformation($"Calculating targetN with targetYield: {60}"); + var targetN = await CalculateTargetN(cropfieldItem, 60); + _logger.LogInformation($"TargetN: {targetN}"); + + _logger.LogInformation("Calculating nitrogen map"); +// var nitrogenMapItem = CalculateNitrogenMap(cropfieldItem,, targetN); + } } } catch (Exception ex) @@ -60,7 +84,7 @@ namespace FarmmapsApiSamples private async Task GetOrCreateCropfieldItem(string parentItemCode) { var cropfieldItems = await - _farmmapsApiService.GetItemChildrenAsync(parentItemCode, "vnd.farmmaps.itemtype.cropfield"); + _farmmapsApiService.GetItemChildrenAsync(parentItemCode,CROPFIELD_ITEMTYPE); if (cropfieldItems.Count > 0) return cropfieldItems[0]; @@ -70,18 +94,24 @@ namespace FarmmapsApiSamples { ParentCode = parentItemCode, ItemType = "vnd.farmmaps.itemtype.cropfield", - Name = "cropfield for VRA", + Name = "Cropfield for VRA", DataDate = currentYear, DataEndDate = currentYear.AddYears(1), Data = JObject.FromObject(new - {startDate = currentYear, endDate = currentYear.AddYears(1)}), + {startDate = currentYear, endDate = currentYear.AddMonths(3)}), Geometry = JObject.Parse( - @"{""type"":""Polygon"",""coordinates"":[[[6.09942873984307,53.070025028087],[6.09992507404607,53.0705617890585],[6.10036959220086,53.0710679529031],[6.10065149010421,53.0714062774307],[6.10087493644271,53.0716712354474],[6.10091082982487,53.0716936039203],[6.10165087441291,53.0712041549161],[6.10204994718318,53.0709349338005],[6.10263143118855,53.0705789370018],[6.10311578125011,53.0702657538294],[6.10331686552072,53.0701314102389],[6.103326530575,53.070119463569],[6.10309137950343,53.0699829669055],[6.10184241586523,53.0692902201371],[6.10168497998891,53.0691984306747],[6.10092987659869,53.0694894453514],[6.09942873984307,53.070025028087]]]}") + @"{ ""type"": ""Polygon"", ""coordinates"": [ [ [ 3.40843828875524, 50.638966444680605 ], [ 3.408953272886064, 50.639197789621612 ], [ 3.409242951459603, 50.639469958681836 ], [ 3.409328782148028, 50.639612846807708 ], [ 3.409457528180712, 50.639789755314411 ], [ 3.409639918393741, 50.640014292074966 ], [ 3.409833037442765, 50.640211611372706 ], [ 3.410069071836049, 50.640395321698435 ], [ 3.410380208081761, 50.640572227259661 ], [ 3.410605513638958, 50.640715112034222 ], [ 3.411925160474145, 50.641177783561204 ], [ 3.411935889310142, 50.640728720085136 ], [ 3.412590348309737, 50.63948356709389 ], [ 3.413244807309242, 50.638224772339846 ], [ 3.413400375432099, 50.637901562841307 ], [ 3.413539850300779, 50.637449065809889 ], [ 3.413475477284437, 50.637418445552932 ], [ 3.40999396998362, 50.637449065810451 ], [ 3.409940325803365, 50.638102293212661 ], [ 3.409575545377398, 50.638483338338325 ], [ 3.409060561246574, 50.638707881340494 ], [ 3.40843828875524, 50.638966444680605 ] ] ] }") }; return await _farmmapsApiService.CreateItemAsync(cropfieldItemRequest); } + /// + /// Calculates TargetN, makes the assumption the cropfield and user.input(targetn) item have the same parent + /// + /// The cropfield to base the calculations on + /// The target yield input for the TargetN calculation + /// The TargetN private async Task CalculateTargetN(Item cropfieldItem, int targetYield) { var targetNItems = await @@ -99,8 +129,6 @@ namespace FarmmapsApiSamples targetNItem = targetNItems[0]; } - _logger.LogInformation($"Calculating targetN with targetYield: {targetYield}"); - var nbsTargetNRequest = new TaskRequest {TaskType = VRANBS_TASK}; nbsTargetNRequest.attributes["operation"] = "targetn"; nbsTargetNRequest.attributes["inputCode"] = targetNItem.Code; @@ -111,19 +139,63 @@ namespace FarmmapsApiSamples await PollTask(TimeSpan.FromSeconds(3), async (tokenSource) => { - var itemTask = await _farmmapsApiService.GetTaskStatusAsync(cropfieldItem.Code, itemTaskCode); - - if (itemTask.State != ItemTaskState.Processing && itemTask.State != ItemTaskState.Scheduled) + var itemTaskStatus = await _farmmapsApiService.GetTaskStatusAsync(cropfieldItem.Code, itemTaskCode); + if (itemTaskStatus.State != ItemTaskState.Processing && itemTaskStatus.State != ItemTaskState.Scheduled) 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 0; + } + var item = await _farmmapsApiService.GetItemAsync(targetNItem.Code); if (item.Data.ContainsKey("TargetN")) return item.Data["TargetN"].Value(); - + return 0; } + private async Task CalculateNitrogenMap(Item cropfieldItem, Item inputItem, double targetN) + { + var nbsNitrogenRequest = new TaskRequest {TaskType = VRANBS_TASK}; + nbsNitrogenRequest.attributes["operation"] = "nitrogen"; + nbsNitrogenRequest.attributes["inputCode"] = inputItem.Code; + nbsNitrogenRequest.attributes["inputType"] = "irmi"; + nbsNitrogenRequest.attributes["targetN"] = targetN.ToString(CultureInfo.InvariantCulture); + + string itemTaskCode = await _farmmapsApiService.QueueTaskAsync(cropfieldItem.Code, nbsNitrogenRequest); + + await PollTask(TimeSpan.FromSeconds(5), async (tokenSource) => + { + var itemTaskStatus = await _farmmapsApiService.GetTaskStatusAsync(cropfieldItem.Code, itemTaskCode); + if (itemTaskStatus.State != ItemTaskState.Processing && itemTaskStatus.State != ItemTaskState.Scheduled) + 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; + } + + var geotiffItems = await + _farmmapsApiService.GetItemChildrenAsync(cropfieldItem.Code, GEOTIFF_ITEMTYPE); + + // how to uniquely know which item is really created!? + var nitrogenItem = geotiffItems.SingleOrDefault(i => i.Name.Contains("nitrogen")); + if (nitrogenItem == null) + { + _logger.LogError("Could not find the nitrogen geotiff child item under cropfield"); + return null; + } + + return nitrogenItem; + } + private ItemRequest CreateTargetNItemRequest(string parentItemCode) { return new ItemRequest() diff --git a/FarmmapsApiSamples/appsettings.json b/FarmmapsApiSamples/appsettings.json index 337d0e9..c795f28 100644 --- a/FarmmapsApiSamples/appsettings.json +++ b/FarmmapsApiSamples/appsettings.json @@ -1,6 +1,7 @@ { "Authority": "https://accounts.farmmaps.awtest.nl/", - "Endpoint": "https://farmmaps.awtest.nl/api/v1/", + "Endpoint": "http://localhost:8083", + "BasePath": "api/v1", "DiscoveryEndpointUrl": "https://accounts.farmmaps.awtest.nl/.well-known/openid-configuration", "RedirectUri": "http://example.nl/api", "ClientId": "",