diff --git a/src/GeoCop.Api/Context.cs b/src/GeoCop.Api/Context.cs
index fee5d5c8..e67fa78c 100644
--- a/src/GeoCop.Api/Context.cs
+++ b/src/GeoCop.Api/Context.cs
@@ -11,7 +11,7 @@ public class Context : DbContext
///
/// Database context to manage the database.
///
- ///
+ /// Configuration options for the Context.
public Context(DbContextOptions options)
: base(options)
{
@@ -32,11 +32,41 @@ public Context(DbContextOptions options)
///
public DbSet Deliveries { get; set; }
+ ///
+ /// Gets the entity with all includes.
+ ///
+ public List DeliveriesWithIncludes
+ {
+ get
+ {
+ return Deliveries
+ .Include(d => d.DeliveryMandate)
+ .Include(d => d.Assets)
+ .AsNoTracking()
+ .ToList();
+ }
+ }
+
///
/// Set of all .
///
public DbSet DeliveryMandates { get; set; }
+ ///
+ /// Gets the entity with all includes.
+ ///
+ public List DeliveryMandatesWithIncludes
+ {
+ get
+ {
+ return DeliveryMandates
+ .Include(d => d.Deliveries)
+ .ThenInclude(d => d.Assets)
+ .AsNoTracking()
+ .ToList();
+ }
+ }
+
///
/// Set of all .
///
diff --git a/src/GeoCop.Api/Controllers/DeliveryController.cs b/src/GeoCop.Api/Controllers/DeliveryController.cs
index 8d413bc7..0c1d4b36 100644
--- a/src/GeoCop.Api/Controllers/DeliveryController.cs
+++ b/src/GeoCop.Api/Controllers/DeliveryController.cs
@@ -2,6 +2,7 @@
using GeoCop.Api.Contracts;
using GeoCop.Api.Models;
using GeoCop.Api.Validation;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@@ -11,6 +12,7 @@ namespace GeoCop.Api.Controllers
/// Controller for declaring deliveries.
///
[ApiController]
+ [Authorize]
[Route("api/v{version:apiVersion}/[controller]")]
public class DeliveryController : ControllerBase
{
diff --git a/src/GeoCop.Api/Controllers/DownloadController.cs b/src/GeoCop.Api/Controllers/DownloadController.cs
index 34bb26e6..3db98038 100644
--- a/src/GeoCop.Api/Controllers/DownloadController.cs
+++ b/src/GeoCop.Api/Controllers/DownloadController.cs
@@ -60,7 +60,7 @@ public IActionResult Download(Guid jobId, string file)
}
var logFile = fileProvider.Open(file);
- var contentType = contentTypeProvider.TryGetContentType(file, out var type) ? type : "application/octet-stream";
+ var contentType = contentTypeProvider.GetContentTypeAsString(file);
var logFileName = Path.GetFileNameWithoutExtension(validationJob.OriginalFileName) + "_log" + Path.GetExtension(file);
return File(logFile, contentType, logFileName);
}
diff --git a/src/GeoCop.Api/GeoCop.Api.csproj b/src/GeoCop.Api/GeoCop.Api.csproj
index 63f837c4..023e6734 100644
--- a/src/GeoCop.Api/GeoCop.Api.csproj
+++ b/src/GeoCop.Api/GeoCop.Api.csproj
@@ -13,6 +13,7 @@
+
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -23,6 +24,8 @@
+
+
diff --git a/src/GeoCop.Api/IContentTypeProviderExtensions.cs b/src/GeoCop.Api/IContentTypeProviderExtensions.cs
new file mode 100644
index 00000000..1ed546dd
--- /dev/null
+++ b/src/GeoCop.Api/IContentTypeProviderExtensions.cs
@@ -0,0 +1,52 @@
+using GeoCop.Api.Models;
+using Microsoft.AspNetCore.StaticFiles;
+using System.Net.Mime;
+
+namespace GeoCop.Api
+{
+ ///
+ /// Provides access to file content types.
+ ///
+ public static class IContentTypeProviderExtensions
+ {
+ private const string DefaultContentType = "application/octet-stream";
+
+ ///
+ /// Returns the for the specified .
+ ///
+ /// The IContentTypeProvider to extend.
+ /// The asset from which the content type should be read.
+ /// The .
+ public static ContentType GetContentType(this IContentTypeProvider contentTypeProvider, Asset asset)
+ {
+ return contentTypeProvider.GetContentType(asset.OriginalFilename);
+ }
+
+ ///
+ /// Returns the for the specified file extension.
+ ///
+ /// The IContentTypeProvider to extend.
+ /// The file from which the content type should be read.
+ /// The .
+ public static ContentType GetContentType(this IContentTypeProvider contentTypeProvider, string fileName)
+ {
+ return new ContentType(contentTypeProvider.GetContentTypeAsString(fileName));
+ }
+
+ ///
+ /// Returns the for the specified file extension.
+ ///
+ /// The IContentTypeProvider to extend.
+ /// The file from which the content type should be read.
+ /// The content type as string.
+ public static string GetContentTypeAsString(this IContentTypeProvider contentTypeProvider, string fileName)
+ {
+ if (!contentTypeProvider.TryGetContentType(fileName, out var contentType))
+ {
+ contentType = DefaultContentType;
+ }
+
+ return contentType;
+ }
+ }
+}
diff --git a/src/GeoCop.Api/Program.cs b/src/GeoCop.Api/Program.cs
index e1fa5950..fa0fd314 100644
--- a/src/GeoCop.Api/Program.cs
+++ b/src/GeoCop.Api/Program.cs
@@ -1,5 +1,6 @@
using Asp.Versioning;
using GeoCop.Api;
+using GeoCop.Api.StacServices;
using GeoCop.Api.Validation;
using GeoCop.Api.Validation.Interlis;
using Microsoft.AspNetCore.Authentication.JwtBearer;
@@ -15,13 +16,21 @@
var builder = WebApplication.CreateBuilder(args);
+builder.Services.AddCors(options =>
+{
+ // DotNetStac.Api uses the "All" policy for access in the STAC browser.
+ options.AddPolicy("All",
+ policy =>
+ {
+ policy.AllowAnyOrigin()
+ .AllowAnyHeader()
+ .AllowAnyMethod();
+ });
+});
+
builder.Services
.AddControllers(options =>
{
- var policy = new AuthorizationPolicyBuilder()
- .RequireAuthenticatedUser()
- .Build();
- options.Filters.Add(new AuthorizeFilter(policy));
})
.AddJsonOptions(options =>
{
@@ -66,7 +75,7 @@
var contentTypeProvider = new FileExtensionContentTypeProvider();
contentTypeProvider.Mappings.TryAdd(".log", "text/plain");
-contentTypeProvider.Mappings.TryAdd(".xtf", "text/xml; charset=utf-8");
+contentTypeProvider.Mappings.TryAdd(".xtf", "application/interlis+xml");
builder.Services.AddSingleton(contentTypeProvider);
builder.Services.AddSingleton();
@@ -87,14 +96,18 @@
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
});
-builder.Services.AddDbContext(options =>
+var configureContextOptions = (DbContextOptionsBuilder options) =>
{
options.UseNpgsql(builder.Configuration.GetConnectionString("Context"), o =>
{
o.UseNetTopologySuite();
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
});
-});
+};
+builder.Services.AddDbContextFactory(configureContextOptions);
+builder.Services.AddDbContext(configureContextOptions);
+
+builder.Services.AddStacData(builder => { });
var app = builder.Build();
@@ -111,6 +124,8 @@
if (app.Environment.IsDevelopment())
{
+ app.UseCors("All");
+
if (!context.DeliveryMandates.Any())
context.SeedTestData();
}
diff --git a/src/GeoCop.Api/StacServices/StacCollectionsProvider.cs b/src/GeoCop.Api/StacServices/StacCollectionsProvider.cs
new file mode 100644
index 00000000..4f1a0792
--- /dev/null
+++ b/src/GeoCop.Api/StacServices/StacCollectionsProvider.cs
@@ -0,0 +1,56 @@
+using Microsoft.EntityFrameworkCore;
+using Stac;
+using Stac.Api.Interfaces;
+using Stac.Api.WebApi.Implementations.Default;
+
+namespace GeoCop.Api.StacServices
+{
+ ///
+ /// Provides access to STAC collections.
+ ///
+ public class StacCollectionsProvider : ICollectionsProvider
+ {
+ private readonly ILogger logger;
+ private readonly IDbContextFactory contextFactory;
+ private readonly StacConverter stacConverter;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public StacCollectionsProvider(ILogger logger, IDbContextFactory contextFactory, StacConverter stacConverter)
+ {
+ this.logger = logger;
+ this.contextFactory = contextFactory;
+ this.stacConverter = stacConverter;
+ }
+
+ ///
+ public Task GetCollectionByIdAsync(string collectionId, IStacApiContext stacApiContext, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ using var db = contextFactory.CreateDbContext();
+ var deliveryMandate = db.DeliveryMandatesWithIncludes.FirstOrDefault(dm => stacConverter.GetCollectionId(dm) == collectionId)
+ ?? throw new InvalidOperationException($"Collection with id {collectionId} does not exist.");
+ var collection = stacConverter.ToStacCollection(deliveryMandate);
+ return Task.FromResult(collection);
+ }
+ catch (Exception ex)
+ {
+ var message = $"Error while getting collection with id {collectionId}";
+ logger.LogError(ex, message);
+ throw new InvalidOperationException(message, ex);
+ }
+ }
+
+ ///
+ public Task> GetCollectionsAsync(IStacApiContext stacApiContext, CancellationToken cancellationToken = default)
+ {
+ using var db = contextFactory.CreateDbContext();
+ var collections = db.DeliveryMandatesWithIncludes.Select(stacConverter.ToStacCollection);
+ stacApiContext.Properties.SetProperty(DefaultConventions.MatchedCountPropertiesKey, collections.Count());
+
+ return Task.FromResult>(collections);
+ }
+ }
+}
diff --git a/src/GeoCop.Api/StacServices/StacConverter.cs b/src/GeoCop.Api/StacServices/StacConverter.cs
new file mode 100644
index 00000000..f6de6e04
--- /dev/null
+++ b/src/GeoCop.Api/StacServices/StacConverter.cs
@@ -0,0 +1,158 @@
+using GeoCop.Api.Models;
+using Itenso.TimePeriod;
+using Microsoft.AspNetCore.StaticFiles;
+using NetTopologySuite.Geometries;
+using Stac;
+using Stac.Api.Interfaces;
+using Stac.Api.WebApi.Services;
+using Stac.Collection;
+
+namespace GeoCop.Api.StacServices
+{
+ ///
+ /// Converts objects of different types to STAC objects.
+ ///
+ public class StacConverter
+ {
+ private IStacLinker StacLinker { get; }
+ private IContentTypeProvider FileContentTypeProvider { get; }
+ private IStacApiContextFactory StacApiContextFactory { get; }
+
+ private const string DeliveryNamePrefix = "Datenabgabe_";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public StacConverter(IStacLinker stacLinker, IStacApiContextFactory stacApiContextFactory, IContentTypeProvider fileContentTypeProvider)
+ {
+ StacLinker = stacLinker;
+ FileContentTypeProvider = fileContentTypeProvider;
+ StacApiContextFactory = stacApiContextFactory;
+ }
+
+ ///
+ /// Returns the collection id for the specified .
+ ///
+ /// The .
+ /// Collection id.
+ public string GetCollectionId(DeliveryMandate mandate) => "coll_" + mandate.Id;
+
+ ///
+ /// Returns the item id for the specified .
+ ///
+ /// The .
+ /// Item id.
+ public string GetItemId(Delivery delivery) => "item_" + delivery.Id;
+
+ ///
+ /// Converts a to a .
+ ///
+ /// The to convert.
+ /// A STAC collection.
+ public StacCollection ToStacCollection(DeliveryMandate mandate)
+ {
+ var collectionId = GetCollectionId(mandate);
+ var items = mandate.Deliveries
+ .Select(ToStacItem)
+ .ToDictionary(i => i.Links.First(l => l.RelationshipType.ToLowerInvariant() == "self").Uri);
+
+ if (items.Values.Count == 0)
+ {
+ var nowTimestamp = DateTime.Now.ToUniversalTime();
+ var extent = new StacExtent(ToStacSpatialExtent(mandate.SpatialExtent), new StacTemporalExtent(nowTimestamp, nowTimestamp));
+ return new StacCollection(collectionId, string.Empty, extent, null, null)
+ {
+ Title = mandate.Name,
+ };
+ }
+ else
+ {
+ var collection = StacCollection.Create(collectionId, string.Empty, items);
+ collection.Title = mandate.Name;
+ return collection;
+ }
+ }
+
+ ///
+ /// Converts a to a .
+ ///
+ /// The to convert.
+ /// The STAC item.
+ public StacItem ToStacItem(Delivery delivery)
+ {
+ var stacId = GetItemId(delivery);
+
+ var item = new StacItem(stacId, ToGeoJsonPolygon(delivery.DeliveryMandate.SpatialExtent))
+ {
+ Collection = GetCollectionId(delivery.DeliveryMandate),
+ Title = DeliveryNamePrefix + delivery.Date.ToString("s"),
+ Description = string.Empty,
+ DateTime = new TimePeriodChain(),
+ };
+ item.DateTime.Setup(delivery.Date, delivery.Date);
+
+ var assets = delivery.Assets.Select(file => ToStacAsset(file, item)).ToDictionary(asset => asset.Title);
+ item.Assets.AddRange(assets);
+
+ var stacApiContext = StacApiContextFactory.Create();
+ StacLinker.Link(item, stacApiContext);
+
+ return item;
+ }
+
+ ///
+ /// Converts a to a .
+ ///
+ /// The to convert.
+ /// The parent to which the asset belongs.
+ /// The STAC asset.
+ public StacAsset ToStacAsset(Asset asset, IStacObject parent)
+ {
+ // TODO: Set correct Url with https://github.com/GeoWerkstatt/geocop/issues/56
+ return new StacAsset(parent, new Uri("https://github.com/GeoWerkstatt/geocop/issues/56"), new List() { asset.AssetType.ToString() }, asset.OriginalFilename, FileContentTypeProvider.GetContentType(asset));
+ }
+
+ ///
+ /// Converts a to a .
+ ///
+ /// The .
+ /// The .
+ public GeoJSON.Net.Geometry.Polygon ToGeoJsonPolygon(Geometry geometry)
+ {
+ var (xMin, yMin, xMax, yMax) = GetCoordinatesBounds(geometry);
+ return new GeoJSON.Net.Geometry.Polygon(new List()
+ {
+ new (new List()
+ {
+ new (xMin, yMin),
+ new (xMax, yMin),
+ new (xMax, yMax),
+ new (xMin, yMax),
+ new (xMin, yMin),
+ }),
+ });
+ }
+
+ ///
+ /// Converts a to a (bounding box of a StacObject).
+ ///
+ /// The .
+ /// The .
+ public StacSpatialExtent ToStacSpatialExtent(Geometry geometry)
+ {
+ var (xMin, yMin, xMax, yMax) = GetCoordinatesBounds(geometry);
+ return new StacSpatialExtent(xMin, yMin, xMax, yMax);
+ }
+
+ private (double xMin, double yMin, double xMax, double yMax) GetCoordinatesBounds(Geometry geometry)
+ {
+ var coordinates = geometry.Coordinates;
+ var xMin = coordinates.Min((Coordinate c) => c.X);
+ var yMin = coordinates.Min((Coordinate c) => c.Y);
+ var xMax = coordinates.Max((Coordinate c) => c.X);
+ var yMax = coordinates.Max((Coordinate c) => c.Y);
+
+ return (xMin, yMin, xMax, yMax);
+ }
+ }
+}
diff --git a/src/GeoCop.Api/StacServices/StacDataServicesProvider.cs b/src/GeoCop.Api/StacServices/StacDataServicesProvider.cs
new file mode 100644
index 00000000..c9d3f048
--- /dev/null
+++ b/src/GeoCop.Api/StacServices/StacDataServicesProvider.cs
@@ -0,0 +1,35 @@
+using Stac.Api.Interfaces;
+
+namespace GeoCop.Api.StacServices
+{
+ ///
+ /// Provides access to STAC data services.
+ ///
+ public class StacDataServicesProvider : IDataServicesProvider
+ {
+ private readonly IServiceProvider serviceProvider;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public StacDataServicesProvider(IServiceProvider serviceProvider)
+ {
+ this.serviceProvider = serviceProvider;
+ }
+
+ ///
+ public ICollectionsProvider GetCollectionsProvider() => serviceProvider.GetRequiredService();
+
+ ///
+ public IItemsBroker GetItemsBroker() => serviceProvider.GetRequiredService();
+
+ ///
+ public IItemsProvider GetItemsProvider() => serviceProvider.GetRequiredService();
+
+ ///
+ public IRootCatalogProvider GetRootCatalogProvider() => serviceProvider.GetRequiredService();
+
+ ///
+ public IStacQueryProvider GetStacQueryProvider(IStacApiContext stacApiContext) => serviceProvider.GetRequiredService();
+ }
+}
diff --git a/src/GeoCop.Api/StacServices/StacItemsBroker.cs b/src/GeoCop.Api/StacServices/StacItemsBroker.cs
new file mode 100644
index 00000000..19981e6b
--- /dev/null
+++ b/src/GeoCop.Api/StacServices/StacItemsBroker.cs
@@ -0,0 +1,35 @@
+using Stac;
+using Stac.Api.Interfaces;
+
+namespace GeoCop.Api.StacServices
+{
+ ///
+ /// Handles STAC items changes.
+ ///
+ public class StacItemsBroker : IItemsBroker
+ {
+ ///
+ public Task CreateItemAsync(StacItem stacItem, IStacApiContext stacApiContext, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ public Task DeleteItemAsync(string featureId, IStacApiContext stacApiContext, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ public Task> RefreshStacCollectionsAsync(IStacApiContext stacApiContext, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ Task IItemsBroker.UpdateItemAsync(StacItem newItem, string featureId, IStacApiContext stacApiContext, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/GeoCop.Api/StacServices/StacItemsProvider.cs b/src/GeoCop.Api/StacServices/StacItemsProvider.cs
new file mode 100644
index 00000000..707e87fd
--- /dev/null
+++ b/src/GeoCop.Api/StacServices/StacItemsProvider.cs
@@ -0,0 +1,90 @@
+using Microsoft.EntityFrameworkCore;
+using Stac;
+using Stac.Api.Interfaces;
+
+namespace GeoCop.Api.StacServices
+{
+ ///
+ /// Provides access to STAC items.
+ ///
+ public class StacItemsProvider : IItemsProvider
+ {
+ private readonly ILogger logger;
+ private readonly IDbContextFactory contextFactory;
+ private readonly StacConverter stacConverter;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public StacItemsProvider(ILogger logger, IDbContextFactory contextFactory, StacConverter stacConverter)
+ {
+ this.logger = logger;
+ this.contextFactory = contextFactory;
+ this.stacConverter = stacConverter;
+ }
+
+ ///
+ public bool AnyItemsExist(IEnumerable items, IStacApiContext stacApiContext)
+ {
+ using var db = contextFactory.CreateDbContext();
+ foreach (var collection in stacApiContext.Collections)
+ {
+ try
+ {
+ return db.DeliveryMandatesWithIncludes.FirstOrDefault(dm => stacConverter.GetCollectionId(dm) == collection)?.Deliveries?.Any() ?? false;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error while checking if items exist.");
+ return false;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ public Task GetItemByIdAsync(string featureId, IStacApiContext stacApiContext, CancellationToken cancellationToken)
+ {
+ try
+ {
+ using var db = contextFactory.CreateDbContext();
+ var delivery = db.DeliveriesWithIncludes
+ .FirstOrDefault(d => stacConverter.GetItemId(d) == featureId && (stacConverter.GetCollectionId(d.DeliveryMandate) == stacApiContext.Collections.First()))
+ ?? throw new InvalidOperationException($"Item with id {featureId} does not exist.");
+ var item = stacConverter.ToStacItem(delivery);
+ return Task.FromResult(item);
+ }
+ catch (Exception ex)
+ {
+ var message = $"Error while getting item with id {featureId}.";
+ logger.LogError(ex, message);
+ throw new InvalidOperationException(message, ex);
+ }
+ }
+
+ ///
+ public string GetItemEtag(string featureId, IStacApiContext stacApiContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ public Task> GetItemsAsync(IStacApiContext stacApiContext, CancellationToken cancellationToken)
+ {
+ IEnumerable items = new List();
+
+ var collectionIds = stacApiContext.Collections?.ToList();
+ using var db = contextFactory.CreateDbContext();
+ var deliveryMandates = db.DeliveryMandatesWithIncludes;
+
+ if (collectionIds?.Any() == true)
+ {
+ deliveryMandates = deliveryMandates.FindAll(dm => collectionIds.Contains(stacConverter.GetCollectionId(dm)));
+ }
+
+ deliveryMandates.ToList().ForEach(dm => items.Concat(dm.Deliveries.Select(d => stacConverter.ToStacItem(d))));
+ return Task.FromResult(items);
+ }
+ }
+}
diff --git a/src/GeoCop.Api/StacServices/StacLinker.cs b/src/GeoCop.Api/StacServices/StacLinker.cs
new file mode 100644
index 00000000..4d205616
--- /dev/null
+++ b/src/GeoCop.Api/StacServices/StacLinker.cs
@@ -0,0 +1,249 @@
+using System.Collections.Specialized;
+using System.Runtime.CompilerServices;
+using System.Web;
+using Microsoft.Extensions.Primitives;
+using Stac;
+using Stac.Api.Clients.Collections;
+using Stac.Api.Interfaces;
+using Stac.Api.Models;
+using Stac.Api.Models.Core;
+using Stac.Api.WebApi.Services;
+
+namespace GeoCop.Api.StacServices
+{
+ ///
+ /// Custom linker for STAC. Based on https://github.com/Terradue/DotNetStac.Api/blob/main/src/Stac.Api.WebApi/Implementations/Default/CollectionBasedStacLinker.cs,
+ /// with some modifications to return correct 'self' links for collections.
+ ///
+ public class StacLinker : IStacLinker
+ {
+ ///
+ public void Link(LandingPage landingPage, IStacApiContext stacApiContext)
+ {
+ var uri = stacApiContext.LinkGenerator.GetUriByAction(stacApiContext.HttpContext, "GetLandingPage", "Core");
+ if (uri == null) throw new InvalidOperationException("Could not generate URL for action GetLandingPage on controller Core.");
+ landingPage.Links.Add(StacLink.CreateSelfLink(new Uri(uri), "application/json"));
+ landingPage.Links.Add(StacLink.CreateRootLink(new Uri(uri), "application/json"));
+ }
+
+ ///
+ public void Link(StacCollection collection, IStacApiContext stacApiContext)
+ {
+ collection.Links.Add(GetSelfLink(collection, stacApiContext));
+ collection.Links.Add(GetRootLink(stacApiContext));
+ }
+
+ ///
+ public void Link(StacCollections collections, IStacApiContext stacApiContext)
+ {
+ collections.Links.Add(GetSelfLink(collections, stacApiContext));
+ collections.Links.Add(GetRootLink(stacApiContext));
+ foreach (StacCollection collection in collections.Collections)
+ {
+ Link(collection, stacApiContext);
+ }
+ }
+
+ ///
+ public void Link(Stac.StacItem item, IStacApiContext stacApiContext)
+ {
+ item.Links.Add(GetSelfLink(item, stacApiContext));
+ item.Links.Add(GetRootLink(stacApiContext));
+ }
+
+ ///
+ public void Link(StacFeatureCollection collection, IStacApiContext stacApiContext)
+ {
+ collection.Links.Add(GetSelfLink(collection, stacApiContext));
+ collection.Links.Add(GetRootLink(stacApiContext));
+ collection.Links.Add(GetParentLink(collection, stacApiContext));
+ AddAdditionalLinks(collection, stacApiContext);
+ }
+
+ private StacApiLink GetRootLink(IStacApiContext stacApiContext)
+ {
+ return new StacApiLink(GetUriByAction(stacApiContext, "GetLandingPage", "Core", new { }, null), "root", null, "application/json");
+ }
+
+ ///
+ /// Create self link for collections.
+ ///
+ /// The for which to create the link.
+ /// The to build the link with.
+ /// A with relationshipType 'self'.
+ protected StacApiLink GetSelfLink(StacCollections stacCollections, IStacApiContext stacApiContext)
+ {
+ return new StacApiLink(GetUriByAction(stacApiContext, "GetCollections", "Collections", new { }, null), "self", "Collections", "application/json");
+ }
+
+ ///
+ /// Create self link for items.
+ ///
+ /// The for which to create the link.
+ /// The to build the link with.
+ /// A with relationshipType 'self'.
+ protected StacApiLink GetSelfLink(Stac.StacItem stacItem, IStacApiContext stacApiContext)
+ {
+ return new StacApiLink(
+ GetUriByAction(
+ stacApiContext,
+ "GetFeature",
+ "Features",
+ new
+ {
+ collectionId = stacItem.Collection,
+ featureId = stacItem.Id,
+ },
+ null),
+ "self",
+ stacItem.Title,
+ stacItem.MediaType.ToString());
+ }
+
+ ///
+ /// Create self link for collection.
+ ///
+ /// The for which to create the link.
+ /// The to build the link with.
+ /// A with relationshipType 'self'.
+ protected StacApiLink GetSelfLink(StacCollection collection, IStacApiContext stacApiContext)
+ {
+ var link = new StacApiLink(GetUriByAction(
+ stacApiContext,
+ "GetCollections",
+ "Collections",
+ new
+ {
+ collectionId = collection.Id,
+ },
+ null),
+ "self",
+ collection.Title,
+ collection.MediaType.ToString());
+ link.Uri = new Uri(link.Uri.ToString().Replace("?collectionId=", "/"));
+ return link;
+ }
+
+ ///
+ /// Create self link for feature collection.
+ ///
+ /// The for which to create the link.
+ /// The to build the link with.
+ /// A with relationshipType 'self'.
+ protected StacApiLink GetSelfLink(StacFeatureCollection collection, IStacApiContext stacApiContext)
+ {
+ var uri = stacApiContext.LinkGenerator.GetUriByRouteValues(stacApiContext.HttpContext, null, stacApiContext.HttpContext.Request.Query.ToDictionary((KeyValuePair x) => x.Key, (KeyValuePair x) => x.Value.ToString()));
+ if (uri == null) throw new InvalidOperationException("Could not generate URL for action GetFeatureCollection on controller Features.");
+ return new StacApiLink(new Uri(uri), "self", null, "application/geo+json");
+ }
+
+ private StacLink GetParentLink(StacFeatureCollection collection, IStacApiContext stacApiContext)
+ {
+ IList collections = stacApiContext.Collections;
+ if (collections != null && collections.Count == 1)
+ {
+ return new StacApiLink(
+ GetUriByAction(
+ stacApiContext,
+ "DescribeCollection",
+ "Collections",
+ new
+ {
+ collectionId = stacApiContext.Collections.First(),
+ },
+ null),
+ "parent",
+ null,
+ "application/json");
+ }
+
+ return new StacApiLink(GetUriByAction(stacApiContext, "GetCollections", "Collections", new { }, null), "parent", null, "application/json");
+ }
+
+ private Uri GetUriByAction(IStacApiContext stacApiContext, string actionName, string controllerName, object? value, IDictionary? queryValues)
+ {
+ string? uriByAction = stacApiContext.LinkGenerator.GetUriByAction(stacApiContext.HttpContext, actionName, controllerName, value);
+ if (uriByAction == null)
+ {
+ DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(61, 3);
+ defaultInterpolatedStringHandler.AppendLiteral("Could not generate URL for action ");
+ defaultInterpolatedStringHandler.AppendFormatted(actionName);
+ defaultInterpolatedStringHandler.AppendLiteral(" on controller ");
+ defaultInterpolatedStringHandler.AppendFormatted(controllerName);
+ defaultInterpolatedStringHandler.AppendLiteral(" with value ");
+ if (value != null) defaultInterpolatedStringHandler.AppendFormatted