diff --git a/.gitignore b/.gitignore
index a2be35f..400ff56 100644
--- a/.gitignore
+++ b/.gitignore
@@ -88,3 +88,8 @@ node_modules/
.sonarqube/
**/appsettings.Development.json
+
+src/TestApi/*
+package.json
+package-lock.json
+
diff --git a/Geo.NET.sln b/Geo.NET.sln
index 9364d4e..616f9df 100644
--- a/Geo.NET.sln
+++ b/Geo.NET.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
-VisualStudioVersion = 18.3.11520.95 d18.3
+VisualStudioVersion = 18.3.11520.95
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{B3BD11A7-1752-49E3-AE50-A65B5FE7B7B2}"
ProjectSection(SolutionItems) = preProject
@@ -57,6 +57,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Geo.Trimble", "src\Geo.Trim
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Geo.Trimble.Tests", "test\Geo.Trimble.Tests\Geo.Trimble.Tests.csproj", "{B2D5A7F3-6E1C-4B9A-8D2F-3C7E5A1B4D60}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Geo.OpenRouteService", "src\Geo.OpenRouteService\Geo.OpenRouteService.csproj", "{A93DD279-64E8-EBD9-8A86-C3009B3477D9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Geo.OpenRouteService.Tests", "test\Geo.OpenRouteService.Tests\Geo.OpenRouteService.Tests.csproj", "{F62B53A7-CD72-6BDD-4F81-5E2B2F9A044E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -143,6 +147,14 @@ Global
{B2D5A7F3-6E1C-4B9A-8D2F-3C7E5A1B4D60}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B2D5A7F3-6E1C-4B9A-8D2F-3C7E5A1B4D60}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B2D5A7F3-6E1C-4B9A-8D2F-3C7E5A1B4D60}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A93DD279-64E8-EBD9-8A86-C3009B3477D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A93DD279-64E8-EBD9-8A86-C3009B3477D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A93DD279-64E8-EBD9-8A86-C3009B3477D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A93DD279-64E8-EBD9-8A86-C3009B3477D9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F62B53A7-CD72-6BDD-4F81-5E2B2F9A044E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F62B53A7-CD72-6BDD-4F81-5E2B2F9A044E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F62B53A7-CD72-6BDD-4F81-5E2B2F9A044E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F62B53A7-CD72-6BDD-4F81-5E2B2F9A044E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -168,6 +180,8 @@ Global
{E09BD60D-6E8A-4210-9274-695A2DFFE976} = {67253D97-9FC9-4749-80DC-A5D84339DC05}
{7F4A2E61-83B1-4C2D-9A5E-1D3F8B7C4E90} = {8F3BA9BC-542C-450C-96C9-F0D72FECC930}
{B2D5A7F3-6E1C-4B9A-8D2F-3C7E5A1B4D60} = {67253D97-9FC9-4749-80DC-A5D84339DC05}
+ {A93DD279-64E8-EBD9-8A86-C3009B3477D9} = {8F3BA9BC-542C-450C-96C9-F0D72FECC930}
+ {F62B53A7-CD72-6BDD-4F81-5E2B2F9A044E} = {67253D97-9FC9-4749-80DC-A5D84339DC05}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C4B688C4-40EC-4577-9EB2-4CF2412DA0B1}
diff --git a/src/Geo.Core/DependencyInjection/BaseBuilder.cs b/src/Geo.Core/DependencyInjection/BaseBuilder.cs
index 5ce5eb7..535f414 100644
--- a/src/Geo.Core/DependencyInjection/BaseBuilder.cs
+++ b/src/Geo.Core/DependencyInjection/BaseBuilder.cs
@@ -23,7 +23,7 @@ public BaseBuilder(IHttpClientBuilder httpClientBuilder)
}
///
- /// Gets the used to configure the HttpClient of the instance.
+ /// Gets the used to configure the HttpClient instance.
///
public IHttpClientBuilder HttpClientBuilder { get; }
}
diff --git a/src/Geo.OpenRouteService/Abstractions/IOpenRouteServiceGeocoding.cs b/src/Geo.OpenRouteService/Abstractions/IOpenRouteServiceGeocoding.cs
new file mode 100644
index 0000000..f04d5a3
--- /dev/null
+++ b/src/Geo.OpenRouteService/Abstractions/IOpenRouteServiceGeocoding.cs
@@ -0,0 +1,55 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.OpenRouteService
+{
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Geo.Core.Models.Exceptions;
+ using Geo.OpenRouteService.Models.Parameters;
+ using Geo.OpenRouteService.Models.Responses;
+
+ ///
+ /// An interface for calling the OpenRouteService geocoding API.
+ ///
+ public interface IOpenRouteServiceGeocoding
+ {
+ ///
+ /// Performs a forward geocoding search, converting a text query to coordinates.
+ ///
+ /// A with the search parameters.
+ /// A used to cancel the request.
+ /// A containing the matching results.
+ /// Thrown when the parameters are null or invalid, or the API call fails.
+ Task SearchAsync(SearchParameters parameters, CancellationToken cancellationToken = default);
+
+ ///
+ /// Performs an autocomplete search for type-ahead suggestions.
+ ///
+ /// An with the autocomplete parameters.
+ /// A used to cancel the request.
+ /// A containing the suggestions.
+ /// Thrown when the parameters are null or invalid, or the API call fails.
+ Task AutocompleteAsync(AutocompleteParameters parameters, CancellationToken cancellationToken = default);
+
+ ///
+ /// Performs a structured geocoding search using individual address components.
+ ///
+ /// A with the address component parameters.
+ /// A used to cancel the request.
+ /// A containing the matching results.
+ /// Thrown when the parameters are null or invalid, or the API call fails.
+ Task StructuredSearchAsync(StructuredSearchParameters parameters, CancellationToken cancellationToken = default);
+
+ ///
+ /// Performs a reverse geocoding search, converting coordinates to an address.
+ ///
+ /// A with the reverse geocoding parameters.
+ /// A used to cancel the request.
+ /// A containing the matching results.
+ /// Thrown when the parameters are null or invalid, or the API call fails.
+ Task ReverseAsync(ReverseSearchParameters parameters, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/src/Geo.OpenRouteService/Converters/CoordinateConverter.cs b/src/Geo.OpenRouteService/Converters/CoordinateConverter.cs
new file mode 100644
index 0000000..3b4771e
--- /dev/null
+++ b/src/Geo.OpenRouteService/Converters/CoordinateConverter.cs
@@ -0,0 +1,98 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.OpenRouteService.Converters
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Globalization;
+ using System.Text.Json;
+ using System.Text.Json.Serialization;
+ using Geo.Core.Extensions;
+ using Geo.OpenRouteService.Models;
+
+ ///
+ /// Converts a GeoJSON [longitude, latitude] coordinate array to and from a .
+ ///
+ public class CoordinateConverter : JsonConverter
+ {
+ ///
+ public override Coordinate Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (typeToConvert == null)
+ {
+ throw new ArgumentNullException(nameof(typeToConvert));
+ }
+
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ if (reader.TokenType == JsonTokenType.Null)
+ {
+ return null;
+ }
+
+ if (reader.TokenType != JsonTokenType.StartArray)
+ {
+ throw new JsonException(string.Format(CultureInfo.InvariantCulture, "Unexpected token while parsing the coordinate. Expected to find an array, instead found {0}", reader.TokenType.GetName()));
+ }
+
+ var values = new List();
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndArray)
+ {
+ break;
+ }
+ else if (reader.TokenType == JsonTokenType.Number)
+ {
+ values.Add(reader.GetDouble());
+ }
+ else
+ {
+ throw new JsonException(string.Format(CultureInfo.InvariantCulture, "Unexpected token while parsing the coordinate. Expected to find a double, instead found '{0}'", reader.GetString()));
+ }
+ }
+
+ if (values.Count != 2)
+ {
+ throw new JsonException(string.Format(CultureInfo.InvariantCulture, "Unexpected end of array while parsing the coordinate. Expected to find 2 doubles, instead found {0}", values.Count));
+ }
+
+ return new Coordinate()
+ {
+ Longitude = values[0],
+ Latitude = values[1],
+ };
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, Coordinate value, JsonSerializerOptions options)
+ {
+ if (writer == null)
+ {
+ throw new ArgumentNullException(nameof(writer));
+ }
+
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ if (value == null)
+ {
+ writer.WriteNullValue();
+ return;
+ }
+
+ writer.WriteStartArray();
+ writer.WriteNumberValue(value.Longitude);
+ writer.WriteNumberValue(value.Latitude);
+ writer.WriteEndArray();
+ }
+ }
+}
diff --git a/src/Geo.OpenRouteService/DependencyInjection/ServiceCollectionExtensions.cs b/src/Geo.OpenRouteService/DependencyInjection/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..3a880dc
--- /dev/null
+++ b/src/Geo.OpenRouteService/DependencyInjection/ServiceCollectionExtensions.cs
@@ -0,0 +1,44 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.Extensions.DependencyInjection
+{
+ using System;
+ using Geo.OpenRouteService;
+ using Geo.OpenRouteService.Services;
+ using Microsoft.Extensions.DependencyInjection;
+ using Microsoft.Extensions.Options;
+
+ ///
+ /// Extension methods for the class.
+ ///
+ public static class ServiceCollectionExtensions
+ {
+ ///
+ /// Adds the OpenRouteService geocoding services to the service collection.
+ ///
+ /// Adds the services:
+ ///
+ /// - of
+ ///
+ ///
+ ///
+ ///
+ /// An to add the OpenRouteService services to.
+ /// A to configure the OpenRouteService geocoding.
+ /// Thrown if is null.
+ public static KeyBuilder AddOpenRouteServiceGeocoding(this IServiceCollection services)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ services.AddKeyOptions();
+
+ return new KeyBuilder(services.AddHttpClient());
+ }
+ }
+}
diff --git a/src/Geo.OpenRouteService/Enums/LayerType.cs b/src/Geo.OpenRouteService/Enums/LayerType.cs
new file mode 100644
index 0000000..c2ba176
--- /dev/null
+++ b/src/Geo.OpenRouteService/Enums/LayerType.cs
@@ -0,0 +1,99 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.OpenRouteService.Enums
+{
+ using System.Runtime.Serialization;
+
+ ///
+ /// The layer types available to filter ORS geocoding results.
+ ///
+ public enum LayerType
+ {
+ ///
+ /// A venue such as a restaurant, shop, or point of interest.
+ ///
+ [EnumMember(Value = "venue")]
+ Venue = 1,
+
+ ///
+ /// A specific address.
+ ///
+ [EnumMember(Value = "address")]
+ Address,
+
+ ///
+ /// A street.
+ ///
+ [EnumMember(Value = "street")]
+ Street,
+
+ ///
+ /// A neighbourhood within a locality.
+ ///
+ [EnumMember(Value = "neighbourhood")]
+ Neighbourhood,
+
+ ///
+ /// A borough within a city.
+ ///
+ [EnumMember(Value = "borough")]
+ Borough,
+
+ ///
+ /// A local administrative area.
+ ///
+ [EnumMember(Value = "localadmin")]
+ LocalAdmin,
+
+ ///
+ /// A city or town.
+ ///
+ [EnumMember(Value = "locality")]
+ Locality,
+
+ ///
+ /// A county.
+ ///
+ [EnumMember(Value = "county")]
+ County,
+
+ ///
+ /// A macro county (larger grouping of counties).
+ ///
+ [EnumMember(Value = "macrocounty")]
+ MacroCounty,
+
+ ///
+ /// A region such as a state or province.
+ ///
+ [EnumMember(Value = "region")]
+ Region,
+
+ ///
+ /// A macro region (larger grouping of regions).
+ ///
+ [EnumMember(Value = "macroregion")]
+ MacroRegion,
+
+ ///
+ /// A country.
+ ///
+ [EnumMember(Value = "country")]
+ Country,
+
+ ///
+ /// A coarse result encompassing multiple administrative levels.
+ ///
+ [EnumMember(Value = "coarse")]
+ Coarse,
+
+ ///
+ /// A postal code area.
+ ///
+ [EnumMember(Value = "postalcode")]
+ PostalCode,
+ }
+}
diff --git a/src/Geo.OpenRouteService/Enums/SourceType.cs b/src/Geo.OpenRouteService/Enums/SourceType.cs
new file mode 100644
index 0000000..2f0bad9
--- /dev/null
+++ b/src/Geo.OpenRouteService/Enums/SourceType.cs
@@ -0,0 +1,39 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.OpenRouteService.Enums
+{
+ using System.Runtime.Serialization;
+
+ ///
+ /// The data source types available to filter ORS geocoding results.
+ ///
+ public enum SourceType
+ {
+ ///
+ /// OpenStreetMap data.
+ ///
+ [EnumMember(Value = "osm")]
+ Osm = 1,
+
+ ///
+ /// OpenAddresses data.
+ ///
+ [EnumMember(Value = "oa")]
+ Oa,
+
+ ///
+ /// GeoNames data.
+ ///
+ [EnumMember(Value = "gn")]
+ Gn,
+
+ ///
+ /// Who's On First data.
+ ///
+ [EnumMember(Value = "wof")]
+ Wof,
+ }
+}
diff --git a/src/Geo.OpenRouteService/Extensions/LoggerExtensions.cs b/src/Geo.OpenRouteService/Extensions/LoggerExtensions.cs
new file mode 100644
index 0000000..9571651
--- /dev/null
+++ b/src/Geo.OpenRouteService/Extensions/LoggerExtensions.cs
@@ -0,0 +1,62 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.OpenRouteService
+{
+ using System;
+ using Geo.OpenRouteService.Services;
+ using Microsoft.Extensions.Logging;
+
+ ///
+ /// Extension methods for the class.
+ ///
+ internal static class LoggerExtensions
+ {
+ private static readonly Action _error = LoggerMessage.Define(
+ LogLevel.Error,
+ new EventId(1, nameof(OpenRouteServiceGeocoding)),
+ "OpenRouteServiceGeocoding: {ErrorMessage}");
+
+ private static readonly Action _warning = LoggerMessage.Define(
+ LogLevel.Warning,
+ new EventId(2, nameof(OpenRouteServiceGeocoding)),
+ "OpenRouteServiceGeocoding: {WarningMessage}");
+
+ private static readonly Action _debug = LoggerMessage.Define(
+ LogLevel.Debug,
+ new EventId(3, nameof(OpenRouteServiceGeocoding)),
+ "OpenRouteServiceGeocoding: {DebugMessage}");
+
+ ///
+ /// "OpenRouteServiceGeocoding: {ErrorMessage}".
+ ///
+ /// An used to log the error message.
+ /// The error message to log.
+ public static void OpenRouteServiceError(this ILogger logger, string errorMessage)
+ {
+ _error(logger, errorMessage, null);
+ }
+
+ ///
+ /// "OpenRouteServiceGeocoding: {WarningMessage}".
+ ///
+ /// An used to log the warning message.
+ /// The warning message to log.
+ public static void OpenRouteServiceWarning(this ILogger logger, string warningMessage)
+ {
+ _warning(logger, warningMessage, null);
+ }
+
+ ///
+ /// "OpenRouteServiceGeocoding: {DebugMessage}".
+ ///
+ /// An used to log the debug message.
+ /// The debug message to log.
+ public static void OpenRouteServiceDebug(this ILogger logger, string debugMessage)
+ {
+ _debug(logger, debugMessage, null);
+ }
+ }
+}
diff --git a/src/Geo.OpenRouteService/Geo.OpenRouteService.csproj b/src/Geo.OpenRouteService/Geo.OpenRouteService.csproj
new file mode 100644
index 0000000..1d8313d
--- /dev/null
+++ b/src/Geo.OpenRouteService/Geo.OpenRouteService.csproj
@@ -0,0 +1,45 @@
+
+
+
+ netstandard2.0;net6.0;net8.0;net10.0
+ Justin Canton
+ Geo.NET
+ Geo.NET OpenRouteService
+ geocoding geo.net openrouteservice ors
+ A lightweight method for communicating with the OpenRouteService geocoding APIs.
+ MIT
+ https://github.com/JustinCanton/Geo.NET
+ true
+ README.md
+
+
+
+
+
+
+
+
+
+
+ True
+ True
+ OpenRouteServiceGeocoding.resx
+
+
+
+
+
+ ResXFileCodeGenerator
+ OpenRouteServiceGeocoding.Designer.cs
+
+
+
+
+
+ True
+ \
+ Always
+
+
+
+
diff --git a/src/Geo.OpenRouteService/Models/Coordinate.cs b/src/Geo.OpenRouteService/Models/Coordinate.cs
new file mode 100644
index 0000000..37dcdd1
--- /dev/null
+++ b/src/Geo.OpenRouteService/Models/Coordinate.cs
@@ -0,0 +1,31 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.OpenRouteService.Models
+{
+ using System.Globalization;
+
+ ///
+ /// A geographic coordinate with latitude and longitude.
+ ///
+ public class Coordinate
+ {
+ ///
+ /// Gets or sets the latitude in decimal degrees.
+ ///
+ public double Latitude { get; set; }
+
+ ///
+ /// Gets or sets the longitude in decimal degrees.
+ ///
+ public double Longitude { get; set; }
+
+ ///
+ public override string ToString()
+ {
+ return string.Format(CultureInfo.InvariantCulture, "{0},{1}", Latitude, Longitude);
+ }
+ }
+}
diff --git a/src/Geo.OpenRouteService/Models/Parameters/AutocompleteParameters.cs b/src/Geo.OpenRouteService/Models/Parameters/AutocompleteParameters.cs
new file mode 100644
index 0000000..8588caa
--- /dev/null
+++ b/src/Geo.OpenRouteService/Models/Parameters/AutocompleteParameters.cs
@@ -0,0 +1,18 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.OpenRouteService.Models.Parameters
+{
+ ///
+ /// Parameters for the ORS autocomplete endpoint (/geocode/autocomplete).
+ ///
+ public class AutocompleteParameters : BaseSearchParameters
+ {
+ ///
+ /// Gets or sets the partial search query text. Required.
+ ///
+ public string Text { get; set; }
+ }
+}
diff --git a/src/Geo.OpenRouteService/Models/Parameters/BaseSearchParameters.cs b/src/Geo.OpenRouteService/Models/Parameters/BaseSearchParameters.cs
new file mode 100644
index 0000000..7dea45e
--- /dev/null
+++ b/src/Geo.OpenRouteService/Models/Parameters/BaseSearchParameters.cs
@@ -0,0 +1,67 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.OpenRouteService.Models.Parameters
+{
+ using System.Collections.Generic;
+ using Geo.OpenRouteService.Enums;
+ using Geo.OpenRouteService.Models;
+
+ ///
+ /// The shared optional parameters common to forward geocoding endpoints (search, autocomplete, structured search).
+ ///
+ public abstract class BaseSearchParameters : IKeyParameters, IAdditionalParameters
+ {
+ ///
+ /// Gets or sets the API key. When set, overrides the key configured via dependency injection.
+ ///
+ public string Key { get; set; }
+
+ ///
+ /// Gets or sets the maximum number of results to return. Defaults to 10; maximum is 40.
+ ///
+ public int? Size { get; set; }
+
+ ///
+ /// Gets or sets a coordinate used to bias results toward a specific location.
+ ///
+ public Coordinate FocusPoint { get; set; }
+
+ ///
+ /// Gets or sets a rectangular bounding box to restrict results to a specific area.
+ ///
+ public BoundingBox BoundaryRect { get; set; }
+
+ ///
+ /// Gets or sets a circular boundary to restrict results to a specific area.
+ ///
+ public BoundingCircle BoundaryCircle { get; set; }
+
+ ///
+ /// Gets the list of ISO 3166-1 alpha-2 or alpha-3 country codes used to restrict results.
+ ///
+ public IList BoundaryCountries { get; } = new List();
+
+ ///
+ /// Gets or sets a Pelias geographic identifier (GID) used to restrict results to a specific administrative area.
+ ///
+ public string BoundaryGid { get; set; }
+
+ ///
+ /// Gets the list of data sources to include in results.
+ ///
+ public IList Sources { get; } = new List();
+
+ ///
+ /// Gets the list of layer types to include in results.
+ ///
+ public IList Layers { get; } = new List();
+
+ ///
+ /// Gets additional parameters to append to the request query string.
+ ///
+ public IDictionary AdditionalParameters { get; } = new Dictionary();
+ }
+}
diff --git a/src/Geo.OpenRouteService/Models/Parameters/BoundingBox.cs b/src/Geo.OpenRouteService/Models/Parameters/BoundingBox.cs
new file mode 100644
index 0000000..6066c78
--- /dev/null
+++ b/src/Geo.OpenRouteService/Models/Parameters/BoundingBox.cs
@@ -0,0 +1,33 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.OpenRouteService.Models.Parameters
+{
+ ///
+ /// A rectangular geographic bounding box defined by its four edges.
+ ///
+ public class BoundingBox
+ {
+ ///
+ /// Gets or sets the minimum longitude (west edge) in decimal degrees.
+ ///
+ public double MinLongitude { get; set; }
+
+ ///
+ /// Gets or sets the maximum longitude (east edge) in decimal degrees.
+ ///
+ public double MaxLongitude { get; set; }
+
+ ///
+ /// Gets or sets the minimum latitude (south edge) in decimal degrees.
+ ///
+ public double MinLatitude { get; set; }
+
+ ///
+ /// Gets or sets the maximum latitude (north edge) in decimal degrees.
+ ///
+ public double MaxLatitude { get; set; }
+ }
+}
diff --git a/src/Geo.OpenRouteService/Models/Parameters/BoundingCircle.cs b/src/Geo.OpenRouteService/Models/Parameters/BoundingCircle.cs
new file mode 100644
index 0000000..18034f5
--- /dev/null
+++ b/src/Geo.OpenRouteService/Models/Parameters/BoundingCircle.cs
@@ -0,0 +1,29 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.OpenRouteService.Models.Parameters
+{
+ ///
+ /// A circular geographic boundary defined by a center point and radius.
+ ///
+ public class BoundingCircle
+ {
+ ///
+ /// Gets or sets the latitude of the circle center in decimal degrees.
+ ///
+ public double Latitude { get; set; }
+
+ ///
+ /// Gets or sets the longitude of the circle center in decimal degrees.
+ ///
+ public double Longitude { get; set; }
+
+ ///
+ /// Gets or sets the radius of the circle in kilometers. For forward geocoding the default is 50 km.
+ /// For reverse geocoding the default is 1 km and the maximum is 5 km.
+ ///
+ public double? Radius { get; set; }
+ }
+}
diff --git a/src/Geo.OpenRouteService/Models/Parameters/ReverseSearchParameters.cs b/src/Geo.OpenRouteService/Models/Parameters/ReverseSearchParameters.cs
new file mode 100644
index 0000000..40e4bbb
--- /dev/null
+++ b/src/Geo.OpenRouteService/Models/Parameters/ReverseSearchParameters.cs
@@ -0,0 +1,62 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.OpenRouteService.Models.Parameters
+{
+ using System.Collections.Generic;
+ using Geo.OpenRouteService.Enums;
+ using Geo.OpenRouteService.Models;
+
+ ///
+ /// Parameters for the ORS reverse geocoding endpoint (/geocode/reverse).
+ ///
+ public class ReverseSearchParameters : IKeyParameters, IAdditionalParameters
+ {
+ ///
+ /// Gets or sets the API key. When set, overrides the key configured via dependency injection.
+ ///
+ public string Key { get; set; }
+
+ ///
+ /// Gets or sets the coordinate to reverse geocode. Required.
+ ///
+ public Coordinate Point { get; set; }
+
+ ///
+ /// Gets or sets the maximum number of results to return. Defaults to 10; maximum is 40.
+ ///
+ public int? Size { get; set; }
+
+ ///
+ /// Gets or sets the search radius in kilometers. Defaults to 1 km; maximum is 5 km.
+ ///
+ public double? BoundaryCircleRadius { get; set; }
+
+ ///
+ /// Gets the list of ISO 3166-1 alpha-2 or alpha-3 country codes used to restrict results.
+ ///
+ public IList BoundaryCountries { get; } = new List();
+
+ ///
+ /// Gets or sets a Pelias geographic identifier (GID) used to restrict results to a specific administrative area.
+ ///
+ public string BoundaryGid { get; set; }
+
+ ///
+ /// Gets the list of data sources to include in results.
+ ///
+ public IList Sources { get; } = new List();
+
+ ///
+ /// Gets the list of layer types to include in results.
+ ///
+ public IList Layers { get; } = new List();
+
+ ///
+ /// Gets additional parameters to append to the request query string.
+ ///
+ public IDictionary AdditionalParameters { get; } = new Dictionary();
+ }
+}
diff --git a/src/Geo.OpenRouteService/Models/Parameters/SearchParameters.cs b/src/Geo.OpenRouteService/Models/Parameters/SearchParameters.cs
new file mode 100644
index 0000000..79de555
--- /dev/null
+++ b/src/Geo.OpenRouteService/Models/Parameters/SearchParameters.cs
@@ -0,0 +1,18 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.OpenRouteService.Models.Parameters
+{
+ ///
+ /// Parameters for the ORS forward geocoding search endpoint (/geocode/search).
+ ///
+ public class SearchParameters : BaseSearchParameters
+ {
+ ///
+ /// Gets or sets the search query text. Required.
+ ///
+ public string Text { get; set; }
+ }
+}
diff --git a/src/Geo.OpenRouteService/Models/Parameters/StructuredSearchParameters.cs b/src/Geo.OpenRouteService/Models/Parameters/StructuredSearchParameters.cs
new file mode 100644
index 0000000..7f2db84
--- /dev/null
+++ b/src/Geo.OpenRouteService/Models/Parameters/StructuredSearchParameters.cs
@@ -0,0 +1,54 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.OpenRouteService.Models.Parameters
+{
+ ///
+ /// Parameters for the ORS structured geocoding endpoint (/geocode/search/structured).
+ /// At least one address component must be provided.
+ ///
+ public class StructuredSearchParameters : BaseSearchParameters
+ {
+ ///
+ /// Gets or sets the street address including house number.
+ ///
+ public string Address { get; set; }
+
+ ///
+ /// Gets or sets the neighbourhood name.
+ ///
+ public string Neighbourhood { get; set; }
+
+ ///
+ /// Gets or sets the borough name.
+ ///
+ public string Borough { get; set; }
+
+ ///
+ /// Gets or sets the locality (city or town) name.
+ ///
+ public string Locality { get; set; }
+
+ ///
+ /// Gets or sets the county name.
+ ///
+ public string County { get; set; }
+
+ ///
+ /// Gets or sets the region (state or province) name.
+ ///
+ public string Region { get; set; }
+
+ ///
+ /// Gets or sets the postal code.
+ ///
+ public string PostalCode { get; set; }
+
+ ///
+ /// Gets or sets the country name or ISO 3166-1 alpha-2/alpha-3 code.
+ ///
+ public string Country { get; set; }
+ }
+}
diff --git a/src/Geo.OpenRouteService/Models/Responses/Feature.cs b/src/Geo.OpenRouteService/Models/Responses/Feature.cs
new file mode 100644
index 0000000..95e63fa
--- /dev/null
+++ b/src/Geo.OpenRouteService/Models/Responses/Feature.cs
@@ -0,0 +1,40 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.OpenRouteService.Models.Responses
+{
+ using System.Collections.Generic;
+ using System.Text.Json.Serialization;
+
+ ///
+ /// A single GeoJSON Feature representing a geocoding result.
+ ///
+ public class Feature
+ {
+ ///
+ /// Gets or sets the GeoJSON object type (always "Feature").
+ ///
+ [JsonPropertyName("type")]
+ public string Type { get; set; }
+
+ ///
+ /// Gets or sets the geometry of the feature.
+ ///
+ [JsonPropertyName("geometry")]
+ public Geometry Geometry { get; set; }
+
+ ///
+ /// Gets or sets the properties of the feature containing address and metadata.
+ ///
+ [JsonPropertyName("properties")]
+ public FeatureProperties Properties { get; set; }
+
+ ///
+ /// Gets or sets the bounding box of the feature as [west, south, east, north].
+ ///
+ [JsonPropertyName("bbox")]
+ public IList BoundingBox { get; set; }
+ }
+}
diff --git a/src/Geo.OpenRouteService/Models/Responses/FeatureCollection.cs b/src/Geo.OpenRouteService/Models/Responses/FeatureCollection.cs
new file mode 100644
index 0000000..1d764a2
--- /dev/null
+++ b/src/Geo.OpenRouteService/Models/Responses/FeatureCollection.cs
@@ -0,0 +1,40 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.OpenRouteService.Models.Responses
+{
+ using System.Collections.Generic;
+ using System.Text.Json.Serialization;
+
+ ///
+ /// A GeoJSON FeatureCollection returned by all ORS geocoding endpoints.
+ ///
+ public class FeatureCollection
+ {
+ ///
+ /// Gets or sets the GeoJSON object type (always "FeatureCollection").
+ ///
+ [JsonPropertyName("type")]
+ public string Type { get; set; }
+
+ ///
+ /// Gets or sets the list of geocoding result features.
+ ///
+ [JsonPropertyName("features")]
+ public IList Features { get; set; } = new List();
+
+ ///
+ /// Gets or sets the bounding box encompassing all features as [west, south, east, north].
+ ///
+ [JsonPropertyName("bbox")]
+ public IList BoundingBox { get; set; }
+
+ ///
+ /// Gets or sets the geocoding metadata including version, attribution, and echoed query parameters.
+ ///
+ [JsonPropertyName("geocoding")]
+ public GeocodingMetadata Geocoding { get; set; }
+ }
+}
diff --git a/src/Geo.OpenRouteService/Models/Responses/FeatureProperties.cs b/src/Geo.OpenRouteService/Models/Responses/FeatureProperties.cs
new file mode 100644
index 0000000..9fea373
--- /dev/null
+++ b/src/Geo.OpenRouteService/Models/Responses/FeatureProperties.cs
@@ -0,0 +1,152 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.OpenRouteService.Models.Responses
+{
+ using System.Text.Json;
+ using System.Text.Json.Serialization;
+
+ ///
+ /// The properties of a GeoJSON feature returned by the ORS geocoding API.
+ ///
+ public class FeatureProperties
+ {
+ /// Gets or sets the feature identifier.
+ [JsonPropertyName("id")]
+ public string Id { get; set; }
+
+ /// Gets or sets the Pelias global identifier.
+ [JsonPropertyName("gid")]
+ public string Gid { get; set; }
+
+ /// Gets or sets the layer type (e.g. venue, address, locality).
+ [JsonPropertyName("layer")]
+ public string Layer { get; set; }
+
+ /// Gets or sets the data source (e.g. osm, oa, gn, wof).
+ [JsonPropertyName("source")]
+ public string Source { get; set; }
+
+ /// Gets or sets the source-specific identifier.
+ [JsonPropertyName("source_id")]
+ public string SourceId { get; set; }
+
+ /// Gets or sets the primary name of the place.
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+
+ /// Gets or sets the house number.
+ [JsonPropertyName("housenumber")]
+ public string HouseNumber { get; set; }
+
+ /// Gets or sets the street name.
+ [JsonPropertyName("street")]
+ public string Street { get; set; }
+
+ /// Gets or sets the postal code.
+ [JsonPropertyName("postalcode")]
+ public string PostalCode { get; set; }
+
+ /// Gets or sets the confidence score (0.0–1.0) of the match.
+ [JsonPropertyName("confidence")]
+ public double? Confidence { get; set; }
+
+ /// Gets or sets the distance from the focus point or query point in kilometers.
+ [JsonPropertyName("distance")]
+ public double? Distance { get; set; }
+
+ /// Gets or sets the accuracy level of the result (e.g. point, centroid).
+ [JsonPropertyName("accuracy")]
+ public string Accuracy { get; set; }
+
+ /// Gets or sets the type of match made (e.g. exact, fallback).
+ [JsonPropertyName("match_type")]
+ public string MatchType { get; set; }
+
+ /// Gets or sets the fully formatted address label.
+ [JsonPropertyName("label")]
+ public string Label { get; set; }
+
+ /// Gets or sets the country name.
+ [JsonPropertyName("country")]
+ public string Country { get; set; }
+
+ /// Gets or sets the Pelias GID of the country.
+ [JsonPropertyName("country_gid")]
+ public string CountryGid { get; set; }
+
+ /// Gets or sets the ISO 3166-1 alpha-2 country code.
+ [JsonPropertyName("country_code")]
+ public string CountryCode { get; set; }
+
+ /// Gets or sets the macro region name.
+ [JsonPropertyName("macroregion")]
+ public string MacroRegion { get; set; }
+
+ /// Gets or sets the Pelias GID of the macro region.
+ [JsonPropertyName("macroregion_gid")]
+ public string MacroRegionGid { get; set; }
+
+ /// Gets or sets the region (state or province) name.
+ [JsonPropertyName("region")]
+ public string Region { get; set; }
+
+ /// Gets or sets the Pelias GID of the region.
+ [JsonPropertyName("region_gid")]
+ public string RegionGid { get; set; }
+
+ /// Gets or sets the macro county name.
+ [JsonPropertyName("macrocounty")]
+ public string MacroCounty { get; set; }
+
+ /// Gets or sets the Pelias GID of the macro county.
+ [JsonPropertyName("macrocounty_gid")]
+ public string MacroCountyGid { get; set; }
+
+ /// Gets or sets the county name.
+ [JsonPropertyName("county")]
+ public string County { get; set; }
+
+ /// Gets or sets the Pelias GID of the county.
+ [JsonPropertyName("county_gid")]
+ public string CountyGid { get; set; }
+
+ /// Gets or sets the local administrative area name.
+ [JsonPropertyName("localadmin")]
+ public string LocalAdmin { get; set; }
+
+ /// Gets or sets the Pelias GID of the local administrative area.
+ [JsonPropertyName("localadmin_gid")]
+ public string LocalAdminGid { get; set; }
+
+ /// Gets or sets the locality (city or town) name.
+ [JsonPropertyName("locality")]
+ public string Locality { get; set; }
+
+ /// Gets or sets the Pelias GID of the locality.
+ [JsonPropertyName("locality_gid")]
+ public string LocalityGid { get; set; }
+
+ /// Gets or sets the borough name.
+ [JsonPropertyName("borough")]
+ public string Borough { get; set; }
+
+ /// Gets or sets the Pelias GID of the borough.
+ [JsonPropertyName("borough_gid")]
+ public string BoroughGid { get; set; }
+
+ /// Gets or sets the neighbourhood name.
+ [JsonPropertyName("neighbourhood")]
+ public string Neighbourhood { get; set; }
+
+ /// Gets or sets the Pelias GID of the neighbourhood.
+ [JsonPropertyName("neighbourhood_gid")]
+ public string NeighbourhoodGid { get; set; }
+
+ /// Gets or sets provider-specific addendum data as a raw JSON element.
+ [JsonPropertyName("addendum")]
+ public JsonElement? Addendum { get; set; }
+ }
+}
diff --git a/src/Geo.OpenRouteService/Models/Responses/GeocodingMetadata.cs b/src/Geo.OpenRouteService/Models/Responses/GeocodingMetadata.cs
new file mode 100644
index 0000000..d9509da
--- /dev/null
+++ b/src/Geo.OpenRouteService/Models/Responses/GeocodingMetadata.cs
@@ -0,0 +1,47 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.OpenRouteService.Models.Responses
+{
+ using System.Collections.Generic;
+ using System.Text.Json;
+ using System.Text.Json.Serialization;
+
+ ///
+ /// Metadata about the geocoding query returned alongside results.
+ ///
+ public class GeocodingMetadata
+ {
+ ///
+ /// Gets or sets the version of the geocoding engine.
+ ///
+ [JsonPropertyName("version")]
+ public string Version { get; set; }
+
+ ///
+ /// Gets or sets the attribution string for the data sources.
+ ///
+ [JsonPropertyName("attribution")]
+ public string Attribution { get; set; }
+
+ ///
+ /// Gets or sets the echoed query parameters as a raw JSON element.
+ ///
+ [JsonPropertyName("query")]
+ public JsonElement? Query { get; set; }
+
+ ///
+ /// Gets or sets any warnings returned by the geocoding engine.
+ ///
+ [JsonPropertyName("warnings")]
+ public IList Warnings { get; set; } = new List();
+
+ ///
+ /// Gets or sets the Unix timestamp of the response.
+ ///
+ [JsonPropertyName("timestamp")]
+ public long? Timestamp { get; set; }
+ }
+}
diff --git a/src/Geo.OpenRouteService/Models/Responses/Geometry.cs b/src/Geo.OpenRouteService/Models/Responses/Geometry.cs
new file mode 100644
index 0000000..a0a5967
--- /dev/null
+++ b/src/Geo.OpenRouteService/Models/Responses/Geometry.cs
@@ -0,0 +1,31 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.OpenRouteService.Models.Responses
+{
+ using System.Text.Json.Serialization;
+ using Geo.OpenRouteService.Converters;
+ using Geo.OpenRouteService.Models;
+
+ ///
+ /// A GeoJSON geometry object. For ORS geocoding results this is always a Point.
+ /// The coordinates are stored as [longitude, latitude] in the JSON and converted via .
+ ///
+ public class Geometry
+ {
+ ///
+ /// Gets or sets the GeoJSON geometry type (always "Point" for geocoding results).
+ ///
+ [JsonPropertyName("type")]
+ public string Type { get; set; }
+
+ ///
+ /// Gets or sets the coordinate represented by this geometry.
+ ///
+ [JsonPropertyName("coordinates")]
+ [JsonConverter(typeof(CoordinateConverter))]
+ public Coordinate Coordinates { get; set; }
+ }
+}
diff --git a/src/Geo.OpenRouteService/Properties/AssemblyInfo.cs b/src/Geo.OpenRouteService/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..9931d60
--- /dev/null
+++ b/src/Geo.OpenRouteService/Properties/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+using System.Runtime.CompilerServices;
+using Microsoft.Extensions.Localization;
+
+[assembly: InternalsVisibleTo("Geo.OpenRouteService.Tests")]
+[assembly: ResourceLocation("Resources")]
diff --git a/src/Geo.OpenRouteService/README.md b/src/Geo.OpenRouteService/README.md
new file mode 100644
index 0000000..4c0f003
--- /dev/null
+++ b/src/Geo.OpenRouteService/README.md
@@ -0,0 +1,42 @@
+# Geo.OpenRouteService
+
+A .NET library for communicating with the [OpenRouteService Geocoding API](https://openrouteservice.org/dev/#/api-docs/geocode).
+
+## Supported Endpoints
+
+- **Search** (`/geocode/search`) — Forward geocoding: convert a text query to coordinates.
+- **Autocomplete** (`/geocode/autocomplete`) — Type-ahead suggestions for real-time search.
+- **Structured Search** (`/geocode/search/structured`) — Forward geocoding using individual address components.
+- **Reverse** (`/geocode/reverse`) — Reverse geocoding: convert coordinates to an address.
+
+## Usage
+
+Register the service with dependency injection:
+
+```csharp
+services.AddOpenRouteServiceGeocoding()
+ .AddKey("YOUR_API_KEY");
+```
+
+Inject and call the service:
+
+```csharp
+public class MyService
+{
+ private readonly IOpenRouteServiceGeocoding _geocoding;
+
+ public MyService(IOpenRouteServiceGeocoding geocoding)
+ {
+ _geocoding = geocoding;
+ }
+
+ public async Task SearchAsync(string query)
+ {
+ return await _geocoding.SearchAsync(new SearchParameters { Text = query });
+ }
+}
+```
+
+## Authentication
+
+Obtain an API key from [openrouteservice.org](https://openrouteservice.org) and pass it via `.AddKey()` during registration, or override it per-request by setting `Key` on the parameters object.
diff --git a/src/Geo.OpenRouteService/Resources/Services/OpenRouteServiceGeocoding.Designer.cs b/src/Geo.OpenRouteService/Resources/Services/OpenRouteServiceGeocoding.Designer.cs
new file mode 100644
index 0000000..12653e0
--- /dev/null
+++ b/src/Geo.OpenRouteService/Resources/Services/OpenRouteServiceGeocoding.Designer.cs
@@ -0,0 +1,180 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Geo.OpenRouteService.Resources.Services {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class OpenRouteServiceGeocoding {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal OpenRouteServiceGeocoding() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Geo.OpenRouteService.Resources.Services.OpenRouteServiceGeocoding", typeof(OpenRouteServiceGeocoding).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Failed to create the OpenRouteService Geocoding uri..
+ ///
+ internal static string Failed_To_Create_Uri {
+ get {
+ return ResourceManager.GetString("Failed To Create Uri", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to At least one address component must be provided..
+ ///
+ internal static string Invalid_Address_Components {
+ get {
+ return ResourceManager.GetString("Invalid Address Components", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The boundary circle is invalid and will not be used..
+ ///
+ internal static string Invalid_Boundary_Circle {
+ get {
+ return ResourceManager.GetString("Invalid Boundary Circle", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The boundary countries are invalid and will not be used..
+ ///
+ internal static string Invalid_Boundary_Countries {
+ get {
+ return ResourceManager.GetString("Invalid Boundary Countries", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The boundary GID is invalid and will not be used..
+ ///
+ internal static string Invalid_Boundary_Gid {
+ get {
+ return ResourceManager.GetString("Invalid Boundary Gid", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The boundary rectangle is invalid and will not be used..
+ ///
+ internal static string Invalid_Boundary_Rect {
+ get {
+ return ResourceManager.GetString("Invalid Boundary Rect", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The focus point is invalid and will not be used..
+ ///
+ internal static string Invalid_Focus_Point {
+ get {
+ return ResourceManager.GetString("Invalid Focus Point", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The layers are invalid and will not be used..
+ ///
+ internal static string Invalid_Layers {
+ get {
+ return ResourceManager.GetString("Invalid Layers", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The reverse geocoding point cannot be null..
+ ///
+ internal static string Invalid_Point {
+ get {
+ return ResourceManager.GetString("Invalid Point", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The size is invalid and will not be used..
+ ///
+ internal static string Invalid_Size {
+ get {
+ return ResourceManager.GetString("Invalid Size", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The sources are invalid and will not be used..
+ ///
+ internal static string Invalid_Sources {
+ get {
+ return ResourceManager.GetString("Invalid Sources", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The text query cannot be null or empty..
+ ///
+ internal static string Invalid_Text {
+ get {
+ return ResourceManager.GetString("Invalid Text", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The OpenRouteService Geocoding parameters are null..
+ ///
+ internal static string Null_Parameters {
+ get {
+ return ResourceManager.GetString("Null Parameters", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Geo.OpenRouteService/Resources/Services/OpenRouteServiceGeocoding.resx b/src/Geo.OpenRouteService/Resources/Services/OpenRouteServiceGeocoding.resx
new file mode 100644
index 0000000..09ea977
--- /dev/null
+++ b/src/Geo.OpenRouteService/Resources/Services/OpenRouteServiceGeocoding.resx
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Failed to create the OpenRouteService Geocoding uri.
+
+
+ At least one address component must be provided.
+
+
+ The boundary circle is invalid and will not be used.
+
+
+ The boundary countries are invalid and will not be used.
+
+
+ The boundary GID is invalid and will not be used.
+
+
+ The boundary rectangle is invalid and will not be used.
+
+
+ The focus point is invalid and will not be used.
+
+
+ The layers are invalid and will not be used.
+
+
+ The reverse geocoding point cannot be null.
+
+
+ The size is invalid and will not be used.
+
+
+ The sources are invalid and will not be used.
+
+
+ The text query cannot be null or empty.
+
+
+ The OpenRouteService Geocoding parameters are null.
+
+
diff --git a/src/Geo.OpenRouteService/Services/OpenRouteServiceGeocoding.cs b/src/Geo.OpenRouteService/Services/OpenRouteServiceGeocoding.cs
new file mode 100644
index 0000000..5dec6fc
--- /dev/null
+++ b/src/Geo.OpenRouteService/Services/OpenRouteServiceGeocoding.cs
@@ -0,0 +1,428 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.OpenRouteService.Services
+{
+ using System;
+ using System.Globalization;
+ using System.Linq;
+ using System.Net.Http;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Geo.Core;
+ using Geo.Core.Extensions;
+ using Geo.Core.Models.Exceptions;
+ using Geo.OpenRouteService.Models.Parameters;
+ using Geo.OpenRouteService.Models.Responses;
+ using Microsoft.Extensions.Logging;
+ using Microsoft.Extensions.Logging.Abstractions;
+ using Microsoft.Extensions.Options;
+
+ ///
+ /// A service to call the OpenRouteService geocoding API.
+ ///
+ public class OpenRouteServiceGeocoding : GeoClient, IOpenRouteServiceGeocoding
+ {
+ private const string SearchUri = "https://api.openrouteservice.org/geocode/search";
+ private const string AutocompleteUri = "https://api.openrouteservice.org/geocode/autocomplete";
+ private const string StructuredSearchUri = "https://api.openrouteservice.org/geocode/search/structured";
+ private const string ReverseUri = "https://api.openrouteservice.org/geocode/reverse";
+
+ private readonly IOptions> _options;
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A used for placing calls to the OpenRouteService geocoding API.
+ /// An of containing the ORS API key.
+ /// An used to create a logger used for logging information.
+ public OpenRouteServiceGeocoding(
+ HttpClient client,
+ IOptions> options,
+ ILoggerFactory loggerFactory = null)
+ : base(client, loggerFactory)
+ {
+ _options = options ?? throw new ArgumentNullException(nameof(options));
+ _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance;
+ }
+
+ ///
+ protected override string ApiName => "OpenRouteService Geocoding";
+
+ ///
+ public async Task SearchAsync(
+ SearchParameters parameters,
+ CancellationToken cancellationToken = default)
+ {
+ var uri = ValidateAndBuildUri(parameters, BuildSearchRequest);
+ return await GetAsync(uri, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public async Task AutocompleteAsync(
+ AutocompleteParameters parameters,
+ CancellationToken cancellationToken = default)
+ {
+ var uri = ValidateAndBuildUri(parameters, BuildAutocompleteRequest);
+ return await GetAsync(uri, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public async Task StructuredSearchAsync(
+ StructuredSearchParameters parameters,
+ CancellationToken cancellationToken = default)
+ {
+ var uri = ValidateAndBuildUri(parameters, BuildStructuredSearchRequest);
+ return await GetAsync(uri, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public async Task ReverseAsync(
+ ReverseSearchParameters parameters,
+ CancellationToken cancellationToken = default)
+ {
+ var uri = ValidateAndBuildUri(parameters, BuildReverseRequest);
+ return await GetAsync(uri, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Validates the uri and builds it based on the parameter type.
+ ///
+ /// The type of the parameters.
+ /// The parameters to validate and create a uri from.
+ /// The method to use to create the uri.
+ /// A with the uri crafted from the parameters.
+ internal Uri ValidateAndBuildUri(TParameters parameters, Func uriBuilderFunction)
+ where TParameters : class
+ {
+ if (parameters is null)
+ {
+ _logger.OpenRouteServiceError(Resources.Services.OpenRouteServiceGeocoding.Null_Parameters);
+ throw new GeoNETException(Resources.Services.OpenRouteServiceGeocoding.Null_Parameters, new ArgumentNullException(nameof(parameters)));
+ }
+
+ try
+ {
+ return uriBuilderFunction(parameters);
+ }
+ catch (ArgumentException ex)
+ {
+ _logger.OpenRouteServiceError(Resources.Services.OpenRouteServiceGeocoding.Failed_To_Create_Uri);
+ throw new GeoNETException(Resources.Services.OpenRouteServiceGeocoding.Failed_To_Create_Uri, ex);
+ }
+ }
+
+ ///
+ /// Builds the forward geocoding search uri based on the passed parameters.
+ ///
+ /// A with the search parameters to build the uri with.
+ /// A with the completed ORS search uri.
+ /// Thrown when the parameter is null or empty.
+ internal Uri BuildSearchRequest(SearchParameters parameters)
+ {
+ if (string.IsNullOrWhiteSpace(parameters.Text))
+ {
+ _logger.OpenRouteServiceError(Resources.Services.OpenRouteServiceGeocoding.Invalid_Text);
+ throw new ArgumentException(Resources.Services.OpenRouteServiceGeocoding.Invalid_Text, nameof(parameters.Text));
+ }
+
+ var uriBuilder = new UriBuilder(SearchUri);
+ var query = QueryString.Empty;
+
+ query = query.Add("text", parameters.Text);
+
+ AddBaseParameters(parameters, ref query);
+ AddOpenRouteServiceKey(parameters, ref query);
+ query = query.AddAdditionalParameters(parameters);
+
+ uriBuilder.AddQuery(query);
+ return uriBuilder.Uri;
+ }
+
+ ///
+ /// Builds the autocomplete uri based on the passed parameters.
+ ///
+ /// An with the autocomplete parameters to build the uri with.
+ /// A with the completed ORS autocomplete uri.
+ /// Thrown when the parameter is null or empty.
+ internal Uri BuildAutocompleteRequest(AutocompleteParameters parameters)
+ {
+ if (string.IsNullOrWhiteSpace(parameters.Text))
+ {
+ _logger.OpenRouteServiceError(Resources.Services.OpenRouteServiceGeocoding.Invalid_Text);
+ throw new ArgumentException(Resources.Services.OpenRouteServiceGeocoding.Invalid_Text, nameof(parameters.Text));
+ }
+
+ var uriBuilder = new UriBuilder(AutocompleteUri);
+ var query = QueryString.Empty;
+
+ query = query.Add("text", parameters.Text);
+
+ AddBaseParameters(parameters, ref query);
+ AddOpenRouteServiceKey(parameters, ref query);
+ query = query.AddAdditionalParameters(parameters);
+
+ uriBuilder.AddQuery(query);
+ return uriBuilder.Uri;
+ }
+
+ ///
+ /// Builds the structured geocoding search uri based on the passed parameters.
+ ///
+ /// A with the address component parameters to build the uri with.
+ /// A with the completed ORS structured search uri.
+ /// Thrown when no address component is provided.
+ internal Uri BuildStructuredSearchRequest(StructuredSearchParameters parameters)
+ {
+ if (string.IsNullOrWhiteSpace(parameters.Address) &&
+ string.IsNullOrWhiteSpace(parameters.Neighbourhood) &&
+ string.IsNullOrWhiteSpace(parameters.Borough) &&
+ string.IsNullOrWhiteSpace(parameters.Locality) &&
+ string.IsNullOrWhiteSpace(parameters.County) &&
+ string.IsNullOrWhiteSpace(parameters.Region) &&
+ string.IsNullOrWhiteSpace(parameters.PostalCode) &&
+ string.IsNullOrWhiteSpace(parameters.Country))
+ {
+ _logger.OpenRouteServiceError(Resources.Services.OpenRouteServiceGeocoding.Invalid_Address_Components);
+ throw new ArgumentException(Resources.Services.OpenRouteServiceGeocoding.Invalid_Address_Components, nameof(parameters.Address));
+ }
+
+ var uriBuilder = new UriBuilder(StructuredSearchUri);
+ var query = QueryString.Empty;
+
+ if (!string.IsNullOrWhiteSpace(parameters.Address))
+ {
+ query = query.Add("address", parameters.Address);
+ }
+
+ if (!string.IsNullOrWhiteSpace(parameters.Neighbourhood))
+ {
+ query = query.Add("neighbourhood", parameters.Neighbourhood);
+ }
+
+ if (!string.IsNullOrWhiteSpace(parameters.Borough))
+ {
+ query = query.Add("borough", parameters.Borough);
+ }
+
+ if (!string.IsNullOrWhiteSpace(parameters.Locality))
+ {
+ query = query.Add("locality", parameters.Locality);
+ }
+
+ if (!string.IsNullOrWhiteSpace(parameters.County))
+ {
+ query = query.Add("county", parameters.County);
+ }
+
+ if (!string.IsNullOrWhiteSpace(parameters.Region))
+ {
+ query = query.Add("region", parameters.Region);
+ }
+
+ if (!string.IsNullOrWhiteSpace(parameters.PostalCode))
+ {
+ query = query.Add("postalcode", parameters.PostalCode);
+ }
+
+ if (!string.IsNullOrWhiteSpace(parameters.Country))
+ {
+ query = query.Add("country", parameters.Country);
+ }
+
+ AddBaseParameters(parameters, ref query);
+ AddOpenRouteServiceKey(parameters, ref query);
+ query = query.AddAdditionalParameters(parameters);
+
+ uriBuilder.AddQuery(query);
+ return uriBuilder.Uri;
+ }
+
+ ///
+ /// Builds the reverse geocoding uri based on the passed parameters.
+ ///
+ /// A with the reverse geocoding parameters to build the uri with.
+ /// A with the completed ORS reverse geocoding uri.
+ /// Thrown when the parameter is null.
+ internal Uri BuildReverseRequest(ReverseSearchParameters parameters)
+ {
+ if (parameters.Point is null)
+ {
+ _logger.OpenRouteServiceError(Resources.Services.OpenRouteServiceGeocoding.Invalid_Point);
+ throw new ArgumentException(Resources.Services.OpenRouteServiceGeocoding.Invalid_Point, nameof(parameters.Point));
+ }
+
+ var uriBuilder = new UriBuilder(ReverseUri);
+ var query = QueryString.Empty;
+
+ query = query.Add("point.lat", parameters.Point.Latitude.ToString(CultureInfo.InvariantCulture));
+ query = query.Add("point.lon", parameters.Point.Longitude.ToString(CultureInfo.InvariantCulture));
+
+ if (parameters.Size.HasValue)
+ {
+ query = query.Add("size", parameters.Size.Value.ToString(CultureInfo.InvariantCulture));
+ }
+ else
+ {
+ _logger.OpenRouteServiceDebug(Resources.Services.OpenRouteServiceGeocoding.Invalid_Size);
+ }
+
+ if (parameters.BoundaryCircleRadius.HasValue)
+ {
+ query = query.Add("boundary.circle.radius", parameters.BoundaryCircleRadius.Value.ToString(CultureInfo.InvariantCulture));
+ }
+
+ if (parameters.BoundaryCountries.Count > 0)
+ {
+ query = query.Add("boundary.country", string.Join(",", parameters.BoundaryCountries));
+ }
+ else
+ {
+ _logger.OpenRouteServiceDebug(Resources.Services.OpenRouteServiceGeocoding.Invalid_Boundary_Countries);
+ }
+
+ if (!string.IsNullOrWhiteSpace(parameters.BoundaryGid))
+ {
+ query = query.Add("boundary.gid", parameters.BoundaryGid);
+ }
+ else
+ {
+ _logger.OpenRouteServiceDebug(Resources.Services.OpenRouteServiceGeocoding.Invalid_Boundary_Gid);
+ }
+
+ if (parameters.Sources.Count > 0)
+ {
+ query = query.Add("sources", string.Join(",", parameters.Sources.Select(x => x.ToEnumString())));
+ }
+ else
+ {
+ _logger.OpenRouteServiceDebug(Resources.Services.OpenRouteServiceGeocoding.Invalid_Sources);
+ }
+
+ if (parameters.Layers.Count > 0)
+ {
+ query = query.Add("layers", string.Join(",", parameters.Layers.Select(x => x.ToEnumString())));
+ }
+ else
+ {
+ _logger.OpenRouteServiceDebug(Resources.Services.OpenRouteServiceGeocoding.Invalid_Layers);
+ }
+
+ AddOpenRouteServiceKey(parameters, ref query);
+ query = query.AddAdditionalParameters(parameters);
+
+ uriBuilder.AddQuery(query);
+ return uriBuilder.Uri;
+ }
+
+ ///
+ /// Adds the shared optional parameters common to search, autocomplete, and structured search.
+ ///
+ /// A with the base parameters to add.
+ /// A with the query parameters.
+ internal void AddBaseParameters(BaseSearchParameters parameters, ref QueryString query)
+ {
+ if (parameters.Size.HasValue)
+ {
+ query = query.Add("size", parameters.Size.Value.ToString(CultureInfo.InvariantCulture));
+ }
+ else
+ {
+ _logger.OpenRouteServiceDebug(Resources.Services.OpenRouteServiceGeocoding.Invalid_Size);
+ }
+
+ if (parameters.FocusPoint != null)
+ {
+ query = query.Add("focus.point.lat", parameters.FocusPoint.Latitude.ToString(CultureInfo.InvariantCulture));
+ query = query.Add("focus.point.lon", parameters.FocusPoint.Longitude.ToString(CultureInfo.InvariantCulture));
+ }
+ else
+ {
+ _logger.OpenRouteServiceDebug(Resources.Services.OpenRouteServiceGeocoding.Invalid_Focus_Point);
+ }
+
+ if (parameters.BoundaryRect != null)
+ {
+ query = query.Add("boundary.rect.min_lon", parameters.BoundaryRect.MinLongitude.ToString(CultureInfo.InvariantCulture));
+ query = query.Add("boundary.rect.max_lon", parameters.BoundaryRect.MaxLongitude.ToString(CultureInfo.InvariantCulture));
+ query = query.Add("boundary.rect.min_lat", parameters.BoundaryRect.MinLatitude.ToString(CultureInfo.InvariantCulture));
+ query = query.Add("boundary.rect.max_lat", parameters.BoundaryRect.MaxLatitude.ToString(CultureInfo.InvariantCulture));
+ }
+ else
+ {
+ _logger.OpenRouteServiceDebug(Resources.Services.OpenRouteServiceGeocoding.Invalid_Boundary_Rect);
+ }
+
+ if (parameters.BoundaryCircle != null)
+ {
+ query = query.Add("boundary.circle.lat", parameters.BoundaryCircle.Latitude.ToString(CultureInfo.InvariantCulture));
+ query = query.Add("boundary.circle.lon", parameters.BoundaryCircle.Longitude.ToString(CultureInfo.InvariantCulture));
+
+ if (parameters.BoundaryCircle.Radius.HasValue)
+ {
+ query = query.Add("boundary.circle.radius", parameters.BoundaryCircle.Radius.Value.ToString(CultureInfo.InvariantCulture));
+ }
+ }
+ else
+ {
+ _logger.OpenRouteServiceDebug(Resources.Services.OpenRouteServiceGeocoding.Invalid_Boundary_Circle);
+ }
+
+ if (parameters.BoundaryCountries.Count > 0)
+ {
+ query = query.Add("boundary.country", string.Join(",", parameters.BoundaryCountries));
+ }
+ else
+ {
+ _logger.OpenRouteServiceDebug(Resources.Services.OpenRouteServiceGeocoding.Invalid_Boundary_Countries);
+ }
+
+ if (!string.IsNullOrWhiteSpace(parameters.BoundaryGid))
+ {
+ query = query.Add("boundary.gid", parameters.BoundaryGid);
+ }
+ else
+ {
+ _logger.OpenRouteServiceDebug(Resources.Services.OpenRouteServiceGeocoding.Invalid_Boundary_Gid);
+ }
+
+ if (parameters.Sources.Count > 0)
+ {
+ query = query.Add("sources", string.Join(",", parameters.Sources.Select(x => x.ToEnumString())));
+ }
+ else
+ {
+ _logger.OpenRouteServiceDebug(Resources.Services.OpenRouteServiceGeocoding.Invalid_Sources);
+ }
+
+ if (parameters.Layers.Count > 0)
+ {
+ query = query.Add("layers", string.Join(",", parameters.Layers.Select(x => x.ToEnumString())));
+ }
+ else
+ {
+ _logger.OpenRouteServiceDebug(Resources.Services.OpenRouteServiceGeocoding.Invalid_Layers);
+ }
+ }
+
+ ///
+ /// Adds the ORS API key to the query parameters.
+ ///
+ /// An to conditionally get the key from.
+ /// A with the query parameters.
+ internal void AddOpenRouteServiceKey(IKeyParameters keyParameter, ref QueryString query)
+ {
+ var key = _options.Value.Key;
+
+ if (!string.IsNullOrWhiteSpace(keyParameter.Key))
+ {
+ key = keyParameter.Key;
+ }
+
+ query = query.Add("api_key", key);
+ }
+ }
+}
diff --git a/test/Geo.OpenRouteService.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/test/Geo.OpenRouteService.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs
new file mode 100644
index 0000000..e51480d
--- /dev/null
+++ b/test/Geo.OpenRouteService.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs
@@ -0,0 +1,75 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.OpenRouteService.Tests.DependencyInjection
+{
+ using System;
+ using System.Net.Http;
+ using FluentAssertions;
+ using Geo.Extensions.DependencyInjection;
+ using Microsoft.Extensions.DependencyInjection;
+ using Microsoft.Extensions.Options;
+ using Xunit;
+
+ ///
+ /// Unit tests for the class.
+ ///
+ public class ServiceCollectionExtensionsTests
+ {
+ [Fact]
+ public void AddOpenRouteServiceGeocoding_WithValidCall_ConfiguresAllServices()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+
+ // Act
+ var builder = services.AddOpenRouteServiceGeocoding();
+ builder.AddKey("abc");
+
+ // Assert
+ var provider = services.BuildServiceProvider();
+
+ var options = provider.GetRequiredService>>();
+ options.Should().NotBeNull();
+ options.Value.Key.Should().Be("abc");
+ provider.GetRequiredService().Should().NotBeNull();
+ }
+
+ [Fact]
+ public void AddOpenRouteServiceGeocoding_WithNullOptions_ConfiguresAllServices()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+
+ // Act
+ services.AddOpenRouteServiceGeocoding();
+
+ // Assert
+ var provider = services.BuildServiceProvider();
+
+ var options = provider.GetRequiredService>>();
+ options.Should().NotBeNull();
+ options.Value.Key.Should().Be(string.Empty);
+ provider.GetRequiredService().Should().NotBeNull();
+ }
+
+ [Fact]
+ public void AddOpenRouteServiceGeocoding_WithClientConfiguration_ConfiguresHttpClientAllServices()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+
+ // Act
+ var builder = services.AddOpenRouteServiceGeocoding();
+ builder.AddKey("abc");
+ builder.HttpClientBuilder.ConfigureHttpClient(httpClient => httpClient.Timeout = TimeSpan.FromSeconds(5));
+
+ // Assert
+ var provider = services.BuildServiceProvider();
+ var client = provider.GetRequiredService().CreateClient("IOpenRouteServiceGeocoding");
+ client.Timeout.Should().Be(TimeSpan.FromSeconds(5));
+ }
+ }
+}
diff --git a/test/Geo.OpenRouteService.Tests/Geo.OpenRouteService.Tests.csproj b/test/Geo.OpenRouteService.Tests/Geo.OpenRouteService.Tests.csproj
new file mode 100644
index 0000000..49e8550
--- /dev/null
+++ b/test/Geo.OpenRouteService.Tests/Geo.OpenRouteService.Tests.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net48;netcoreapp3.1;net6.0;net8.0;net10.0
+ false
+
+
+
+
+
+
+
+
+
diff --git a/test/Geo.OpenRouteService.Tests/GlobalSuppressions.cs b/test/Geo.OpenRouteService.Tests/GlobalSuppressions.cs
new file mode 100644
index 0000000..b0b48cd
--- /dev/null
+++ b/test/Geo.OpenRouteService.Tests/GlobalSuppressions.cs
@@ -0,0 +1,13 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Using Microsoft recommended unit test naming", Scope = "namespaceanddescendants", Target = "~N:Geo.OpenRouteService.Tests")]
+[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "The name of the test should explain the test", Scope = "namespaceanddescendants", Target = "~N:Geo.OpenRouteService.Tests")]
diff --git a/test/Geo.OpenRouteService.Tests/Services/OpenRouteServiceGeocodingShould.cs b/test/Geo.OpenRouteService.Tests/Services/OpenRouteServiceGeocodingShould.cs
new file mode 100644
index 0000000..bb71472
--- /dev/null
+++ b/test/Geo.OpenRouteService.Tests/Services/OpenRouteServiceGeocodingShould.cs
@@ -0,0 +1,698 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.OpenRouteService.Tests.Services
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Globalization;
+ using System.Net;
+ using System.Net.Http;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using System.Web;
+ using FluentAssertions;
+ using Geo.Core;
+ using Geo.Core.Models.Exceptions;
+ using Geo.OpenRouteService.Enums;
+ using Geo.OpenRouteService.Models;
+ using Geo.OpenRouteService.Models.Parameters;
+ using Geo.OpenRouteService.Services;
+ using Microsoft.Extensions.Options;
+ using Moq;
+ using Moq.Protected;
+ using Xunit;
+
+ ///
+ /// Unit tests for the class.
+ ///
+ public class OpenRouteServiceGeocodingShould : IDisposable
+ {
+ private const string SearchJson =
+ "{\"type\":\"FeatureCollection\"," +
+ "\"features\":[{\"type\":\"Feature\"," +
+ "\"geometry\":{\"type\":\"Point\",\"coordinates\":[8.68417,49.41461]}," +
+ "\"properties\":{\"id\":\"node:6863027853\",\"gid\":\"osm:venue:node:6863027853\"," +
+ "\"layer\":\"venue\",\"source\":\"osm\",\"source_id\":\"node:6863027853\"," +
+ "\"name\":\"Heidelberg\",\"label\":\"Heidelberg, Baden-Württemberg, Germany\"," +
+ "\"confidence\":1.0,\"distance\":0.0,\"match_type\":\"exact\",\"accuracy\":\"point\"," +
+ "\"country\":\"Germany\",\"country_code\":\"DE\",\"region\":\"Baden-Württemberg\"," +
+ "\"locality\":\"Heidelberg\"}}]," +
+ "\"bbox\":[8.572,49.351,8.797,49.470]," +
+ "\"geocoding\":{\"version\":\"0.2\",\"attribution\":\"https://openrouteservice.org/\",\"warnings\":[]}}";
+
+ private readonly HttpClient _httpClient;
+ private readonly Mock>> _options = new Mock>>();
+ private readonly List _responseMessages = new List();
+ private bool _disposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public OpenRouteServiceGeocodingShould()
+ {
+ _options
+ .Setup(x => x.Value)
+ .Returns(new KeyOptions()
+ {
+ Key = "abc123",
+ });
+
+ var mockHandler = new Mock();
+
+ _responseMessages.Add(new HttpResponseMessage()
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = new StringContent(SearchJson),
+ });
+
+ mockHandler
+ .Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.Is(x => x.RequestUri.AbsolutePath.Contains("/geocode/search/structured")),
+ ItExpr.IsAny())
+ .ReturnsAsync(_responseMessages[_responseMessages.Count - 1]);
+
+ _responseMessages.Add(new HttpResponseMessage()
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = new StringContent(SearchJson),
+ });
+
+ mockHandler
+ .Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.Is(x => x.RequestUri.AbsolutePath.Contains("/geocode/autocomplete")),
+ ItExpr.IsAny())
+ .ReturnsAsync(_responseMessages[_responseMessages.Count - 1]);
+
+ _responseMessages.Add(new HttpResponseMessage()
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = new StringContent(SearchJson),
+ });
+
+ mockHandler
+ .Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.Is(x => x.RequestUri.AbsolutePath.Contains("/geocode/reverse")),
+ ItExpr.IsAny())
+ .ReturnsAsync(_responseMessages[_responseMessages.Count - 1]);
+
+ _responseMessages.Add(new HttpResponseMessage()
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = new StringContent(SearchJson),
+ });
+
+ mockHandler
+ .Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.Is(x => x.RequestUri.AbsolutePath.Contains("/geocode/search") && !x.RequestUri.AbsolutePath.Contains("structured")),
+ ItExpr.IsAny())
+ .ReturnsAsync(_responseMessages[_responseMessages.Count - 1]);
+
+ _httpClient = new HttpClient(mockHandler.Object);
+ }
+
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ [Fact]
+ public void AddOpenRouteServiceKey_WithOptions_SuccessfullyAddsKey()
+ {
+ var sut = BuildService();
+ var query = QueryString.Empty;
+
+ sut.AddOpenRouteServiceKey(new SearchParameters(), ref query);
+
+ var queryParameters = HttpUtility.ParseQueryString(query.ToString());
+ queryParameters.Count.Should().Be(1);
+ queryParameters["api_key"].Should().Be("abc123");
+ }
+
+ [Fact]
+ public void AddOpenRouteServiceKey_WithParameterOverride_SuccessfullyAddsKey()
+ {
+ var sut = BuildService();
+ var query = QueryString.Empty;
+
+ sut.AddOpenRouteServiceKey(new SearchParameters() { Key = "override123" }, ref query);
+
+ var queryParameters = HttpUtility.ParseQueryString(query.ToString());
+ queryParameters.Count.Should().Be(1);
+ queryParameters["api_key"].Should().Be("override123");
+ }
+
+ [Fact]
+ public void AddBaseParametersSuccessfully()
+ {
+ var sut = BuildService();
+ var query = QueryString.Empty;
+
+ var parameters = new SearchParameters()
+ {
+ Text = "Heidelberg",
+ Size = 5,
+ FocusPoint = new Coordinate() { Latitude = 49.41, Longitude = 8.68 },
+ BoundaryRect = new BoundingBox()
+ {
+ MinLongitude = 7.0,
+ MaxLongitude = 10.0,
+ MinLatitude = 48.0,
+ MaxLatitude = 51.0,
+ },
+ BoundaryCircle = new BoundingCircle()
+ {
+ Latitude = 49.41,
+ Longitude = 8.68,
+ Radius = 50.0,
+ },
+ BoundaryGid = "whosonfirst:region:85682571",
+ };
+
+ parameters.BoundaryCountries.Add("DE");
+ parameters.BoundaryCountries.Add("AT");
+ parameters.Sources.Add(SourceType.Osm);
+ parameters.Sources.Add(SourceType.Gn);
+ parameters.Layers.Add(LayerType.Locality);
+ parameters.Layers.Add(LayerType.Address);
+
+ sut.AddBaseParameters(parameters, ref query);
+
+ var queryParameters = HttpUtility.ParseQueryString(query.ToString());
+ queryParameters["size"].Should().Be("5");
+ queryParameters["focus.point.lat"].Should().Be("49.41");
+ queryParameters["focus.point.lon"].Should().Be("8.68");
+ queryParameters["boundary.rect.min_lon"].Should().Be("7");
+ queryParameters["boundary.rect.max_lon"].Should().Be("10");
+ queryParameters["boundary.rect.min_lat"].Should().Be("48");
+ queryParameters["boundary.rect.max_lat"].Should().Be("51");
+ queryParameters["boundary.circle.lat"].Should().Be("49.41");
+ queryParameters["boundary.circle.lon"].Should().Be("8.68");
+ queryParameters["boundary.circle.radius"].Should().Be("50");
+ queryParameters["boundary.country"].Should().Be("DE,AT");
+ queryParameters["boundary.gid"].Should().Be("whosonfirst:region:85682571");
+ queryParameters["sources"].Should().Be("osm,gn");
+ queryParameters["layers"].Should().Be("locality,address");
+ }
+
+ [Theory]
+ [ClassData(typeof(CultureTestData))]
+ public void BuildSearchRequestSuccessfully(CultureInfo culture)
+ {
+ var oldCulture = Thread.CurrentThread.CurrentCulture;
+ Thread.CurrentThread.CurrentCulture = culture;
+
+ var sut = BuildService();
+
+ var parameters = new SearchParameters()
+ {
+ Text = "Heidelberg",
+ Size = 10,
+ FocusPoint = new Coordinate() { Latitude = 49.41461, Longitude = 8.68417 },
+ BoundaryRect = new BoundingBox()
+ {
+ MinLongitude = 7.5,
+ MaxLongitude = 9.5,
+ MinLatitude = 48.5,
+ MaxLatitude = 50.5,
+ },
+ BoundaryGid = "whosonfirst:region:85682571",
+ };
+
+ parameters.BoundaryCountries.Add("DE");
+ parameters.Sources.Add(SourceType.Osm);
+ parameters.Layers.Add(LayerType.Locality);
+
+ var uri = sut.BuildSearchRequest(parameters);
+ var query = HttpUtility.UrlDecode(uri.PathAndQuery);
+
+ query.Should().Contain("text=Heidelberg");
+ query.Should().Contain("size=10");
+ query.Should().Contain("focus.point.lat=49.41461");
+ query.Should().Contain("focus.point.lon=8.68417");
+ query.Should().Contain("boundary.rect.min_lon=7.5");
+ query.Should().Contain("boundary.rect.max_lon=9.5");
+ query.Should().Contain("boundary.rect.min_lat=48.5");
+ query.Should().Contain("boundary.rect.max_lat=50.5");
+ query.Should().Contain("boundary.country=DE");
+ query.Should().Contain("boundary.gid=whosonfirst:region:85682571");
+ query.Should().Contain("sources=osm");
+ query.Should().Contain("layers=locality");
+ query.Should().Contain("api_key=abc123");
+
+ Thread.CurrentThread.CurrentCulture = oldCulture;
+ }
+
+ [Fact]
+ public void BuildSearchRequest_WithAdditionalParameters_AddsThemToQueryString()
+ {
+ var sut = BuildService();
+
+ var parameters = new SearchParameters() { Text = "Heidelberg" };
+ parameters.AdditionalParameters.Add("customKey1", "customValue1");
+ parameters.AdditionalParameters.Add("customKey2", "customValue2");
+
+ var uri = sut.BuildSearchRequest(parameters);
+ var query = HttpUtility.UrlDecode(uri.PathAndQuery);
+
+ query.Should().Contain("customKey1=customValue1");
+ query.Should().Contain("customKey2=customValue2");
+ }
+
+ [Fact]
+ public void BuildSearchRequestFailsWithException()
+ {
+ var sut = BuildService();
+
+ Action act = () => sut.BuildSearchRequest(new SearchParameters());
+
+ act.Should()
+ .Throw()
+#if NETCOREAPP3_1_OR_GREATER
+ .WithMessage("*(Parameter 'Text')");
+#else
+ .WithMessage("*Parameter name: Text");
+#endif
+ }
+
+ [Fact]
+ public void ValidateAndCraftSearchUri_WithNullParameters_ThrowsGeoNETException()
+ {
+ var sut = BuildService();
+
+ Action act = () => sut.ValidateAndBuildUri(null, sut.BuildSearchRequest);
+
+ act.Should()
+ .Throw()
+ .WithMessage("*See the inner exception for more information.")
+ .WithInnerException();
+ }
+
+ [Fact]
+ public void ValidateAndCraftSearchUri_WithInvalidParameters_ThrowsGeoNETException()
+ {
+ var sut = BuildService();
+
+ Action act = () => sut.ValidateAndBuildUri(new SearchParameters(), sut.BuildSearchRequest);
+
+ act.Should()
+ .Throw()
+ .WithMessage("*See the inner exception for more information.")
+ .WithInnerException()
+#if NETCOREAPP3_1_OR_GREATER
+ .WithMessage("*(Parameter 'Text')");
+#else
+ .WithMessage("*Parameter name: Text");
+#endif
+ }
+
+ [Fact]
+ public async Task SearchAsyncSuccessfully()
+ {
+ var sut = BuildService();
+
+ var parameters = new SearchParameters()
+ {
+ Text = "Heidelberg",
+ Size = 5,
+ };
+
+ parameters.BoundaryCountries.Add("DE");
+
+ var result = await sut.SearchAsync(parameters);
+
+ result.Features.Count.Should().Be(1);
+ result.Features[0].Properties.Name.Should().Be("Heidelberg");
+ result.Features[0].Properties.Country.Should().Be("Germany");
+ result.Features[0].Geometry.Coordinates.Latitude.Should().Be(49.41461);
+ result.Features[0].Geometry.Coordinates.Longitude.Should().Be(8.68417);
+ }
+
+ [Theory]
+ [ClassData(typeof(CultureTestData))]
+ public void BuildAutocompleteRequestSuccessfully(CultureInfo culture)
+ {
+ var oldCulture = Thread.CurrentThread.CurrentCulture;
+ Thread.CurrentThread.CurrentCulture = culture;
+
+ var sut = BuildService();
+
+ var parameters = new AutocompleteParameters()
+ {
+ Text = "Heidel",
+ Size = 5,
+ FocusPoint = new Coordinate() { Latitude = 49.41461, Longitude = 8.68417 },
+ };
+
+ parameters.BoundaryCountries.Add("DE");
+ parameters.Layers.Add(LayerType.Locality);
+
+ var uri = sut.BuildAutocompleteRequest(parameters);
+ var query = HttpUtility.UrlDecode(uri.PathAndQuery);
+
+ query.Should().Contain("text=Heidel");
+ query.Should().Contain("size=5");
+ query.Should().Contain("focus.point.lat=49.41461");
+ query.Should().Contain("focus.point.lon=8.68417");
+ query.Should().Contain("boundary.country=DE");
+ query.Should().Contain("layers=locality");
+ query.Should().Contain("api_key=abc123");
+
+ var path = uri.AbsolutePath;
+ path.Should().Contain("/geocode/autocomplete");
+
+ Thread.CurrentThread.CurrentCulture = oldCulture;
+ }
+
+ [Fact]
+ public void BuildAutocompleteRequest_WithAdditionalParameters_AddsThemToQueryString()
+ {
+ var sut = BuildService();
+
+ var parameters = new AutocompleteParameters() { Text = "Heidel" };
+ parameters.AdditionalParameters.Add("customKey1", "customValue1");
+
+ var uri = sut.BuildAutocompleteRequest(parameters);
+ var query = HttpUtility.UrlDecode(uri.PathAndQuery);
+
+ query.Should().Contain("customKey1=customValue1");
+ }
+
+ [Fact]
+ public void BuildAutocompleteRequestFailsWithException()
+ {
+ var sut = BuildService();
+
+ Action act = () => sut.BuildAutocompleteRequest(new AutocompleteParameters());
+
+ act.Should()
+ .Throw()
+#if NETCOREAPP3_1_OR_GREATER
+ .WithMessage("*(Parameter 'Text')");
+#else
+ .WithMessage("*Parameter name: Text");
+#endif
+ }
+
+ [Fact]
+ public void ValidateAndCraftAutocompleteUri_WithNullParameters_ThrowsGeoNETException()
+ {
+ var sut = BuildService();
+
+ Action act = () => sut.ValidateAndBuildUri(null, sut.BuildAutocompleteRequest);
+
+ act.Should()
+ .Throw()
+ .WithInnerException();
+ }
+
+ [Fact]
+ public void ValidateAndCraftAutocompleteUri_WithInvalidParameters_ThrowsGeoNETException()
+ {
+ var sut = BuildService();
+
+ Action act = () => sut.ValidateAndBuildUri(new AutocompleteParameters(), sut.BuildAutocompleteRequest);
+
+ act.Should()
+ .Throw()
+ .WithInnerException();
+ }
+
+ [Fact]
+ public async Task AutocompleteAsyncSuccessfully()
+ {
+ var sut = BuildService();
+
+ var result = await sut.AutocompleteAsync(new AutocompleteParameters() { Text = "Heidel" });
+
+ result.Features.Count.Should().Be(1);
+ result.Features[0].Properties.Name.Should().Be("Heidelberg");
+ }
+
+ [Theory]
+ [ClassData(typeof(CultureTestData))]
+ public void BuildStructuredSearchRequestSuccessfully(CultureInfo culture)
+ {
+ var oldCulture = Thread.CurrentThread.CurrentCulture;
+ Thread.CurrentThread.CurrentCulture = culture;
+
+ var sut = BuildService();
+
+ var parameters = new StructuredSearchParameters()
+ {
+ Address = "Hauptstraße 1",
+ Locality = "Heidelberg",
+ Region = "Baden-Württemberg",
+ PostalCode = "69117",
+ Country = "DE",
+ Size = 3,
+ FocusPoint = new Coordinate() { Latitude = 49.41461, Longitude = 8.68417 },
+ };
+
+ var uri = sut.BuildStructuredSearchRequest(parameters);
+ var query = HttpUtility.UrlDecode(uri.PathAndQuery);
+
+ query.Should().Contain("address=Hauptstraße 1");
+ query.Should().Contain("locality=Heidelberg");
+ query.Should().Contain("region=Baden-Württemberg");
+ query.Should().Contain("postalcode=69117");
+ query.Should().Contain("country=DE");
+ query.Should().Contain("size=3");
+ query.Should().Contain("focus.point.lat=49.41461");
+ query.Should().Contain("focus.point.lon=8.68417");
+ query.Should().Contain("api_key=abc123");
+
+ var path = uri.AbsolutePath;
+ path.Should().Contain("/geocode/search/structured");
+
+ Thread.CurrentThread.CurrentCulture = oldCulture;
+ }
+
+ [Fact]
+ public void BuildStructuredSearchRequest_WithAdditionalParameters_AddsThemToQueryString()
+ {
+ var sut = BuildService();
+
+ var parameters = new StructuredSearchParameters() { Locality = "Heidelberg" };
+ parameters.AdditionalParameters.Add("customKey1", "customValue1");
+
+ var uri = sut.BuildStructuredSearchRequest(parameters);
+ var query = HttpUtility.UrlDecode(uri.PathAndQuery);
+
+ query.Should().Contain("customKey1=customValue1");
+ }
+
+ [Fact]
+ public void BuildStructuredSearchRequestFailsWithException()
+ {
+ var sut = BuildService();
+
+ Action act = () => sut.BuildStructuredSearchRequest(new StructuredSearchParameters());
+
+ act.Should()
+ .Throw()
+#if NETCOREAPP3_1_OR_GREATER
+ .WithMessage("*(Parameter 'Address')");
+#else
+ .WithMessage("*Parameter name: Address");
+#endif
+ }
+
+ [Fact]
+ public void ValidateAndCraftStructuredSearchUri_WithNullParameters_ThrowsGeoNETException()
+ {
+ var sut = BuildService();
+
+ Action act = () => sut.ValidateAndBuildUri(null, sut.BuildStructuredSearchRequest);
+
+ act.Should()
+ .Throw()
+ .WithInnerException();
+ }
+
+ [Fact]
+ public void ValidateAndCraftStructuredSearchUri_WithInvalidParameters_ThrowsGeoNETException()
+ {
+ var sut = BuildService();
+
+ Action act = () => sut.ValidateAndBuildUri(new StructuredSearchParameters(), sut.BuildStructuredSearchRequest);
+
+ act.Should()
+ .Throw()
+ .WithInnerException();
+ }
+
+ [Fact]
+ public async Task StructuredSearchAsyncSuccessfully()
+ {
+ var sut = BuildService();
+
+ var result = await sut.StructuredSearchAsync(new StructuredSearchParameters() { Locality = "Heidelberg", Country = "DE" });
+
+ result.Features.Count.Should().Be(1);
+ result.Features[0].Properties.Locality.Should().Be("Heidelberg");
+ }
+
+ [Theory]
+ [ClassData(typeof(CultureTestData))]
+ public void BuildReverseRequestSuccessfully(CultureInfo culture)
+ {
+ var oldCulture = Thread.CurrentThread.CurrentCulture;
+ Thread.CurrentThread.CurrentCulture = culture;
+
+ var sut = BuildService();
+
+ var parameters = new ReverseSearchParameters()
+ {
+ Point = new Coordinate() { Latitude = 49.41461, Longitude = 8.68417 },
+ Size = 5,
+ BoundaryCircleRadius = 1.5,
+ BoundaryGid = "whosonfirst:region:85682571",
+ };
+
+ parameters.BoundaryCountries.Add("DE");
+ parameters.Sources.Add(SourceType.Osm);
+ parameters.Layers.Add(LayerType.Address);
+
+ var uri = sut.BuildReverseRequest(parameters);
+ var query = HttpUtility.UrlDecode(uri.PathAndQuery);
+
+ query.Should().Contain("point.lat=49.41461");
+ query.Should().Contain("point.lon=8.68417");
+ query.Should().Contain("size=5");
+ query.Should().Contain("boundary.circle.radius=1.5");
+ query.Should().Contain("boundary.country=DE");
+ query.Should().Contain("boundary.gid=whosonfirst:region:85682571");
+ query.Should().Contain("sources=osm");
+ query.Should().Contain("layers=address");
+ query.Should().Contain("api_key=abc123");
+
+ var path = uri.AbsolutePath;
+ path.Should().Contain("/geocode/reverse");
+
+ Thread.CurrentThread.CurrentCulture = oldCulture;
+ }
+
+ [Fact]
+ public void BuildReverseRequest_WithAdditionalParameters_AddsThemToQueryString()
+ {
+ var sut = BuildService();
+
+ var parameters = new ReverseSearchParameters()
+ {
+ Point = new Coordinate() { Latitude = 49.41461, Longitude = 8.68417 },
+ };
+
+ parameters.AdditionalParameters.Add("customKey1", "customValue1");
+ parameters.AdditionalParameters.Add("customKey2", "customValue2");
+
+ var uri = sut.BuildReverseRequest(parameters);
+ var query = HttpUtility.UrlDecode(uri.PathAndQuery);
+
+ query.Should().Contain("customKey1=customValue1");
+ query.Should().Contain("customKey2=customValue2");
+ }
+
+ [Fact]
+ public void BuildReverseRequestFailsWithException()
+ {
+ var sut = BuildService();
+
+ Action act = () => sut.BuildReverseRequest(new ReverseSearchParameters());
+
+ act.Should()
+ .Throw()
+#if NETCOREAPP3_1_OR_GREATER
+ .WithMessage("*(Parameter 'Point')");
+#else
+ .WithMessage("*Parameter name: Point");
+#endif
+ }
+
+ [Fact]
+ public void ValidateAndCraftReverseUri_WithNullParameters_ThrowsGeoNETException()
+ {
+ var sut = BuildService();
+
+ Action act = () => sut.ValidateAndBuildUri(null, sut.BuildReverseRequest);
+
+ act.Should()
+ .Throw()
+ .WithInnerException();
+ }
+
+ [Fact]
+ public void ValidateAndCraftReverseUri_WithInvalidParameters_ThrowsGeoNETException()
+ {
+ var sut = BuildService();
+
+ Action act = () => sut.ValidateAndBuildUri(new ReverseSearchParameters(), sut.BuildReverseRequest);
+
+ act.Should()
+ .Throw()
+ .WithInnerException()
+#if NETCOREAPP3_1_OR_GREATER
+ .WithMessage("*(Parameter 'Point')");
+#else
+ .WithMessage("*Parameter name: Point");
+#endif
+ }
+
+ [Fact]
+ public async Task ReverseAsyncSuccessfully()
+ {
+ var sut = BuildService();
+
+ var result = await sut.ReverseAsync(new ReverseSearchParameters()
+ {
+ Point = new Coordinate() { Latitude = 49.41461, Longitude = 8.68417 },
+ });
+
+ result.Features.Count.Should().Be(1);
+ result.Features[0].Properties.Name.Should().Be("Heidelberg");
+ result.Features[0].Geometry.Coordinates.Latitude.Should().Be(49.41461);
+ result.Features[0].Geometry.Coordinates.Longitude.Should().Be(8.68417);
+ }
+
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ ///
+ /// A boolean flag indicating whether or not to dispose of objects.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ _httpClient?.Dispose();
+
+ foreach (var message in _responseMessages)
+ {
+ message?.Dispose();
+ }
+ }
+
+ _disposed = true;
+ }
+
+ private OpenRouteServiceGeocoding BuildService()
+ {
+ return new OpenRouteServiceGeocoding(_httpClient, _options.Object);
+ }
+ }
+}
diff --git a/test/Geo.OpenRouteService.Tests/TestData/CultureTestData.cs b/test/Geo.OpenRouteService.Tests/TestData/CultureTestData.cs
new file mode 100644
index 0000000..fa76ca4
--- /dev/null
+++ b/test/Geo.OpenRouteService.Tests/TestData/CultureTestData.cs
@@ -0,0 +1,40 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.OpenRouteService.Tests
+{
+ using System.Collections;
+ using System.Collections.Generic;
+ using System.Globalization;
+
+ ///
+ /// Test data when testing different cultures. This test data returns a representative set of cultures
+ /// to test culture-invariant behaviour without running tests for every culture in dotnet.
+ /// Covers: invariant, period-decimal (en-US), comma-decimal (de-DE, fr-FR, ru-RU), Arabic, and Chinese.
+ ///
+ public class CultureTestData : IEnumerable