diff --git a/Geo.NET.sln b/Geo.NET.sln
index be941ab..9364d4e 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 17
-VisualStudioVersion = 17.1.32421.90
+# Visual Studio Version 18
+VisualStudioVersion = 18.3.11520.95 d18.3
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{B3BD11A7-1752-49E3-AE50-A65B5FE7B7B2}"
ProjectSection(SolutionItems) = preProject
@@ -53,6 +53,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Geo.Positionstack", "src\Ge
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Geo.Positionstack.Tests", "test\Geo.Positionstack.Tests\Geo.Positionstack.Tests.csproj", "{E09BD60D-6E8A-4210-9274-695A2DFFE976}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Geo.Trimble", "src\Geo.Trimble\Geo.Trimble.csproj", "{7F4A2E61-83B1-4C2D-9A5E-1D3F8B7C4E90}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Geo.Trimble.Tests", "test\Geo.Trimble.Tests\Geo.Trimble.Tests.csproj", "{B2D5A7F3-6E1C-4B9A-8D2F-3C7E5A1B4D60}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -131,6 +135,14 @@ Global
{E09BD60D-6E8A-4210-9274-695A2DFFE976}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E09BD60D-6E8A-4210-9274-695A2DFFE976}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E09BD60D-6E8A-4210-9274-695A2DFFE976}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7F4A2E61-83B1-4C2D-9A5E-1D3F8B7C4E90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7F4A2E61-83B1-4C2D-9A5E-1D3F8B7C4E90}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7F4A2E61-83B1-4C2D-9A5E-1D3F8B7C4E90}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7F4A2E61-83B1-4C2D-9A5E-1D3F8B7C4E90}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B2D5A7F3-6E1C-4B9A-8D2F-3C7E5A1B4D60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -154,6 +166,8 @@ Global
{1A320DE3-B14B-46EE-A0E6-C6783E585F73} = {67253D97-9FC9-4749-80DC-A5D84339DC05}
{03C7B4AE-7905-4292-8133-3F4AA6EDCA3A} = {8F3BA9BC-542C-450C-96C9-F0D72FECC930}
{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}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C4B688C4-40EC-4577-9EB2-4CF2412DA0B1}
diff --git a/src/Geo.Trimble/Abstractions/ITrimbleGeocoding.cs b/src/Geo.Trimble/Abstractions/ITrimbleGeocoding.cs
new file mode 100644
index 0000000..d8ae4c3
--- /dev/null
+++ b/src/Geo.Trimble/Abstractions/ITrimbleGeocoding.cs
@@ -0,0 +1,38 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.Trimble
+{
+ using System.Collections.Generic;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Geo.Core.Models.Exceptions;
+ using Geo.Trimble.Models.Parameters;
+ using Geo.Trimble.Models.Responses;
+
+ ///
+ /// An interface for calling the Trimble Maps geocoding methods.
+ ///
+ public interface ITrimbleGeocoding
+ {
+ ///
+ /// Calls the Trimble Maps geocoding API and returns the results.
+ ///
+ /// A with the parameters of the request.
+ /// A used to cancel the request.
+ /// A list of with the response from Trimble Maps.
+ /// Thrown for multiple different reasons. Check the inner exception for more information.
+ Task> GeocodingAsync(GeocodingParameters parameters, CancellationToken cancellationToken = default);
+
+ ///
+ /// Calls the Trimble Maps reverse geocoding API and returns the results.
+ ///
+ /// A with the parameters of the request.
+ /// A used to cancel the request.
+ /// A with the response from Trimble Maps.
+ /// Thrown for multiple different reasons. Check the inner exception for more information.
+ Task ReverseGeocodingAsync(ReverseGeocodingParameters parameters, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/src/Geo.Trimble/DependencyInjection/ServiceCollectionExtensions.cs b/src/Geo.Trimble/DependencyInjection/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..9697025
--- /dev/null
+++ b/src/Geo.Trimble/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.Trimble;
+ using Geo.Trimble.Services;
+ using Microsoft.Extensions.DependencyInjection;
+ using Microsoft.Extensions.Options;
+
+ ///
+ /// Extension methods for the class.
+ ///
+ public static class ServiceCollectionExtensions
+ {
+ ///
+ /// Adds the Trimble Maps geocoding services to the service collection.
+ ///
+ /// Adds the services:
+ ///
+ /// - of
+ ///
+ ///
+ ///
+ ///
+ /// An to add the Trimble Maps services to.
+ /// An to configure the Trimble Maps geocoding.
+ /// Thrown if is null.
+ public static KeyBuilder AddTrimbleGeocoding(this IServiceCollection services)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ services.AddKeyOptions();
+
+ return new KeyBuilder(services.AddHttpClient());
+ }
+ }
+}
diff --git a/src/Geo.Trimble/Extensions/LoggerExtensions.cs b/src/Geo.Trimble/Extensions/LoggerExtensions.cs
new file mode 100644
index 0000000..6451c23
--- /dev/null
+++ b/src/Geo.Trimble/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.Trimble
+{
+ using System;
+ using Geo.Trimble.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(TrimbleGeocoding)),
+ "TrimbleGeocoding: {ErrorMessage}");
+
+ private static readonly Action _warning = LoggerMessage.Define(
+ LogLevel.Warning,
+ new EventId(2, nameof(TrimbleGeocoding)),
+ "TrimbleGeocoding: {WarningMessage}");
+
+ private static readonly Action _debug = LoggerMessage.Define(
+ LogLevel.Debug,
+ new EventId(3, nameof(TrimbleGeocoding)),
+ "TrimbleGeocoding: {DebugMessage}");
+
+ ///
+ /// "TrimbleGeocoding: {ErrorMessage}".
+ ///
+ /// An used to log the error message.
+ /// The error message to log.
+ public static void TrimbleError(this ILogger logger, string errorMessage)
+ {
+ _error(logger, errorMessage, null);
+ }
+
+ ///
+ /// "TrimbleGeocoding: {WarningMessage}".
+ ///
+ /// An used to log the warning message.
+ /// The warning message to log.
+ public static void TrimbleWarning(this ILogger logger, string warningMessage)
+ {
+ _warning(logger, warningMessage, null);
+ }
+
+ ///
+ /// "TrimbleGeocoding: {DebugMessage}".
+ ///
+ /// An used to log the debug message.
+ /// The debug message to log.
+ public static void TrimbleDebug(this ILogger logger, string debugMessage)
+ {
+ _debug(logger, debugMessage, null);
+ }
+ }
+}
diff --git a/src/Geo.Trimble/Geo.Trimble.csproj b/src/Geo.Trimble/Geo.Trimble.csproj
new file mode 100644
index 0000000..d547a9a
--- /dev/null
+++ b/src/Geo.Trimble/Geo.Trimble.csproj
@@ -0,0 +1,45 @@
+
+
+
+ netstandard2.0;net6.0;net8.0;net10.0
+ Justin Canton
+ Geo.NET
+ Geo.NET Trimble
+ geocoding geo.net Trimble
+ A lightweight method for communicating with the Trimble Maps geocoding APIs. This includes models and interfaces for calling Trimble Maps.
+ MIT
+ https://github.com/JustinCanton/Geo.NET
+ true
+ README.md
+
+
+
+
+
+
+
+
+
+
+ True
+ True
+ TrimbleGeocoding.resx
+
+
+
+
+
+ ResXFileCodeGenerator
+ TrimbleGeocoding.Designer.cs
+
+
+
+
+
+ True
+ \
+ Always
+
+
+
+
diff --git a/src/Geo.Trimble/Models/Enums/CountryAbbrevType.cs b/src/Geo.Trimble/Models/Enums/CountryAbbrevType.cs
new file mode 100644
index 0000000..76dd3dd
--- /dev/null
+++ b/src/Geo.Trimble/Models/Enums/CountryAbbrevType.cs
@@ -0,0 +1,28 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.Trimble.Models.Enums
+{
+ ///
+ /// The format used for country abbreviations in Trimble Maps API responses.
+ ///
+ public enum CountryAbbrevType
+ {
+ /// FIPS country code.
+ FIPS,
+
+ /// ISO 3166-1 alpha-2 (two-letter) country code.
+ ISO2,
+
+ /// ISO 3166-1 alpha-3 (three-letter) country code.
+ ISO3,
+
+ /// GENC two-letter country code.
+ GENC2,
+
+ /// GENC three-letter country code.
+ GENC3,
+ }
+}
diff --git a/src/Geo.Trimble/Models/Enums/Region.cs b/src/Geo.Trimble/Models/Enums/Region.cs
new file mode 100644
index 0000000..d3e49c7
--- /dev/null
+++ b/src/Geo.Trimble/Models/Enums/Region.cs
@@ -0,0 +1,37 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.Trimble.Models.Enums
+{
+ ///
+ /// The geographic region used to scope Trimble Maps API requests.
+ ///
+ public enum Region
+ {
+ /// North America (default).
+ NorthAmerica = 0,
+
+ /// Africa.
+ Africa = 1,
+
+ /// Asia.
+ Asia = 2,
+
+ /// Europe.
+ Europe = 3,
+
+ /// Oceania.
+ Oceania = 4,
+
+ /// South America.
+ SouthAmerica = 5,
+
+ /// Middle East.
+ MiddleEast = 6,
+
+ /// Global.
+ Global = 7,
+ }
+}
diff --git a/src/Geo.Trimble/Models/Parameters/Coordinate.cs b/src/Geo.Trimble/Models/Parameters/Coordinate.cs
new file mode 100644
index 0000000..07e4455
--- /dev/null
+++ b/src/Geo.Trimble/Models/Parameters/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.Trimble.Models.Parameters
+{
+ using System.Globalization;
+
+ ///
+ /// The coordinates (longitude, latitude) used for Trimble Maps reverse geocoding.
+ ///
+ public class Coordinate
+ {
+ ///
+ /// Gets or sets the latitude of the location. For example: "40.7128".
+ ///
+ public double Latitude { get; set; }
+
+ ///
+ /// Gets or sets the longitude of the location. For example: "-74.0060".
+ ///
+ public double Longitude { get; set; }
+
+ ///
+ public override string ToString()
+ {
+ return string.Format(CultureInfo.InvariantCulture, "{0},{1}", Longitude, Latitude);
+ }
+ }
+}
diff --git a/src/Geo.Trimble/Models/Parameters/GeocodingParameters.cs b/src/Geo.Trimble/Models/Parameters/GeocodingParameters.cs
new file mode 100644
index 0000000..3fccf68
--- /dev/null
+++ b/src/Geo.Trimble/Models/Parameters/GeocodingParameters.cs
@@ -0,0 +1,83 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.Trimble.Models.Parameters
+{
+ using System.Collections.Generic;
+ using Geo.Trimble.Models.Enums;
+
+ ///
+ /// The parameters possible to use during a Trimble Maps geocoding request.
+ ///
+ public class GeocodingParameters : IKeyParameters, IAdditionalParameters
+ {
+ ///
+ /// Gets or sets the street address to geocode.
+ /// Optional.
+ ///
+ public string Street { get; set; }
+
+ ///
+ /// Gets or sets the city to geocode.
+ /// Optional.
+ ///
+ public string City { get; set; }
+
+ ///
+ /// Gets or sets the state or province to geocode.
+ /// Optional.
+ ///
+ public string State { get; set; }
+
+ ///
+ /// Gets or sets the postal/ZIP code to geocode.
+ /// Optional.
+ ///
+ public string Zip { get; set; }
+
+ ///
+ /// Gets or sets the county to geocode.
+ /// Optional.
+ ///
+ public string County { get; set; }
+
+ ///
+ /// Gets or sets the country to geocode.
+ /// Optional.
+ ///
+ public string Country { get; set; }
+
+ ///
+ /// Gets or sets the geographic region.
+ /// Optional.
+ /// Default: .
+ ///
+ public Region Region { get; set; } = Region.NorthAmerica;
+
+ ///
+ /// Gets or sets a specific data version to query.
+ /// Optional.
+ ///
+ public string Dataset { get; set; }
+
+ ///
+ /// Gets or sets the maximum number of results to return.
+ /// Optional.
+ ///
+ public int? MaxResults { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether results should be limited to named roads only.
+ /// Optional.
+ ///
+ public bool MatchNamedRoadsOnly { get; set; }
+
+ ///
+ public string Key { get; set; }
+
+ ///
+ public IDictionary AdditionalParameters { get; } = new Dictionary();
+ }
+}
diff --git a/src/Geo.Trimble/Models/Parameters/ReverseGeocodingParameters.cs b/src/Geo.Trimble/Models/Parameters/ReverseGeocodingParameters.cs
new file mode 100644
index 0000000..400da9e
--- /dev/null
+++ b/src/Geo.Trimble/Models/Parameters/ReverseGeocodingParameters.cs
@@ -0,0 +1,95 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.Trimble.Models.Parameters
+{
+ using System.Collections.Generic;
+ using Geo.Trimble.Models.Enums;
+
+ ///
+ /// The parameters possible to use during a Trimble Maps reverse geocoding request.
+ ///
+ public class ReverseGeocodingParameters : IKeyParameters, IAdditionalParameters
+ {
+ ///
+ /// Gets or sets the coordinates to reverse geocode.
+ /// Required.
+ ///
+ public Coordinate Coordinate { get; set; }
+
+ ///
+ /// Gets or sets the geographic region.
+ /// Optional.
+ /// Default: .
+ ///
+ public Region Region { get; set; } = Region.NorthAmerica;
+
+ ///
+ /// Gets or sets a specific data version to query.
+ /// Optional.
+ ///
+ public string Dataset { get; set; }
+
+ ///
+ /// Gets or sets the preferred response language.
+ /// Optional.
+ ///
+ public string Lang { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether results should be limited to named roads only.
+ /// Optional.
+ ///
+ public bool MatchNamedRoadsOnly { get; set; }
+
+ ///
+ /// Gets or sets the maximum distance in miles to search for the nearest road.
+ /// Optional.
+ ///
+ public double? MaxCleanupMiles { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to include posted speed limit data in the response.
+ /// Optional.
+ ///
+ public bool IncludePostedSpeedLimit { get; set; }
+
+ ///
+ /// Gets or sets the vehicle type classification.
+ /// Optional.
+ ///
+ public string VehicleType { get; set; }
+
+ ///
+ /// Gets or sets the direction of travel in degrees.
+ /// Optional.
+ ///
+ public double? Heading { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to include road link identifiers in the response.
+ /// Optional.
+ ///
+ public bool IncludeLinkInfo { get; set; }
+
+ ///
+ /// Gets or sets the format to use for country abbreviations in the response.
+ /// Optional.
+ ///
+ public CountryAbbrevType? CountryAbbrevType { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to include Trimble place database information.
+ /// Optional.
+ ///
+ public bool IncludeTrimblePlaceIds { get; set; }
+
+ ///
+ public string Key { get; set; }
+
+ ///
+ public IDictionary AdditionalParameters { get; } = new Dictionary();
+ }
+}
diff --git a/src/Geo.Trimble/Models/Responses/GeocodeAddress.cs b/src/Geo.Trimble/Models/Responses/GeocodeAddress.cs
new file mode 100644
index 0000000..c4bf5ef
--- /dev/null
+++ b/src/Geo.Trimble/Models/Responses/GeocodeAddress.cs
@@ -0,0 +1,57 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.Trimble.Models.Responses
+{
+ using System.Text.Json.Serialization;
+
+ ///
+ /// The address portion of a Trimble Maps geocoding response.
+ ///
+ public class GeocodeAddress
+ {
+ ///
+ /// Gets or sets the street address.
+ ///
+ [JsonPropertyName("StreetAddress")]
+ public string StreetAddress { get; set; }
+
+ ///
+ /// Gets or sets the city.
+ ///
+ [JsonPropertyName("City")]
+ public string City { get; set; }
+
+ ///
+ /// Gets or sets the state or province.
+ ///
+ [JsonPropertyName("State")]
+ public string State { get; set; }
+
+ ///
+ /// Gets or sets the postal/ZIP code.
+ ///
+ [JsonPropertyName("Zip")]
+ public string Zip { get; set; }
+
+ ///
+ /// Gets or sets the county.
+ ///
+ [JsonPropertyName("County")]
+ public string County { get; set; }
+
+ ///
+ /// Gets or sets the country.
+ ///
+ [JsonPropertyName("Country")]
+ public string Country { get; set; }
+
+ ///
+ /// Gets or sets the abbreviated country code.
+ ///
+ [JsonPropertyName("CountryAbbreviation")]
+ public string CountryAbbreviation { get; set; }
+ }
+}
diff --git a/src/Geo.Trimble/Models/Responses/GeocodeCoords.cs b/src/Geo.Trimble/Models/Responses/GeocodeCoords.cs
new file mode 100644
index 0000000..65c2515
--- /dev/null
+++ b/src/Geo.Trimble/Models/Responses/GeocodeCoords.cs
@@ -0,0 +1,27 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.Trimble.Models.Responses
+{
+ using System.Text.Json.Serialization;
+
+ ///
+ /// The coordinate portion of a Trimble Maps geocoding response.
+ ///
+ public class GeocodeCoords
+ {
+ ///
+ /// Gets or sets the latitude as a string.
+ ///
+ [JsonPropertyName("Lat")]
+ public string Lat { get; set; }
+
+ ///
+ /// Gets or sets the longitude as a string.
+ ///
+ [JsonPropertyName("Lon")]
+ public string Lon { get; set; }
+ }
+}
diff --git a/src/Geo.Trimble/Models/Responses/GeocodeError.cs b/src/Geo.Trimble/Models/Responses/GeocodeError.cs
new file mode 100644
index 0000000..a091495
--- /dev/null
+++ b/src/Geo.Trimble/Models/Responses/GeocodeError.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.Trimble.Models.Responses
+{
+ using System.Text.Json.Serialization;
+
+ ///
+ /// An error or warning returned in a Trimble Maps geocoding response.
+ ///
+ public class GeocodeError
+ {
+ ///
+ /// Gets or sets the error type (Warning or Exception).
+ ///
+ [JsonPropertyName("Type")]
+ public string Type { get; set; }
+
+ ///
+ /// Gets or sets the error code.
+ ///
+ [JsonPropertyName("Code")]
+ public string Code { get; set; }
+
+ ///
+ /// Gets or sets the legacy numeric error code.
+ ///
+ [JsonPropertyName("LegacyErrorCode")]
+ public int? LegacyErrorCode { get; set; }
+
+ ///
+ /// Gets or sets a human-readable description of the error.
+ ///
+ [JsonPropertyName("Description")]
+ public string Description { get; set; }
+ }
+}
diff --git a/src/Geo.Trimble/Models/Responses/GeocodeResponse.cs b/src/Geo.Trimble/Models/Responses/GeocodeResponse.cs
new file mode 100644
index 0000000..2f45bcc
--- /dev/null
+++ b/src/Geo.Trimble/Models/Responses/GeocodeResponse.cs
@@ -0,0 +1,88 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.Trimble.Models.Responses
+{
+ using System.Collections.Generic;
+ using System.Text.Json.Serialization;
+
+ ///
+ /// A geocoding result returned by the Trimble Maps API.
+ ///
+ public class GeocodeResponse
+ {
+ ///
+ /// Gets or sets the matched address.
+ ///
+ [JsonPropertyName("Address")]
+ public GeocodeAddress Address { get; set; }
+
+ ///
+ /// Gets or sets the coordinates of the matched location.
+ ///
+ [JsonPropertyName("Coords")]
+ public GeocodeCoords Coords { get; set; }
+
+ ///
+ /// Gets or sets the formatted label for the location.
+ ///
+ [JsonPropertyName("Label")]
+ public string Label { get; set; }
+
+ ///
+ /// Gets or sets the place name, which may include a Trimble place ID.
+ ///
+ [JsonPropertyName("PlaceName")]
+ public string PlaceName { get; set; }
+
+ ///
+ /// Gets or sets the numeric geographic region code.
+ ///
+ [JsonPropertyName("Region")]
+ public int? Region { get; set; }
+
+ ///
+ /// Gets or sets the time zone name.
+ ///
+ [JsonPropertyName("TimeZone")]
+ public string TimeZone { get; set; }
+
+ ///
+ /// Gets or sets the UTC offset string (e.g. "GMT-5:00").
+ ///
+ [JsonPropertyName("TimeZoneOffset")]
+ public string TimeZoneOffset { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether daylight saving time is in effect.
+ ///
+ [JsonPropertyName("isDST")]
+ public bool? IsDst { get; set; }
+
+ ///
+ /// Gets or sets the confidence level of the match (Exact, Good, Uncertain, or Failed).
+ ///
+ [JsonPropertyName("ConfidenceLevel")]
+ public string ConfidenceLevel { get; set; }
+
+ ///
+ /// Gets or sets the distance in miles from the input coordinates to the nearest road.
+ ///
+ [JsonPropertyName("DistanceFromRoad")]
+ public double? DistanceFromRoad { get; set; }
+
+ ///
+ /// Gets or sets the speed limit information for the nearest road.
+ ///
+ [JsonPropertyName("SpeedLimitInfo")]
+ public SpeedLimitInfo SpeedLimitInfo { get; set; }
+
+ ///
+ /// Gets the errors or warnings associated with this result.
+ ///
+ [JsonPropertyName("Errors")]
+ public IList Errors { get; } = new List();
+ }
+}
diff --git a/src/Geo.Trimble/Models/Responses/SpeedLimitInfo.cs b/src/Geo.Trimble/Models/Responses/SpeedLimitInfo.cs
new file mode 100644
index 0000000..1ce8e34
--- /dev/null
+++ b/src/Geo.Trimble/Models/Responses/SpeedLimitInfo.cs
@@ -0,0 +1,45 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.Trimble.Models.Responses
+{
+ using System.Text.Json.Serialization;
+
+ ///
+ /// Speed limit information returned in a Trimble Maps reverse geocoding response.
+ ///
+ public class SpeedLimitInfo
+ {
+ ///
+ /// Gets or sets the speed limit value.
+ ///
+ [JsonPropertyName("Speed")]
+ public int? Speed { get; set; }
+
+ ///
+ /// Gets or sets the speed limit type classification.
+ ///
+ [JsonPropertyName("SpeedType")]
+ public int? SpeedType { get; set; }
+
+ ///
+ /// Gets or sets the road link identifiers.
+ ///
+ [JsonPropertyName("LinkIds")]
+ public long? LinkIds { get; set; }
+
+ ///
+ /// Gets or sets the road class designation.
+ ///
+ [JsonPropertyName("RoadClass")]
+ public string RoadClass { get; set; }
+
+ ///
+ /// Gets or sets the measurement units (KPH or MPH).
+ ///
+ [JsonPropertyName("Units")]
+ public string Units { get; set; }
+ }
+}
diff --git a/src/Geo.Trimble/Properties/AssemblyInfo.cs b/src/Geo.Trimble/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..b97f9f9
--- /dev/null
+++ b/src/Geo.Trimble/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.Trimble.Tests")]
+[assembly: ResourceLocation("Resources")]
diff --git a/src/Geo.Trimble/README.md b/src/Geo.Trimble/README.md
new file mode 100644
index 0000000..6b253f4
--- /dev/null
+++ b/src/Geo.Trimble/README.md
@@ -0,0 +1,39 @@
+# Trimble Maps Geocoding
+
+This allows the simple calling of Trimble Maps geocoding APIs. The supported Trimble Maps geocoding endpoints are:
+- [Geocoding](https://pcmiler.alk.com/apis/rest/v1.0/service.svc/locations)
+- [Reverse Geocoding](https://pcmiler.alk.com/apis/rest/v1.0/service.svc/locations/reverse)
+
+## Configuration
+
+In the startup `ConfigureServices` method, add the configuration for the Trimble Maps service:
+```
+using Geo.Extensions.DependencyInjection;
+.
+.
+.
+public void ConfigureServices(IServiceCollection services)
+{
+ .
+ .
+ .
+ var builder = services.AddTrimbleGeocoding();
+ builder.AddKey(your_Trimble_Maps_api_key_here);
+ builder.HttpClientBuilder.ConfigureHttpClient(configure_client);
+ .
+ .
+ .
+}
+```
+
+## Sample Usage
+
+By calling `AddTrimbleGeocoding`, the `ITrimbleGeocoding` interface has been added to the IOC container. Just request it as a DI item:
+```
+public MyService(ITrimbleGeocoding trimbleGeocoding)
+{
+ ...
+}
+```
+
+Now simply call the geocoding methods in the interface.
diff --git a/src/Geo.Trimble/Resources/Services/TrimbleGeocoding.Designer.cs b/src/Geo.Trimble/Resources/Services/TrimbleGeocoding.Designer.cs
new file mode 100644
index 0000000..532ee4f
--- /dev/null
+++ b/src/Geo.Trimble/Resources/Services/TrimbleGeocoding.Designer.cs
@@ -0,0 +1,99 @@
+//------------------------------------------------------------------------------
+//
+// 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.Trimble.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 TrimbleGeocoding {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal TrimbleGeocoding() {
+ }
+
+ ///
+ /// 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.Trimble.Resources.Services.TrimbleGeocoding", typeof(TrimbleGeocoding).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 Trimble Maps 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 field must be provided..
+ ///
+ internal static string Invalid_Address {
+ get {
+ return ResourceManager.GetString("Invalid Address", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The coordinate cannot be null or invalid..
+ ///
+ internal static string Invalid_Coordinate {
+ get {
+ return ResourceManager.GetString("Invalid Coordinate", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The Trimble Maps parameters are null..
+ ///
+ internal static string Null_Parameters {
+ get {
+ return ResourceManager.GetString("Null Parameters", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Geo.Trimble/Resources/Services/TrimbleGeocoding.resx b/src/Geo.Trimble/Resources/Services/TrimbleGeocoding.resx
new file mode 100644
index 0000000..ff1cd57
--- /dev/null
+++ b/src/Geo.Trimble/Resources/Services/TrimbleGeocoding.resx
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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 Trimble Maps uri.
+
+
+ At least one address field must be provided.
+
+
+ The coordinate cannot be null or invalid.
+
+
+ The Trimble Maps parameters are null.
+
+
diff --git a/src/Geo.Trimble/Services/TrimbleGeocoding.cs b/src/Geo.Trimble/Services/TrimbleGeocoding.cs
new file mode 100644
index 0000000..4b726a0
--- /dev/null
+++ b/src/Geo.Trimble/Services/TrimbleGeocoding.cs
@@ -0,0 +1,292 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.Trimble.Services
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Globalization;
+ 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.Trimble.Models.Enums;
+ using Geo.Trimble.Models.Parameters;
+ using Geo.Trimble.Models.Responses;
+ using Microsoft.Extensions.Logging;
+ using Microsoft.Extensions.Logging.Abstractions;
+ using Microsoft.Extensions.Options;
+
+ ///
+ /// A service to call the Trimble Maps geocoding API.
+ ///
+ public class TrimbleGeocoding : GeoClient, ITrimbleGeocoding
+ {
+ private const string GeocodeUri = "https://pcmiler.alk.com/apis/rest/v1.0/service.svc/locations";
+ private const string ReverseGeocodeUri = "https://pcmiler.alk.com/apis/rest/v1.0/service.svc/locations/reverse";
+
+ private readonly IOptions> _options;
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A used for placing calls to the Trimble Maps Geocoding API.
+ /// An of containing Trimble Maps key information.
+ /// An used to create a logger used for logging information.
+ public TrimbleGeocoding(
+ 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 => "Trimble";
+
+ ///
+ public async Task> GeocodingAsync(
+ GeocodingParameters parameters,
+ CancellationToken cancellationToken = default)
+ {
+ var uri = ValidateAndBuildUri(parameters, BuildGeocodingRequest);
+
+ return await GetAsync>(uri, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public async Task ReverseGeocodingAsync(
+ ReverseGeocodingParameters parameters,
+ CancellationToken cancellationToken = default)
+ {
+ var uri = ValidateAndBuildUri(parameters, BuildReverseGeocodingRequest);
+
+ 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.TrimbleError(Resources.Services.TrimbleGeocoding.Null_Parameters);
+ throw new GeoNETException(Resources.Services.TrimbleGeocoding.Null_Parameters, new ArgumentNullException(nameof(parameters)));
+ }
+
+ try
+ {
+ return uriBuilderFunction(parameters);
+ }
+ catch (ArgumentException ex)
+ {
+ _logger.TrimbleError(Resources.Services.TrimbleGeocoding.Failed_To_Create_Uri);
+ throw new GeoNETException(Resources.Services.TrimbleGeocoding.Failed_To_Create_Uri, ex);
+ }
+ }
+
+ ///
+ /// Builds the geocoding uri based on the passed parameters.
+ ///
+ /// A with the geocoding parameters to build the uri with.
+ /// A with the completed Trimble Maps geocoding uri.
+ /// Thrown when no address fields are provided.
+ internal Uri BuildGeocodingRequest(GeocodingParameters parameters)
+ {
+ var uriBuilder = new UriBuilder(GeocodeUri);
+ var query = QueryString.Empty;
+
+ if (string.IsNullOrWhiteSpace(parameters.Street) &&
+ string.IsNullOrWhiteSpace(parameters.City) &&
+ string.IsNullOrWhiteSpace(parameters.State) &&
+ string.IsNullOrWhiteSpace(parameters.Zip) &&
+ string.IsNullOrWhiteSpace(parameters.County) &&
+ string.IsNullOrWhiteSpace(parameters.Country))
+ {
+ _logger.TrimbleError(Resources.Services.TrimbleGeocoding.Invalid_Address);
+ throw new ArgumentException(Resources.Services.TrimbleGeocoding.Invalid_Address, nameof(parameters));
+ }
+
+ AddAddressParameters(parameters, ref query);
+ AddTrimbleKey(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 Trimble Maps reverse geocoding uri.
+ /// Thrown when the parameter is null.
+ internal Uri BuildReverseGeocodingRequest(ReverseGeocodingParameters parameters)
+ {
+ var uriBuilder = new UriBuilder(ReverseGeocodeUri);
+ var query = QueryString.Empty;
+
+ if (parameters.Coordinate is null)
+ {
+ _logger.TrimbleError(Resources.Services.TrimbleGeocoding.Invalid_Coordinate);
+ throw new ArgumentException(Resources.Services.TrimbleGeocoding.Invalid_Coordinate, nameof(parameters.Coordinate));
+ }
+
+ query = query.Add("Coords", parameters.Coordinate.ToString());
+
+ AddReverseParameters(parameters, ref query);
+ AddTrimbleKey(parameters, ref query);
+ query = query.AddAdditionalParameters(parameters);
+
+ uriBuilder.AddQuery(query);
+
+ return uriBuilder.Uri;
+ }
+
+ ///
+ /// Adds the structured address fields to the query string.
+ ///
+ /// The to read address fields from.
+ /// A with the query parameters.
+ internal void AddAddressParameters(GeocodingParameters parameters, ref QueryString query)
+ {
+ if (!string.IsNullOrWhiteSpace(parameters.Street))
+ {
+ query = query.Add("Street", parameters.Street);
+ }
+
+ if (!string.IsNullOrWhiteSpace(parameters.City))
+ {
+ query = query.Add("City", parameters.City);
+ }
+
+ if (!string.IsNullOrWhiteSpace(parameters.State))
+ {
+ query = query.Add("State", parameters.State);
+ }
+
+ if (!string.IsNullOrWhiteSpace(parameters.Zip))
+ {
+ query = query.Add("Zip", parameters.Zip);
+ }
+
+ if (!string.IsNullOrWhiteSpace(parameters.County))
+ {
+ query = query.Add("County", parameters.County);
+ }
+
+ if (!string.IsNullOrWhiteSpace(parameters.Country))
+ {
+ query = query.Add("Country", parameters.Country);
+ }
+
+ query = query.Add("Region", ((int)parameters.Region).ToString(CultureInfo.InvariantCulture));
+
+ if (!string.IsNullOrWhiteSpace(parameters.Dataset))
+ {
+ query = query.Add("Dataset", parameters.Dataset);
+ }
+
+ if (parameters.MaxResults.HasValue && parameters.MaxResults.Value > 0)
+ {
+ query = query.Add("MaxResults", parameters.MaxResults.Value.ToString(CultureInfo.InvariantCulture));
+ }
+
+ if (parameters.MatchNamedRoadsOnly)
+ {
+ query = query.Add("MatchNamedRoadsOnly", "true");
+ }
+ }
+
+ ///
+ /// Adds the optional reverse geocoding parameters to the query string.
+ ///
+ /// The to read optional fields from.
+ /// A with the query parameters.
+ internal void AddReverseParameters(ReverseGeocodingParameters parameters, ref QueryString query)
+ {
+ query = query.Add("Region", ((int)parameters.Region).ToString(CultureInfo.InvariantCulture));
+
+ if (!string.IsNullOrWhiteSpace(parameters.Dataset))
+ {
+ query = query.Add("Dataset", parameters.Dataset);
+ }
+
+ if (!string.IsNullOrWhiteSpace(parameters.Lang))
+ {
+ query = query.Add("lang", parameters.Lang);
+ }
+
+ if (parameters.MatchNamedRoadsOnly)
+ {
+ query = query.Add("matchNamedRoadsOnly", "true");
+ }
+
+ if (parameters.MaxCleanupMiles.HasValue)
+ {
+ query = query.Add("maxCleanupMiles", parameters.MaxCleanupMiles.Value.ToString(CultureInfo.InvariantCulture));
+ }
+
+ if (parameters.IncludePostedSpeedLimit)
+ {
+ query = query.Add("includePostedSpeedLimit", "true");
+ }
+
+ if (!string.IsNullOrWhiteSpace(parameters.VehicleType))
+ {
+ query = query.Add("vehicleType", parameters.VehicleType);
+ }
+
+ if (parameters.Heading.HasValue)
+ {
+ query = query.Add("heading", parameters.Heading.Value.ToString(CultureInfo.InvariantCulture));
+ }
+
+ if (parameters.IncludeLinkInfo)
+ {
+ query = query.Add("includeLinkInfo", "true");
+ }
+
+ if (parameters.CountryAbbrevType.HasValue)
+ {
+ query = query.Add("countryAbbrevType", parameters.CountryAbbrevType.Value.ToString());
+ }
+
+ if (parameters.IncludeTrimblePlaceIds)
+ {
+ query = query.Add("includeTrimblePlaceIds", "true");
+ }
+ }
+
+ ///
+ /// Adds the Trimble Maps API key to the request as the authToken query parameter.
+ ///
+ /// An to conditionally get the key from.
+ /// A with the query parameters.
+ internal void AddTrimbleKey(IKeyParameters keyParameter, ref QueryString query)
+ {
+ var key = _options.Value.Key;
+
+ if (!string.IsNullOrWhiteSpace(keyParameter.Key))
+ {
+ key = keyParameter.Key;
+ }
+
+ query = query.Add("authToken", key);
+ }
+ }
+}
diff --git a/test/Geo.Trimble.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/test/Geo.Trimble.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs
new file mode 100644
index 0000000..8937849
--- /dev/null
+++ b/test/Geo.Trimble.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs
@@ -0,0 +1,88 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.Trimble.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 AddTrimbleGeocoding_WithValidCall_ConfiguresAllServices()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+
+ // Act
+ var builder = services.AddTrimbleGeocoding();
+ 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 AddTrimbleGeocoding_WithNullOptions_ConfiguresAllServices()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+
+ // Act
+ services.AddTrimbleGeocoding();
+
+ // 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 AddTrimbleGeocoding_WithClientConfiguration_ConfiguresHttpClientAllServices()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+
+ // Act
+ var builder = services.AddTrimbleGeocoding();
+ builder.AddKey("abc");
+ builder.HttpClientBuilder.ConfigureHttpClient(httpClient => httpClient.Timeout = TimeSpan.FromSeconds(5));
+
+ // Assert
+ var provider = services.BuildServiceProvider();
+ var client = provider.GetRequiredService().CreateClient("ITrimbleGeocoding");
+ client.Timeout.Should().Be(TimeSpan.FromSeconds(5));
+ }
+
+ [Fact]
+ public void AddTrimbleGeocoding_WithNullServices_ThrowsArgumentNullException()
+ {
+ // Arrange
+ IServiceCollection services = null;
+
+ // Act
+ Action act = () => services.AddTrimbleGeocoding();
+
+ // Assert
+ act.Should().Throw();
+ }
+ }
+}
diff --git a/test/Geo.Trimble.Tests/Geo.Trimble.Tests.csproj b/test/Geo.Trimble.Tests/Geo.Trimble.Tests.csproj
new file mode 100644
index 0000000..8359b2d
--- /dev/null
+++ b/test/Geo.Trimble.Tests/Geo.Trimble.Tests.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net48;netcoreapp3.1;net6.0;net8.0;net10.0
+
+ false
+
+
+
+
+
+
+
+
+
diff --git a/test/Geo.Trimble.Tests/Services/TrimbleGeocodingShould.cs b/test/Geo.Trimble.Tests/Services/TrimbleGeocodingShould.cs
new file mode 100644
index 0000000..e2099d5
--- /dev/null
+++ b/test/Geo.Trimble.Tests/Services/TrimbleGeocodingShould.cs
@@ -0,0 +1,315 @@
+//
+// Copyright (c) Geo.NET.
+// Licensed under the MIT license. See the LICENSE file in the solution root for full license information.
+//
+
+namespace Geo.Trimble.Tests.Services
+{
+ using System;
+ using System.Globalization;
+ using System.Net.Http;
+ using System.Threading;
+ using System.Web;
+ using FluentAssertions;
+ using Geo.Core;
+ using Geo.Core.Models.Exceptions;
+ using Geo.Trimble.Models.Enums;
+ using Geo.Trimble.Models.Parameters;
+ using Geo.Trimble.Services;
+ using Microsoft.Extensions.Options;
+ using Moq;
+ using Xunit;
+
+ ///
+ /// Unit tests for the class.
+ ///
+ public class TrimbleGeocodingShould : IDisposable
+ {
+ private readonly HttpClient _httpClient;
+ private readonly Mock>> _options = new Mock>>();
+ private bool _disposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TrimbleGeocodingShould()
+ {
+ _options
+ .Setup(x => x.Value)
+ .Returns(new KeyOptions()
+ {
+ Key = "abc123",
+ });
+
+ _httpClient = new HttpClient(new Mock().Object);
+ }
+
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ [Fact]
+ public void AddTrimbleKey_WithOptions_SuccessfullyAddsKey()
+ {
+ var sut = BuildService();
+
+ var query = QueryString.Empty;
+
+ sut.AddTrimbleKey(new GeocodingParameters(), ref query);
+
+ var queryParameters = HttpUtility.ParseQueryString(query.ToString());
+ queryParameters.Count.Should().Be(1);
+ queryParameters["authToken"].Should().Be("abc123");
+ }
+
+ [Fact]
+ public void AddTrimbleKey_WithParameterOverride_SuccessfullyAddsKey()
+ {
+ var sut = BuildService();
+
+ var query = QueryString.Empty;
+
+ sut.AddTrimbleKey(new GeocodingParameters() { Key = "override123" }, ref query);
+
+ var queryParameters = HttpUtility.ParseQueryString(query.ToString());
+ queryParameters.Count.Should().Be(1);
+ queryParameters["authToken"].Should().Be("override123");
+ }
+
+ [Fact]
+ public void AddAddressParameters_WithAllFields_AddsAllParameters()
+ {
+ var sut = BuildService();
+
+ var query = QueryString.Empty;
+ var parameters = new GeocodingParameters()
+ {
+ Street = "100 Main St",
+ City = "Springfield",
+ State = "IL",
+ Zip = "62701",
+ County = "Sangamon",
+ Country = "US",
+ Region = Region.NorthAmerica,
+ Dataset = "Current",
+ MaxResults = 5,
+ MatchNamedRoadsOnly = true,
+ };
+
+ sut.AddAddressParameters(parameters, ref query);
+
+ var queryParameters = HttpUtility.ParseQueryString(query.ToString());
+ queryParameters["Street"].Should().Be("100 Main St");
+ queryParameters["City"].Should().Be("Springfield");
+ queryParameters["State"].Should().Be("IL");
+ queryParameters["Zip"].Should().Be("62701");
+ queryParameters["County"].Should().Be("Sangamon");
+ queryParameters["Country"].Should().Be("US");
+ queryParameters["Region"].Should().Be("0");
+ queryParameters["Dataset"].Should().Be("Current");
+ queryParameters["MaxResults"].Should().Be("5");
+ queryParameters["MatchNamedRoadsOnly"].Should().Be("true");
+ }
+
+ [Fact]
+ public void AddAddressParameters_WithMinimalFields_OnlyAddsPresentFields()
+ {
+ var sut = BuildService();
+
+ var query = QueryString.Empty;
+ var parameters = new GeocodingParameters()
+ {
+ City = "Chicago",
+ State = "IL",
+ };
+
+ sut.AddAddressParameters(parameters, ref query);
+
+ var queryParameters = HttpUtility.ParseQueryString(query.ToString());
+ queryParameters["City"].Should().Be("Chicago");
+ queryParameters["State"].Should().Be("IL");
+ queryParameters["Street"].Should().BeNull();
+ queryParameters["Zip"].Should().BeNull();
+ }
+
+ [Theory]
+ [ClassData(typeof(CultureTestData))]
+ public void BuildGeocodingRequest_WithValidParameters_SuccessfullyBuildsUrl(CultureInfo culture)
+ {
+ // Arrange
+ var oldCulture = Thread.CurrentThread.CurrentCulture;
+ Thread.CurrentThread.CurrentCulture = culture;
+
+ var sut = BuildService();
+
+ var parameters = new GeocodingParameters()
+ {
+ Street = "100 Main St",
+ City = "Springfield",
+ State = "IL",
+ Key = "abc123",
+ };
+
+ // Act
+ var uri = sut.BuildGeocodingRequest(parameters);
+
+ // Assert
+ var query = HttpUtility.UrlDecode(uri.PathAndQuery);
+ query.Should().Contain("Street=100 Main St");
+ query.Should().Contain("City=Springfield");
+ query.Should().Contain("State=IL");
+ query.Should().Contain("authToken=abc123");
+
+ Thread.CurrentThread.CurrentCulture = oldCulture;
+ }
+
+ [Fact]
+ public void BuildGeocodingRequest_WithNoAddressFields_ThrowsArgumentException()
+ {
+ var sut = BuildService();
+
+ Action act = () => sut.BuildGeocodingRequest(new GeocodingParameters());
+
+ act.Should()
+ .Throw();
+ }
+
+ [Fact]
+ public void BuildGeocodingRequest_WithAdditionalParameters_AddsThemToQueryString()
+ {
+ var sut = BuildService();
+
+ var parameters = new GeocodingParameters()
+ {
+ City = "Chicago",
+ };
+
+ parameters.AdditionalParameters.Add("customKey1", "customValue1");
+ parameters.AdditionalParameters.Add("customKey2", "customValue2");
+
+ var uri = sut.BuildGeocodingRequest(parameters);
+ var query = HttpUtility.UrlDecode(uri.PathAndQuery);
+ query.Should().Contain("customKey1=customValue1");
+ query.Should().Contain("customKey2=customValue2");
+ }
+
+ [Theory]
+ [ClassData(typeof(CultureTestData))]
+ public void BuildReverseGeocodingRequest_WithValidParameters_SuccessfullyBuildsUrl(CultureInfo culture)
+ {
+ // Arrange
+ var oldCulture = Thread.CurrentThread.CurrentCulture;
+ Thread.CurrentThread.CurrentCulture = culture;
+
+ var sut = BuildService();
+
+ var parameters = new ReverseGeocodingParameters()
+ {
+ Coordinate = new Coordinate()
+ {
+ Latitude = 40.7128,
+ Longitude = -74.0060,
+ },
+ Key = "abc123",
+ };
+
+ // Act
+ var uri = sut.BuildReverseGeocodingRequest(parameters);
+
+ // Assert
+ var query = HttpUtility.UrlDecode(uri.PathAndQuery);
+ query.Should().Contain("Coords=-74.006,40.7128");
+ query.Should().Contain("authToken=abc123");
+
+ Thread.CurrentThread.CurrentCulture = oldCulture;
+ }
+
+ [Fact]
+ public void BuildReverseGeocodingRequest_WithNullCoordinate_ThrowsArgumentException()
+ {
+ var sut = BuildService();
+
+ Action act = () => sut.BuildReverseGeocodingRequest(new ReverseGeocodingParameters());
+
+ act.Should()
+ .Throw()
+#if NETCOREAPP3_1_OR_GREATER
+ .WithMessage("*(Parameter 'Coordinate')");
+#else
+ .WithMessage("*Parameter name: Coordinate");
+#endif
+ }
+
+ [Fact]
+ public void BuildReverseGeocodingRequest_WithAllOptionalParameters_SuccessfullyBuildsUrl()
+ {
+ var sut = BuildService();
+
+ var parameters = new ReverseGeocodingParameters()
+ {
+ Coordinate = new Coordinate() { Latitude = 40.7128, Longitude = -74.0060 },
+ Region = Region.Europe,
+ Dataset = "Current",
+ Lang = "en",
+ MatchNamedRoadsOnly = true,
+ MaxCleanupMiles = 0.5,
+ IncludePostedSpeedLimit = true,
+ VehicleType = "Truck",
+ Heading = 90.0,
+ IncludeLinkInfo = true,
+ CountryAbbrevType = CountryAbbrevType.ISO2,
+ IncludeTrimblePlaceIds = true,
+ };
+
+ var uri = sut.BuildReverseGeocodingRequest(parameters);
+ var query = HttpUtility.UrlDecode(uri.PathAndQuery);
+
+ query.Should().Contain("Region=3");
+ query.Should().Contain("Dataset=Current");
+ query.Should().Contain("lang=en");
+ query.Should().Contain("matchNamedRoadsOnly=true");
+ query.Should().Contain("maxCleanupMiles=0.5");
+ query.Should().Contain("includePostedSpeedLimit=true");
+ query.Should().Contain("vehicleType=Truck");
+ query.Should().Contain("heading=90");
+ query.Should().Contain("includeLinkInfo=true");
+ query.Should().Contain("countryAbbrevType=ISO2");
+ query.Should().Contain("includeTrimblePlaceIds=true");
+ }
+
+ [Fact]
+ public void ValidateAndBuildUri_WithNullParameters_ThrowsGeoNETException()
+ {
+ var sut = BuildService();
+
+ Action act = () => sut.ValidateAndBuildUri(null, p => new Uri("https://example.com"));
+
+ act.Should().Throw();
+ }
+
+ ///
+ /// Releases resources.
+ ///
+ /// A flag indicating whether managed resources should be disposed.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ _httpClient?.Dispose();
+ }
+
+ _disposed = true;
+ }
+ }
+
+ private TrimbleGeocoding BuildService()
+ {
+ return new TrimbleGeocoding(_httpClient, _options.Object);
+ }
+ }
+}
diff --git a/test/Geo.Trimble.Tests/TestData/CultureTestData.cs b/test/Geo.Trimble.Tests/TestData/CultureTestData.cs
new file mode 100644
index 0000000..35c6724
--- /dev/null
+++ b/test/Geo.Trimble.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.Trimble.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