diff --git a/.gitignore b/.gitignore
index dfcfd56f..91958d18 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,13 @@
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
@@ -23,6 +33,7 @@ mono_crash.*
[Rr]eleases/
x64/
x86/
+[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
@@ -61,6 +72,9 @@ project.lock.json
project.fragment.lock.json
artifacts/
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
# StyleCop
StyleCopReport.xml
@@ -137,6 +151,9 @@ _TeamCity*
.axoCover/*
!.axoCover/settings.json
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*[.json, .xml, .info]
+
# Visual Studio code coverage results
*.coverage
*.coveragexml
@@ -348,3 +365,6 @@ MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 00000000..5ef10c3d
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,10 @@
+language: csharp
+dist: xenial
+mono: none
+dotnet: 3.0
+solution: "./src/DotNetStac.sln"
+install:
+- dotnet restore src/
+script:
+- dotnet build src/
+- dotnet test src/DotNetStac.Test
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 00000000..a7eaeea2
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,27 @@
+{
+ // Use IntelliSense to find out which attributes exist for C# debugging
+ // Use hover for the description of the existing attributes
+ // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": ".NET Core Launch (console)",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build",
+ // If you have changed target frameworks, make sure to update the program path.
+ "program": "${workspaceFolder}/src/DotNetStac.Test/bin/Debug/netcoreapp3.0/DotNetStac.Test.dll",
+ "args": [],
+ "cwd": "${workspaceFolder}/src/DotNetStac.Test",
+ // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
+ "console": "internalConsole",
+ "stopAtEntry": false
+ },
+ {
+ "name": ".NET Core Attach",
+ "type": "coreclr",
+ "request": "attach",
+ "processId": "${command:pickProcess}"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..e0e1ac93
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "dotnet-test-explorer.testProjectPath": "**/*Test.@(csproj|vbproj|fsproj)"
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 00000000..ffa93901
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,43 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=733558
+ // for the documentation about the tasks.json format
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "build",
+ "command": "dotnet",
+ "type": "shell",
+ "args": [
+ "build",
+ // Ask dotnet build to generate full paths for file names.
+ "/property:GenerateFullPaths=true",
+ // Do not generate summary otherwise it leads to duplicate errors in Problems panel
+ "/consoleloggerparameters:NoSummary",
+ "src/DotNetStac.sln"
+ ],
+ "group": "build",
+ "presentation": {
+ "reveal": "silent"
+ },
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "pack",
+ "command": "dotnet",
+ "type": "shell",
+ "args": [
+ "pack",
+ // Ask dotnet build to generate full paths for file names.
+ "/property:GenerateFullPaths=true",
+ // Do not generate summary otherwise it leads to duplicate errors in Problems panel
+ "/consoleloggerparameters:NoSummary",
+ "src/DotNetStac.sln"
+ ],
+ "group": "build",
+ "presentation": {
+ "reveal": "silent"
+ },
+ "problemMatcher": "$msCompile"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 22121972..8a4672dc 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,260 @@
-# DotNetStac
-.Net library for working with any SpatioTemporal Asset Catalog (STAC)
+
+
+
DotNetStac
+
+
+
+.Net library for working with Spatio Temporal Asset Catalogs (STAC)
+
+ ![](docs/logo/logo-wide.png)
+
+
+
+
+
+ [![Build Status](https://travis-ci.com/Terradue/DotNetStac.svg?branch=develop)](https://travis-ci.com/Terradue/DotNetStac)
+ [![NuGet](https://img.shields.io/nuget/vpre/DotNetStac)](https://www.nuget.org/packages/DotNetStac/)
+ [![Gitter](https://img.shields.io/gitter/room/SpatioTemporal-Asset-Catalog/Lobby?color=yellow)](https://gitter.im/SpatioTemporal-Asset-Catalog/Lobby)
+ [![License](https://img.shields.io/badge/license-AGPL3-blue.svg)](LICENSE)
+
+
+
+
+
+**DotNetStac** helps you to work with [STAC](https://stacspec.org) ([catalog](https://github.com/radiantearth/stac-spec/tree/master/catalog-spec), [collection](https://github.com/radiantearth/stac-spec/tree/master/collection-spec), [item](https://github.com/radiantearth/stac-spec/tree/master/catalog-spec))
+
+In a nutshell, the library allows manipulating STAC JSON documents (Serialization/Deserialization using [Newtonsoft.JSON](https://www.newtonsoft.com/json)) with properties represented in enhanced objects such as geometries, time stamp/period/span, numerical values and many more via STAC extension plugins engine.
+
+## Features
+
+### Current features
+
+* (De)Serialization engine supporting current and older versions of the specifications with an upgrade mechanism
+* Navigation methods to seamlessly traverse a STAC catalog through collections and items
+* Enhanced extensions support with plugin system for embedding extension related functions (e.g. sat: orbit file download, sar: interferometric search, eo: calibration parameters)
+
+### Other features to come
+
+* STAC API Client
+* Importers from other spatio temporal domains (e.g. OGC O&M, Earth Observation profile…)
+
+## Getting Started
+
+### Installation
+
+DotNetStac can be installed using the Nuget package manager or the `dotnet` CLI.
+
+```
+dotnet add package DotNetStac
+```
+
+### Example #1 : Deserialization and Navigation
+
+The following example load a catalog and loads all its items
+
+```csharp
+using Stac;
+
+ IStacCatalog catalog = (IStacCatalog)StacFactory.Load("https://cbers-stac-0-7.s3.amazonaws.com/CBERS4/MUX/027/069/catalog.json");
+
+ Console.Out.WriteLine(catalog.Id);
+ Console.Out.WriteLine(catalog.StacVersion);
+
+ foreach (var item in catalog.GetItems().Values)
+ {
+ Console.Out.WriteLine(item.Id);
+ }
+
+```
+
+### Example #2 : Serialization of a created collection
+
+The following example load a catalog and loads all its items
+
+```csharp
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Stac;
+using Stac.Catalog;
+using Stac.Collection;
+using System;
+using System.Collections.Generic;
+
+ StacExtent extent = new StacExtent();
+ extent.Spatial = new StacSpatialExtent(-180, -56, 180, 83);
+ extent.Temporal = new StacTemporalExtent(DateTime.Parse("2015-06-23T00:00:00Z").ToUniversalTime(), null);
+
+ StacCollection collection = new StacCollection("COPERNICUS/S2",
+ "Sentinel-2 is a wide-swath, high-resolution, multi-spectral\nimaging mission supporting Copernicus Land Monitoring studies,...",
+ extent);
+
+ collection.Title = "Sentinel-2 MSI: MultiSpectral Instrument, Level-1C";
+
+ collection.Links.Add(StacLink.CreateSelfLink(new Uri("https://storage.cloud.google.com/earthengine-test/catalog/COPERNICUS_S2.json")));
+ collection.Links.Add(StacLink.CreateParentLink(new Uri("https://storage.cloud.google.com/earthengine-test/catalog/catalog.json")));
+ collection.Links.Add(StacLink.CreateRootLink(new Uri("https://storage.cloud.google.com/earthengine-test/catalog/catalog.json")));
+ collection.Links.Add(new StacLink(new Uri("https://scihub.copernicus.eu/twiki/pub/SciHubWebPortal/TermsConditions/Sentinel_Data_Terms_and_Conditions.pdf"), "license", "Legal notice on the use of Copernicus Sentinel Data and Service Information", null));
+
+ collection.Keywords = new System.Collections.ObjectModel.Collection(new string[] {
+ "copernicus",
+ "esa",
+ "eu",
+ "msi",
+ "radiance",
+ "sentinel"});
+
+ collection.Providers = new System.Collections.ObjectModel.Collection(
+ new StacProvider[]{new StacProvider("European Union/ESA/Copernicus"){
+ Roles = new List() { StacProviderRole.producer, StacProviderRole.licensor},
+ Uri = new Uri("https://sentinel.esa.int/web/sentinel/user-guides/sentinel-2-msi")
+ }});
+
+ collection.Summaries.Add("datetime",
+ new StacSummaryStatsObject(
+ DateTime.Parse("2015-06-23T00:00:00Z").ToUniversalTime(),
+ DateTime.Parse("2019-07-10T13:44:56Z").ToUniversalTime()
+ )
+ );
+
+ collection.Summaries.Add("platform",
+ new StacSummaryValueSet(new string[] { "sentinel-2a", "sentinel-2b" })
+ );
+
+ collection.Summaries.Add("constellation",
+ new StacSummaryValueSet(new string[] { "sentinel-2" })
+ );
+
+ collection.Summaries.Add("instruments",
+ new StacSummaryValueSet(new string[] { "msi" })
+ );
+
+ collection.Summaries.Add("view:off_nadir",
+ new StacSummaryStatsObject(
+ 0.0,
+ 100
+ )
+ );
+
+ collection.Summaries.Add("view:sun_elevation",
+ new StacSummaryStatsObject(
+ 6.78,
+ 89.9
+ )
+ );
+
+ collection.Summaries.Add("sci:citation",
+ new StacSummaryValueSet(new string[] { "Copernicus Sentinel data [Year]" })
+ );
+
+ collection.Summaries.Add("gsd",
+ new StacSummaryValueSet(new int[] {
+ 10,
+ 30,
+ 60
+ })
+ );
+
+ collection.Summaries.Add("proj:epsg",
+ new StacSummaryValueSet(new int[]
+ { 32601,32602,32603,32604,32605,32606,32607,32608,32609,32610,32611,32612,32613,32614,32615,32616,32617,32618,32619,32620,32621,32622,32623,32624,32625,32626,32627,32628,32629,32630,32631,32632,32633,32634,32635,32636,32637,32638,32639,32640,32641,32642,32643,32644,32645,32646,32647,32648,32649,32650,32651,32652,32653,32654,32655,32656,32657,32658,32659,32660}
+ )
+ );
+
+ collection.Summaries.Add("eo:bands",
+ new StacSummaryValueSet(new JObject[] {
+ new JObject {
+ { "name", "B1" },
+ { "common_name", "coastal" },
+ { "center_wavelength", 4.439 }
+ },
+ new JObject {
+ { "name", "B2"},
+ { "common_name", "blue"},
+ { "center_wavelength", 4.966}
+ },
+ new JObject {
+ { "name", "B3"},
+ { "common_name", "green"},
+ { "center_wavelength", 5.6}
+ },
+ new JObject {
+ { "name", "B4"},
+ { "common_name", "red"},
+ { "center_wavelength", 6.645}
+ },
+ new JObject {
+ { "name", "B5"},
+ { "center_wavelength", 7.039}
+ },
+ new JObject {
+ { "name", "B6"},
+ { "center_wavelength", 7.402}
+ },
+ new JObject {
+ { "name", "B7"},
+ { "center_wavelength", 7.825}
+ },
+ new JObject {
+ { "name", "B8"},
+ { "common_name", "nir"},
+ { "center_wavelength", 8.351}
+ },
+ new JObject {
+ { "name", "B8A"},
+ { "center_wavelength", 8.648}
+ },
+ new JObject {
+ { "name", "B9"},
+ { "center_wavelength", 9.45}
+ },
+ new JObject {
+ { "name", "B10"},
+ { "center_wavelength", 1.3735}
+ },
+ new JObject {
+ { "name", "B11"},
+ { "common_name", "swir16"},
+ { "center_wavelength", 1.6137}
+ },
+ new JObject {
+ { "name", "B12"},
+ { "common_name", "swir22"},
+ { "center_wavelength", 2.2024}
+ }
+ })
+ );
+
+ var json = JsonConvert.SerializeObject(collection);
+
+ Console.WriteLine(json);
+
+```
+
+## Documentation
+
+*Full Documentation shall be available soon*
+
+## Developing
+
+To ensure development libraries are installed, restore all dependencies
+
+```
+> dotnet restore src
+```
+
+### Unit Tests
+
+Unit tests are in the `src/DotNetStac.Test` folder. To run unit tests:
+
+```
+> dotnet test src
+```
+
diff --git a/src/DotNetStac.Test/Catalog/CatalogTests.cs b/src/DotNetStac.Test/Catalog/CatalogTests.cs
new file mode 100644
index 00000000..38d0b19e
--- /dev/null
+++ b/src/DotNetStac.Test/Catalog/CatalogTests.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Linq;
+using Newtonsoft.Json;
+using Stac.Catalog;
+using Xunit;
+
+namespace Stac.Test.Catalog
+{
+ public class CatalogTests : TestBase
+ {
+ [Fact]
+ public void CanDeserializeMinimalSample()
+ {
+ var json = GetExpectedJson("Catalog");
+
+ var catalog = JsonConvert.DeserializeObject(json);
+
+ Assert.NotNull(catalog);
+
+ Assert.Equal("1.0.0-beta.1", catalog.StacVersion);
+
+ Assert.Empty(catalog.StacExtensions);
+
+ Assert.Equal("NAIP", catalog.Id);
+
+ Assert.Equal("Catalog of NAIP Imagery", catalog.Description);
+
+ Assert.Contains(catalog.Links, link => link.RelationshipType == "self" && link.Uri == new Uri("https://www.fsa.usda.gov/naip/catalog.json") );
+ Assert.Contains(catalog.Links, link => link.RelationshipType == "child" && link.Uri == new Uri("https://www.fsa.usda.gov/naip/30087/catalog.json") );
+ Assert.Contains(catalog.Links, link => link.RelationshipType == "root" && link.Uri == new Uri("https://www.fsa.usda.gov/catalog.json") );
+
+
+ }
+
+ [Fact]
+ public void CanSerializeMinimalSample()
+ {
+
+ StacCatalog collection = new StacCatalog("NAIP", "Catalog of NAIP Imagery");
+
+ collection.Links.Add(StacLink.CreateSelfLink(new Uri("https://www.fsa.usda.gov/naip/catalog.json")));
+ collection.Links.Add(new StacLink(new Uri("https://www.fsa.usda.gov/naip/30087/catalog.json"), "child", null, null));
+ collection.Links.Add(StacLink.CreateRootLink(new Uri("https://www.fsa.usda.gov/catalog.json")));
+
+ var actualJson = JsonConvert.SerializeObject(collection);
+
+ Console.WriteLine(actualJson);
+
+ var expectedJson = GetExpectedJson("Catalog");
+
+ JsonAssert.AreEqual(expectedJson, actualJson);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac.Test/Collection/CollectionTests.cs b/src/DotNetStac.Test/Collection/CollectionTests.cs
new file mode 100644
index 00000000..b17a80db
--- /dev/null
+++ b/src/DotNetStac.Test/Collection/CollectionTests.cs
@@ -0,0 +1,210 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using GeoJSON.Net;
+using GeoJSON.Net.Geometry;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Stac.Collection;
+using Xunit;
+
+namespace Stac.Test.Collection
+{
+ public class CollectionTests : TestBase
+ {
+ [Fact]
+ public void CanDeserializeSentinel2Sample()
+ {
+ var json = GetExpectedJson("Collection");
+
+ var item = JsonConvert.DeserializeObject(json);
+
+ Assert.NotNull(item);
+
+ Assert.NotNull(item.Summaries);
+
+ Assert.Equal("1.0.0-beta.1", item.StacVersion);
+
+ Assert.Empty(item.StacExtensions);
+
+ Assert.NotEmpty(item.Summaries);
+
+ Assert.True(item.Summaries.ContainsKey("datetime"));
+ Assert.True(item.Summaries.ContainsKey("platform"));
+ Assert.True(item.Summaries.ContainsKey("constellation"));
+ Assert.True(item.Summaries.ContainsKey("view:off_nadir"));
+ Assert.True(item.Summaries.ContainsKey("eo:bands"));
+
+ Assert.IsType>(item.Summaries["datetime"]);
+
+ Assert.Equal(DateTime.Parse("2015-06-23T00:00:00Z").ToUniversalTime(), (item.Summaries["datetime"] as StacSummaryStatsObject).Min);
+
+ Assert.Equal(32601, (item.Summaries["proj:epsg"] as StacSummaryValueSet).Min());
+ Assert.Equal(32660, (item.Summaries["proj:epsg"] as StacSummaryValueSet).Max());
+
+ Assert.Equal(13, item.Summaries["eo:bands"].LongCount());
+ Assert.Equal("B1", item.Summaries["eo:bands"][0]["name"]);
+ Assert.Equal(4.439, item.Summaries["eo:bands"][0]["center_wavelength"]);
+
+ Assert.Equal(2, item.Summaries["view:sun_elevation"].LongCount());
+
+ }
+
+ [Fact]
+ public void CanSerializeSentinel2Sample()
+ {
+ StacExtent extent = new StacExtent();
+ extent.Spatial = new StacSpatialExtent(-180, -56, 180, 83);
+ extent.Temporal = new StacTemporalExtent(DateTime.Parse("2015-06-23T00:00:00Z").ToUniversalTime(), null);
+
+ StacCollection collection = new StacCollection("COPERNICUS/S2",
+ "Sentinel-2 is a wide-swath, high-resolution, multi-spectral\nimaging mission supporting Copernicus Land Monitoring studies,\nincluding the monitoring of vegetation, soil and water cover,\nas well as observation of inland waterways and coastal areas.\n\nThe Sentinel-2 data contain 13 UINT16 spectral bands representing\nTOA reflectance scaled by 10000. See the [Sentinel-2 User Handbook](https://sentinel.esa.int/documents/247904/685211/Sentinel-2_User_Handbook)\nfor details. In addition, three QA bands are present where one\n(QA60) is a bitmask band with cloud mask information. For more\ndetails, [see the full explanation of how cloud masks are computed.](https://sentinel.esa.int/web/sentinel/technical-guides/sentinel-2-msi/level-1c/cloud-masks)\n\nEach Sentinel-2 product (zip archive) may contain multiple\ngranules. Each granule becomes a separate Earth Engine asset.\nEE asset ids for Sentinel-2 assets have the following format:\nCOPERNICUS/S2/20151128T002653_20151128T102149_T56MNN. Here the\nfirst numeric part represents the sensing date and time, the\nsecond numeric part represents the product generation date and\ntime, and the final 6-character string is a unique granule identifier\nindicating its UTM grid reference (see [MGRS](https://en.wikipedia.org/wiki/Military_Grid_Reference_System)).\n\nFor more details on Sentinel-2 radiometric resoltuon, [see this page](https://earth.esa.int/web/sentinel/user-guides/sentinel-2-msi/resolutions/radiometric).\n",
+ extent);
+
+ collection.Title = "Sentinel-2 MSI: MultiSpectral Instrument, Level-1C";
+
+ collection.Links.Add(StacLink.CreateSelfLink(new Uri("https://storage.cloud.google.com/earthengine-test/catalog/COPERNICUS_S2.json")));
+ collection.Links.Add(StacLink.CreateParentLink(new Uri("https://storage.cloud.google.com/earthengine-test/catalog/catalog.json")));
+ collection.Links.Add(StacLink.CreateRootLink(new Uri("https://storage.cloud.google.com/earthengine-test/catalog/catalog.json")));
+ collection.Links.Add(new StacLink(new Uri("https://scihub.copernicus.eu/twiki/pub/SciHubWebPortal/TermsConditions/Sentinel_Data_Terms_and_Conditions.pdf"), "license", "Legal notice on the use of Copernicus Sentinel Data and Service Information", null));
+
+ collection.Keywords = new System.Collections.ObjectModel.Collection(new string[] {
+ "copernicus",
+ "esa",
+ "eu",
+ "msi",
+ "radiance",
+ "sentinel"});
+
+ collection.Providers = new System.Collections.ObjectModel.Collection(
+ new StacProvider[]{new StacProvider("European Union/ESA/Copernicus"){
+ Roles = new List() { StacProviderRole.producer, StacProviderRole.licensor},
+ Uri = new Uri("https://sentinel.esa.int/web/sentinel/user-guides/sentinel-2-msi")
+ }});
+
+ collection.Summaries.Add("datetime",
+ new StacSummaryStatsObject(
+ DateTime.Parse("2015-06-23T00:00:00Z").ToUniversalTime(),
+ DateTime.Parse("2019-07-10T13:44:56Z").ToUniversalTime()
+ )
+ );
+
+ collection.Summaries.Add("platform",
+ new StacSummaryValueSet(new string[] { "sentinel-2a", "sentinel-2b" })
+ );
+
+ collection.Summaries.Add("constellation",
+ new StacSummaryValueSet(new string[] { "sentinel-2" })
+ );
+
+ collection.Summaries.Add("instruments",
+ new StacSummaryValueSet(new string[] { "msi" })
+ );
+
+ collection.Summaries.Add("view:off_nadir",
+ new StacSummaryStatsObject(
+ 0.0,
+ 100
+ )
+ );
+
+ collection.Summaries.Add("view:sun_elevation",
+ new StacSummaryStatsObject(
+ 6.78,
+ 89.9
+ )
+ );
+
+ collection.Summaries.Add("sci:citation",
+ new StacSummaryValueSet(new string[] { "Copernicus Sentinel data [Year]" })
+ );
+
+ collection.Summaries.Add("gsd",
+ new StacSummaryValueSet(new int[] {
+ 10,
+ 30,
+ 60
+ })
+ );
+
+ collection.Summaries.Add("proj:epsg",
+ new StacSummaryValueSet(new int[]
+ { 32601,32602,32603,32604,32605,32606,32607,32608,32609,32610,32611,32612,32613,32614,32615,32616,32617,32618,32619,32620,32621,32622,32623,32624,32625,32626,32627,32628,32629,32630,32631,32632,32633,32634,32635,32636,32637,32638,32639,32640,32641,32642,32643,32644,32645,32646,32647,32648,32649,32650,32651,32652,32653,32654,32655,32656,32657,32658,32659,32660}
+ )
+ );
+
+ collection.Summaries.Add("eo:bands",
+ new StacSummaryValueSet(new JObject[] {
+ new JObject {
+ { "name", "B1" },
+ { "common_name", "coastal" },
+ { "center_wavelength", 4.439 }
+ },
+ new JObject {
+ { "name", "B2"},
+ { "common_name", "blue"},
+ { "center_wavelength", 4.966}
+ },
+ new JObject {
+ { "name", "B3"},
+ { "common_name", "green"},
+ { "center_wavelength", 5.6}
+ },
+ new JObject {
+ { "name", "B4"},
+ { "common_name", "red"},
+ { "center_wavelength", 6.645}
+ },
+ new JObject {
+ { "name", "B5"},
+ { "center_wavelength", 7.039}
+ },
+ new JObject {
+ { "name", "B6"},
+ { "center_wavelength", 7.402}
+ },
+ new JObject {
+ { "name", "B7"},
+ { "center_wavelength", 7.825}
+ },
+ new JObject {
+ { "name", "B8"},
+ { "common_name", "nir"},
+ { "center_wavelength", 8.351}
+ },
+ new JObject {
+ { "name", "B8A"},
+ { "center_wavelength", 8.648}
+ },
+ new JObject {
+ { "name", "B9"},
+ { "center_wavelength", 9.45}
+ },
+ new JObject {
+ { "name", "B10"},
+ { "center_wavelength", 1.3735}
+ },
+ new JObject {
+ { "name", "B11"},
+ { "common_name", "swir16"},
+ { "center_wavelength", 1.6137}
+ },
+ new JObject {
+ { "name", "B12"},
+ { "common_name", "swir22"},
+ { "center_wavelength", 2.2024}
+ }
+ })
+ );
+
+ var actualJson = JsonConvert.SerializeObject(collection);
+
+ Console.WriteLine(actualJson);
+
+ var expectedJson = GetExpectedJson("Collection");
+
+ JsonAssert.AreEqual(expectedJson, actualJson);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac.Test/DotNetStac.Test.csproj b/src/DotNetStac.Test/DotNetStac.Test.csproj
new file mode 100644
index 00000000..95801da8
--- /dev/null
+++ b/src/DotNetStac.Test/DotNetStac.Test.csproj
@@ -0,0 +1,20 @@
+
+
+ netcoreapp3.0
+ false
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/DotNetStac.Test/Examples/Example1Test.cs b/src/DotNetStac.Test/Examples/Example1Test.cs
new file mode 100644
index 00000000..ba447613
--- /dev/null
+++ b/src/DotNetStac.Test/Examples/Example1Test.cs
@@ -0,0 +1,182 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Stac;
+using Stac.Catalog;
+using Stac.Collection;
+using System;
+using System.Collections.Generic;
+using Xunit;
+
+namespace Stac.Test.Example
+{
+ public class Example1Test
+ {
+ [Fact]
+ public void Deserialize()
+ {
+ IStacCatalog catalog = (IStacCatalog)StacFactory.Load("https://cbers-stac-0-7.s3.amazonaws.com/CBERS4/MUX/027/069/catalog.json");
+
+ Console.Out.WriteLine(catalog.Id);
+ Console.Out.WriteLine(catalog.StacVersion);
+
+ foreach (var item in catalog.GetItems().Values)
+ {
+ Console.Out.WriteLine(item.Id);
+ }
+ }
+
+ [Fact]
+ public void CanSerializeSentinel2Sample()
+ {
+ StacExtent extent = new StacExtent();
+ extent.Spatial = new StacSpatialExtent(-180, -56, 180, 83);
+ extent.Temporal = new StacTemporalExtent(DateTime.Parse("2015-06-23T00:00:00Z").ToUniversalTime(), null);
+
+ StacCollection collection = new StacCollection("COPERNICUS/S2",
+ "Sentinel-2 is a wide-swath, high-resolution, multi-spectral\nimaging mission supporting Copernicus Land Monitoring studies,\nincluding the monitoring of vegetation, soil and water cover,\nas well as observation of inland waterways and coastal areas.\n\nThe Sentinel-2 data contain 13 UINT16 spectral bands representing\nTOA reflectance scaled by 10000. See the [Sentinel-2 User Handbook](https://sentinel.esa.int/documents/247904/685211/Sentinel-2_User_Handbook)\nfor details. In addition, three QA bands are present where one\n(QA60) is a bitmask band with cloud mask information. For more\ndetails, [see the full explanation of how cloud masks are computed.](https://sentinel.esa.int/web/sentinel/technical-guides/sentinel-2-msi/level-1c/cloud-masks)\n\nEach Sentinel-2 product (zip archive) may contain multiple\ngranules. Each granule becomes a separate Earth Engine asset.\nEE asset ids for Sentinel-2 assets have the following format:\nCOPERNICUS/S2/20151128T002653_20151128T102149_T56MNN. Here the\nfirst numeric part represents the sensing date and time, the\nsecond numeric part represents the product generation date and\ntime, and the final 6-character string is a unique granule identifier\nindicating its UTM grid reference (see [MGRS](https://en.wikipedia.org/wiki/Military_Grid_Reference_System)).\n\nFor more details on Sentinel-2 radiometric resoltuon, [see this page](https://earth.esa.int/web/sentinel/user-guides/sentinel-2-msi/resolutions/radiometric).\n",
+ extent);
+
+ collection.Title = "Sentinel-2 MSI: MultiSpectral Instrument, Level-1C";
+
+ collection.Links.Add(StacLink.CreateSelfLink(new Uri("https://storage.cloud.google.com/earthengine-test/catalog/COPERNICUS_S2.json")));
+ collection.Links.Add(StacLink.CreateParentLink(new Uri("https://storage.cloud.google.com/earthengine-test/catalog/catalog.json")));
+ collection.Links.Add(StacLink.CreateRootLink(new Uri("https://storage.cloud.google.com/earthengine-test/catalog/catalog.json")));
+ collection.Links.Add(new StacLink(new Uri("https://scihub.copernicus.eu/twiki/pub/SciHubWebPortal/TermsConditions/Sentinel_Data_Terms_and_Conditions.pdf"), "license", "Legal notice on the use of Copernicus Sentinel Data and Service Information", null));
+
+ collection.Keywords = new System.Collections.ObjectModel.Collection(new string[] {
+ "copernicus",
+ "esa",
+ "eu",
+ "msi",
+ "radiance",
+ "sentinel"});
+
+ collection.Providers = new System.Collections.ObjectModel.Collection(
+ new StacProvider[]{new StacProvider("European Union/ESA/Copernicus"){
+ Roles = new List() { StacProviderRole.producer, StacProviderRole.licensor},
+ Uri = new Uri("https://sentinel.esa.int/web/sentinel/user-guides/sentinel-2-msi")
+ }});
+
+ collection.Summaries.Add("datetime",
+ new StacSummaryStatsObject(
+ DateTime.Parse("2015-06-23T00:00:00Z").ToUniversalTime(),
+ DateTime.Parse("2019-07-10T13:44:56Z").ToUniversalTime()
+ )
+ );
+
+ collection.Summaries.Add("platform",
+ new StacSummaryValueSet(new string[] { "sentinel-2a", "sentinel-2b" })
+ );
+
+ collection.Summaries.Add("constellation",
+ new StacSummaryValueSet(new string[] { "sentinel-2" })
+ );
+
+ collection.Summaries.Add("instruments",
+ new StacSummaryValueSet(new string[] { "msi" })
+ );
+
+ collection.Summaries.Add("view:off_nadir",
+ new StacSummaryStatsObject(
+ 0.0,
+ 100
+ )
+ );
+
+ collection.Summaries.Add("view:sun_elevation",
+ new StacSummaryStatsObject(
+ 6.78,
+ 89.9
+ )
+ );
+
+ collection.Summaries.Add("sci:citation",
+ new StacSummaryValueSet(new string[] { "Copernicus Sentinel data [Year]" })
+ );
+
+ collection.Summaries.Add("gsd",
+ new StacSummaryValueSet(new int[] {
+ 10,
+ 30,
+ 60
+ })
+ );
+
+ collection.Summaries.Add("proj:epsg",
+ new StacSummaryValueSet(new int[]
+ { 32601,32602,32603,32604,32605,32606,32607,32608,32609,32610,32611,32612,32613,32614,32615,32616,32617,32618,32619,32620,32621,32622,32623,32624,32625,32626,32627,32628,32629,32630,32631,32632,32633,32634,32635,32636,32637,32638,32639,32640,32641,32642,32643,32644,32645,32646,32647,32648,32649,32650,32651,32652,32653,32654,32655,32656,32657,32658,32659,32660}
+ )
+ );
+
+ collection.Summaries.Add("eo:bands",
+ new StacSummaryValueSet(new JObject[] {
+ new JObject {
+ { "name", "B1" },
+ { "common_name", "coastal" },
+ { "center_wavelength", 4.439 }
+ },
+ new JObject {
+ { "name", "B2"},
+ { "common_name", "blue"},
+ { "center_wavelength", 4.966}
+ },
+ new JObject {
+ { "name", "B3"},
+ { "common_name", "green"},
+ { "center_wavelength", 5.6}
+ },
+ new JObject {
+ { "name", "B4"},
+ { "common_name", "red"},
+ { "center_wavelength", 6.645}
+ },
+ new JObject {
+ { "name", "B5"},
+ { "center_wavelength", 7.039}
+ },
+ new JObject {
+ { "name", "B6"},
+ { "center_wavelength", 7.402}
+ },
+ new JObject {
+ { "name", "B7"},
+ { "center_wavelength", 7.825}
+ },
+ new JObject {
+ { "name", "B8"},
+ { "common_name", "nir"},
+ { "center_wavelength", 8.351}
+ },
+ new JObject {
+ { "name", "B8A"},
+ { "center_wavelength", 8.648}
+ },
+ new JObject {
+ { "name", "B9"},
+ { "center_wavelength", 9.45}
+ },
+ new JObject {
+ { "name", "B10"},
+ { "center_wavelength", 1.3735}
+ },
+ new JObject {
+ { "name", "B11"},
+ { "common_name", "swir16"},
+ { "center_wavelength", 1.6137}
+ },
+ new JObject {
+ { "name", "B12"},
+ { "common_name", "swir22"},
+ { "center_wavelength", 2.2024}
+ }
+ })
+ );
+
+ var json = JsonConvert.SerializeObject(collection);
+
+ Console.WriteLine(json);
+
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac.Test/Item/ItemTests.cs b/src/DotNetStac.Test/Item/ItemTests.cs
new file mode 100644
index 00000000..bdac0045
--- /dev/null
+++ b/src/DotNetStac.Test/Item/ItemTests.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using GeoJSON.Net;
+using GeoJSON.Net.Geometry;
+using Newtonsoft.Json;
+using Stac.Item;
+using Xunit;
+
+namespace Stac.Test.Item
+{
+ public class ItemTests : TestBase
+ {
+ [Fact]
+ public void CanDeserializeMinimalSample()
+ {
+ var json = GetExpectedJson("Item");
+
+ var item = JsonConvert.DeserializeObject(json);
+
+ Assert.NotNull(item);
+
+ Assert.NotNull(item.Properties);
+
+ Assert.Equal("1.0.0-beta.1", item.StacVersion);
+
+ Assert.Empty(item.StacExtensions);
+
+ Assert.Equal(GeoJSONObjectType.Feature, item.Type);
+
+ Assert.Equal("CS3-20160503_132130_04", item.Id);
+
+ Assert.IsType(item.Geometry);
+
+ Assert.Equal(new double[4] { -122.59750209, 37.48803556, -122.2880486, 37.613537207 }, item.BoundingBoxes);
+
+ Assert.True(item.Properties.ContainsKey("collection"));
+ Assert.Equal("CS3", item.Properties["collection"]);
+
+ Assert.Equal(DateTime.Parse("2016-05-03T13:21:30.040Z").ToUniversalTime(), item.Properties["datetime"]);
+
+ Assert.Contains(item.Links, l => l.RelationshipType == "self" && l.Uri.ToString() == "http://cool-sat.com/catalog/CS3-20160503_132130_04/CS3-20160503_132130_04.json");
+
+ Assert.Contains(item.Links, l => l.RelationshipType == "collection" && l.Uri.ToString() == "http://cool-sat.com/catalog.json");
+
+ Assert.Equal("relative-path/to/analytic.tif", item.Assets["analytic"].Uri.ToString());
+ Assert.Equal("4-Band Analytic", item.Assets["analytic"].Title);
+
+ Assert.Equal("http://cool-sat.com/catalog/CS3-20160503_132130_04/thumbnail.png", item.Assets["thumbnail"].Uri.ToString());
+ Assert.Equal("Thumbnail", item.Assets["thumbnail"].Title);
+ Assert.Contains("thumbnail", item.Assets["thumbnail"].SemanticRoles);
+ }
+
+ [Fact]
+ public void CanSerializeMinimalSample()
+ {
+ var coordinates = new[]
+ {
+ new List
+ {
+ new Position(37.488035566,-122.308150179),
+ new Position(37.538869539,-122.597502109),
+ new Position(37.613537207,-122.576687533),
+ new Position(37.562818007,-122.288048600),
+ new Position(37.488035566,-122.308150179)
+ }
+ };
+
+ var geometry = new Polygon(new LineString[] { new LineString(coordinates[0]) });
+
+ var properties = new Dictionary();
+
+ properties.Add("datetime", DateTime.Parse("2016-05-03T13:21:30.040Z").ToUniversalTime());
+ properties.Add("collection", "CS3");
+
+ StacItem item = new StacItem(geometry, properties, "CS3-20160503_132130_04");
+
+ item.Links.Add(StacLink.CreateSelfLink(new Uri("http://cool-sat.com/catalog/CS3-20160503_132130_04/CS3-20160503_132130_04.json")));
+ item.Links.Add(StacLink.CreateCollectionLink(new Uri("http://cool-sat.com/catalog.json")));
+
+ item.Assets.Add("analytic", new StacAsset(new Uri("relative-path/to/analytic.tif", UriKind.Relative), null, "4-Band Analytic", null));
+ item.Assets.Add("thumbnail", StacAsset.CreateThumbnailAsset(new Uri("http://cool-sat.com/catalog/CS3-20160503_132130_04/thumbnail.png"), null, "Thumbnail"));
+
+ item.BoundingBoxes = new double[4] { -122.59750209, 37.48803556, -122.2880486, 37.613537207 };
+
+ var actualJson = JsonConvert.SerializeObject(item);
+
+ Console.WriteLine(actualJson);
+
+ var expectedJson = GetExpectedJson("Item");
+
+ JsonAssert.AreEqual(expectedJson, actualJson);
+ }
+
+ [Fact]
+ public void CanManageDates()
+ {
+ var json = GetExpectedJson("Item");
+
+ var item = JsonConvert.DeserializeObject(json);
+
+ Assert.Equal(item.DateTime, new Itenso.TimePeriod.TimeInterval(DateTime.Parse("2016-05-03T13:22:30Z").ToUniversalTime()));
+ }
+ }
+}
diff --git a/src/DotNetStac.Test/JsonAssert.cs b/src/DotNetStac.Test/JsonAssert.cs
new file mode 100644
index 00000000..cde6cb99
--- /dev/null
+++ b/src/DotNetStac.Test/JsonAssert.cs
@@ -0,0 +1,95 @@
+using System.Linq;
+using Newtonsoft.Json.Linq;
+using Xunit;
+
+namespace Stac.Test
+{
+ ///
+ /// Assertions for json strings
+ ///
+ public static class JsonAssert
+ {
+ ///
+ /// Asserts that the json strings are equal.
+ ///
+ ///
+ /// Parses each json string into a , sorts the properties of each
+ /// and then serializes each back to a json string for comparison.
+ ///
+ /// The expect json.
+ /// The actual json.
+ public static void AreEqual(string expectJson, string actualJson)
+ {
+ Assert.Equal(
+ JObject.Parse(expectJson).SortProperties().ToString(),
+ JObject.Parse(actualJson).SortProperties().ToString());
+ }
+
+ ///
+ /// Asserts that contains
+ ///
+ /// The expected json.
+ /// The actual json.
+ public static void Contains(string expectedJson, string actualJson)
+ {
+ Assert.True(actualJson.Contains(expectedJson), string.Format("expected {0} to contain {1}", actualJson, expectedJson));
+ }
+
+ ///
+ /// Sorts the properties of a JObject
+ ///
+ /// The json object whhose properties to sort
+ /// A new instance of a with sorted properties
+ private static JObject SortProperties(this JObject jObject)
+ {
+ var result = new JObject();
+
+ foreach (var property in jObject.Properties().OrderBy(p => p.Name))
+ {
+ var value = property.Value as JObject;
+
+ if (value != null)
+ {
+ value = value.SortProperties();
+ result.Add(property.Name, value);
+ continue;
+ }
+
+ var avalues = property.Value as JArray;
+
+ if (avalues != null)
+ {
+ avalues = avalues.SortProperties();
+ result.Add(property.Name, avalues);
+ continue;
+ }
+
+ result.Add(property.Name, property.Value);
+ }
+
+ return result;
+ }
+
+ private static JArray SortProperties(this JArray jArray)
+ {
+ var result = new JArray();
+
+ foreach (var item in jArray)
+ {
+ var value = item as JObject;
+
+ if (value != null)
+ {
+ value = value.SortProperties();
+ result.Add(value);
+ }
+ else
+ {
+ result.Add(item);
+ }
+ }
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac.Test/PriorityOrderer.cs b/src/DotNetStac.Test/PriorityOrderer.cs
new file mode 100644
index 00000000..2d6d92a4
--- /dev/null
+++ b/src/DotNetStac.Test/PriorityOrderer.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace Stac.Test
+{
+ public class PriorityOrderer : ITestCaseOrderer
+{
+ public IEnumerable OrderTestCases(IEnumerable testCases) where TTestCase : ITestCase
+ {
+ var sortedMethods = new SortedDictionary>();
+
+ foreach (TTestCase testCase in testCases)
+ {
+ int priority = 0;
+
+ foreach (IAttributeInfo attr in testCase.TestMethod.Method.GetCustomAttributes((typeof(TestPriorityAttribute).AssemblyQualifiedName)))
+ priority = attr.GetNamedArgument("Priority");
+
+ GetOrCreate(sortedMethods, priority).Add(testCase);
+ }
+
+ foreach (var list in sortedMethods.Keys.Select(priority => sortedMethods[priority]))
+ {
+ list.Sort((x, y) => StringComparer.OrdinalIgnoreCase.Compare(x.TestMethod.Method.Name, y.TestMethod.Method.Name));
+ foreach (TTestCase testCase in list) yield return testCase;
+
+ }
+ }
+
+ static TValue GetOrCreate(IDictionary dictionary, TKey key)
+ where TValue : new()
+ {
+ TValue result;
+
+ if (dictionary.TryGetValue(key, out result)) return result;
+
+ result = new TValue();
+ dictionary[key] = result;
+
+ return result;
+ }
+}
+}
diff --git a/src/DotNetStac.Test/Resources/Catalog/CatalogTests_CanDeserializeMinimalSample.json b/src/DotNetStac.Test/Resources/Catalog/CatalogTests_CanDeserializeMinimalSample.json
new file mode 100644
index 00000000..2143f65c
--- /dev/null
+++ b/src/DotNetStac.Test/Resources/Catalog/CatalogTests_CanDeserializeMinimalSample.json
@@ -0,0 +1,10 @@
+{
+ "stac_version": "1.0.0-beta.1",
+ "id": "NAIP",
+ "description": "Catalog of NAIP Imagery",
+ "links": [
+ { "rel": "self", "href": "https://www.fsa.usda.gov/naip/catalog.json" },
+ { "rel": "child", "href": "https://www.fsa.usda.gov/naip/30087/catalog.json" },
+ { "rel": "root", "href": "https://www.fsa.usda.gov/catalog.json" }
+ ]
+ }
\ No newline at end of file
diff --git a/src/DotNetStac.Test/Resources/Catalog/CatalogTests_CanSerializeMinimalSample.json b/src/DotNetStac.Test/Resources/Catalog/CatalogTests_CanSerializeMinimalSample.json
new file mode 100644
index 00000000..740d41db
--- /dev/null
+++ b/src/DotNetStac.Test/Resources/Catalog/CatalogTests_CanSerializeMinimalSample.json
@@ -0,0 +1,11 @@
+{
+ "stac_version": "1.0.0-beta.1",
+ "stac_extensions": [],
+ "id": "NAIP",
+ "description": "Catalog of NAIP Imagery",
+ "links": [
+ { "rel": "self", "href": "https://www.fsa.usda.gov/naip/catalog.json" },
+ { "rel": "child", "href": "https://www.fsa.usda.gov/naip/30087/catalog.json" },
+ { "rel": "root", "href": "https://www.fsa.usda.gov/catalog.json" }
+ ]
+ }
\ No newline at end of file
diff --git a/src/DotNetStac.Test/Resources/Collection/CollectionTests_CanDeserializeSentinel2Sample.json b/src/DotNetStac.Test/Resources/Collection/CollectionTests_CanDeserializeSentinel2Sample.json
new file mode 100644
index 00000000..368b2643
--- /dev/null
+++ b/src/DotNetStac.Test/Resources/Collection/CollectionTests_CanDeserializeSentinel2Sample.json
@@ -0,0 +1,148 @@
+{
+ "stac_version": "1.0.0-beta.1",
+ "stac_extensions": [],
+ "id": "COPERNICUS/S2",
+ "title": "Sentinel-2 MSI: MultiSpectral Instrument, Level-1C",
+ "description": "Sentinel-2 is a wide-swath, high-resolution, multi-spectral\nimaging mission supporting Copernicus Land Monitoring studies,\nincluding the monitoring of vegetation, soil and water cover,\nas well as observation of inland waterways and coastal areas.\n\nThe Sentinel-2 data contain 13 UINT16 spectral bands representing\nTOA reflectance scaled by 10000. See the [Sentinel-2 User Handbook](https://sentinel.esa.int/documents/247904/685211/Sentinel-2_User_Handbook)\nfor details. In addition, three QA bands are present where one\n(QA60) is a bitmask band with cloud mask information. For more\ndetails, [see the full explanation of how cloud masks are computed.](https://sentinel.esa.int/web/sentinel/technical-guides/sentinel-2-msi/level-1c/cloud-masks)\n\nEach Sentinel-2 product (zip archive) may contain multiple\ngranules. Each granule becomes a separate Earth Engine asset.\nEE asset ids for Sentinel-2 assets have the following format:\nCOPERNICUS/S2/20151128T002653_20151128T102149_T56MNN. Here the\nfirst numeric part represents the sensing date and time, the\nsecond numeric part represents the product generation date and\ntime, and the final 6-character string is a unique granule identifier\nindicating its UTM grid reference (see [MGRS](https://en.wikipedia.org/wiki/Military_Grid_Reference_System)).\n\nFor more details on Sentinel-2 radiometric resoltuon, [see this page](https://earth.esa.int/web/sentinel/user-guides/sentinel-2-msi/resolutions/radiometric).\n",
+ "license": "proprietary",
+ "keywords": [
+ "copernicus",
+ "esa",
+ "eu",
+ "msi",
+ "radiance",
+ "sentinel"
+ ],
+ "providers": [
+ {
+ "name": "European Union/ESA/Copernicus",
+ "roles": [
+ "producer",
+ "licensor"
+ ],
+ "url": "https://sentinel.esa.int/web/sentinel/user-guides/sentinel-2-msi"
+ }
+ ],
+ "extent": {
+ "spatial": {
+ "bbox": [
+ [
+ -180,
+ -56,
+ 180,
+ 83
+ ]
+ ]
+ },
+ "temporal": {
+ "interval": [
+ [
+ "2015-06-23T00:00:00Z",
+ null
+ ]
+ ]
+ }
+ },
+
+ "summaries": {
+ "datetime": {
+ "min": "2015-06-23T00:00:00Z",
+ "max": "2019-07-10T13:44:56Z"
+ },
+ "platform": ["sentinel-2a","sentinel-2b"],
+ "constellation": ["sentinel-2"],
+ "instruments": ["msi"],
+ "view:off_nadir": {
+ "min": 0.0,
+ "max": 100
+ },
+ "view:sun_elevation": {
+ "min": 6.78,
+ "max": 89.9
+ },
+ "sci:citation": ["Copernicus Sentinel data [Year]"],
+ "gsd": [10,30,60],
+ "proj:epsg": [32601,32602,32603,32604,32605,32606,32607,32608,32609,32610,32611,32612,32613,32614,32615,32616,32617,32618,32619,32620,32621,32622,32623,32624,32625,32626,32627,32628,32629,32630,32631,32632,32633,32634,32635,32636,32637,32638,32639,32640,32641,32642,32643,32644,32645,32646,32647,32648,32649,32650,32651,32652,32653,32654,32655,32656,32657,32658,32659,32660],
+ "eo:bands": [
+ {
+ "name": "B1",
+ "common_name": "coastal",
+ "center_wavelength": 4.439
+ },
+ {
+ "name": "B2",
+ "common_name": "blue",
+ "center_wavelength": 4.966
+ },
+ {
+ "name": "B3",
+ "common_name": "green",
+ "center_wavelength": 5.6
+ },
+ {
+ "name": "B4",
+ "common_name": "red",
+ "center_wavelength": 6.645
+ },
+ {
+ "name": "B5",
+ "center_wavelength": 7.039
+ },
+ {
+ "name": "B6",
+ "center_wavelength": 7.402
+ },
+ {
+ "name": "B7",
+ "center_wavelength": 7.825
+ },
+ {
+ "name": "B8",
+ "common_name": "nir",
+ "center_wavelength": 8.351
+ },
+ {
+ "name": "B8A",
+ "center_wavelength": 8.648
+ },
+ {
+ "name": "B9",
+ "center_wavelength": 9.45
+ },
+ {
+ "name": "B10",
+ "center_wavelength": 1.3735
+ },
+ {
+ "name": "B11",
+ "common_name": "swir16",
+ "center_wavelength": 1.6137
+ },
+ {
+ "name": "B12",
+ "common_name": "swir22",
+ "center_wavelength": 2.2024
+ }
+ ]
+ },
+ "links": [
+ {
+ "rel": "self",
+ "href": "https://storage.cloud.google.com/earthengine-test/catalog/COPERNICUS_S2.json"
+ },
+ {
+ "rel": "parent",
+ "href": "https://storage.cloud.google.com/earthengine-test/catalog/catalog.json"
+ },
+ {
+ "rel": "root",
+ "href": "https://storage.cloud.google.com/earthengine-test/catalog/catalog.json"
+ },
+ {
+ "rel": "license",
+ "href": "https://scihub.copernicus.eu/twiki/pub/SciHubWebPortal/TermsConditions/Sentinel_Data_Terms_and_Conditions.pdf",
+ "title": "Legal notice on the use of Copernicus Sentinel Data and Service Information"
+ }
+ ]
+ }
+
\ No newline at end of file
diff --git a/src/DotNetStac.Test/Resources/Collection/CollectionTests_CanSerializeSentinel2Sample.json b/src/DotNetStac.Test/Resources/Collection/CollectionTests_CanSerializeSentinel2Sample.json
new file mode 100644
index 00000000..a3316164
--- /dev/null
+++ b/src/DotNetStac.Test/Resources/Collection/CollectionTests_CanSerializeSentinel2Sample.json
@@ -0,0 +1,148 @@
+{
+ "stac_version": "1.0.0-beta.1",
+ "stac_extensions": [],
+ "id": "COPERNICUS/S2",
+ "title": "Sentinel-2 MSI: MultiSpectral Instrument, Level-1C",
+ "description": "Sentinel-2 is a wide-swath, high-resolution, multi-spectral\nimaging mission supporting Copernicus Land Monitoring studies,\nincluding the monitoring of vegetation, soil and water cover,\nas well as observation of inland waterways and coastal areas.\n\nThe Sentinel-2 data contain 13 UINT16 spectral bands representing\nTOA reflectance scaled by 10000. See the [Sentinel-2 User Handbook](https://sentinel.esa.int/documents/247904/685211/Sentinel-2_User_Handbook)\nfor details. In addition, three QA bands are present where one\n(QA60) is a bitmask band with cloud mask information. For more\ndetails, [see the full explanation of how cloud masks are computed.](https://sentinel.esa.int/web/sentinel/technical-guides/sentinel-2-msi/level-1c/cloud-masks)\n\nEach Sentinel-2 product (zip archive) may contain multiple\ngranules. Each granule becomes a separate Earth Engine asset.\nEE asset ids for Sentinel-2 assets have the following format:\nCOPERNICUS/S2/20151128T002653_20151128T102149_T56MNN. Here the\nfirst numeric part represents the sensing date and time, the\nsecond numeric part represents the product generation date and\ntime, and the final 6-character string is a unique granule identifier\nindicating its UTM grid reference (see [MGRS](https://en.wikipedia.org/wiki/Military_Grid_Reference_System)).\n\nFor more details on Sentinel-2 radiometric resoltuon, [see this page](https://earth.esa.int/web/sentinel/user-guides/sentinel-2-msi/resolutions/radiometric).\n",
+ "license": "proprietary",
+ "keywords": [
+ "copernicus",
+ "esa",
+ "eu",
+ "msi",
+ "radiance",
+ "sentinel"
+ ],
+ "providers": [
+ {
+ "name": "European Union/ESA/Copernicus",
+ "roles": [
+ "producer",
+ "licensor"
+ ],
+ "url": "https://sentinel.esa.int/web/sentinel/user-guides/sentinel-2-msi"
+ }
+ ],
+ "extent": {
+ "spatial": {
+ "bbox": [
+ [
+ -180.0,
+ -56.0,
+ 180.0,
+ 83.0
+ ]
+ ]
+ },
+ "temporal": {
+ "interval": [
+ [
+ "2015-06-23T00:00:00Z",
+ null
+ ]
+ ]
+ }
+ },
+
+ "summaries": {
+ "datetime": {
+ "min": "2015-06-23T00:00:00Z",
+ "max": "2019-07-10T13:44:56Z"
+ },
+ "platform": ["sentinel-2a","sentinel-2b"],
+ "constellation": ["sentinel-2"],
+ "instruments": ["msi"],
+ "view:off_nadir": {
+ "min": 0.0,
+ "max": 100.0
+ },
+ "view:sun_elevation": {
+ "min": 6.78,
+ "max": 89.9
+ },
+ "sci:citation": ["Copernicus Sentinel data [Year]"],
+ "gsd": [10,30,60],
+ "proj:epsg": [32601,32602,32603,32604,32605,32606,32607,32608,32609,32610,32611,32612,32613,32614,32615,32616,32617,32618,32619,32620,32621,32622,32623,32624,32625,32626,32627,32628,32629,32630,32631,32632,32633,32634,32635,32636,32637,32638,32639,32640,32641,32642,32643,32644,32645,32646,32647,32648,32649,32650,32651,32652,32653,32654,32655,32656,32657,32658,32659,32660],
+ "eo:bands": [
+ {
+ "name": "B1",
+ "common_name": "coastal",
+ "center_wavelength": 4.439
+ },
+ {
+ "name": "B2",
+ "common_name": "blue",
+ "center_wavelength": 4.966
+ },
+ {
+ "name": "B3",
+ "common_name": "green",
+ "center_wavelength": 5.6
+ },
+ {
+ "name": "B4",
+ "common_name": "red",
+ "center_wavelength": 6.645
+ },
+ {
+ "name": "B5",
+ "center_wavelength": 7.039
+ },
+ {
+ "name": "B6",
+ "center_wavelength": 7.402
+ },
+ {
+ "name": "B7",
+ "center_wavelength": 7.825
+ },
+ {
+ "name": "B8",
+ "common_name": "nir",
+ "center_wavelength": 8.351
+ },
+ {
+ "name": "B8A",
+ "center_wavelength": 8.648
+ },
+ {
+ "name": "B9",
+ "center_wavelength": 9.45
+ },
+ {
+ "name": "B10",
+ "center_wavelength": 1.3735
+ },
+ {
+ "name": "B11",
+ "common_name": "swir16",
+ "center_wavelength": 1.6137
+ },
+ {
+ "name": "B12",
+ "common_name": "swir22",
+ "center_wavelength": 2.2024
+ }
+ ]
+ },
+ "links": [
+ {
+ "rel": "self",
+ "href": "https://storage.cloud.google.com/earthengine-test/catalog/COPERNICUS_S2.json"
+ },
+ {
+ "rel": "parent",
+ "href": "https://storage.cloud.google.com/earthengine-test/catalog/catalog.json"
+ },
+ {
+ "rel": "root",
+ "href": "https://storage.cloud.google.com/earthengine-test/catalog/catalog.json"
+ },
+ {
+ "rel": "license",
+ "href": "https://scihub.copernicus.eu/twiki/pub/SciHubWebPortal/TermsConditions/Sentinel_Data_Terms_and_Conditions.pdf",
+ "title": "Legal notice on the use of Copernicus Sentinel Data and Service Information"
+ }
+ ]
+ }
+
\ No newline at end of file
diff --git a/src/DotNetStac.Test/Resources/Item/ItemTests_CanDeserializeMinimalSample.json b/src/DotNetStac.Test/Resources/Item/ItemTests_CanDeserializeMinimalSample.json
new file mode 100644
index 00000000..85308997
--- /dev/null
+++ b/src/DotNetStac.Test/Resources/Item/ItemTests_CanDeserializeMinimalSample.json
@@ -0,0 +1,66 @@
+{
+ "stac_version": "1.0.0-beta.1",
+ "stac_extensions": [],
+ "type": "Feature",
+ "id": "CS3-20160503_132130_04",
+ "bbox": [
+ -122.59750209,
+ 37.48803556,
+ -122.2880486,
+ 37.613537207
+ ],
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ -122.308150179,
+ 37.488035566
+ ],
+ [
+ -122.597502109,
+ 37.538869539
+ ],
+ [
+ -122.576687533,
+ 37.613537207
+ ],
+ [
+ -122.288048600,
+ 37.562818007
+ ],
+ [
+ -122.308150179,
+ 37.488035566
+ ]
+ ]
+ ]
+ },
+ "properties": {
+ "datetime": "2016-05-03T13:21:30.040Z",
+ "collection": "CS3"
+ },
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/CS3-20160503_132130_04.json"
+ },
+ {
+ "rel": "collection",
+ "href": "http://cool-sat.com/catalog.json"
+ }
+ ],
+ "assets": {
+ "analytic": {
+ "href": "relative-path/to/analytic.tif",
+ "title": "4-Band Analytic"
+ },
+ "thumbnail": {
+ "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/thumbnail.png",
+ "title": "Thumbnail",
+ "roles": [
+ "thumbnail"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac.Test/Resources/Item/ItemTests_CanManageDates.json b/src/DotNetStac.Test/Resources/Item/ItemTests_CanManageDates.json
new file mode 100644
index 00000000..024a08f7
--- /dev/null
+++ b/src/DotNetStac.Test/Resources/Item/ItemTests_CanManageDates.json
@@ -0,0 +1,101 @@
+{
+ "stac_version": "1.0.0-beta.1",
+ "stac_extensions": [
+ "eo",
+ "view",
+ "https://example.com/cs-extension/1.0/schema.json"
+ ],
+ "type": "Feature",
+ "id" : "CS3-20160503_132131_05",
+ "bbox": [-122.59750209, 37.48803556, -122.2880486, 37.613537207],
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [-122.308150179, 37.488035566],
+ [-122.597502109, 37.538869539],
+ [-122.576687533, 37.613537207],
+ [-122.288048600, 37.562818007],
+ [-122.308150179, 37.488035566]
+ ]
+ ]
+ },
+ "properties": {
+ "datetime": "2016-05-03T13:22:30Z",
+ "title": "A CS3 item",
+ "license": "PDDL-1.0",
+ "providers": [
+ {
+ "name": "CoolSat",
+ "roles": [
+ "producer",
+ "licensor"
+ ],
+ "url": "https://cool-sat.com/"
+ }
+ ],
+ "created": "2016-05-04T00:00:01Z",
+ "updated": "2017-01-01T00:30:55Z",
+ "view:sun_azimuth": 168.7,
+ "eo:cloud_cover": 0.12,
+ "view:off_nadir": 1.4,
+ "platform": "coolsat2",
+ "instruments": ["cool_sensor_v1"],
+ "eo:bands": [
+ {
+ "name": "band1"
+ },
+ {
+ "name": "band1"
+ },
+ {
+ "name": "band2"
+ },
+ {
+ "name": "band3"
+ }
+ ],
+ "view:sun_elevation": 33.4,
+ "gsd": 0.512,
+ "cs:type": "scene",
+ "cs:anomalous_pixels": 0.14,
+ "cs:earth_sun_distance": 1.0141560,
+ "cs:sat_id": "CS3",
+ "cs:product_level": "LV1B"
+ },
+ "collection": "CS3",
+ "links": [
+ {"rel": "self", "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/CS3-20160503_132130_04.json"},
+ {"rel": "root", "href": "http://cool-sat.com/catalog/catalog.json"},
+ {"rel": "parent", "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/catalog.json"},
+ {"rel": "collection", "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/catalog.json"},
+ {"rel": "alternate", "type": "text/html", "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/CS3-20160503_132130_04.html"}
+ ],
+ "assets": {
+ "analytic": {
+ "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/analytic.tif",
+ "title": "4-Band Analytic"
+ },
+ "thumbnail": {
+ "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/thumbnail.png",
+ "title": "Thumbnail",
+ "type": "image/png",
+ "roles": [ "thumbnail" ]
+ },
+ "udm": {
+ "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/UDM.tif",
+ "title": "Unusable Data Mask"
+ },
+ "json-metadata": {
+ "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/extended-metadata.json",
+ "title": "Extended Metadata",
+ "type": "application/json",
+ "roles": [ "metadata" ]
+ },
+ "ephemeris": {
+ "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/S3-20160503_132130_04.EPH",
+ "title": "Satellite Ephemeris Metadata"
+ }
+ }
+
+ }
\ No newline at end of file
diff --git a/src/DotNetStac.Test/Resources/Item/ItemTests_CanSerializeMinimalSample.json b/src/DotNetStac.Test/Resources/Item/ItemTests_CanSerializeMinimalSample.json
new file mode 100644
index 00000000..85308997
--- /dev/null
+++ b/src/DotNetStac.Test/Resources/Item/ItemTests_CanSerializeMinimalSample.json
@@ -0,0 +1,66 @@
+{
+ "stac_version": "1.0.0-beta.1",
+ "stac_extensions": [],
+ "type": "Feature",
+ "id": "CS3-20160503_132130_04",
+ "bbox": [
+ -122.59750209,
+ 37.48803556,
+ -122.2880486,
+ 37.613537207
+ ],
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ -122.308150179,
+ 37.488035566
+ ],
+ [
+ -122.597502109,
+ 37.538869539
+ ],
+ [
+ -122.576687533,
+ 37.613537207
+ ],
+ [
+ -122.288048600,
+ 37.562818007
+ ],
+ [
+ -122.308150179,
+ 37.488035566
+ ]
+ ]
+ ]
+ },
+ "properties": {
+ "datetime": "2016-05-03T13:21:30.040Z",
+ "collection": "CS3"
+ },
+ "links": [
+ {
+ "rel": "self",
+ "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/CS3-20160503_132130_04.json"
+ },
+ {
+ "rel": "collection",
+ "href": "http://cool-sat.com/catalog.json"
+ }
+ ],
+ "assets": {
+ "analytic": {
+ "href": "relative-path/to/analytic.tif",
+ "title": "4-Band Analytic"
+ },
+ "thumbnail": {
+ "href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/thumbnail.png",
+ "title": "Thumbnail",
+ "roles": [
+ "thumbnail"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac.Test/Resources/UseCases/Sentinel2/catalog.json b/src/DotNetStac.Test/Resources/UseCases/Sentinel2/catalog.json
new file mode 100644
index 00000000..77314207
--- /dev/null
+++ b/src/DotNetStac.Test/Resources/UseCases/Sentinel2/catalog.json
@@ -0,0 +1,19 @@
+{
+ "id": "sentinel-stac",
+ "stac_version": "0.6.0",
+ "description": "STAC for Sentinel data",
+ "links": [
+ {
+ "rel": "self",
+ "href": "https://sentinel-stac.s3.amazonaws.com/catalog.json"
+ },
+ {
+ "rel": "root",
+ "href": "./catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "sentinel-2-l1c/catalog.json"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/DotNetStac.Test/Resources/UseCases/Sentinel2/sentinel-2-l1c/catalog.json b/src/DotNetStac.Test/Resources/UseCases/Sentinel2/sentinel-2-l1c/catalog.json
new file mode 100644
index 00000000..09088856
--- /dev/null
+++ b/src/DotNetStac.Test/Resources/UseCases/Sentinel2/sentinel-2-l1c/catalog.json
@@ -0,0 +1,518 @@
+{
+ "id": "sentinel-2-l1c",
+ "title": "Sentinel 2 L1C",
+ "description": "Sentinel-2a and Sentinel-2b imagery",
+ "keywords": [
+ "sentinel",
+ "earth observation",
+ "esa"
+ ],
+ "version": "0.1.0",
+ "stac_version": "0.6.0",
+ "extent": {
+ "spatial": [
+ -180,
+ -90,
+ 180,
+ 90
+ ],
+ "temporal": [
+ "2013-06-01",
+ null
+ ],
+ },
+ "providers": [
+ {
+ "name": "ESA",
+ "roles": [
+ "producer"
+ ],
+ "url": "https://earth.esa.int/web/guest/home"
+ },
+ {
+ "name": "Synergise",
+ "roles": [
+ "processor"
+ ],
+ "url": "https://registry.opendata.aws/sentinel-2/"
+ },
+ {
+ "name": "AWS",
+ "roles": [
+ "host"
+ ],
+ "url": "http://sentinel-pds.s3-website.eu-central-1.amazonaws.com/"
+ },
+ {
+ "name": "Development Seed",
+ "roles": [
+ "processor"
+ ],
+ "url": "https://github.com/sat-utils/sat-stac-sentinel"
+ }
+ ],
+ "license": "proprietary",
+ "properties": {
+ "collection": "sentinel-2-l1c",
+ "eo:gsd": 10,
+ "eo:instrument": "MSI",
+ "eo:off_nadir": 0,
+ "eo:bands": [
+ {
+ "name": "B01",
+ "common_name": "coastal",
+ "gsd": 60.0,
+ "center_wavelength": 0.4439,
+ "full_width_half_max": 0.027
+ },
+ {
+ "name": "B02",
+ "common_name": "blue",
+ "gsd": 10.0,
+ "center_wavelength": 0.4966,
+ "full_width_half_max": 0.098
+ },
+ {
+ "name": "B03",
+ "common_name": "green",
+ "gsd": 10.0,
+ "center_wavelength": 0.56,
+ "full_width_half_max": 0.045
+ },
+ {
+ "name": "B04",
+ "common_name": "red",
+ "gsd": 10.0,
+ "center_wavelength": 0.6645,
+ "full_width_half_max": 0.038
+ },
+ {
+ "name": "B05",
+ "gsd": 20.0,
+ "center_wavelength": 0.7039,
+ "full_width_half_max": 0.019
+ },
+ {
+ "name": "B06",
+ "gsd": 20.0,
+ "center_wavelength": 0.7402,
+ "full_width_half_max": 0.018
+ },
+ {
+ "name": "B07",
+ "gsd": 20.0,
+ "center_wavelength": 0.7825,
+ "full_width_half_max": 0.028
+ },
+ {
+ "name": "B08",
+ "common_name": "nir",
+ "gsd": 10.0,
+ "center_wavelength": 0.8351,
+ "full_width_half_max": 0.145
+ },
+ {
+ "name": "B8A",
+ "gsd": 20.0,
+ "center_wavelength": 0.8648,
+ "full_width_half_max": 0.033
+ },
+ {
+ "name": "B09",
+ "gsd": 60.0,
+ "center_wavelength": 0.945,
+ "full_width_half_max": 0.026
+ },
+ {
+ "name": "B10",
+ "common_name": "cirrus",
+ "gsd": 60.0,
+ "center_wavelength": 1.3735,
+ "full_width_half_max": 0.075
+ },
+ {
+ "name": "B11",
+ "common_name": "swir16",
+ "gsd": 20.0,
+ "center_wavelength": 1.6137,
+ "full_width_half_max": 0.143
+ },
+ {
+ "name": "B12",
+ "common_name": "swir22",
+ "gsd": 20.0,
+ "center_wavelength": 2.22024,
+ "full_width_half_max": 0.242
+ }
+ ]
+ },
+ "assets": {
+ "thumbnail": {
+ "title": "Thumbnail"
+ },
+ "info": {
+ "title": "Basic JSON metadata"
+ },
+ "metadata": {
+ "title": "Complete XML metadata"
+ },
+ "tki": {
+ "title": "True color image",
+ "type": "image/jp2",
+ "eo:bands": [
+ 3,
+ 2,
+ 1
+ ]
+ },
+ "B01": {
+ "title": "Band 1 (coastal)",
+ "type": "image/jp2",
+ "eo:bands": [
+ 0
+ ]
+ },
+ "B02": {
+ "title": "Band 2 (blue)",
+ "type": "image/jp2",
+ "eo:bands": [
+ 2
+ ]
+ },
+ "B03": {
+ "title": "Band 3 (green)",
+ "type": "image/jp2",
+ "eo:bands": [
+ 2
+ ]
+ },
+ "B04": {
+ "title": "Band 4 (red)",
+ "type": "image/jp2",
+ "eo:bands": [
+ 3
+ ]
+ },
+ "B05": {
+ "title": "Band 5",
+ "type": "image/jp2",
+ "eo:bands": [
+ 4
+ ]
+ },
+ "B06": {
+ "title": "Band 6",
+ "type": "image/jp2",
+ "eo:bands": [
+ 5
+ ]
+ },
+ "B07": {
+ "title": "Band 7",
+ "type": "image/jp2",
+ "eo:bands": [
+ 6
+ ]
+ },
+ "B08": {
+ "title": "Band 8 (nir)",
+ "type": "image/jp2",
+ "eo:bands": [
+ 7
+ ]
+ },
+ "B8A": {
+ "title": "Band 8A",
+ "type": "image/jp2",
+ "eo:bands": [
+ 8
+ ]
+ },
+ "B09": {
+ "title": "Band 9",
+ "type": "image/jp2",
+ "eo:bands": [
+ 9
+ ]
+ },
+ "B10": {
+ "title": "Band 10 (cirrus)",
+ "type": "image/jp2",
+ "eo:bands": [
+ 10
+ ]
+ },
+ "B11": {
+ "title": "Band 11 (swir16)",
+ "type": "image/jp2",
+ "eo:bands": [
+ 11
+ ]
+ },
+ "B12": {
+ "title": "Band 12 (swir22)",
+ "type": "image/jp2",
+ "eo:bands": [
+ 12
+ ]
+ }
+ },
+ "links": [
+ {
+ "rel": "license",
+ "href": "https://sentinel.esa.int/documents/247904/690755/Sentinel_Data_Legal_Notice"
+ },
+ {
+ "rel": "self",
+ "href": "https://sentinel-stac.s3.amazonaws.com/sentinel-2-l1c/catalog.json"
+ },
+ {
+ "rel": "root",
+ "href": "../catalog.json"
+ },
+ {
+ "rel": "parent",
+ "href": "../catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "9/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "47/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "2/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "42/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "35/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "55/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "33/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "7/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "51/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "39/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "30/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "41/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "23/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "6/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "40/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "53/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "60/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "16/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "50/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "27/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "19/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "12/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "32/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "37/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "11/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "54/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "29/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "24/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "49/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "5/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "34/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "13/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "59/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "57/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "20/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "4/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "46/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "45/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "21/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "18/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "31/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "17/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "3/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "36/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "14/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "38/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "25/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "58/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "22/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "43/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "44/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "1/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "28/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "15/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "48/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "8/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "56/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "10/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "52/catalog.json"
+ },
+ {
+ "rel": "child",
+ "href": "26/catalog.json"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/DotNetStac.Test/TestBase.cs b/src/DotNetStac.Test/TestBase.cs
new file mode 100644
index 00000000..2633b89a
--- /dev/null
+++ b/src/DotNetStac.Test/TestBase.cs
@@ -0,0 +1,54 @@
+using System;
+using System.IO;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+namespace Stac.Test
+{
+ public abstract class TestBase
+ {
+ private static readonly Assembly ThisAssembly = typeof(TestBase)
+#if NETCOREAPP1_1
+ .GetTypeInfo()
+#endif
+ .Assembly;
+ private static readonly string AssemblyName = ThisAssembly.GetName().Name;
+
+ public static string AssemblyDirectory
+ {
+ get
+ {
+ string codeBase = ThisAssembly.CodeBase;
+ UriBuilder uri = new UriBuilder(codeBase);
+ string path = Uri.UnescapeDataString(uri.Path);
+ return Path.GetDirectoryName(path);
+ }
+ }
+
+ protected string GetExpectedJson(string folder, [CallerMemberName] string name = null)
+ {
+ var type = GetType().Name;
+ var path = Path.Combine(AssemblyDirectory, @"../../..", "Resources", folder, type + "_" + name + ".json");
+
+ if (!File.Exists(path))
+ {
+ throw new FileNotFoundException("file not found at " + path);
+ }
+
+ return File.ReadAllText(path);
+ }
+
+ protected Uri GetUseCaseFileUri(string name)
+ {
+ var type = GetType().Name;
+ var path = Path.Combine(AssemblyDirectory, @"../../..", "Resources/UseCases", type, name);
+
+ if (!File.Exists(path))
+ {
+ throw new FileNotFoundException("file not found at " + path);
+ }
+
+ return new Uri(path);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac.Test/TestPriorityAttribute.cs b/src/DotNetStac.Test/TestPriorityAttribute.cs
new file mode 100644
index 00000000..ea193020
--- /dev/null
+++ b/src/DotNetStac.Test/TestPriorityAttribute.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace Stac.Test
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
+ public class TestPriorityAttribute : Attribute
+ {
+ public TestPriorityAttribute(int priority)
+ {
+ Priority = priority;
+ }
+
+ public int Priority { get; private set; }
+ }
+}
diff --git a/src/DotNetStac.Test/UseCases/Sentinel2.cs b/src/DotNetStac.Test/UseCases/Sentinel2.cs
new file mode 100644
index 00000000..b9ce9d81
--- /dev/null
+++ b/src/DotNetStac.Test/UseCases/Sentinel2.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Stac.Catalog;
+using Stac.Collection;
+using Stac.Test;
+using Xunit;
+
+namespace Stac.Test.UseCases
+{
+ [TestCaseOrderer("Stac.Test.PriorityOrderer", "DotNetStac.Test")]
+ public class Sentinel2 : TestBase
+ {
+ private static IStacCatalog catalog;
+
+ [Fact, TestPriority(1)]
+ public void LoadRootCatalog()
+ {
+ catalog = StacCatalog.LoadUri(GetUseCaseFileUri("catalog.json")).Result;
+
+ Assert.NotNull(catalog);
+ Assert.IsAssignableFrom(catalog);
+ Assert.Equal("sentinel-stac", catalog.Id);
+
+ }
+
+ [Fact, TestPriority(2)]
+ public void LoadRootChildren()
+ {
+ IDictionary children = catalog.GetChildren();
+
+ Assert.Equal(1, children.Count);
+
+ Assert.IsType(children.First().Value);
+ }
+
+
+ }
+}
diff --git a/src/DotNetStac.Test/runsettings.xml b/src/DotNetStac.Test/runsettings.xml
new file mode 100644
index 00000000..37478809
--- /dev/null
+++ b/src/DotNetStac.Test/runsettings.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ lcov,cobertura
+
+
+
+
+
\ No newline at end of file
diff --git a/src/DotNetStac.sln b/src/DotNetStac.sln
new file mode 100644
index 00000000..8977a984
--- /dev/null
+++ b/src/DotNetStac.sln
@@ -0,0 +1,48 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.26124.0
+MinimumVisualStudioVersion = 15.0.26124.0
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetStac", "DotNetStac\DotNetStac.csproj", "{19E9F528-4825-45F6-B515-FDD88DAB95F5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetStac.Test", "DotNetStac.Test\DotNetStac.Test.csproj", "{19D1373B-24F2-4776-85BA-2EF7B7D81FFD}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {19E9F528-4825-45F6-B515-FDD88DAB95F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {19E9F528-4825-45F6-B515-FDD88DAB95F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {19E9F528-4825-45F6-B515-FDD88DAB95F5}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {19E9F528-4825-45F6-B515-FDD88DAB95F5}.Debug|x64.Build.0 = Debug|Any CPU
+ {19E9F528-4825-45F6-B515-FDD88DAB95F5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {19E9F528-4825-45F6-B515-FDD88DAB95F5}.Debug|x86.Build.0 = Debug|Any CPU
+ {19E9F528-4825-45F6-B515-FDD88DAB95F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {19E9F528-4825-45F6-B515-FDD88DAB95F5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {19E9F528-4825-45F6-B515-FDD88DAB95F5}.Release|x64.ActiveCfg = Release|Any CPU
+ {19E9F528-4825-45F6-B515-FDD88DAB95F5}.Release|x64.Build.0 = Release|Any CPU
+ {19E9F528-4825-45F6-B515-FDD88DAB95F5}.Release|x86.ActiveCfg = Release|Any CPU
+ {19E9F528-4825-45F6-B515-FDD88DAB95F5}.Release|x86.Build.0 = Release|Any CPU
+ {19D1373B-24F2-4776-85BA-2EF7B7D81FFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {19D1373B-24F2-4776-85BA-2EF7B7D81FFD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {19D1373B-24F2-4776-85BA-2EF7B7D81FFD}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {19D1373B-24F2-4776-85BA-2EF7B7D81FFD}.Debug|x64.Build.0 = Debug|Any CPU
+ {19D1373B-24F2-4776-85BA-2EF7B7D81FFD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {19D1373B-24F2-4776-85BA-2EF7B7D81FFD}.Debug|x86.Build.0 = Debug|Any CPU
+ {19D1373B-24F2-4776-85BA-2EF7B7D81FFD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {19D1373B-24F2-4776-85BA-2EF7B7D81FFD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {19D1373B-24F2-4776-85BA-2EF7B7D81FFD}.Release|x64.ActiveCfg = Release|Any CPU
+ {19D1373B-24F2-4776-85BA-2EF7B7D81FFD}.Release|x64.Build.0 = Release|Any CPU
+ {19D1373B-24F2-4776-85BA-2EF7B7D81FFD}.Release|x86.ActiveCfg = Release|Any CPU
+ {19D1373B-24F2-4776-85BA-2EF7B7D81FFD}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/src/DotNetStac/Catalog/StacCatalog.Helper.cs b/src/DotNetStac/Catalog/StacCatalog.Helper.cs
new file mode 100644
index 00000000..e425eea5
--- /dev/null
+++ b/src/DotNetStac/Catalog/StacCatalog.Helper.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Runtime.Serialization;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Stac.Collection;
+using Stac.Item;
+using Stac.Model;
+
+namespace Stac.Catalog
+{
+ public partial class StacCatalog
+ {
+
+ public static async Task LoadUri(Uri uri)
+ {
+ var catalog = await StacFactory.LoadUriAsync(uri);
+ if (catalog is IStacCatalog)
+ return (IStacCatalog)catalog;
+ throw new InvalidOperationException(string.Format("This is not a STAC catalog {0}", catalog.Uri));
+ }
+
+ public static async Task LoadStacLink(StacLink link)
+ {
+ var catalog = await StacFactory.LoadStacLink(link);
+ if (catalog is IStacCatalog)
+ return (IStacCatalog)catalog;
+ throw new InvalidOperationException(string.Format("This is not a STAC catalog {0}", catalog.Uri));
+ }
+
+ public static IStacCatalog LoadJToken(JToken jsonRoot, Uri uri)
+ {
+ IStacCatalog catalog;
+ if (jsonRoot["extent"] != null)
+ catalog = Stac.Collection.StacCollection.LoadStacCollection(jsonRoot);
+ else
+ catalog = LoadStacCatalog(jsonRoot);
+ ((IInternalStacObject)catalog).Uri = uri;
+ return catalog;
+
+ }
+
+ private static IStacCatalog LoadStacCatalog(JToken jsonRoot)
+ {
+ Type catalogType = null;
+ if (jsonRoot["stac_version"] == null)
+ {
+ throw new InvalidDataException("The document is not a STAC document. No 'stac_version' property found");
+ }
+
+ try
+ {
+ catalogType = Stac.Model.SchemaDictionary.GetCatalogTypeFromVersion(jsonRoot["stac_version"].Value());
+ }
+ catch (KeyNotFoundException)
+ {
+ throw new NotSupportedException(string.Format("The document has a non supprted version: '{0}'.", jsonRoot["stac_version"].Value()));
+ }
+
+ return (IStacCatalog)jsonRoot.ToObject(catalogType);
+
+ }
+
+ }
+}
diff --git a/src/DotNetStac/Catalog/StacCatalog.Model.cs b/src/DotNetStac/Catalog/StacCatalog.Model.cs
new file mode 100644
index 00000000..05918d77
--- /dev/null
+++ b/src/DotNetStac/Catalog/StacCatalog.Model.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Runtime.Serialization;
+using DotNetStac;
+using DotNetStac.Converters;
+using GeoJSON.Net.Geometry;
+using Newtonsoft.Json;
+using Stac.Converters;
+using Stac.Extensions;
+using Stac.Model;
+
+namespace Stac.Catalog
+{
+ [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
+ public partial class StacCatalog : IStacObject, IStacCatalog, IInternalStacObject
+ {
+ private readonly string id;
+ private Collection links;
+
+ private string stacVersion = StacVersionList.Current;
+
+ private Collection extensions;
+
+ private string description;
+
+
+ private string title;
+
+ private Uri sourceUri;
+
+ public Uri Uri { get => sourceUri; set => sourceUri = value; }
+
+
+ [JsonConstructor]
+ public StacCatalog(string id, string description, IEnumerable links = null)
+ {
+ this.id = id;
+ this.description = description;
+ if (links == null)
+ this.links = new Collection();
+ else
+ this.links = new Collection(links.ToList());
+ }
+
+ [JsonProperty("stac_extensions")]
+ [JsonConverter(typeof(StacExtensionConverter))]
+ public Collection StacExtensions
+ {
+ get
+ {
+ if (extensions == null)
+ extensions = new Collection();
+ return extensions;
+ }
+ set
+ {
+ extensions = value;
+ }
+ }
+
+ [JsonProperty("stac_version")]
+ public string StacVersion
+ {
+ get
+ {
+ return stacVersion;
+ }
+
+ set
+ {
+ stacVersion = value;
+ }
+ }
+
+ [JsonConverter(typeof(CollectionConverter))]
+ [JsonProperty("links")]
+ public Collection Links
+ {
+ get
+ {
+ if (links == null)
+ links = new Collection();
+ return links;
+ }
+ set
+ {
+ links = value;
+ }
+ }
+
+ [JsonProperty("description")]
+ public string Description
+ {
+ get
+ {
+ return description;
+ }
+ set
+ {
+ description = value;
+ }
+ }
+
+ [JsonProperty("id")]
+ public string Id { get => id; }
+
+ [JsonProperty("title")]
+ public string Title { get => title; set => title = value; }
+
+ [OnDeserialized]
+ internal void OnDeserializedMethod(StreamingContext context)
+ {
+ foreach (StacLink link in Links)
+ {
+ link.Parent = this;
+ }
+ }
+
+ public IStacObject Upgrade()
+ {
+ return this;
+ }
+ }
+}
diff --git a/src/DotNetStac/Collection/IStacSummaryItem.cs b/src/DotNetStac/Collection/IStacSummaryItem.cs
new file mode 100644
index 00000000..48af1c83
--- /dev/null
+++ b/src/DotNetStac/Collection/IStacSummaryItem.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Newtonsoft.Json.Linq;
+
+namespace Stac.Collection
+{
+ public interface IStacSummaryItem : IEnumerable
+ {
+ SummaryItemType SummaryType { get; }
+
+ JToken this[object key] { get; }
+
+ JToken AsJToken { get; }
+
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/Collection/StacCollection.Helper.cs b/src/DotNetStac/Collection/StacCollection.Helper.cs
new file mode 100644
index 00000000..9487e4af
--- /dev/null
+++ b/src/DotNetStac/Collection/StacCollection.Helper.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Runtime.Serialization;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Stac.Catalog;
+using Stac.Collection;
+using Stac.Model;
+
+namespace Stac.Collection
+{
+ public partial class StacCollection : IStacObject
+ {
+ internal static StacCollection LoadStacCollection(JToken jsonRoot)
+ {
+ Type collectionType = null;
+ if (jsonRoot["stac_version"] == null)
+ {
+ throw new InvalidDataException("The document is not a STAC document. No 'stac_version' property found");
+ }
+
+ if (jsonRoot["extent"] == null)
+ {
+ throw new InvalidDataException("The document is not a STAC collection document. No 'extent' property found. Probably a catalog.");
+ }
+
+ try
+ {
+ collectionType = Stac.Model.SchemaDictionary.GetCollectionTypeFromVersion(jsonRoot["stac_version"].Value());
+ }
+ catch (KeyNotFoundException)
+ {
+ throw new NotSupportedException(string.Format("The document has a non supprted version: '{0}'.", jsonRoot["stac_version"].Value()));
+ }
+
+ IStacObject catalog = (IStacObject)jsonRoot.ToObject(collectionType);
+
+ while (catalog.GetType() != typeof(StacCollection))
+ {
+ catalog = catalog.Upgrade();
+ }
+
+ return (StacCollection)catalog;
+ }
+
+ }
+}
diff --git a/src/DotNetStac/Collection/StacCollection.Model.cs b/src/DotNetStac/Collection/StacCollection.Model.cs
new file mode 100644
index 00000000..f932b009
--- /dev/null
+++ b/src/DotNetStac/Collection/StacCollection.Model.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using DotNetStac;
+using DotNetStac.Converters;
+using GeoJSON.Net.Geometry;
+using Newtonsoft.Json;
+using Stac.Catalog;
+using Stac.Converters;
+using Stac.Extensions;
+using Stac.Model;
+
+namespace Stac.Collection
+{
+ [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
+ public partial class StacCollection : StacCatalog, IStacObject, IStacCollection
+ {
+ private string license;
+ private StacExtent extent;
+ private Dictionary summaries;
+ private Collection providers;
+ private Collection keywords;
+
+ [JsonConstructor]
+ public StacCollection(string id, string description, StacExtent extent, IEnumerable links = null, string license = "proprietary") :
+ base(id, description, links)
+ {
+ this.license = license;
+ this.extent = extent;
+ }
+
+ [JsonProperty("extent")]
+ public StacExtent Extent { get => extent; set => extent = value; }
+
+ [JsonProperty("summaries")]
+ [JsonConverter(typeof(StacSummariesConverter))]
+ public Dictionary Summaries
+ {
+ get
+ {
+ if (summaries == null)
+ summaries = new Dictionary();
+ return summaries;
+ }
+ set
+ {
+ summaries = value;
+ }
+ }
+
+ [JsonProperty("license")]
+ public string License { get => license; set => license = value; }
+
+ [JsonProperty("providers")]
+ public Collection Providers
+ {
+ get { return providers; }
+ set
+ {
+ providers = value;
+ }
+ }
+
+ [JsonProperty("keywords", DefaultValueHandling = DefaultValueHandling.Ignore)]
+ public Collection Keywords
+ {
+ get
+ {
+ if (keywords == null)
+ keywords = new Collection();
+ return keywords;
+ }
+ set
+ {
+ keywords = value;
+ }
+ }
+
+ }
+}
diff --git a/src/DotNetStac/Collection/StacProvider.cs b/src/DotNetStac/Collection/StacProvider.cs
new file mode 100644
index 00000000..1bda4b69
--- /dev/null
+++ b/src/DotNetStac/Collection/StacProvider.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace Stac.Collection
+{
+ [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
+ public class StacProvider
+ {
+ private string name;
+
+ private string description;
+
+ private List roles;
+ private Uri uri;
+
+ public StacProvider(string name)
+ {
+ this.name = name;
+ }
+
+ [JsonProperty("name")]
+ public string Name { get => name; set => name = value; }
+
+ [JsonProperty("description")]
+ public string Description { get => description; set => description = value; }
+
+ [JsonProperty("roles")]
+ public List Roles { get => roles; set => roles = value; }
+
+ [JsonProperty("url")]
+ public Uri Uri { get => uri; set => uri = value; }
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/Collection/StacProviderRole.cs b/src/DotNetStac/Collection/StacProviderRole.cs
new file mode 100644
index 00000000..50bd525a
--- /dev/null
+++ b/src/DotNetStac/Collection/StacProviderRole.cs
@@ -0,0 +1,15 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace Stac.Collection
+{
+ [JsonConverter(typeof(StringEnumConverter))]
+ public enum StacProviderRole
+ {
+ licensor,
+ producer,
+ processor,
+ host
+
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/Collection/StacStatsObject.cs b/src/DotNetStac/Collection/StacStatsObject.cs
new file mode 100644
index 00000000..3ecf40be
--- /dev/null
+++ b/src/DotNetStac/Collection/StacStatsObject.cs
@@ -0,0 +1,6 @@
+namespace Stac.Collection
+{
+ public class StacStatsObject
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/Collection/StacSummaryItem.cs b/src/DotNetStac/Collection/StacSummaryItem.cs
new file mode 100644
index 00000000..08c895ad
--- /dev/null
+++ b/src/DotNetStac/Collection/StacSummaryItem.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Newtonsoft.Json.Linq;
+
+namespace Stac.Collection
+{
+ public abstract class StacSummaryItem : IStacSummaryItem
+ {
+ protected readonly JToken summary;
+
+ protected StacSummaryItem(JToken summary)
+ {
+ this.summary = summary;
+ }
+
+ public JToken this[object key]
+ {
+ get
+ {
+ return summary[key];
+ }
+ }
+
+ public abstract SummaryItemType SummaryType { get; }
+
+ public JToken AsJToken => summary;
+
+ public IEnumerator GetEnumerator()
+ {
+ return summary.Children().GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return summary.Children().GetEnumerator();
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/Collection/StacSummaryStatsObject.cs b/src/DotNetStac/Collection/StacSummaryStatsObject.cs
new file mode 100644
index 00000000..da1c8612
--- /dev/null
+++ b/src/DotNetStac/Collection/StacSummaryStatsObject.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Stac.Collection
+{
+ public class StacSummaryStatsObject : StacSummaryItem
+ {
+ public StacSummaryStatsObject(JObject summary) : base(summary)
+ {
+ if (!summary.ContainsKey("min") || !summary.ContainsKey("max"))
+ throw new ArgumentException("summary stats must contains min and max");
+ }
+
+ public StacSummaryStatsObject(T min, T max) : base(new JObject())
+ {
+ Min = min;
+ Max = max;
+ }
+
+ public T Min { get => summary["min"].Value(); set => summary["min"] = new JValue(value); }
+
+ public T Max { get => summary["max"].Value(); set => summary["max"] = new JValue(value); }
+
+ public override SummaryItemType SummaryType => SummaryItemType.StatsObject;
+
+
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/Collection/StacSummaryValueSet.cs b/src/DotNetStac/Collection/StacSummaryValueSet.cs
new file mode 100644
index 00000000..81a6a436
--- /dev/null
+++ b/src/DotNetStac/Collection/StacSummaryValueSet.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Stac.Collection;
+
+namespace Stac.Collection
+{
+ public class StacSummaryValueSet : StacSummaryItem, IEnumerable
+ {
+
+ public StacSummaryValueSet(JArray summarySet) : base(summarySet)
+ {
+ }
+
+ public StacSummaryValueSet(IEnumerable summarySet) : base(new JArray(summarySet))
+ {
+ }
+
+ public void Add(T item){
+ ((JArray)summary).Add(item);
+ }
+
+ public override SummaryItemType SummaryType => SummaryItemType.Set;
+
+ public int Count => summary.Count();
+
+ public IEnumerable SummarySet { get => summary.ToObject>(); }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return SummarySet.GetEnumerator();
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/Collection/SummaryItemType.cs b/src/DotNetStac/Collection/SummaryItemType.cs
new file mode 100644
index 00000000..9088d190
--- /dev/null
+++ b/src/DotNetStac/Collection/SummaryItemType.cs
@@ -0,0 +1,9 @@
+namespace Stac.Collection
+{
+ public enum SummaryItemType
+ {
+ Set,
+
+ StatsObject
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/Converters/CollectionConverter.cs b/src/DotNetStac/Converters/CollectionConverter.cs
new file mode 100644
index 00000000..9e8862f4
--- /dev/null
+++ b/src/DotNetStac/Converters/CollectionConverter.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace DotNetStac.Converters
+{
+ public class CollectionConverter : JsonConverter
+ {
+ public override bool CanConvert(Type objectType)
+ {
+ return (objectType == typeof(Collection));
+ }
+
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ JToken token = JToken.Load(reader);
+ if (token.Type == JTokenType.Array)
+ {
+ return new Collection(token.ToObject>());
+ }
+ return new Collection();
+ }
+
+ public override bool CanRead => true;
+
+ public override bool CanWrite => true;
+
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ Collection collection = (Collection)value;
+ writer.WriteStartArray();
+ foreach (var item in collection)
+ {
+ serializer.Serialize(writer, item);
+ }
+ writer.WriteEndArray();
+ }
+ }
+}
diff --git a/src/DotNetStac/Converters/StacExtensionConverter.cs b/src/DotNetStac/Converters/StacExtensionConverter.cs
new file mode 100644
index 00000000..ed2cc40d
--- /dev/null
+++ b/src/DotNetStac/Converters/StacExtensionConverter.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Stac.Extensions;
+
+namespace Stac.Converters
+{
+ internal class StacExtensionConverter : JsonConverter
+ {
+ private readonly IStacExtensionsFactory stacExtensionFactory;
+
+ public StacExtensionConverter() : this(StacExtensionsFactory.Default) { }
+
+ public StacExtensionConverter(IStacExtensionsFactory stacExtensionFactory)
+ {
+ this.stacExtensionFactory = stacExtensionFactory;
+ }
+
+ public override bool CanConvert(Type objectType)
+ {
+ return objectType.IsAssignableFrom(typeof(IStacExtension));
+ }
+
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ Collection stacExtensions = new Collection();
+ if (reader.TokenType != JsonToken.Null)
+ {
+ if (reader.TokenType == JsonToken.StartArray)
+ {
+ JToken token = JToken.Load(reader);
+ List extensionPrefixes = token.ToObject>();
+ foreach (var extensionPrefix in extensionPrefixes)
+ {
+ var stacExtension = stacExtensionFactory.CreateStacExtension(extensionPrefix, null);
+ if (stacExtension != null)
+ stacExtensions.Add(stacExtension);
+ }
+ }
+ }
+ return stacExtensions;
+ }
+
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ Collection stacExtensions = (Collection)value;
+ writer.WriteStartArray();
+ foreach (var stacExtension in stacExtensions)
+ {
+ writer.WriteValue(stacExtension.Id);
+ }
+ writer.WriteEndArray();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/Converters/StacSummariesConverter.cs b/src/DotNetStac/Converters/StacSummariesConverter.cs
new file mode 100644
index 00000000..465ca386
--- /dev/null
+++ b/src/DotNetStac/Converters/StacSummariesConverter.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Mime;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Stac.Collection;
+
+namespace Stac.Converters
+{
+ internal class StacSummariesConverter : JsonConverter
+ {
+ public override bool CanConvert(Type objectType)
+ {
+ return (objectType == typeof(Dictionary));
+ }
+
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ Dictionary summaries = new Dictionary();
+ Dictionary objDic = serializer.Deserialize>(reader);
+
+ foreach (var key in objDic.Keys)
+ {
+ if (objDic[key] is JArray)
+ {
+ JArray enumerable = (objDic[key] as JArray);
+ switch (enumerable.First().Type)
+ {
+ case JTokenType.Boolean:
+ summaries.Add(key, new StacSummaryValueSet(enumerable));
+ break;
+ case JTokenType.Date:
+ summaries.Add(key, new StacSummaryValueSet(enumerable));
+ break;
+ case JTokenType.String:
+ summaries.Add(key, new StacSummaryValueSet(enumerable));
+ break;
+ case JTokenType.Integer:
+ summaries.Add(key, new StacSummaryValueSet(enumerable));
+ break;
+ case JTokenType.Float:
+ summaries.Add(key, new StacSummaryValueSet(enumerable));
+ break;
+ case JTokenType.Object:
+ summaries.Add(key, new StacSummaryValueSet(enumerable));
+ break;
+ }
+ }
+ if (objDic[key] is JObject)
+ {
+ JObject obj = (objDic[key] as JObject);
+ if (obj.ContainsKey("min") && obj.ContainsKey("max"))
+ {
+ switch (obj["min"].Type)
+ {
+ case JTokenType.Date:
+ summaries.Add(key, new StacSummaryStatsObject(obj));
+ break;
+ case JTokenType.String:
+ summaries.Add(key, new StacSummaryStatsObject(obj));
+ break;
+ case JTokenType.Integer:
+ summaries.Add(key, new StacSummaryStatsObject(obj));
+ break;
+ case JTokenType.Float:
+ summaries.Add(key, new StacSummaryStatsObject(obj));
+ break;
+ }
+ }
+ }
+ }
+
+ return summaries;
+ }
+
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ Dictionary summaries = (Dictionary)value;
+
+ serializer.Serialize(writer, summaries.ToDictionary(k => k.Key, k => k.Value.AsJToken));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/DotNetStac.csproj b/src/DotNetStac/DotNetStac.csproj
new file mode 100644
index 00000000..175c66cf
--- /dev/null
+++ b/src/DotNetStac/DotNetStac.csproj
@@ -0,0 +1,26 @@
+
+
+ <_ExtraTargetFrameworks Condition="'$(OS)' == 'Windows_NT' or '$(MSBuildRuntimeType)' == 'Mono'">net472
+ netstandard2.0;$(RoslynPortableTargetFrameworks);$(_ExtraTargetFrameworks)
+ win;linux
+ DotNetStac
+ Terradue .Net library for working with any SpatioTemporal Asset Catalog
+ LICENSE
+ 0.2.0-beta
+ Emmanuel Mathot
+ Terradue
+ https://github.com/Terradue/DotNetStac
+ STAC;Terradue;Geo;Json
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/DotNetStac/Extensions/GenericStacExtension.cs b/src/DotNetStac/Extensions/GenericStacExtension.cs
new file mode 100644
index 00000000..7d31d138
--- /dev/null
+++ b/src/DotNetStac/Extensions/GenericStacExtension.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace Stac.Extensions
+{
+ internal class GenericStacExtension : IStacExtension
+ {
+ private string prefix;
+
+ public GenericStacExtension(string prefix)
+ {
+ this.prefix = prefix;
+ }
+
+ public string Id => prefix;
+
+ internal static IStacExtension CreateForStacObject(string prefix, IStacObject stacObject)
+ {
+ return new GenericStacExtension(prefix);
+ }
+
+ public IStacExtension CopyForStacObject(IStacObject stacObject)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/Extensions/IStacExtension.cs b/src/DotNetStac/Extensions/IStacExtension.cs
new file mode 100644
index 00000000..f701f6f3
--- /dev/null
+++ b/src/DotNetStac/Extensions/IStacExtension.cs
@@ -0,0 +1,9 @@
+namespace Stac.Extensions
+{
+ public interface IStacExtension
+ {
+ string Id { get; }
+
+ IStacExtension CopyForStacObject(IStacObject stacObject);
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/Extensions/IStacExtensionsFactory.cs b/src/DotNetStac/Extensions/IStacExtensionsFactory.cs
new file mode 100644
index 00000000..6f725697
--- /dev/null
+++ b/src/DotNetStac/Extensions/IStacExtensionsFactory.cs
@@ -0,0 +1,9 @@
+namespace Stac.Extensions
+{
+ public interface IStacExtensionsFactory
+ {
+
+ IStacExtension CreateStacExtension(string prefix, IStacObject stacObject);
+
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/Extensions/Sat/SatStacExtension.cs b/src/DotNetStac/Extensions/Sat/SatStacExtension.cs
new file mode 100644
index 00000000..7a5d4f69
--- /dev/null
+++ b/src/DotNetStac/Extensions/Sat/SatStacExtension.cs
@@ -0,0 +1,20 @@
+using System;
+using Stac;
+using Stac.Extensions;
+
+namespace DotNetStac.Extensions.Sat
+{
+ public class SatStacExtension : IStacExtension
+ {
+ public SatStacExtension()
+ {
+ }
+
+ public string Id => "sat";
+
+ public IStacExtension CopyForStacObject(IStacObject stacObject)
+ {
+ return new SatStacExtension();
+ }
+ }
+}
diff --git a/src/DotNetStac/Extensions/StacExtensionsFactory.cs b/src/DotNetStac/Extensions/StacExtensionsFactory.cs
new file mode 100644
index 00000000..46962d2d
--- /dev/null
+++ b/src/DotNetStac/Extensions/StacExtensionsFactory.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Stac.Extensions
+{
+ public class StacExtensionsFactory : IStacExtensionsFactory
+ {
+
+ Dictionary stacExtensionsDictionary = new Dictionary();
+
+ public StacExtensionsFactory(IStacExtension[] stacExtensions)
+ {
+ this.stacExtensionsDictionary = stacExtensions.ToDictionary(se => se.Id, se => se);
+ }
+
+ internal StacExtensionsFactory()
+ {
+ this.stacExtensionsDictionary = new IStacExtension[] {
+ new DotNetStac.Extensions.Sat.SatStacExtension()
+ }.ToDictionary(se => se.Id, se => se);
+ }
+
+ public static StacExtensionsFactory Default
+ {
+ get
+ {
+ return StacExtensionsFactory.CreateDefaultFactory();
+ }
+ }
+
+ public static StacExtensionsFactory CreateDefaultFactory()
+ {
+ return new StacExtensionsFactory();
+ }
+
+ public IStacExtension CreateStacExtension(string prefix, IStacObject stacObject)
+ {
+ if ( stacExtensionsDictionary.ContainsKey(prefix) )
+ return stacExtensionsDictionary[prefix].CopyForStacObject(stacObject);
+
+ return GenericStacExtension.CreateForStacObject(prefix, stacObject);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/IInternalStacObject.cs b/src/DotNetStac/IInternalStacObject.cs
new file mode 100644
index 00000000..fcc7f06f
--- /dev/null
+++ b/src/DotNetStac/IInternalStacObject.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.ObjectModel;
+using Stac.Extensions;
+
+namespace Stac
+{
+ internal interface IInternalStacObject
+ {
+ string Id { get; }
+
+ string StacVersion { get; set; }
+
+ Uri Uri { get; set; }
+
+ Collection StacExtensions { get; set; }
+
+ Collection Links { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/IStacCatalog.cs b/src/DotNetStac/IStacCatalog.cs
new file mode 100644
index 00000000..6ec23704
--- /dev/null
+++ b/src/DotNetStac/IStacCatalog.cs
@@ -0,0 +1,8 @@
+using Stac.Catalog;
+
+namespace Stac
+{
+ public interface IStacCatalog: IStacObject
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/IStacCollection.cs b/src/DotNetStac/IStacCollection.cs
new file mode 100644
index 00000000..ea4ba655
--- /dev/null
+++ b/src/DotNetStac/IStacCollection.cs
@@ -0,0 +1,8 @@
+using Stac.Collection;
+
+namespace Stac
+{
+ public interface IStacCollection: IStacObject
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/IStacItem.cs b/src/DotNetStac/IStacItem.cs
new file mode 100644
index 00000000..a8408975
--- /dev/null
+++ b/src/DotNetStac/IStacItem.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using Stac.Item;
+
+namespace Stac
+{
+ public interface IStacItem : IStacObject
+ {
+ IDictionary Assets { get; }
+
+ IDictionary Properties { get; }
+
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/IStacObject.cs b/src/DotNetStac/IStacObject.cs
new file mode 100644
index 00000000..c86087d3
--- /dev/null
+++ b/src/DotNetStac/IStacObject.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.ObjectModel;
+using Stac.Extensions;
+
+namespace Stac
+{
+ public interface IStacObject
+ {
+ string Id { get; }
+
+ string StacVersion { get; }
+
+ Uri Uri { get; }
+
+ Collection StacExtensions { get; }
+
+ Collection Links { get; }
+
+ IStacObject Upgrade();
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/Item/StacItem.Helper.cs b/src/DotNetStac/Item/StacItem.Helper.cs
new file mode 100644
index 00000000..efa1a63f
--- /dev/null
+++ b/src/DotNetStac/Item/StacItem.Helper.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using DotNetStac;
+using DotNetStac.Converters;
+using GeoJSON.Net.Geometry;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Stac.Catalog;
+using Stac.Converters;
+using Stac.Extensions;
+using Stac.Model;
+
+namespace Stac.Item
+{
+ public partial class StacItem
+ {
+
+ public static async Task LoadUri(Uri uri)
+ {
+ var catalog = await StacFactory.LoadUriAsync(uri);
+ if (catalog is IStacItem)
+ return (IStacItem)catalog;
+ throw new InvalidOperationException(string.Format("This is not a STAC item {0}", catalog.Uri));
+ }
+
+ public static async Task LoadStacLink(StacLink link)
+ {
+ var catalog = await StacFactory.LoadStacLink(link);
+ if (catalog is IStacItem)
+ return (IStacItem)catalog;
+ throw new InvalidOperationException(string.Format("This is not a STAC item {0}", catalog.Uri));
+ }
+
+ public static IStacItem LoadJToken(JToken jsonRoot, Uri uri)
+ {
+ IStacItem item = LoadStacItem(jsonRoot);
+ ((IInternalStacObject)item).Uri = uri;
+ return item;
+ }
+
+ private static IStacItem LoadStacItem(JToken jsonRoot)
+ {
+ Type itemType = null;
+ if (jsonRoot["stac_version"] == null)
+ {
+ throw new InvalidDataException("The document is not a STAC document. No 'stac_version' property found");
+ }
+
+ try
+ {
+ itemType = Stac.Model.SchemaDictionary.GetItemTypeFromVersion(jsonRoot["stac_version"].Value());
+ }
+ catch (KeyNotFoundException)
+ {
+ throw new NotSupportedException(string.Format("The document has a non supprted version: '{0}'.", jsonRoot["stac_version"].Value()));
+ }
+
+ return (IStacItem)jsonRoot.ToObject(itemType);
+
+ }
+ }
+}
diff --git a/src/DotNetStac/Item/StacItem.Model.cs b/src/DotNetStac/Item/StacItem.Model.cs
new file mode 100644
index 00000000..6632ff5d
--- /dev/null
+++ b/src/DotNetStac/Item/StacItem.Model.cs
@@ -0,0 +1,170 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Runtime.Serialization;
+using DotNetStac;
+using DotNetStac.Converters;
+using GeoJSON.Net.Geometry;
+using Newtonsoft.Json;
+using Stac.Converters;
+using Stac.Extensions;
+using Stac.Model;
+
+namespace Stac.Item
+{
+ [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
+ public partial class StacItem : GeoJSON.Net.Feature.Feature, IStacObject, IInternalStacObject
+ {
+ private Collection links;
+
+ private Dictionary assets;
+
+ private string stacVersion = StacVersionList.Current;
+
+ private Collection extensions;
+ private string collection;
+
+ private Uri sourceUri;
+
+ [JsonConstructor]
+ public StacItem(IGeometryObject geometry, IDictionary properties = null, string id = null) : base(geometry, properties, id)
+ { }
+
+ public StacItem(IGeometryObject geometry, object properties, string id = null) : base(geometry, properties, id)
+ { }
+
+ [JsonProperty("stac_extensions")]
+ [JsonConverter(typeof(StacExtensionConverter))]
+ public Collection StacExtensions
+ {
+ get
+ {
+ if (extensions == null)
+ extensions = new Collection();
+ return extensions;
+ }
+ set
+ {
+ extensions = value;
+ }
+ }
+
+ [JsonProperty("stac_version")]
+ public string StacVersion
+ {
+ get
+ {
+ return stacVersion;
+ }
+
+ set
+ {
+ stacVersion = value;
+ }
+ }
+
+ [JsonConverter(typeof(CollectionConverter))]
+ [JsonProperty("links")]
+ public Collection Links
+ {
+ get
+ {
+ if (links == null)
+ links = new Collection();
+ return links;
+ }
+ set
+ {
+ links = value;
+ }
+ }
+
+ [JsonProperty("assets")]
+ public IDictionary Assets
+ {
+ get
+ {
+ if (assets == null)
+ assets = new Dictionary();
+ return assets;
+ }
+ }
+
+ [JsonProperty("collection")]
+ public string Collection
+ {
+ get
+ {
+ return collection;
+ }
+ set
+ {
+ collection = value;
+ }
+ }
+
+ [JsonIgnore]
+ public Itenso.TimePeriod.ITimePeriod DateTime
+ {
+ get
+ {
+ if (Properties.ContainsKey("datetime"))
+ {
+ if (Properties["datetime"] is DateTime)
+ return new Itenso.TimePeriod.TimeInterval((DateTime)Properties["datetime"]);
+ else
+ {
+ try
+ {
+ return new Itenso.TimePeriod.TimeInterval(System.DateTime.Parse(Properties["datetime"].ToString()));
+ }
+ catch (Exception e)
+ {
+ throw new FormatException(string.Format("{0} is not a valid"), e);
+ }
+ }
+ }
+ if (Properties.ContainsKey("start_datetime") && Properties.ContainsKey("end_datetime"))
+ {
+ if (Properties["start_datetime"] is DateTime && Properties["end_datetime"] is DateTime)
+ return new Itenso.TimePeriod.TimeInterval((DateTime)Properties["start_datetime"],
+ (DateTime)Properties["end_datetime"]);
+ else
+ {
+ try
+ {
+ return new Itenso.TimePeriod.TimeInterval(System.DateTime.Parse(Properties["start_datetime"].ToString()),
+ System.DateTime.Parse(Properties["end_datetime"].ToString()));
+ }
+ catch (Exception e)
+ {
+ throw new FormatException(string.Format("{0} is not a valid"), e);
+ }
+ }
+ }
+
+ return null;
+ }
+ }
+
+ public Uri Uri { get => sourceUri; set => sourceUri = value; }
+
+ public IStacObject Upgrade()
+ {
+ return this;
+ }
+
+ [OnDeserialized]
+ internal void OnDeserializedMethod(StreamingContext context)
+ {
+ foreach (StacLink link in Links)
+ {
+ link.Parent = this;
+ }
+ foreach (StacAsset asset in Assets.Values)
+ {
+ asset.Parent = this;
+ }
+ }
+ }
+}
diff --git a/src/DotNetStac/Model/SchemaDictionary.cs b/src/DotNetStac/Model/SchemaDictionary.cs
new file mode 100644
index 00000000..37c7ed36
--- /dev/null
+++ b/src/DotNetStac/Model/SchemaDictionary.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using Newtonsoft.Json.Linq;
+
+namespace Stac.Model
+{
+
+ public static class SchemaDictionary
+ {
+ private static Dictionary catalogVersionDictionary = new Dictionary()
+ {
+ { "0.6.0", typeof(Stac.Model.v060.StacCatalog060) },
+ { "0.7.0", typeof(Stac.Model.v070.StacCatalog070) },
+ { "1.0.0-beta.1", typeof(Stac.Catalog.StacCatalog) }
+ };
+
+ private static Dictionary collectionVersionDictionary = new Dictionary()
+ {
+ { "0.6.0", typeof(Stac.Model.v060.StacCollection060) },
+ { "0.7.0", typeof(Stac.Model.v070.StacCollection070) },
+ { "1.0.0-beta.1", typeof(Stac.Collection.StacCollection) }
+ };
+
+ private static Dictionary itemVersionDictionary = new Dictionary()
+ {
+ { "0.6.0", typeof(Stac.Model.v060.StacItem060) },
+ { "0.7.0", typeof(Stac.Model.v070.StacItem070) },
+ { "1.0.0-beta.1", typeof(Stac.Item.StacItem) }
+ };
+
+ internal static Type GetCatalogTypeFromVersion(string version)
+ {
+ return catalogVersionDictionary[version];
+ }
+
+ internal static Type GetCollectionTypeFromVersion(string version)
+ {
+ return collectionVersionDictionary[version];
+ }
+
+ internal static Type GetItemTypeFromVersion(string version)
+ {
+ return itemVersionDictionary[version];
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/Model/v060/StacCatalog.cs b/src/DotNetStac/Model/v060/StacCatalog.cs
new file mode 100644
index 00000000..6c2e840e
--- /dev/null
+++ b/src/DotNetStac/Model/v060/StacCatalog.cs
@@ -0,0 +1,127 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Runtime.Serialization;
+using DotNetStac;
+using DotNetStac.Converters;
+using GeoJSON.Net.Geometry;
+using Newtonsoft.Json;
+using Stac.Converters;
+using Stac.Extensions;
+
+namespace Stac.Model.v060
+{
+ [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
+ internal class StacCatalog060 : IStacObject, IStacCatalog, IInternalStacObject
+ {
+ private readonly string id;
+ private Collection links;
+
+ private string stacVersion = StacVersionList.Current;
+
+ private Collection extensions;
+
+ private string description;
+
+
+ private string title;
+ private Uri sourceUri;
+
+ [JsonConstructor]
+ public StacCatalog060(string id, string description, IEnumerable links = null)
+ {
+ this.id = id;
+ this.description = description;
+ if (links == null)
+ this.links = new Collection();
+ else
+ this.links = new Collection(links.ToList());
+ }
+
+ [JsonProperty("stac_extensions")]
+ [JsonConverter(typeof(StacExtensionConverter))]
+ public Collection StacExtensions
+ {
+ get
+ {
+ if (extensions == null)
+ extensions = new Collection();
+ return extensions;
+ }
+ set
+ {
+ extensions = value;
+ }
+ }
+
+ [JsonProperty("stac_version")]
+ public string StacVersion
+ {
+ get
+ {
+ return stacVersion;
+ }
+
+ set
+ {
+ stacVersion = value;
+ }
+ }
+
+ [JsonConverter(typeof(CollectionConverter))]
+ [JsonProperty("links")]
+ public Collection Links
+ {
+ get
+ {
+ if (links == null)
+ links = new Collection();
+ return links;
+ }
+ set
+ {
+ links = value;
+ }
+ }
+
+ [JsonProperty("description")]
+ public string Description
+ {
+ get
+ {
+ return description;
+ }
+ set
+ {
+ description = value;
+ }
+ }
+
+ [JsonProperty("id")]
+ public string Id => id;
+
+ public string Title { get => title; set => title = value; }
+
+ public Uri Uri { get => sourceUri; set => sourceUri = value; }
+
+ public virtual IStacObject Upgrade()
+ {
+ var catalog = new v070.StacCatalog070(this.Id,
+ this.Description,
+ this.Links);
+ catalog.StacExtensions = this.StacExtensions;
+ catalog.Title = this.Title;
+ return catalog;
+ }
+
+ [OnDeserialized]
+ internal void OnDeserializedMethod(StreamingContext context)
+ {
+ foreach (StacLink link in Links)
+ {
+ link.Parent = this;
+ }
+ }
+ }
+}
diff --git a/src/DotNetStac/Model/v060/StacCollection.cs b/src/DotNetStac/Model/v060/StacCollection.cs
new file mode 100644
index 00000000..2a004a2e
--- /dev/null
+++ b/src/DotNetStac/Model/v060/StacCollection.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using DotNetStac;
+using DotNetStac.Converters;
+using GeoJSON.Net.Geometry;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Stac.Catalog;
+using Stac.Converters;
+using Stac.Extensions;
+
+namespace Stac.Model.v060
+{
+ [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
+ internal class StacCollection060 : StacCatalog060, IStacObject, IStacCollection
+ {
+ private string license;
+ private StacExtent060 extent;
+ private Collection providers;
+ private Collection keywords;
+
+ [JsonConstructor]
+ public StacCollection060(string id, string description, StacExtent060 extent, IEnumerable links = null, string license = "proprietary") :
+ base(id, description, links)
+ {
+ this.license = license;
+ this.extent = extent;
+ }
+
+ [JsonProperty("extent")]
+ public StacExtent060 Extent { get => extent; set => extent = value; }
+
+ [JsonProperty("license")]
+ public string License { get => license; set => license = value; }
+
+ [JsonProperty("providers")]
+ public Collection Providers
+ {
+ get { return providers; }
+ set
+ {
+ providers = value;
+ }
+ }
+
+ [JsonProperty("keywords", DefaultValueHandling = DefaultValueHandling.Ignore)]
+ public Collection Keywords
+ {
+ get
+ {
+ if (keywords == null)
+ keywords = new Collection();
+ return keywords;
+ }
+ set
+ {
+ keywords = value;
+ }
+ }
+
+ [JsonExtensionData]
+ public Dictionary Properties { get; set; }
+
+ public override IStacObject Upgrade()
+ {
+ var collection = new v070.StacCollection070(this.Id,
+ this.Description,
+ this.Extent,
+ this.Links);
+ collection.StacExtensions = this.StacExtensions;
+ collection.Title = this.Title;
+ collection.Keywords = this.Keywords;
+ collection.License = this.License;
+ collection.Providers = this.Providers;
+ return collection;
+ }
+ }
+}
diff --git a/src/DotNetStac/Model/v060/StacExtent.cs b/src/DotNetStac/Model/v060/StacExtent.cs
new file mode 100644
index 00000000..dcb7c111
--- /dev/null
+++ b/src/DotNetStac/Model/v060/StacExtent.cs
@@ -0,0 +1,23 @@
+using System;
+using Newtonsoft.Json;
+
+namespace Stac.Model.v060
+{
+ [JsonObject]
+ internal class StacExtent060
+ {
+ [JsonProperty("spatial")]
+ public double[] Spatial { get; set; }
+
+ [JsonProperty("temporal")]
+ public DateTime?[] Temporal { get; set; }
+
+ internal StacExtent Upgrade()
+ {
+ return new StacExtent(){
+ Spatial = new StacSpatialExtent(this.Spatial[0], this.Spatial[1], this.Spatial[2], this.Spatial[3]),
+ Temporal = new StacTemporalExtent(this.Temporal[0], this.Temporal[1])
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/Model/v060/StacItem.cs b/src/DotNetStac/Model/v060/StacItem.cs
new file mode 100644
index 00000000..756ddb6a
--- /dev/null
+++ b/src/DotNetStac/Model/v060/StacItem.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using DotNetStac;
+using DotNetStac.Converters;
+using GeoJSON.Net.Geometry;
+using Newtonsoft.Json;
+using Stac.Converters;
+using Stac.Extensions;
+
+namespace Stac.Model.v060
+{
+ [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
+ internal class StacItem060 : GeoJSON.Net.Feature.Feature, IStacObject, IStacItem
+ {
+ private Collection links;
+
+ private Dictionary assets;
+
+ private string stacVersion = StacVersionList.Current;
+
+ private string collection;
+
+ [JsonConstructor]
+ public StacItem060(IGeometryObject geometry, IDictionary properties = null, string id = null) : base(geometry, properties, id)
+ { }
+
+ public StacItem060(IGeometryObject geometry, object properties, string id = null) : base(geometry, properties, id)
+ { }
+
+ [JsonConverter(typeof(CollectionConverter))]
+ [JsonProperty("links")]
+ public Collection Links
+ {
+ get
+ {
+ if (links == null)
+ links = new Collection();
+ return links;
+ }
+ set
+ {
+ links = value;
+ }
+ }
+
+ [JsonProperty("assets")]
+ public IDictionary Assets
+ {
+ get
+ {
+ if (assets == null)
+ assets = new Dictionary();
+ return assets;
+ }
+ }
+
+ [JsonProperty("collection")]
+ public string Collection
+ {
+ get
+ {
+ return collection;
+ }
+ set
+ {
+ collection = value;
+ }
+ }
+
+ [JsonIgnore]
+ public Itenso.TimePeriod.ITimePeriod DateTime
+ {
+ get
+ {
+ if (Properties.ContainsKey("datetime"))
+ {
+ if (Properties["datetime"] is DateTime)
+ return new Itenso.TimePeriod.TimeInterval((DateTime)Properties["datetime"]);
+ else
+ {
+ try
+ {
+ return new Itenso.TimePeriod.TimeInterval(System.DateTime.Parse(Properties["datetime"].ToString()));
+ }
+ catch (Exception e)
+ {
+ throw new FormatException(string.Format("{0} is not a valid"), e);
+ }
+ }
+ }
+ if (Properties.ContainsKey("start_datetime") && Properties.ContainsKey("end_datetime"))
+ {
+ if (Properties["start_datetime"] is DateTime && Properties["end_datetime"] is DateTime)
+ return new Itenso.TimePeriod.TimeInterval((DateTime)Properties["start_datetime"],
+ (DateTime)Properties["end_datetime"]);
+ else
+ {
+ try
+ {
+ return new Itenso.TimePeriod.TimeInterval(System.DateTime.Parse(Properties["start_datetime"].ToString()),
+ System.DateTime.Parse(Properties["end_datetime"].ToString()));
+ }
+ catch (Exception e)
+ {
+ throw new FormatException(string.Format("{0} is not a valid"), e);
+ }
+ }
+ }
+
+ return null;
+ }
+ }
+
+ [JsonIgnore]
+ public Uri Uri => throw new NotImplementedException();
+
+ [JsonIgnore]
+ public string StacVersion => StacVersionList.V060;
+
+ [JsonIgnore]
+ public Collection StacExtensions => null;
+
+ public IStacObject Upgrade()
+ {
+ var item = new v070.StacItem070(this.Geometry,
+ this.Properties,
+ this.Id);
+ item.Links = this.links;
+ foreach (var asset in this.Assets)
+ item.Assets.Add(asset.Key, asset.Value);
+ item.Collection = this.Collection;
+ item.BoundingBoxes = this.BoundingBoxes;
+ return item;
+ }
+
+
+ }
+}
diff --git a/src/DotNetStac/Model/v070/StacCatalog.cs b/src/DotNetStac/Model/v070/StacCatalog.cs
new file mode 100644
index 00000000..2176b385
--- /dev/null
+++ b/src/DotNetStac/Model/v070/StacCatalog.cs
@@ -0,0 +1,131 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Runtime.Serialization;
+using DotNetStac;
+using DotNetStac.Converters;
+using GeoJSON.Net.Geometry;
+using Newtonsoft.Json;
+using Stac.Converters;
+using Stac.Extensions;
+using Stac.Model.v060;
+
+namespace Stac.Model.v070
+{
+ [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
+ internal class StacCatalog070 : IStacObject, IStacCatalog, IInternalStacObject
+ {
+ private readonly string id;
+ private Collection links;
+
+ private string stacVersion = StacVersionList.Current;
+
+ private Collection extensions;
+
+ private string description;
+
+
+ private string title;
+ private Uri sourceUri;
+
+ [JsonConstructor]
+ public StacCatalog070(string id, string description, IEnumerable links = null)
+ {
+ this.id = id;
+ this.description = description;
+ if (links == null)
+ this.links = new Collection();
+ else
+ this.links = new Collection(links.ToList());
+ }
+
+ [JsonProperty("stac_extensions")]
+ [JsonConverter(typeof(StacExtensionConverter))]
+ public Collection StacExtensions
+ {
+ get
+ {
+ if (extensions == null)
+ extensions = new Collection();
+ return extensions;
+ }
+ set
+ {
+ extensions = value;
+ }
+ }
+
+ [JsonProperty("stac_version")]
+ public string StacVersion
+ {
+ get
+ {
+ return stacVersion;
+ }
+
+ set
+ {
+ stacVersion = value;
+ }
+ }
+
+ [JsonConverter(typeof(CollectionConverter))]
+ [JsonProperty("links")]
+ public Collection Links
+ {
+ get
+ {
+ if (links == null)
+ links = new Collection();
+ return links;
+ }
+ set
+ {
+ links = value;
+ }
+ }
+
+
+
+ [JsonProperty("description")]
+ public string Description
+ {
+ get
+ {
+ return description;
+ }
+ set
+ {
+ description = value;
+ }
+ }
+
+ [JsonProperty("id")]
+ public string Id => id;
+
+ public string Title { get => title; set => title = value; }
+
+ public Uri Uri { get => sourceUri; set => sourceUri = value; }
+
+ public virtual IStacObject Upgrade()
+ {
+ var catalog = new Catalog.StacCatalog(this.Id,
+ this.Description,
+ this.Links);
+ catalog.StacExtensions = this.StacExtensions;
+ catalog.Title = this.Title;
+
+ return catalog;
+ }
+
+ [OnDeserialized]
+ internal void OnDeserializedMethod(StreamingContext context)
+ {
+ foreach (StacLink link in Links)
+ {
+ link.Parent = this;
+ }
+ }
+ }
+}
diff --git a/src/DotNetStac/Model/v070/StacCollection.cs b/src/DotNetStac/Model/v070/StacCollection.cs
new file mode 100644
index 00000000..ae900eda
--- /dev/null
+++ b/src/DotNetStac/Model/v070/StacCollection.cs
@@ -0,0 +1,83 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using Newtonsoft.Json;
+using Stac.Converters;
+
+namespace Stac.Model.v070
+{
+ [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
+ internal class StacCollection070 : StacCatalog070, IStacObject, IStacCollection, IInternalStacObject
+ {
+ private string license;
+ private v060.StacExtent060 extent;
+ private Dictionary summaries;
+ private Collection providers;
+ private Collection keywords;
+
+ [JsonConstructor]
+ public StacCollection070(string id, string description, v060.StacExtent060 extent, IEnumerable links = null, string license = "proprietary") :
+ base(id, description, links)
+ {
+ this.license = license;
+ this.extent = extent;
+ }
+
+ [JsonProperty("extent")]
+ public Stac.Model.v060.StacExtent060 Extent { get => extent; set => extent = value; }
+
+ [JsonProperty("summaries")]
+ [JsonConverter(typeof(StacSummariesConverter))]
+ public Dictionary Summaries
+ {
+ get
+ {
+ return summaries;
+ }
+ set
+ {
+ summaries = value;
+ }
+ }
+
+ [JsonProperty("license")]
+ public string License { get => license; set => license = value; }
+
+ [JsonProperty("providers")]
+ public Collection Providers
+ {
+ get { return providers; }
+ set
+ {
+ providers = value;
+ }
+ }
+
+ [JsonProperty("keywords", DefaultValueHandling = DefaultValueHandling.Ignore)]
+ public Collection Keywords
+ {
+ get
+ {
+ if (keywords == null)
+ keywords = new Collection();
+ return keywords;
+ }
+ set
+ {
+ keywords = value;
+ }
+ }
+
+ public override IStacObject Upgrade()
+ {
+ var collection = new Collection.StacCollection(this.Id,
+ this.Description, this.extent.Upgrade(), this.Links);
+ collection.StacExtensions = this.StacExtensions;
+ collection.Title = this.Title;
+ collection.Keywords = this.Keywords;
+ collection.License = this.License;
+ collection.Providers = this.Providers;
+ collection.Summaries = this.Summaries;
+ return collection;
+ }
+ }
+}
diff --git a/src/DotNetStac/Model/v070/StacItem.cs b/src/DotNetStac/Model/v070/StacItem.cs
new file mode 100644
index 00000000..7847abc0
--- /dev/null
+++ b/src/DotNetStac/Model/v070/StacItem.cs
@@ -0,0 +1,134 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using DotNetStac;
+using DotNetStac.Converters;
+using GeoJSON.Net.Geometry;
+using Newtonsoft.Json;
+using Stac.Converters;
+using Stac.Extensions;
+
+namespace Stac.Model.v070
+{
+ [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
+ internal class StacItem070 : GeoJSON.Net.Feature.Feature, IStacObject, IStacItem, IInternalStacObject
+ {
+ private Collection links;
+
+ private Dictionary assets;
+
+ private string collection;
+ private Uri sourceUri;
+
+ [JsonConstructor]
+ public StacItem070(IGeometryObject geometry, IDictionary properties = null, string id = null) : base(geometry, properties, id)
+ { }
+
+ public StacItem070(IGeometryObject geometry, object properties, string id = null) : base(geometry, properties, id)
+ { }
+
+ [JsonConverter(typeof(CollectionConverter))]
+ [JsonProperty("links")]
+ public Collection Links
+ {
+ get
+ {
+ if (links == null)
+ links = new Collection();
+ return links;
+ }
+ set
+ {
+ links = value;
+ }
+ }
+
+ [JsonProperty("assets")]
+ public IDictionary Assets
+ {
+ get
+ {
+ if (assets == null)
+ assets = new Dictionary();
+ return assets;
+ }
+ }
+
+ [JsonProperty("collection")]
+ public string Collection
+ {
+ get
+ {
+ return collection;
+ }
+ set
+ {
+ collection = value;
+ }
+ }
+
+ [JsonIgnore]
+ public Itenso.TimePeriod.ITimePeriod DateTime
+ {
+ get
+ {
+ if (Properties.ContainsKey("datetime"))
+ {
+ if (Properties["datetime"] is DateTime)
+ return new Itenso.TimePeriod.TimeInterval((DateTime)Properties["datetime"]);
+ else
+ {
+ try
+ {
+ return new Itenso.TimePeriod.TimeInterval(System.DateTime.Parse(Properties["datetime"].ToString()));
+ }
+ catch (Exception e)
+ {
+ throw new FormatException(string.Format("{0} is not a valid"), e);
+ }
+ }
+ }
+ if (Properties.ContainsKey("start_datetime") && Properties.ContainsKey("end_datetime"))
+ {
+ if (Properties["start_datetime"] is DateTime && Properties["end_datetime"] is DateTime)
+ return new Itenso.TimePeriod.TimeInterval((DateTime)Properties["start_datetime"],
+ (DateTime)Properties["end_datetime"]);
+ else
+ {
+ try
+ {
+ return new Itenso.TimePeriod.TimeInterval(System.DateTime.Parse(Properties["start_datetime"].ToString()),
+ System.DateTime.Parse(Properties["end_datetime"].ToString()));
+ }
+ catch (Exception e)
+ {
+ throw new FormatException(string.Format("{0} is not a valid"), e);
+ }
+ }
+ }
+
+ return null;
+ }
+ }
+
+ public Uri Uri { get => sourceUri; set => sourceUri = value; }
+
+ public string StacVersion { get => StacVersionList.V070; set { } }
+
+ public Collection StacExtensions { get => null; set { } }
+
+
+ public IStacObject Upgrade()
+ {
+ var item = new Item.StacItem(this.Geometry,
+ this.Properties,
+ this.Id);
+ item.Links = this.links;
+ foreach (var asset in this.Assets)
+ item.Assets.Add(asset.Key, asset.Value);
+ item.Collection = this.Collection;
+ item.BoundingBoxes = this.BoundingBoxes;
+ return item;
+ }
+ }
+}
diff --git a/src/DotNetStac/StacAsset.cs b/src/DotNetStac/StacAsset.cs
new file mode 100644
index 00000000..20aa4f5e
--- /dev/null
+++ b/src/DotNetStac/StacAsset.cs
@@ -0,0 +1,161 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using DotNetStac.Converters;
+using Newtonsoft.Json;
+
+namespace Stac
+{
+ [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
+ public class StacAsset
+ {
+
+ #region Static members
+
+ public static StacAsset CreateThumbnailAsset(Uri uri, string mediaType, string title = null)
+ {
+ return new StacAsset(uri, new string[] { "thumbnail" }, title, mediaType);
+ }
+
+ public static StacAsset CreateOverviewAsset(Uri uri, string mediaType, string title = null)
+ {
+ return new StacAsset(uri, new string[] { "overview" }, title, mediaType);
+ }
+
+ public static StacAsset CreateDataAsset(Uri uri, string mediaType, string title = null)
+ {
+ return new StacAsset(uri, new string[] { "data" }, title, mediaType);
+ }
+
+ public static StacAsset CreateMetadataAsset(Uri uri, string mediaType, string title = null)
+ {
+ return new StacAsset(uri, new string[] { "metadata" }, title, mediaType);
+ }
+
+ #endregion
+
+ Uri base_uri, href;
+ string title, type, description;
+
+ Collection semanticRoles;
+ private IStacObject hostObject;
+
+ public StacAsset()
+ {
+ }
+
+ public StacAsset(Uri uri, IStacObject hostObject)
+ {
+ Uri = uri;
+ this.hostObject = hostObject;
+ }
+
+ public StacAsset(Uri uri, IEnumerable semanticRoles, string title, string mediaType)
+ {
+ Uri = uri;
+ this.semanticRoles = semanticRoles == null ? new Collection() : new Collection(semanticRoles.ToList());
+ Title = title;
+ MediaType = mediaType;
+ }
+
+ public StacAsset(StacAsset source)
+ {
+ if (source == null)
+ throw new ArgumentNullException("source");
+ base_uri = source.base_uri;
+ href = source.href;
+ semanticRoles = source.semanticRoles;
+ title = source.title;
+ type = source.type;
+ description = source.description;
+ }
+
+ [JsonProperty("type")]
+ public string MediaType
+ {
+ get { return type; }
+ set { type = value; }
+ }
+
+ [JsonProperty("roles")]
+ public List Roles
+ {
+ get
+ {
+ if (semanticRoles == null || semanticRoles.Count == 0)
+ return null;
+ return semanticRoles.ToList();
+ }
+ set
+ {
+ if (value == null)
+ semanticRoles = null;
+ else
+ semanticRoles = new Collection(value);
+ }
+ }
+
+ [JsonIgnore]
+ public Collection SemanticRoles
+ {
+ get
+ {
+ if (semanticRoles == null)
+ semanticRoles = new Collection();
+ return semanticRoles;
+ }
+ }
+
+ [JsonProperty("title")]
+ public string Title
+ {
+ get { return title; }
+ set { title = value; }
+ }
+
+ [JsonProperty("href")]
+ public Uri Uri
+ {
+ get { return href; }
+ set { href = value; }
+ }
+
+ [JsonProperty("description")]
+ public string Description
+ {
+ get { return description; }
+ set { description = value; }
+ }
+
+ public virtual StacAsset Clone()
+ {
+ return new StacAsset(this);
+ }
+
+ [JsonIgnore]
+ public Uri AbsoluteUri
+ {
+ get
+ {
+ if (Uri.IsAbsoluteUri)
+ return Uri;
+
+ if (hostObject != null)
+ return new Uri(new Uri(hostObject.Uri.AbsoluteUri.Substring(0, hostObject.Uri.AbsoluteUri.LastIndexOf('/') + 1)), Uri);
+
+ return null;
+ }
+ }
+
+ public IStacObject Parent
+ {
+ get => hostObject;
+ internal set
+ {
+ hostObject = value;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/StacExtensionsHelper.cs b/src/DotNetStac/StacExtensionsHelper.cs
new file mode 100644
index 00000000..64f2c647
--- /dev/null
+++ b/src/DotNetStac/StacExtensionsHelper.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Runtime.Serialization;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Stac.Collection;
+using Stac.Item;
+using Stac.Model;
+
+namespace Stac.Catalog
+{
+ public static class StacExtensionsHelper
+ {
+
+ public static IDictionary GetChildren(this IStacObject stacObject)
+ {
+ return GetChildrenAsync(stacObject).ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Result);
+ }
+
+ public static IDictionary> GetChildrenAsync(this IStacObject stacObject)
+ {
+ return stacObject.Links.Where(l => l.RelationshipType == "child").ToDictionary(link => link.Uri, link => StacCatalog.LoadStacLink(link));
+ }
+
+ public static IDictionary GetItems(this IStacObject stacObject)
+ {
+ return GetItemsAsync(stacObject).ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Result);
+ }
+
+ public static IDictionary> GetItemsAsync(this IStacObject stacObject)
+ {
+ return stacObject.Links.Where(l => l.RelationshipType == "item").ToDictionary(link => link.Uri, link => StacItem.LoadStacLink(link));
+ }
+
+ public static IEnumerable GetChildrenLinks(this IStacObject stacObject, bool absolute = true)
+ {
+ return stacObject.Links.Where(l => l.RelationshipType == "child");
+ }
+
+ public static IEnumerable GetItemLinks(this IStacObject stacObject, bool absolute = true)
+ {
+ return stacObject.Links.Where(l => l.RelationshipType == "item");
+ }
+
+ public static StacItem UpgradeToCurrentVersion(this IStacItem item1)
+ {
+ IStacObject item = (IStacObject)item1;
+ while (!(item is Item.StacItem))
+ {
+ item = item.Upgrade();
+ }
+ return (Item.StacItem)item;
+ }
+
+ public static StacCatalog UpgradeToCurrentVersion(this IStacCatalog catalog1)
+ {
+ IStacObject catalog = (IStacObject)catalog1;
+ while (!(catalog is StacCatalog))
+ {
+ catalog = catalog.Upgrade();
+ }
+ return (StacCatalog)catalog;
+ }
+ }
+}
diff --git a/src/DotNetStac/StacExtent.cs b/src/DotNetStac/StacExtent.cs
new file mode 100644
index 00000000..2fa9d90c
--- /dev/null
+++ b/src/DotNetStac/StacExtent.cs
@@ -0,0 +1,14 @@
+using Newtonsoft.Json;
+
+namespace Stac
+{
+ [JsonObject]
+ public class StacExtent
+ {
+ [JsonProperty("spatial")]
+ public StacSpatialExtent Spatial { get; set; }
+
+ [JsonProperty("temporal")]
+ public StacTemporalExtent Temporal { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/StacFactory.cs b/src/DotNetStac/StacFactory.cs
new file mode 100644
index 00000000..4e523e20
--- /dev/null
+++ b/src/DotNetStac/StacFactory.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Stac.Collection;
+using Stac.Item;
+using Stac.Model;
+
+namespace Stac
+{
+ public class StacFactory
+ {
+ public static async Task LoadUriAsync(Uri uri)
+ {
+ WebClient client = new WebClient();
+ return await client.DownloadStringTaskAsync(uri).ContinueWith(json =>
+ {
+ var jsonToken = JsonConvert.DeserializeObject(json.Result);
+ if (jsonToken["geometry"] != null)
+ return StacItem.LoadJToken(jsonToken, uri);
+
+ return StacCollection.LoadJToken(jsonToken, uri);
+
+ });
+ }
+
+ public static IStacObject Load(string uri)
+ {
+ return LoadUriAsync(new Uri(uri)).Result;
+ }
+
+ public static async Task LoadStacLink(StacLink link)
+ {
+ WebClient client = new WebClient();
+ var jsonToken = JsonConvert.DeserializeObject(await client.DownloadStringTaskAsync(link.AbsoluteUri));
+ if (jsonToken["stac_version"] == null && link.Parent != null && Version.Parse(link.Parent.StacVersion).CompareTo(Version.Parse("0.8.0")) < 0)
+ jsonToken["stac_version"] = link.Parent.StacVersion;
+
+ if (jsonToken["geometry"] != null)
+ return StacItem.LoadJToken(jsonToken, link.AbsoluteUri);
+
+ return StacCollection.LoadJToken(jsonToken, link.AbsoluteUri);
+ }
+
+ }
+}
diff --git a/src/DotNetStac/StacLink.cs b/src/DotNetStac/StacLink.cs
new file mode 100644
index 00000000..38974418
--- /dev/null
+++ b/src/DotNetStac/StacLink.cs
@@ -0,0 +1,140 @@
+using System;
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+namespace Stac
+{
+ [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)]
+ public class StacLink
+ {
+ #region Static members
+
+ public static StacLink CreateSelfLink(Uri uri, string mediaType = null)
+ {
+ return new StacLink(uri, "self", null, mediaType);
+ }
+
+ public static StacLink CreateRootLink(Uri uri, string mediaType = null)
+ {
+ return new StacLink(uri, "root", null, mediaType);
+ }
+
+ public static StacLink CreateParentLink(Uri uri, string mediaType = null)
+ {
+ return new StacLink(uri, "parent", null, mediaType);
+ }
+
+ public static StacLink CreateCollectionLink(Uri uri, string mediaType = null)
+ {
+ return new StacLink(uri, "collection", null, mediaType);
+ }
+
+ public static StacLink CreateDerivedFromLink(Uri uri, string mediaType = null)
+ {
+ return new StacLink(uri, "derived_from", null, mediaType);
+ }
+
+ public static StacLink CreateAlternateLink(Uri uri, string mediaType = null)
+ {
+ return new StacLink(uri, "alternate", null, mediaType);
+ }
+
+ #endregion
+
+ Uri href;
+ string rel, title, type;
+ private IStacObject hostObject;
+
+ public StacLink()
+ {
+ }
+
+ public StacLink(Uri uri)
+ {
+ Uri = uri;
+ }
+
+ public StacLink(Uri uri, IStacObject hostObject)
+ {
+ Uri = uri;
+ this.hostObject = hostObject;
+ }
+
+ public StacLink(Uri uri, string relationshipType, string title, string mediaType)
+ {
+ Uri = uri;
+ RelationshipType = relationshipType;
+ Title = title;
+ MediaType = mediaType;
+ }
+
+ public StacLink(StacLink source)
+ {
+ if (source == null)
+ throw new ArgumentNullException("source");
+ href = source.href;
+ rel = source.rel;
+ title = source.title;
+ type = source.type;
+ hostObject = source.hostObject;
+ }
+
+ [JsonProperty("type")]
+ public string MediaType
+ {
+ get { return type; }
+ set { type = value; }
+ }
+
+ [JsonProperty("rel")]
+ public string RelationshipType
+ {
+ get { return rel; }
+ set { rel = value; }
+ }
+
+ [JsonProperty("title")]
+ public string Title
+ {
+ get { return title; }
+ set { title = value; }
+ }
+
+ [JsonProperty("href")]
+ public Uri Uri
+ {
+ get { return href; }
+ set { href = value; }
+ }
+
+ [JsonIgnore]
+ public Uri AbsoluteUri
+ {
+ get
+ {
+ if (Uri.IsAbsoluteUri)
+ return Uri;
+
+ if (hostObject != null)
+ return new Uri(new Uri(hostObject.Uri.AbsoluteUri.Substring(0, hostObject.Uri.AbsoluteUri.LastIndexOf('/') + 1)), Uri);
+
+ return null;
+ }
+ }
+
+ public IStacObject Parent
+ {
+ get => hostObject;
+ internal set
+ {
+ hostObject = value;
+ }
+ }
+
+ public virtual StacLink Clone()
+ {
+ return new StacLink(this);
+ }
+
+ }
+}
diff --git a/src/DotNetStac/StacSpatialExtent.cs b/src/DotNetStac/StacSpatialExtent.cs
new file mode 100644
index 00000000..d06bf248
--- /dev/null
+++ b/src/DotNetStac/StacSpatialExtent.cs
@@ -0,0 +1,20 @@
+using Newtonsoft.Json;
+
+namespace Stac
+{
+ [JsonObject]
+ public class StacSpatialExtent
+ {
+ public StacSpatialExtent()
+ {
+ }
+
+ public StacSpatialExtent(double minX, double minY, double maxX, double maxY)
+ {
+ BoundingBoxes = new double[1][] { new double[4] { minX, minY, maxX, maxY } };
+ }
+
+ [JsonProperty("bbox")]
+ public double[][] BoundingBoxes { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/StacTemporalExtent.cs b/src/DotNetStac/StacTemporalExtent.cs
new file mode 100644
index 00000000..3378a011
--- /dev/null
+++ b/src/DotNetStac/StacTemporalExtent.cs
@@ -0,0 +1,21 @@
+using System;
+using Newtonsoft.Json;
+
+namespace Stac
+{
+ [JsonObject]
+ public class StacTemporalExtent
+ {
+ public StacTemporalExtent()
+ {
+ }
+
+ public StacTemporalExtent(DateTime? start, DateTime? end)
+ {
+ Interval = new DateTime?[1][] { new DateTime?[2] { start, end } };
+ }
+
+ [JsonProperty("interval")]
+ public DateTime?[][] Interval { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/DotNetStac/StacVersionList.cs b/src/DotNetStac/StacVersionList.cs
new file mode 100644
index 00000000..33d7ce52
--- /dev/null
+++ b/src/DotNetStac/StacVersionList.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace DotNetStac
+{
+ public static class StacVersionList
+ {
+ public static string Current => V100beta1;
+
+ public static string V100beta1 => "1.0.0-beta.1";
+
+ public static string V060 => "0.6.0";
+
+ public static string V070 => "0.7.0";
+ }
+}
diff --git a/src/json-schemas/item.json b/src/json-schemas/item.json
new file mode 100644
index 00000000..c96ab234
--- /dev/null
+++ b/src/json-schemas/item.json
@@ -0,0 +1,179 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "item.json#",
+ "title": "STAC Item",
+ "type": "object",
+ "description": "This object represents the metadata for an item in a SpatioTemporal Asset Catalog.",
+ "additionalProperties": true,
+ "allOf": [
+ {
+ "$ref": "#/definitions/core"
+ }
+ ],
+ "definitions": {
+ "common_metadata": {
+ "allOf": [
+ {
+ "$ref": "basics.json"
+ },
+ {
+ "$ref": "datetime.json"
+ },
+ {
+ "$ref": "instrument.json"
+ },
+ {
+ "$ref": "licensing.json"
+ },
+ {
+ "$ref": "provider.json"
+ }
+ ]
+ },
+ "core": {
+ "allOf": [
+ {
+ "$ref": "https://geojson.org/schema/Feature.json"
+ },
+ {
+ "type": "object",
+ "required": [
+ "stac_version",
+ "id",
+ "links",
+ "assets",
+ "bbox",
+ "properties"
+ ],
+ "properties": {
+ "stac_version": {
+ "title": "STAC version",
+ "type": "string",
+ "const": "1.0.0-beta.1"
+ },
+ "stac_extensions": {
+ "title": "STAC extensions",
+ "type": "array",
+ "uniqueItems": true,
+ "items": {
+ "anyOf": [
+ {
+ "title": "Reference to a JSON Schema",
+ "type": "string",
+ "format": "uri"
+ },
+ {
+ "title": "Reference to a core extension",
+ "type": "string"
+ }
+ ]
+ }
+ },
+ "id": {
+ "title": "Provider ID",
+ "description": "Provider item ID",
+ "type": "string"
+ },
+ "bbox": {
+ "type": "array",
+ "minItems": 4,
+ "items": {
+ "type": "number"
+ }
+ },
+ "links": {
+ "title": "Item links",
+ "description": "Links to item relations",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/link"
+ }
+ },
+ "assets": {
+ "$ref": "#/definitions/assets"
+ },
+ "properties": {
+ "$ref": "#/definitions/common_metadata"
+ },
+ "collection": {
+ "title": "Collection ID",
+ "description": "The ID of the STAC Collection this Item references to.",
+ "type": "string"
+ }
+ }
+ }
+ ]
+ },
+ "link": {
+ "type": "object",
+ "required": [
+ "rel",
+ "href"
+ ],
+ "properties": {
+ "href": {
+ "title": "Link reference",
+ "type": "string"
+ },
+ "rel": {
+ "title": "Link relation type",
+ "type": "string"
+ },
+ "type": {
+ "title": "Link type",
+ "type": "string"
+ },
+ "title": {
+ "title": "Link title",
+ "type": "string"
+ },
+ "created": {
+ "$ref": "datetime.json#/definitions/created"
+ },
+ "updated": {
+ "$ref": "datetime.json#/definitions/updated"
+ }
+ }
+ },
+ "assets": {
+ "title": "Asset links",
+ "description": "Links to assets",
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/definitions/asset"
+ }
+ },
+ "asset": {
+ "type": "object",
+ "required": [
+ "href"
+ ],
+ "properties": {
+ "href": {
+ "title": "Asset reference",
+ "type": "string"
+ },
+ "title": {
+ "title": "Asset title",
+ "type": "string"
+ },
+ "description": {
+ "title": "Asset description",
+ "type": "string"
+ },
+ "type": {
+ "title": "Asset type",
+ "type": "string"
+ },
+ "roles": {
+ "title": "Asset roles",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+
\ No newline at end of file