Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tvd and custom ids #1769

Merged
merged 14 commits into from
Dec 19, 2024
3 changes: 3 additions & 0 deletions src/api/BdmsContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,9 @@ public static void SeedData(this BdmsContext context)
.RuleFor(o => o.PrecisionLocationY, f => f.PickRandom(Enumerable.Range(0, 10)))
.RuleFor(o => o.PrecisionLocationXLV03, f => f.PickRandom(Enumerable.Range(0, 10)))
.RuleFor(o => o.PrecisionLocationYLV03, f => f.PickRandom(Enumerable.Range(0, 10)))
.RuleFor(o => o.TotalDepthTvd, _ => null)
.RuleFor(o => o.TopBedrockFreshTvd, _ => null)
.RuleFor(o => o.TopBedrockWeatheredTvd, _ => null)
.RuleFor(o => o.Observations, _ => new Collection<Observation>())
.FinishWith((f, o) => { o.Name = o.OriginalName; });

Expand Down
2 changes: 1 addition & 1 deletion src/api/BoreholeGeometry/AzIncFormat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ internal sealed class AzIncFormat : IBoreholeGeometryFormat
public IList<BoreholeGeometryElement> ReadCsv(IFormFile file, int boreholeId)
{
using var reader = new StreamReader(file.OpenReadStream());
using var csv = new CsvReader(reader, CsvConfigHelper.CsvConfig);
using var csv = new CsvReader(reader, CsvConfigHelper.CsvReadConfig);

var data = csv.GetRecords<Geometry>().ToList();

Expand Down
2 changes: 1 addition & 1 deletion src/api/BoreholeGeometry/PitchRollFormat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ internal sealed class PitchRollFormat : IBoreholeGeometryFormat
public IList<BoreholeGeometryElement> ReadCsv(IFormFile file, int boreholeId)
{
using var reader = new StreamReader(file.OpenReadStream());
using var csv = new CsvReader(reader, CsvConfigHelper.CsvConfig);
using var csv = new CsvReader(reader, CsvConfigHelper.CsvReadConfig);

var data = csv.GetRecords<Geometry>().ToList();

Expand Down
2 changes: 1 addition & 1 deletion src/api/BoreholeGeometry/XYZFormat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ internal sealed class XYZFormat : IBoreholeGeometryFormat
public IList<BoreholeGeometryElement> ReadCsv(IFormFile file, int boreholeId)
{
using var reader = new StreamReader(file.OpenReadStream());
using var csv = new CsvReader(reader, CsvConfigHelper.CsvConfig);
using var csv = new CsvReader(reader, CsvConfigHelper.CsvReadConfig);

var data = csv.GetRecords<Geometry>();
return ToBoreholeGeometry(data, boreholeId);
Expand Down
57 changes: 0 additions & 57 deletions src/api/Controllers/BoreholeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,63 +120,6 @@ public async Task<ActionResult<Borehole>> GetByIdAsync(int id)
return Ok(borehole);
}

/// <summary>
/// Exports the details of up to <see cref="MaxPageSize"></see> boreholes as a CSV file. Filters the boreholes based on the provided list of IDs.
/// </summary>
/// <param name="ids">The list of IDs for the boreholes to be exported.</param>
/// <returns>A CSV file containing the details specified boreholes.</returns>
[HttpGet("export-csv")]
[Authorize(Policy = PolicyNames.Viewer)]
public async Task<IActionResult> DownloadCsvAsync([FromQuery][MaxLength(MaxPageSize)] IEnumerable<int> ids)
{
ids = ids.Take(MaxPageSize).ToList();
if (!ids.Any()) return BadRequest("The list of IDs must not be empty.");

var boreholes = await Context.Boreholes
.Where(borehole => ids.Contains(borehole.Id))
.Select(b => new
{
b.Id,
b.OriginalName,
b.ProjectName,
b.Name,
b.RestrictionId,
b.RestrictionUntil,
b.NationalInterest,
b.LocationX,
b.LocationY,
b.LocationPrecisionId,
b.ElevationZ,
b.ElevationPrecisionId,
b.ReferenceElevation,
b.ReferenceElevationTypeId,
b.ReferenceElevationPrecisionId,
b.HrsId,
b.TypeId,
b.PurposeId,
b.StatusId,
b.Remarks,
b.TotalDepth,
b.DepthPrecisionId,
b.TopBedrockFreshMd,
b.TopBedrockWeatheredMd,
b.HasGroundwater,
b.LithologyTopBedrockId,
b.ChronostratigraphyTopBedrockId,
b.LithostratigraphyTopBedrockId,
})
.ToListAsync()
.ConfigureAwait(false);

if (boreholes.Count == 0) return NotFound("No borehole(s) found for the provided id(s).");

using var stringWriter = new StringWriter();
using var csvWriter = new CsvWriter(stringWriter, CultureInfo.InvariantCulture);
await csvWriter.WriteRecordsAsync(boreholes).ConfigureAwait(false);

return File(Encoding.UTF8.GetBytes(stringWriter.ToString()), "text/csv", "boreholes_export.csv");
}

/// <summary>
/// Asynchronously copies a <see cref="Borehole"/>.
/// </summary>
Expand Down
167 changes: 167 additions & 0 deletions src/api/Controllers/ExportController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
using BDMS.Authentication;
using BDMS.Models;
using CsvHelper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.Text;

namespace BDMS.Controllers;

[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public class ExportController : ControllerBase
{
// Limit the maximum number of items per request to 100.
// This also applies to the number of filtered ids to ensure the URL length does not exceed the maximum allowed length.
private const int MaxPageSize = 100;
private readonly BdmsContext context;

public ExportController(BdmsContext context)
{
this.context = context;
}

/// <summary>
/// Exports the details of up to <see cref="MaxPageSize"></see> boreholes as a CSV file. Filters the boreholes based on the provided list of IDs.
/// </summary>
/// <param name="ids">The list of IDs for the boreholes to be exported.</param>
/// <returns>A CSV file containing the details of the specified boreholes.</returns>
[HttpGet("csv")]
[Authorize(Policy = PolicyNames.Viewer)]
public async Task<IActionResult> ExportCsvAsync([FromQuery][MaxLength(MaxPageSize)] IEnumerable<int> ids)
{
List<int> idList = ids.Take(MaxPageSize).ToList();
if (idList.Count < 1) return BadRequest("The list of IDs must not be empty.");

var boreholes = await context.Boreholes
.Include(b => b.BoreholeCodelists).ThenInclude(bc => bc.Codelist)
.Where(borehole => idList.Contains(borehole.Id))
.OrderBy(b => idList.IndexOf(b.Id))
.ToListAsync()
.ConfigureAwait(false);

if (boreholes.Count == 0) return NotFound("No borehole(s) found for the provided id(s).");

using var stringWriter = new StringWriter();
using var csvWriter = new CsvWriter(stringWriter, CsvConfigHelper.CsvWriteConfig);

// Write headers for standard fields
csvWriter.WriteField(nameof(Borehole.Id));
csvWriter.WriteField(nameof(Borehole.OriginalName));
csvWriter.WriteField(nameof(Borehole.ProjectName));
csvWriter.WriteField(nameof(Borehole.Name));
csvWriter.WriteField(nameof(Borehole.RestrictionId));
csvWriter.WriteField(nameof(Borehole.RestrictionUntil));
csvWriter.WriteField(nameof(Borehole.NationalInterest));
csvWriter.WriteField(nameof(Borehole.LocationX));
csvWriter.WriteField(nameof(Borehole.LocationY));
csvWriter.WriteField(nameof(Borehole.LocationXLV03));
csvWriter.WriteField(nameof(Borehole.LocationYLV03));
csvWriter.WriteField(nameof(Borehole.LocationPrecisionId));
csvWriter.WriteField(nameof(Borehole.ElevationZ));
csvWriter.WriteField(nameof(Borehole.ElevationPrecisionId));
csvWriter.WriteField(nameof(Borehole.ReferenceElevation));
csvWriter.WriteField(nameof(Borehole.ReferenceElevationTypeId));
csvWriter.WriteField(nameof(Borehole.ReferenceElevationPrecisionId));
csvWriter.WriteField(nameof(Borehole.HrsId));
csvWriter.WriteField(nameof(Borehole.TypeId));
csvWriter.WriteField(nameof(Borehole.PurposeId));
csvWriter.WriteField(nameof(Borehole.StatusId));
csvWriter.WriteField(nameof(Borehole.Remarks));
csvWriter.WriteField(nameof(Borehole.TotalDepth));
csvWriter.WriteField(nameof(Borehole.DepthPrecisionId));
csvWriter.WriteField(nameof(Borehole.TopBedrockFreshMd));
csvWriter.WriteField(nameof(Borehole.TopBedrockWeatheredMd));
csvWriter.WriteField(nameof(Borehole.TotalDepthTvd));
csvWriter.WriteField(nameof(Borehole.TopBedrockFreshTvd));
csvWriter.WriteField(nameof(Borehole.TopBedrockWeatheredTvd));
csvWriter.WriteField(nameof(Borehole.HasGroundwater));
csvWriter.WriteField(nameof(Borehole.LithologyTopBedrockId));
csvWriter.WriteField(nameof(Borehole.ChronostratigraphyTopBedrockId));
csvWriter.WriteField(nameof(Borehole.LithostratigraphyTopBedrockId));

// Write dynamic headers for each distinct custom Id
var customIdHeaders = boreholes
.SelectMany(b => GetBoreholeCodelists(b))
MiraGeowerkstatt marked this conversation as resolved.
Show resolved Hide resolved
.Select(bc => new { bc.CodelistId, bc.Codelist?.En })
.Distinct()
.OrderBy(x => x.CodelistId)
.ToList();

foreach (var header in customIdHeaders)
{
csvWriter.WriteField(header.En.Replace(" ", "", StringComparison.OrdinalIgnoreCase));
}

// Move to the next line
await csvWriter.NextRecordAsync().ConfigureAwait(false);

// Write data for standard fields
foreach (var b in boreholes)
{
var boreholeGeometry = await context.BoreholeGeometry
.AsNoTracking()
.Where(g => g.BoreholeId == b.Id)
.ToListAsync()
.ConfigureAwait(false);

b.TotalDepthTvd = boreholeGeometry.GetTVDIfGeometryExists(b.TotalDepth);
b.TopBedrockFreshTvd = boreholeGeometry.GetTVDIfGeometryExists(b.TopBedrockFreshMd);
b.TopBedrockWeatheredTvd = boreholeGeometry.GetTVDIfGeometryExists(b.TopBedrockWeatheredMd);

csvWriter.WriteField(b.Id);
csvWriter.WriteField(b.OriginalName);
csvWriter.WriteField(b.ProjectName);
csvWriter.WriteField(b.Name);
csvWriter.WriteField(b.RestrictionId);
csvWriter.WriteField(b.RestrictionUntil);
csvWriter.WriteField(b.NationalInterest);
csvWriter.WriteField(b.LocationX);
csvWriter.WriteField(b.LocationY);
csvWriter.WriteField(b.LocationXLV03);
csvWriter.WriteField(b.LocationYLV03);
csvWriter.WriteField(b.LocationPrecisionId);
csvWriter.WriteField(b.ElevationZ);
csvWriter.WriteField(b.ElevationPrecisionId);
csvWriter.WriteField(b.ReferenceElevation);
csvWriter.WriteField(b.ReferenceElevationTypeId);
csvWriter.WriteField(b.ReferenceElevationPrecisionId);
csvWriter.WriteField(b.HrsId);
csvWriter.WriteField(b.TypeId);
csvWriter.WriteField(b.PurposeId);
csvWriter.WriteField(b.StatusId);
csvWriter.WriteField(b.Remarks);
csvWriter.WriteField(b.TotalDepth);
csvWriter.WriteField(b.DepthPrecisionId);
csvWriter.WriteField(b.TopBedrockFreshMd);
csvWriter.WriteField(b.TopBedrockWeatheredMd);
csvWriter.WriteField(b.TotalDepthTvd);
csvWriter.WriteField(b.TopBedrockFreshTvd);
csvWriter.WriteField(b.TopBedrockWeatheredTvd);
csvWriter.WriteField(b.HasGroundwater);
csvWriter.WriteField(b.LithologyTopBedrockId);
csvWriter.WriteField(b.ChronostratigraphyTopBedrockId);
csvWriter.WriteField(b.LithostratigraphyTopBedrockId);

// Write dynamic fields for custom Ids
foreach (var header in customIdHeaders)
{
var codelistValue = GetBoreholeCodelists(b).FirstOrDefault(bc => bc.CodelistId == header.CodelistId)?.Value;
csvWriter.WriteField(codelistValue ?? "");
}

// Move to the next line
await csvWriter.NextRecordAsync().ConfigureAwait(false);
}

await csvWriter.FlushAsync().ConfigureAwait(false);
return File(Encoding.UTF8.GetBytes(stringWriter.ToString()), "text/csv", "boreholes_export.csv");
}

private static IEnumerable<BoreholeCodelist> GetBoreholeCodelists(Borehole borehole)
{
return borehole.BoreholeCodelists ?? Enumerable.Empty<BoreholeCodelist>();
}
}
24 changes: 6 additions & 18 deletions src/api/Controllers/ImportController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,6 @@ public class ImportController : ControllerBase
private readonly int sridLv95 = 2056;
private readonly int sridLv03 = 21781;
private readonly string nullOrEmptyMsg = "Field '{0}' is required.";
private readonly CsvConfiguration csvConfig = new(new CultureInfo("de-CH"))
{
Delimiter = ";",
IgnoreReferences = true,
PrepareHeaderForMatch = args => args.Header.Humanize(LetterCasing.Title),
MissingFieldFound = null,
};

private static readonly JsonSerializerOptions jsonImportOptions = new() { PropertyNameCaseInsensitive = true };

Expand Down Expand Up @@ -347,10 +340,10 @@ internal static bool CompareValuesWithTolerance(double? firstValue, double? seco
return Math.Abs(firstValue.Value - secondValue.Value) <= tolerance;
}

private List<BoreholeImport> ReadBoreholesFromCsv(IFormFile file)
private static List<BoreholeImport> ReadBoreholesFromCsv(IFormFile file)
{
using var reader = new StreamReader(file.OpenReadStream());
using var csv = new CsvReader(reader, csvConfig);
using var csv = new CsvReader(reader, CsvConfigHelper.CsvReadConfig);

csv.Context.RegisterClassMap(new CsvImportBoreholeMap());

Expand Down Expand Up @@ -388,17 +381,9 @@ private void AddValidationErrorToModelState(int boreholeIndex, string errorMessa

private sealed class CsvImportBoreholeMap : ClassMap<BoreholeImport>
{
private readonly CultureInfo swissCulture = new("de-CH");

public CsvImportBoreholeMap()
{
var config = new CsvConfiguration(swissCulture)
{
IgnoreReferences = true,
PrepareHeaderForMatch = args => args.Header.Humanize(LetterCasing.Title),
};

AutoMap(config);
AutoMap(CsvConfigHelper.CsvReadConfig);

// Define all optional properties of Borehole (ef navigation properties do not need to be defined as optional).
Map(m => m.CreatedById).Optional();
Expand Down Expand Up @@ -445,6 +430,9 @@ public CsvImportBoreholeMap()
Map(b => b.Canton).Ignore();
Map(b => b.Country).Ignore();
Map(m => m.Id).Ignore();
Map(m => m.TotalDepthTvd).Ignore();
Map(m => m.TopBedrockFreshTvd).Ignore();
Map(m => m.TopBedrockWeatheredTvd).Ignore();

// Define additional mapping logic
Map(m => m.BoreholeCodelists).Convert(args =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,32 @@
using NetTopologySuite.Utilities;
using System.Globalization;

namespace BDMS.BoreholeGeometry;
namespace BDMS;

public static class CsvConfigHelper
{
internal static readonly CsvConfiguration CsvConfig = new(new CultureInfo("de-CH"))
internal static readonly CsvConfiguration CsvReadConfig = new(new CultureInfo("de-CH"))
{
Delimiter = ";",
IgnoreReferences = true,
PrepareHeaderForMatch = args => args.Header.Humanize(LetterCasing.Title),
MissingFieldFound = null,
};

internal static readonly CsvConfiguration CsvWriteConfig = new(new CultureInfo("de-CH"))
{
Delimiter = ";",
};

/// <summary>
/// Get the CSV header <see cref="CsvHelper"/> for a class of type <typeparamref name="T"/>.
/// Uses the map generated by <see cref="CsvHelper.CsvContext.AutoMap{T}()"/>.
/// Uses the map generated by <see cref="CsvContext.AutoMap{T}()"/>.
/// If a property has multiple possible column names only the first is considered.
/// </summary>
/// <typeparam name="T">The class to get the header for.</typeparam>
internal static string GetCsvHeader<T>()
{
var context = new CsvContext(CsvConfig);
var context = new CsvContext(CsvReadConfig);
var map = context.AutoMap<T>();
return string.Join("; ", map.MemberMaps
.Select(m =>
Expand Down
19 changes: 19 additions & 0 deletions src/api/Models/Borehole.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using NetTopologySuite.Geometries;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics;
using System.Text.Json.Serialization;

namespace BDMS.Models;
Expand Down Expand Up @@ -350,6 +351,24 @@ public class Borehole : IChangeTracking, IIdentifyable
[Column("reference_elevation_type_id_cli")]
public int? ReferenceElevationTypeId { get; set; }

/// <summary>
/// Gets or sets the <see cref="Borehole"/>'s true vertical total depth.
/// </summary>
[NotMapped]
public double? TotalDepthTvd { get; set; }

/// <summary>
/// Gets or sets the <see cref="Borehole"/>'s true vertical top bedrock fresh depth.
/// </summary>
[NotMapped]
public double? TopBedrockFreshTvd { get; set; }

/// <summary>
/// Gets or sets the <see cref="Borehole"/>'s true vertical top bedrock weathered depth.
/// </summary>
[NotMapped]
public double? TopBedrockWeatheredTvd { get; set; }

/// <summary>
/// Gets or sets the <see cref="Borehole"/>'s reference elevation type.
/// </summary>
Expand Down
Loading
Loading