Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion Artskart3.Api/Controllers/LookupController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ namespace Artskart3.Api.Controllers;
public class LookupController : ControllerBase
{
private readonly ILookupService _lookupService;
private readonly ITaxonHierarchyService _taxonHierarchy;

public LookupController(ILookupService lookupService)
public LookupController(ILookupService lookupService, ITaxonHierarchyService taxonHierarchy)
{
_lookupService = lookupService ?? throw new ArgumentNullException(nameof(lookupService));
_taxonHierarchy = taxonHierarchy ?? throw new ArgumentNullException(nameof(taxonHierarchy));
}

/// <summary>
Expand Down Expand Up @@ -82,4 +84,17 @@ public async Task<ActionResult<IEnumerable<BasisOfRecordDto>>> GetBasisOfRecords
var basisOfRecords = await _lookupService.GetBasisOfRecordsAsync(cancellationToken);
return Ok(basisOfRecords);
}

/// <summary>
/// Returnerer direkte barn i taksonomien for en gitt forelder-taxon.
/// Uten parentTaxonId returneres rotnodene (kingdom-nivå).
/// Data hentes fra minne — ingen databasekall per request.
/// </summary>
[HttpGet("TaxonTree")]
[Produces("application/json")]
public ActionResult<List<TaxonTreeNodeDto>> GetTaxonTree([FromQuery] int? parentTaxonId = null, CancellationToken cancellationToken = default)
{
var children = _taxonHierarchy.GetChildren(parentTaxonId);
return Ok(children);
}
}
2 changes: 2 additions & 0 deletions Artskart3.Api/Controllers/SearchController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ private bool ValidateFilterArraySizes(ObservationSearchFilterDto filter, out Bad
ReadOnlySpan<(string name, int? length)> arrays =
[
(nameof(filter.TaxonGroupIds), filter.TaxonGroupIds?.Length),
(nameof(filter.TaxonIds), filter.TaxonIds?.Length),
(nameof(filter.CategoryIds), filter.CategoryIds?.Length),
(nameof(filter.OrganizationIds), filter.OrganizationIds?.Length),
(nameof(filter.MunicipalityIds), filter.MunicipalityIds?.Length),
Expand Down Expand Up @@ -343,6 +344,7 @@ private bool ValidateLocationFilterArraySizes(LocationSearchFilterDto filter, ou
ReadOnlySpan<(string name, int? length)> arrays =
[
(nameof(filter.TaxonGroupIds), filter.TaxonGroupIds?.Length),
(nameof(filter.TaxonIds), filter.TaxonIds?.Length),
(nameof(filter.CategoryIds), filter.CategoryIds?.Length),
(nameof(filter.OrganizationIds), filter.OrganizationIds?.Length),
(nameof(filter.MunicipalityIds), filter.MunicipalityIds?.Length),
Expand Down
1 change: 1 addition & 0 deletions Artskart3.Core/Application/DTOs/IObservationFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace Artskart3.Core.Application.DTOs;
public interface IObservationFilter
{
int[]? TaxonGroupIds { get; }
int[]? TaxonIds { get; }
int[]? CategoryIds { get; }
int[]? OrganizationIds { get; }
string[]? MunicipalityIds { get; }
Expand Down
4 changes: 4 additions & 0 deletions Artskart3.Core/Application/DTOs/LocationSearchFilterDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ public class LocationSearchFilterDto : IObservationFilter
{
public int[]? TaxonGroupIds { get; set; }

public int[]? TaxonIds { get; set; }

public int[]? CategoryIds { get; set; }

public int[]? BasisOfRecordIds { get; set; }
Expand All @@ -29,6 +31,7 @@ public class LocationSearchFilterDto : IObservationFilter
/// </summary>
public bool HasActiveFilters =>
TaxonGroupIds?.Length > 0 ||
TaxonIds?.Length > 0 ||
CategoryIds?.Length > 0 ||
BasisOfRecordIds?.Length > 0 ||
OrganizationIds?.Length > 0 ||
Expand All @@ -50,6 +53,7 @@ public class LocationSearchFilterDto : IObservationFilter
/// </summary>
public bool HasObservationAttributeFilters =>
TaxonGroupIds?.Length > 0 ||
TaxonIds?.Length > 0 ||
CategoryIds?.Length > 0 ||
BasisOfRecordIds?.Length > 0 ||
OrganizationIds?.Length > 0 ||
Expand Down
2 changes: 2 additions & 0 deletions Artskart3.Core/Application/DTOs/ObservationSearchFilterDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public class ObservationSearchFilterDto : PaginatedRequestDto, IObservationFilte

public int[]? TaxonGroupIds { get; set; }

public int[]? TaxonIds { get; set; }

public int[]? CategoryIds { get; set; }

public int[]? OrganizationIds { get; set; }
Expand Down
14 changes: 14 additions & 0 deletions Artskart3.Core/Application/DTOs/TaxonTreeNodeDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Artskart3.Core.Application.DTOs;

public class TaxonTreeNodeDto
{
public int Id { get; set; }
public string? ValidScientificName { get; set; }
public string? PreferredPopularName { get; set; }
public int TaxonRankId { get; set; }
public int TaxonGroupId { get; set; }
public int? CumulativeObservationCount { get; set; }
public bool ExistsInCountry { get; set; }
public bool HasChildren { get; set; }
public List<TaxonTreeNodeDto> Children { get; set; } = [];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Artskart3.Core.Application.DTOs;

namespace Artskart3.Core.Application.Services.Interfaces;

/// <summary>
/// Oppstartslastet oppslag for taksonhierarki.
/// Data lastes fra Taxon-tabellen og holdes i minne, oppdateres periodisk.
/// </summary>
public interface ITaxonHierarchyService
{
/// <summary>
/// Returnerer TaxonRankId for et gitt taxonId, eller null hvis ukjent.
/// Brukes for å bestemme hvilken kolonne i ObservationTaxonHierarchy som skal spørres.
/// </summary>
int? GetTaxonRankId(int taxonId);

/// <summary>
/// Returnerer direkte barn av en gitt taxon som trenoder.
/// Hvis parentTaxonId er null, returneres rotnodene (kingdom-nivå).
/// Filtrerer til kun taxoner med observasjoner eller som finnes i landet.
/// </summary>
List<TaxonTreeNodeDto> GetChildren(int? parentTaxonId);
}
38 changes: 38 additions & 0 deletions Artskart3.Core/Domain/Entities/ObservationTaxonHierarchy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace Artskart3.Core.Domain.Entities;

/// <summary>
/// Denormalisert hierarkitabell for raske taksonomiske oppslag.
/// Hver rad knytter en observasjon til alle sine forfedre i taksonomien,
/// med én kolonne per rangsnivå (kingdom, phylum, class, osv.).
/// </summary>
public class ObservationTaxonHierarchy
{
public int ObservationId { get; set; }

public int? KingdomTaxonId { get; set; } // TaxonRankId 1
public int? SubkingdomTaxonId { get; set; } // TaxonRankId 2
public int? PhylumTaxonId { get; set; } // TaxonRankId 3
public int? SubphylumTaxonId { get; set; } // TaxonRankId 4
public int? SuperclassTaxonId { get; set; } // TaxonRankId 5
public int? ClassTaxonId { get; set; } // TaxonRankId 6
public int? SubclassTaxonId { get; set; } // TaxonRankId 7
public int? InfraclassTaxonId { get; set; } // TaxonRankId 8
public int? CohortTaxonId { get; set; } // TaxonRankId 9
public int? SuperorderTaxonId { get; set; } // TaxonRankId 10
public int? OrderTaxonId { get; set; } // TaxonRankId 11
public int? SuborderTaxonId { get; set; } // TaxonRankId 12
public int? InfraorderTaxonId { get; set; } // TaxonRankId 13
public int? SuperfamilyTaxonId { get; set; } // TaxonRankId 14
public int? FamilyTaxonId { get; set; } // TaxonRankId 15
public int? SubfamilyTaxonId { get; set; } // TaxonRankId 16
public int? TribeTaxonId { get; set; } // TaxonRankId 17
public int? SubtribeTaxonId { get; set; } // TaxonRankId 18
public int? GenusTaxonId { get; set; } // TaxonRankId 19
public int? SubgenusTaxonId { get; set; } // TaxonRankId 20
public int? SectionTaxonId { get; set; } // TaxonRankId 21
public int? SpeciesTaxonId { get; set; } // TaxonRankId 22
public int? SubspeciesTaxonId { get; set; } // TaxonRankId 23
public int? VarietyTaxonId { get; set; } // TaxonRankId 24
public int? FormTaxonId { get; set; } // TaxonRankId 25
public int? NotSetTaxonId { get; set; } // TaxonRankId 26
}
34 changes: 34 additions & 0 deletions Artskart3.Infrastructure/Data/DbContext/ArtskartDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,40 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
entity.HasIndex(e => new { e.EntityTypeId, e.EntityId, e.ObservationId }).HasDatabaseName("IX_ObservationEntityIndex_EntityType_EntityId_ObsId");
});

modelBuilder.Entity<ObservationTaxonHierarchy>(entity =>
{
entity.HasKey(e => e.ObservationId);
entity.Property(e => e.ObservationId).ValueGeneratedNever();
entity.ToTable("ObservationTaxonHierarchy");

entity.HasIndex(e => e.KingdomTaxonId).HasDatabaseName("IX_OTH_Kingdom").HasFilter("[KingdomTaxonId] IS NOT NULL");
entity.HasIndex(e => e.SubkingdomTaxonId).HasDatabaseName("IX_OTH_Subkingdom").HasFilter("[SubkingdomTaxonId] IS NOT NULL");
entity.HasIndex(e => e.PhylumTaxonId).HasDatabaseName("IX_OTH_Phylum").HasFilter("[PhylumTaxonId] IS NOT NULL");
entity.HasIndex(e => e.SubphylumTaxonId).HasDatabaseName("IX_OTH_Subphylum").HasFilter("[SubphylumTaxonId] IS NOT NULL");
entity.HasIndex(e => e.SuperclassTaxonId).HasDatabaseName("IX_OTH_Superclass").HasFilter("[SuperclassTaxonId] IS NOT NULL");
entity.HasIndex(e => e.ClassTaxonId).HasDatabaseName("IX_OTH_Class").HasFilter("[ClassTaxonId] IS NOT NULL");
entity.HasIndex(e => e.SubclassTaxonId).HasDatabaseName("IX_OTH_Subclass").HasFilter("[SubclassTaxonId] IS NOT NULL");
entity.HasIndex(e => e.InfraclassTaxonId).HasDatabaseName("IX_OTH_Infraclass").HasFilter("[InfraclassTaxonId] IS NOT NULL");
entity.HasIndex(e => e.CohortTaxonId).HasDatabaseName("IX_OTH_Cohort").HasFilter("[CohortTaxonId] IS NOT NULL");
entity.HasIndex(e => e.SuperorderTaxonId).HasDatabaseName("IX_OTH_Superorder").HasFilter("[SuperorderTaxonId] IS NOT NULL");
entity.HasIndex(e => e.OrderTaxonId).HasDatabaseName("IX_OTH_Order").HasFilter("[OrderTaxonId] IS NOT NULL");
entity.HasIndex(e => e.SuborderTaxonId).HasDatabaseName("IX_OTH_Suborder").HasFilter("[SuborderTaxonId] IS NOT NULL");
entity.HasIndex(e => e.InfraorderTaxonId).HasDatabaseName("IX_OTH_Infraorder").HasFilter("[InfraorderTaxonId] IS NOT NULL");
entity.HasIndex(e => e.SuperfamilyTaxonId).HasDatabaseName("IX_OTH_Superfamily").HasFilter("[SuperfamilyTaxonId] IS NOT NULL");
entity.HasIndex(e => e.FamilyTaxonId).HasDatabaseName("IX_OTH_Family").HasFilter("[FamilyTaxonId] IS NOT NULL");
entity.HasIndex(e => e.SubfamilyTaxonId).HasDatabaseName("IX_OTH_Subfamily").HasFilter("[SubfamilyTaxonId] IS NOT NULL");
entity.HasIndex(e => e.TribeTaxonId).HasDatabaseName("IX_OTH_Tribe").HasFilter("[TribeTaxonId] IS NOT NULL");
entity.HasIndex(e => e.SubtribeTaxonId).HasDatabaseName("IX_OTH_Subtribe").HasFilter("[SubtribeTaxonId] IS NOT NULL");
entity.HasIndex(e => e.GenusTaxonId).HasDatabaseName("IX_OTH_Genus").HasFilter("[GenusTaxonId] IS NOT NULL");
entity.HasIndex(e => e.SubgenusTaxonId).HasDatabaseName("IX_OTH_Subgenus").HasFilter("[SubgenusTaxonId] IS NOT NULL");
entity.HasIndex(e => e.SectionTaxonId).HasDatabaseName("IX_OTH_Section").HasFilter("[SectionTaxonId] IS NOT NULL");
entity.HasIndex(e => e.SpeciesTaxonId).HasDatabaseName("IX_OTH_Species").HasFilter("[SpeciesTaxonId] IS NOT NULL");
entity.HasIndex(e => e.SubspeciesTaxonId).HasDatabaseName("IX_OTH_Subspecies").HasFilter("[SubspeciesTaxonId] IS NOT NULL");
entity.HasIndex(e => e.VarietyTaxonId).HasDatabaseName("IX_OTH_Variety").HasFilter("[VarietyTaxonId] IS NOT NULL");
entity.HasIndex(e => e.FormTaxonId).HasDatabaseName("IX_OTH_Form").HasFilter("[FormTaxonId] IS NOT NULL");
entity.HasIndex(e => e.NotSetTaxonId).HasDatabaseName("IX_OTH_NotSet").HasFilter("[NotSetTaxonId] IS NOT NULL");
});

OnModelCreatingPartial(modelBuilder);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection
services.AddSingleton<AreaHierarchyService>();
services.AddSingleton<IAreaHierarchyService>(sp => sp.GetRequiredService<AreaHierarchyService>());
services.AddHostedService(sp => sp.GetRequiredService<AreaHierarchyService>());
services.AddSingleton<TaxonHierarchyService>();
services.AddSingleton<ITaxonHierarchyService>(sp => sp.GetRequiredService<TaxonHierarchyService>());
services.AddHostedService(sp => sp.GetRequiredService<TaxonHierarchyService>());
services.AddScoped<ISearchService, SearchService>();
services.AddScoped<INotificationsService, NotificationsService>();
services.AddScoped<IUserService, UserService>();
Expand Down
Loading