Skip to content

Commit

Permalink
Add tvd and custom ids (#1769)
Browse files Browse the repository at this point in the history
  • Loading branch information
MiraGeowerkstatt authored Dec 19, 2024
2 parents f780876 + ff824f2 commit 6e35e9f
Show file tree
Hide file tree
Showing 14 changed files with 614 additions and 158 deletions.
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))
.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

0 comments on commit 6e35e9f

Please sign in to comment.