From 4b6297ab7d2de1574cab798a869cee6a90676348 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 16 Jun 2026 19:28:43 -0400 Subject: [PATCH 1/3] feat(openrouteservice): add geocoding support for OpenRouteService API --- Geo.NET.sln | 178 +++++ .../IOpenRouteServiceGeocoding.cs | 55 ++ .../Converters/CoordinateConverter.cs | 98 +++ .../ServiceCollectionExtensions.cs | 44 ++ src/Geo.OpenRouteService/Enums/LayerType.cs | 99 +++ src/Geo.OpenRouteService/Enums/SourceType.cs | 39 + .../Extensions/LoggerExtensions.cs | 62 ++ .../Geo.OpenRouteService.csproj | 45 ++ src/Geo.OpenRouteService/Models/Coordinate.cs | 31 + .../Parameters/AutocompleteParameters.cs | 18 + .../Models/Parameters/BaseSearchParameters.cs | 67 ++ .../Models/Parameters/BoundingBox.cs | 33 + .../Models/Parameters/BoundingCircle.cs | 29 + .../Parameters/ReverseSearchParameters.cs | 62 ++ .../Models/Parameters/SearchParameters.cs | 18 + .../Parameters/StructuredSearchParameters.cs | 54 ++ .../Models/Responses/Feature.cs | 40 + .../Models/Responses/FeatureCollection.cs | 40 + .../Models/Responses/FeatureProperties.cs | 152 ++++ .../Models/Responses/GeocodingMetadata.cs | 47 ++ .../Models/Responses/Geometry.cs | 31 + .../Properties/AssemblyInfo.cs | 10 + src/Geo.OpenRouteService/README.md | 42 ++ .../OpenRouteServiceGeocoding.Designer.cs | 180 +++++ .../Services/OpenRouteServiceGeocoding.resx | 159 ++++ .../Services/OpenRouteServiceGeocoding.cs | 428 +++++++++++ .../ServiceCollectionExtensionsTests.cs | 75 ++ .../Geo.OpenRouteService.Tests.csproj | 14 + .../GlobalSuppressions.cs | 13 + .../OpenRouteServiceGeocodingShould.cs | 698 ++++++++++++++++++ .../TestData/CultureTestData.cs | 40 + 31 files changed, 2901 insertions(+) create mode 100644 src/Geo.OpenRouteService/Abstractions/IOpenRouteServiceGeocoding.cs create mode 100644 src/Geo.OpenRouteService/Converters/CoordinateConverter.cs create mode 100644 src/Geo.OpenRouteService/DependencyInjection/ServiceCollectionExtensions.cs create mode 100644 src/Geo.OpenRouteService/Enums/LayerType.cs create mode 100644 src/Geo.OpenRouteService/Enums/SourceType.cs create mode 100644 src/Geo.OpenRouteService/Extensions/LoggerExtensions.cs create mode 100644 src/Geo.OpenRouteService/Geo.OpenRouteService.csproj create mode 100644 src/Geo.OpenRouteService/Models/Coordinate.cs create mode 100644 src/Geo.OpenRouteService/Models/Parameters/AutocompleteParameters.cs create mode 100644 src/Geo.OpenRouteService/Models/Parameters/BaseSearchParameters.cs create mode 100644 src/Geo.OpenRouteService/Models/Parameters/BoundingBox.cs create mode 100644 src/Geo.OpenRouteService/Models/Parameters/BoundingCircle.cs create mode 100644 src/Geo.OpenRouteService/Models/Parameters/ReverseSearchParameters.cs create mode 100644 src/Geo.OpenRouteService/Models/Parameters/SearchParameters.cs create mode 100644 src/Geo.OpenRouteService/Models/Parameters/StructuredSearchParameters.cs create mode 100644 src/Geo.OpenRouteService/Models/Responses/Feature.cs create mode 100644 src/Geo.OpenRouteService/Models/Responses/FeatureCollection.cs create mode 100644 src/Geo.OpenRouteService/Models/Responses/FeatureProperties.cs create mode 100644 src/Geo.OpenRouteService/Models/Responses/GeocodingMetadata.cs create mode 100644 src/Geo.OpenRouteService/Models/Responses/Geometry.cs create mode 100644 src/Geo.OpenRouteService/Properties/AssemblyInfo.cs create mode 100644 src/Geo.OpenRouteService/README.md create mode 100644 src/Geo.OpenRouteService/Resources/Services/OpenRouteServiceGeocoding.Designer.cs create mode 100644 src/Geo.OpenRouteService/Resources/Services/OpenRouteServiceGeocoding.resx create mode 100644 src/Geo.OpenRouteService/Services/OpenRouteServiceGeocoding.cs create mode 100644 test/Geo.OpenRouteService.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs create mode 100644 test/Geo.OpenRouteService.Tests/Geo.OpenRouteService.Tests.csproj create mode 100644 test/Geo.OpenRouteService.Tests/GlobalSuppressions.cs create mode 100644 test/Geo.OpenRouteService.Tests/Services/OpenRouteServiceGeocodingShould.cs create mode 100644 test/Geo.OpenRouteService.Tests/TestData/CultureTestData.cs diff --git a/Geo.NET.sln b/Geo.NET.sln index be941ab..8228acb 100644 --- a/Geo.NET.sln +++ b/Geo.NET.sln @@ -53,84 +53,260 @@ 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Geo.OpenRouteService", "src\Geo.OpenRouteService\Geo.OpenRouteService.csproj", "{4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Geo.OpenRouteService.Tests", "test\Geo.OpenRouteService.Tests\Geo.OpenRouteService.Tests.csproj", "{255390E0-3B2B-481A-938E-82F359DC1D45}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Debug|x64.ActiveCfg = Debug|Any CPU + {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Debug|x64.Build.0 = Debug|Any CPU + {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Debug|x86.ActiveCfg = Debug|Any CPU + {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Debug|x86.Build.0 = Debug|Any CPU {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Release|Any CPU.ActiveCfg = Release|Any CPU {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Release|Any CPU.Build.0 = Release|Any CPU + {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Release|x64.ActiveCfg = Release|Any CPU + {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Release|x64.Build.0 = Release|Any CPU + {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Release|x86.ActiveCfg = Release|Any CPU + {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Release|x86.Build.0 = Release|Any CPU {CCC422FD-3B45-457B-87A7-461D50E91334}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CCC422FD-3B45-457B-87A7-461D50E91334}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CCC422FD-3B45-457B-87A7-461D50E91334}.Debug|x64.ActiveCfg = Debug|Any CPU + {CCC422FD-3B45-457B-87A7-461D50E91334}.Debug|x64.Build.0 = Debug|Any CPU + {CCC422FD-3B45-457B-87A7-461D50E91334}.Debug|x86.ActiveCfg = Debug|Any CPU + {CCC422FD-3B45-457B-87A7-461D50E91334}.Debug|x86.Build.0 = Debug|Any CPU {CCC422FD-3B45-457B-87A7-461D50E91334}.Release|Any CPU.ActiveCfg = Release|Any CPU {CCC422FD-3B45-457B-87A7-461D50E91334}.Release|Any CPU.Build.0 = Release|Any CPU + {CCC422FD-3B45-457B-87A7-461D50E91334}.Release|x64.ActiveCfg = Release|Any CPU + {CCC422FD-3B45-457B-87A7-461D50E91334}.Release|x64.Build.0 = Release|Any CPU + {CCC422FD-3B45-457B-87A7-461D50E91334}.Release|x86.ActiveCfg = Release|Any CPU + {CCC422FD-3B45-457B-87A7-461D50E91334}.Release|x86.Build.0 = Release|Any CPU {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Debug|x64.ActiveCfg = Debug|Any CPU + {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Debug|x64.Build.0 = Debug|Any CPU + {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Debug|x86.ActiveCfg = Debug|Any CPU + {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Debug|x86.Build.0 = Debug|Any CPU {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Release|Any CPU.ActiveCfg = Release|Any CPU {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Release|Any CPU.Build.0 = Release|Any CPU + {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Release|x64.ActiveCfg = Release|Any CPU + {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Release|x64.Build.0 = Release|Any CPU + {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Release|x86.ActiveCfg = Release|Any CPU + {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Release|x86.Build.0 = Release|Any CPU {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Debug|x64.ActiveCfg = Debug|Any CPU + {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Debug|x64.Build.0 = Debug|Any CPU + {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Debug|x86.ActiveCfg = Debug|Any CPU + {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Debug|x86.Build.0 = Debug|Any CPU {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Release|Any CPU.ActiveCfg = Release|Any CPU {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Release|Any CPU.Build.0 = Release|Any CPU + {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Release|x64.ActiveCfg = Release|Any CPU + {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Release|x64.Build.0 = Release|Any CPU + {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Release|x86.ActiveCfg = Release|Any CPU + {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Release|x86.Build.0 = Release|Any CPU {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Debug|x64.ActiveCfg = Debug|Any CPU + {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Debug|x64.Build.0 = Debug|Any CPU + {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Debug|x86.ActiveCfg = Debug|Any CPU + {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Debug|x86.Build.0 = Debug|Any CPU {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Release|Any CPU.ActiveCfg = Release|Any CPU {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Release|Any CPU.Build.0 = Release|Any CPU + {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Release|x64.ActiveCfg = Release|Any CPU + {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Release|x64.Build.0 = Release|Any CPU + {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Release|x86.ActiveCfg = Release|Any CPU + {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Release|x86.Build.0 = Release|Any CPU {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Debug|x64.ActiveCfg = Debug|Any CPU + {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Debug|x64.Build.0 = Debug|Any CPU + {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Debug|x86.ActiveCfg = Debug|Any CPU + {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Debug|x86.Build.0 = Debug|Any CPU {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Release|Any CPU.ActiveCfg = Release|Any CPU {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Release|Any CPU.Build.0 = Release|Any CPU + {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Release|x64.ActiveCfg = Release|Any CPU + {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Release|x64.Build.0 = Release|Any CPU + {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Release|x86.ActiveCfg = Release|Any CPU + {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Release|x86.Build.0 = Release|Any CPU {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Debug|x64.ActiveCfg = Debug|Any CPU + {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Debug|x64.Build.0 = Debug|Any CPU + {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Debug|x86.ActiveCfg = Debug|Any CPU + {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Debug|x86.Build.0 = Debug|Any CPU {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Release|Any CPU.ActiveCfg = Release|Any CPU {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Release|Any CPU.Build.0 = Release|Any CPU + {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Release|x64.ActiveCfg = Release|Any CPU + {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Release|x64.Build.0 = Release|Any CPU + {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Release|x86.ActiveCfg = Release|Any CPU + {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Release|x86.Build.0 = Release|Any CPU {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Debug|x64.ActiveCfg = Debug|Any CPU + {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Debug|x64.Build.0 = Debug|Any CPU + {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Debug|x86.ActiveCfg = Debug|Any CPU + {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Debug|x86.Build.0 = Debug|Any CPU {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Release|Any CPU.ActiveCfg = Release|Any CPU {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Release|Any CPU.Build.0 = Release|Any CPU + {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Release|x64.ActiveCfg = Release|Any CPU + {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Release|x64.Build.0 = Release|Any CPU + {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Release|x86.ActiveCfg = Release|Any CPU + {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Release|x86.Build.0 = Release|Any CPU {35450863-2923-488A-9A43-B5E7A1A72AD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {35450863-2923-488A-9A43-B5E7A1A72AD1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35450863-2923-488A-9A43-B5E7A1A72AD1}.Debug|x64.ActiveCfg = Debug|Any CPU + {35450863-2923-488A-9A43-B5E7A1A72AD1}.Debug|x64.Build.0 = Debug|Any CPU + {35450863-2923-488A-9A43-B5E7A1A72AD1}.Debug|x86.ActiveCfg = Debug|Any CPU + {35450863-2923-488A-9A43-B5E7A1A72AD1}.Debug|x86.Build.0 = Debug|Any CPU {35450863-2923-488A-9A43-B5E7A1A72AD1}.Release|Any CPU.ActiveCfg = Release|Any CPU {35450863-2923-488A-9A43-B5E7A1A72AD1}.Release|Any CPU.Build.0 = Release|Any CPU + {35450863-2923-488A-9A43-B5E7A1A72AD1}.Release|x64.ActiveCfg = Release|Any CPU + {35450863-2923-488A-9A43-B5E7A1A72AD1}.Release|x64.Build.0 = Release|Any CPU + {35450863-2923-488A-9A43-B5E7A1A72AD1}.Release|x86.ActiveCfg = Release|Any CPU + {35450863-2923-488A-9A43-B5E7A1A72AD1}.Release|x86.Build.0 = Release|Any CPU {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Debug|Any CPU.Build.0 = Debug|Any CPU + {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Debug|x64.ActiveCfg = Debug|Any CPU + {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Debug|x64.Build.0 = Debug|Any CPU + {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Debug|x86.ActiveCfg = Debug|Any CPU + {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Debug|x86.Build.0 = Debug|Any CPU {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Release|Any CPU.ActiveCfg = Release|Any CPU {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Release|Any CPU.Build.0 = Release|Any CPU + {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Release|x64.ActiveCfg = Release|Any CPU + {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Release|x64.Build.0 = Release|Any CPU + {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Release|x86.ActiveCfg = Release|Any CPU + {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Release|x86.Build.0 = Release|Any CPU {E7302F39-7639-4C03-8A28-8855849B5DC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E7302F39-7639-4C03-8A28-8855849B5DC0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7302F39-7639-4C03-8A28-8855849B5DC0}.Debug|x64.ActiveCfg = Debug|Any CPU + {E7302F39-7639-4C03-8A28-8855849B5DC0}.Debug|x64.Build.0 = Debug|Any CPU + {E7302F39-7639-4C03-8A28-8855849B5DC0}.Debug|x86.ActiveCfg = Debug|Any CPU + {E7302F39-7639-4C03-8A28-8855849B5DC0}.Debug|x86.Build.0 = Debug|Any CPU {E7302F39-7639-4C03-8A28-8855849B5DC0}.Release|Any CPU.ActiveCfg = Release|Any CPU {E7302F39-7639-4C03-8A28-8855849B5DC0}.Release|Any CPU.Build.0 = Release|Any CPU + {E7302F39-7639-4C03-8A28-8855849B5DC0}.Release|x64.ActiveCfg = Release|Any CPU + {E7302F39-7639-4C03-8A28-8855849B5DC0}.Release|x64.Build.0 = Release|Any CPU + {E7302F39-7639-4C03-8A28-8855849B5DC0}.Release|x86.ActiveCfg = Release|Any CPU + {E7302F39-7639-4C03-8A28-8855849B5DC0}.Release|x86.Build.0 = Release|Any CPU {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Debug|x64.ActiveCfg = Debug|Any CPU + {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Debug|x64.Build.0 = Debug|Any CPU + {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Debug|x86.ActiveCfg = Debug|Any CPU + {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Debug|x86.Build.0 = Debug|Any CPU {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Release|Any CPU.ActiveCfg = Release|Any CPU {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Release|Any CPU.Build.0 = Release|Any CPU + {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Release|x64.ActiveCfg = Release|Any CPU + {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Release|x64.Build.0 = Release|Any CPU + {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Release|x86.ActiveCfg = Release|Any CPU + {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Release|x86.Build.0 = Release|Any CPU {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Debug|x64.ActiveCfg = Debug|Any CPU + {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Debug|x64.Build.0 = Debug|Any CPU + {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Debug|x86.ActiveCfg = Debug|Any CPU + {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Debug|x86.Build.0 = Debug|Any CPU {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Release|Any CPU.ActiveCfg = Release|Any CPU {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Release|Any CPU.Build.0 = Release|Any CPU + {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Release|x64.ActiveCfg = Release|Any CPU + {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Release|x64.Build.0 = Release|Any CPU + {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Release|x86.ActiveCfg = Release|Any CPU + {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Release|x86.Build.0 = Release|Any CPU {85FF4115-0880-4DF6-816A-B314CA0432D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {85FF4115-0880-4DF6-816A-B314CA0432D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85FF4115-0880-4DF6-816A-B314CA0432D3}.Debug|x64.ActiveCfg = Debug|Any CPU + {85FF4115-0880-4DF6-816A-B314CA0432D3}.Debug|x64.Build.0 = Debug|Any CPU + {85FF4115-0880-4DF6-816A-B314CA0432D3}.Debug|x86.ActiveCfg = Debug|Any CPU + {85FF4115-0880-4DF6-816A-B314CA0432D3}.Debug|x86.Build.0 = Debug|Any CPU {85FF4115-0880-4DF6-816A-B314CA0432D3}.Release|Any CPU.ActiveCfg = Release|Any CPU {85FF4115-0880-4DF6-816A-B314CA0432D3}.Release|Any CPU.Build.0 = Release|Any CPU + {85FF4115-0880-4DF6-816A-B314CA0432D3}.Release|x64.ActiveCfg = Release|Any CPU + {85FF4115-0880-4DF6-816A-B314CA0432D3}.Release|x64.Build.0 = Release|Any CPU + {85FF4115-0880-4DF6-816A-B314CA0432D3}.Release|x86.ActiveCfg = Release|Any CPU + {85FF4115-0880-4DF6-816A-B314CA0432D3}.Release|x86.Build.0 = Release|Any CPU {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Debug|x64.ActiveCfg = Debug|Any CPU + {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Debug|x64.Build.0 = Debug|Any CPU + {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Debug|x86.ActiveCfg = Debug|Any CPU + {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Debug|x86.Build.0 = Debug|Any CPU {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Release|Any CPU.ActiveCfg = Release|Any CPU {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Release|Any CPU.Build.0 = Release|Any CPU + {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Release|x64.ActiveCfg = Release|Any CPU + {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Release|x64.Build.0 = Release|Any CPU + {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Release|x86.ActiveCfg = Release|Any CPU + {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Release|x86.Build.0 = Release|Any CPU {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Debug|x64.ActiveCfg = Debug|Any CPU + {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Debug|x64.Build.0 = Debug|Any CPU + {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Debug|x86.ActiveCfg = Debug|Any CPU + {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Debug|x86.Build.0 = Debug|Any CPU {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Release|Any CPU.ActiveCfg = Release|Any CPU {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Release|Any CPU.Build.0 = Release|Any CPU + {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Release|x64.ActiveCfg = Release|Any CPU + {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Release|x64.Build.0 = Release|Any CPU + {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Release|x86.ActiveCfg = Release|Any CPU + {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Release|x86.Build.0 = Release|Any CPU {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Debug|x64.ActiveCfg = Debug|Any CPU + {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Debug|x64.Build.0 = Debug|Any CPU + {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Debug|x86.ActiveCfg = Debug|Any CPU + {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Debug|x86.Build.0 = Debug|Any CPU {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Release|Any CPU.ActiveCfg = Release|Any CPU {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Release|Any CPU.Build.0 = Release|Any CPU + {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Release|x64.ActiveCfg = Release|Any CPU + {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Release|x64.Build.0 = Release|Any CPU + {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Release|x86.ActiveCfg = Release|Any CPU + {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Release|x86.Build.0 = Release|Any CPU {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Debug|x64.ActiveCfg = Debug|Any CPU + {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Debug|x64.Build.0 = Debug|Any CPU + {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Debug|x86.ActiveCfg = Debug|Any CPU + {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Debug|x86.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 + {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Release|x64.ActiveCfg = Release|Any CPU + {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Release|x64.Build.0 = Release|Any CPU + {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Release|x86.ActiveCfg = Release|Any CPU + {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Release|x86.Build.0 = Release|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Debug|x64.ActiveCfg = Debug|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Debug|x64.Build.0 = Debug|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Debug|x86.ActiveCfg = Debug|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Debug|x86.Build.0 = Debug|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Release|Any CPU.Build.0 = Release|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Release|x64.ActiveCfg = Release|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Release|x64.Build.0 = Release|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Release|x86.ActiveCfg = Release|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Release|x86.Build.0 = Release|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Debug|Any CPU.Build.0 = Debug|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Debug|x64.ActiveCfg = Debug|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Debug|x64.Build.0 = Debug|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Debug|x86.ActiveCfg = Debug|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Debug|x86.Build.0 = Debug|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Release|Any CPU.ActiveCfg = Release|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Release|Any CPU.Build.0 = Release|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Release|x64.ActiveCfg = Release|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Release|x64.Build.0 = Release|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Release|x86.ActiveCfg = Release|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -154,6 +330,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} + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E} = {8F3BA9BC-542C-450C-96C9-F0D72FECC930} + {255390E0-3B2B-481A-938E-82F359DC1D45} = {67253D97-9FC9-4749-80DC-A5D84339DC05} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C4B688C4-40EC-4577-9EB2-4CF2412DA0B1} 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 + { + /// + /// Gets the enumerator for the test data. + /// + /// An of []. + public IEnumerator GetEnumerator() + { + yield return new object[] { CultureInfo.InvariantCulture }; + yield return new object[] { new CultureInfo("en-US") }; + yield return new object[] { new CultureInfo("de-DE") }; + yield return new object[] { new CultureInfo("fr-FR") }; + yield return new object[] { new CultureInfo("ar-SA") }; + yield return new object[] { new CultureInfo("zh-CN") }; + yield return new object[] { new CultureInfo("ru-RU") }; + } + + /// + /// Gets the enumerator for the test data. + /// + /// An . + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} From baf4533f8571946dbab116f46328e86effad7847 Mon Sep 17 00:00:00 2001 From: JustinCanton <67930245+JustinCanton@users.noreply.github.com> Date: Tue, 16 Jun 2026 19:28:43 -0400 Subject: [PATCH 2/3] feat(openrouteservice): add geocoding support for OpenRouteService API --- Geo.NET.sln | 178 +++++ .../IOpenRouteServiceGeocoding.cs | 55 ++ .../Converters/CoordinateConverter.cs | 98 +++ .../ServiceCollectionExtensions.cs | 44 ++ src/Geo.OpenRouteService/Enums/LayerType.cs | 99 +++ src/Geo.OpenRouteService/Enums/SourceType.cs | 39 + .../Extensions/LoggerExtensions.cs | 62 ++ .../Geo.OpenRouteService.csproj | 45 ++ src/Geo.OpenRouteService/Models/Coordinate.cs | 31 + .../Parameters/AutocompleteParameters.cs | 18 + .../Models/Parameters/BaseSearchParameters.cs | 67 ++ .../Models/Parameters/BoundingBox.cs | 33 + .../Models/Parameters/BoundingCircle.cs | 29 + .../Parameters/ReverseSearchParameters.cs | 62 ++ .../Models/Parameters/SearchParameters.cs | 18 + .../Parameters/StructuredSearchParameters.cs | 54 ++ .../Models/Responses/Feature.cs | 40 + .../Models/Responses/FeatureCollection.cs | 40 + .../Models/Responses/FeatureProperties.cs | 152 ++++ .../Models/Responses/GeocodingMetadata.cs | 47 ++ .../Models/Responses/Geometry.cs | 31 + .../Properties/AssemblyInfo.cs | 10 + src/Geo.OpenRouteService/README.md | 42 ++ .../OpenRouteServiceGeocoding.Designer.cs | 180 +++++ .../Services/OpenRouteServiceGeocoding.resx | 159 ++++ .../Services/OpenRouteServiceGeocoding.cs | 428 +++++++++++ .../ServiceCollectionExtensionsTests.cs | 75 ++ .../Geo.OpenRouteService.Tests.csproj | 14 + .../GlobalSuppressions.cs | 13 + .../OpenRouteServiceGeocodingShould.cs | 698 ++++++++++++++++++ .../TestData/CultureTestData.cs | 40 + 31 files changed, 2901 insertions(+) create mode 100644 src/Geo.OpenRouteService/Abstractions/IOpenRouteServiceGeocoding.cs create mode 100644 src/Geo.OpenRouteService/Converters/CoordinateConverter.cs create mode 100644 src/Geo.OpenRouteService/DependencyInjection/ServiceCollectionExtensions.cs create mode 100644 src/Geo.OpenRouteService/Enums/LayerType.cs create mode 100644 src/Geo.OpenRouteService/Enums/SourceType.cs create mode 100644 src/Geo.OpenRouteService/Extensions/LoggerExtensions.cs create mode 100644 src/Geo.OpenRouteService/Geo.OpenRouteService.csproj create mode 100644 src/Geo.OpenRouteService/Models/Coordinate.cs create mode 100644 src/Geo.OpenRouteService/Models/Parameters/AutocompleteParameters.cs create mode 100644 src/Geo.OpenRouteService/Models/Parameters/BaseSearchParameters.cs create mode 100644 src/Geo.OpenRouteService/Models/Parameters/BoundingBox.cs create mode 100644 src/Geo.OpenRouteService/Models/Parameters/BoundingCircle.cs create mode 100644 src/Geo.OpenRouteService/Models/Parameters/ReverseSearchParameters.cs create mode 100644 src/Geo.OpenRouteService/Models/Parameters/SearchParameters.cs create mode 100644 src/Geo.OpenRouteService/Models/Parameters/StructuredSearchParameters.cs create mode 100644 src/Geo.OpenRouteService/Models/Responses/Feature.cs create mode 100644 src/Geo.OpenRouteService/Models/Responses/FeatureCollection.cs create mode 100644 src/Geo.OpenRouteService/Models/Responses/FeatureProperties.cs create mode 100644 src/Geo.OpenRouteService/Models/Responses/GeocodingMetadata.cs create mode 100644 src/Geo.OpenRouteService/Models/Responses/Geometry.cs create mode 100644 src/Geo.OpenRouteService/Properties/AssemblyInfo.cs create mode 100644 src/Geo.OpenRouteService/README.md create mode 100644 src/Geo.OpenRouteService/Resources/Services/OpenRouteServiceGeocoding.Designer.cs create mode 100644 src/Geo.OpenRouteService/Resources/Services/OpenRouteServiceGeocoding.resx create mode 100644 src/Geo.OpenRouteService/Services/OpenRouteServiceGeocoding.cs create mode 100644 test/Geo.OpenRouteService.Tests/DependencyInjection/ServiceCollectionExtensionsTests.cs create mode 100644 test/Geo.OpenRouteService.Tests/Geo.OpenRouteService.Tests.csproj create mode 100644 test/Geo.OpenRouteService.Tests/GlobalSuppressions.cs create mode 100644 test/Geo.OpenRouteService.Tests/Services/OpenRouteServiceGeocodingShould.cs create mode 100644 test/Geo.OpenRouteService.Tests/TestData/CultureTestData.cs diff --git a/Geo.NET.sln b/Geo.NET.sln index be941ab..8228acb 100644 --- a/Geo.NET.sln +++ b/Geo.NET.sln @@ -53,84 +53,260 @@ 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Geo.OpenRouteService", "src\Geo.OpenRouteService\Geo.OpenRouteService.csproj", "{4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Geo.OpenRouteService.Tests", "test\Geo.OpenRouteService.Tests\Geo.OpenRouteService.Tests.csproj", "{255390E0-3B2B-481A-938E-82F359DC1D45}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Debug|x64.ActiveCfg = Debug|Any CPU + {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Debug|x64.Build.0 = Debug|Any CPU + {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Debug|x86.ActiveCfg = Debug|Any CPU + {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Debug|x86.Build.0 = Debug|Any CPU {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Release|Any CPU.ActiveCfg = Release|Any CPU {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Release|Any CPU.Build.0 = Release|Any CPU + {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Release|x64.ActiveCfg = Release|Any CPU + {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Release|x64.Build.0 = Release|Any CPU + {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Release|x86.ActiveCfg = Release|Any CPU + {0A0E2B68-07CE-4800-885D-B5BAAFC8F214}.Release|x86.Build.0 = Release|Any CPU {CCC422FD-3B45-457B-87A7-461D50E91334}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CCC422FD-3B45-457B-87A7-461D50E91334}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CCC422FD-3B45-457B-87A7-461D50E91334}.Debug|x64.ActiveCfg = Debug|Any CPU + {CCC422FD-3B45-457B-87A7-461D50E91334}.Debug|x64.Build.0 = Debug|Any CPU + {CCC422FD-3B45-457B-87A7-461D50E91334}.Debug|x86.ActiveCfg = Debug|Any CPU + {CCC422FD-3B45-457B-87A7-461D50E91334}.Debug|x86.Build.0 = Debug|Any CPU {CCC422FD-3B45-457B-87A7-461D50E91334}.Release|Any CPU.ActiveCfg = Release|Any CPU {CCC422FD-3B45-457B-87A7-461D50E91334}.Release|Any CPU.Build.0 = Release|Any CPU + {CCC422FD-3B45-457B-87A7-461D50E91334}.Release|x64.ActiveCfg = Release|Any CPU + {CCC422FD-3B45-457B-87A7-461D50E91334}.Release|x64.Build.0 = Release|Any CPU + {CCC422FD-3B45-457B-87A7-461D50E91334}.Release|x86.ActiveCfg = Release|Any CPU + {CCC422FD-3B45-457B-87A7-461D50E91334}.Release|x86.Build.0 = Release|Any CPU {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Debug|x64.ActiveCfg = Debug|Any CPU + {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Debug|x64.Build.0 = Debug|Any CPU + {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Debug|x86.ActiveCfg = Debug|Any CPU + {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Debug|x86.Build.0 = Debug|Any CPU {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Release|Any CPU.ActiveCfg = Release|Any CPU {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Release|Any CPU.Build.0 = Release|Any CPU + {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Release|x64.ActiveCfg = Release|Any CPU + {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Release|x64.Build.0 = Release|Any CPU + {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Release|x86.ActiveCfg = Release|Any CPU + {A8F0D064-3C22-4D57-ABE1-0F63CC9CC420}.Release|x86.Build.0 = Release|Any CPU {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Debug|x64.ActiveCfg = Debug|Any CPU + {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Debug|x64.Build.0 = Debug|Any CPU + {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Debug|x86.ActiveCfg = Debug|Any CPU + {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Debug|x86.Build.0 = Debug|Any CPU {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Release|Any CPU.ActiveCfg = Release|Any CPU {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Release|Any CPU.Build.0 = Release|Any CPU + {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Release|x64.ActiveCfg = Release|Any CPU + {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Release|x64.Build.0 = Release|Any CPU + {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Release|x86.ActiveCfg = Release|Any CPU + {10A8AD17-37B9-43F7-A4ED-4A45A2780633}.Release|x86.Build.0 = Release|Any CPU {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Debug|x64.ActiveCfg = Debug|Any CPU + {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Debug|x64.Build.0 = Debug|Any CPU + {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Debug|x86.ActiveCfg = Debug|Any CPU + {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Debug|x86.Build.0 = Debug|Any CPU {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Release|Any CPU.ActiveCfg = Release|Any CPU {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Release|Any CPU.Build.0 = Release|Any CPU + {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Release|x64.ActiveCfg = Release|Any CPU + {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Release|x64.Build.0 = Release|Any CPU + {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Release|x86.ActiveCfg = Release|Any CPU + {DF4A57B8-8457-4E55-93AD-4F2360624EAC}.Release|x86.Build.0 = Release|Any CPU {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Debug|x64.ActiveCfg = Debug|Any CPU + {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Debug|x64.Build.0 = Debug|Any CPU + {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Debug|x86.ActiveCfg = Debug|Any CPU + {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Debug|x86.Build.0 = Debug|Any CPU {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Release|Any CPU.ActiveCfg = Release|Any CPU {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Release|Any CPU.Build.0 = Release|Any CPU + {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Release|x64.ActiveCfg = Release|Any CPU + {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Release|x64.Build.0 = Release|Any CPU + {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Release|x86.ActiveCfg = Release|Any CPU + {5BC3B60C-EE20-4820-8B67-3D5C9B282F1B}.Release|x86.Build.0 = Release|Any CPU {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Debug|x64.ActiveCfg = Debug|Any CPU + {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Debug|x64.Build.0 = Debug|Any CPU + {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Debug|x86.ActiveCfg = Debug|Any CPU + {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Debug|x86.Build.0 = Debug|Any CPU {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Release|Any CPU.ActiveCfg = Release|Any CPU {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Release|Any CPU.Build.0 = Release|Any CPU + {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Release|x64.ActiveCfg = Release|Any CPU + {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Release|x64.Build.0 = Release|Any CPU + {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Release|x86.ActiveCfg = Release|Any CPU + {D4524DAE-9E5A-4AAE-9143-897E0D7443D0}.Release|x86.Build.0 = Release|Any CPU {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Debug|x64.ActiveCfg = Debug|Any CPU + {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Debug|x64.Build.0 = Debug|Any CPU + {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Debug|x86.ActiveCfg = Debug|Any CPU + {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Debug|x86.Build.0 = Debug|Any CPU {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Release|Any CPU.ActiveCfg = Release|Any CPU {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Release|Any CPU.Build.0 = Release|Any CPU + {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Release|x64.ActiveCfg = Release|Any CPU + {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Release|x64.Build.0 = Release|Any CPU + {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Release|x86.ActiveCfg = Release|Any CPU + {E4659FE2-0285-46CA-9ECF-842F30D6C594}.Release|x86.Build.0 = Release|Any CPU {35450863-2923-488A-9A43-B5E7A1A72AD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {35450863-2923-488A-9A43-B5E7A1A72AD1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35450863-2923-488A-9A43-B5E7A1A72AD1}.Debug|x64.ActiveCfg = Debug|Any CPU + {35450863-2923-488A-9A43-B5E7A1A72AD1}.Debug|x64.Build.0 = Debug|Any CPU + {35450863-2923-488A-9A43-B5E7A1A72AD1}.Debug|x86.ActiveCfg = Debug|Any CPU + {35450863-2923-488A-9A43-B5E7A1A72AD1}.Debug|x86.Build.0 = Debug|Any CPU {35450863-2923-488A-9A43-B5E7A1A72AD1}.Release|Any CPU.ActiveCfg = Release|Any CPU {35450863-2923-488A-9A43-B5E7A1A72AD1}.Release|Any CPU.Build.0 = Release|Any CPU + {35450863-2923-488A-9A43-B5E7A1A72AD1}.Release|x64.ActiveCfg = Release|Any CPU + {35450863-2923-488A-9A43-B5E7A1A72AD1}.Release|x64.Build.0 = Release|Any CPU + {35450863-2923-488A-9A43-B5E7A1A72AD1}.Release|x86.ActiveCfg = Release|Any CPU + {35450863-2923-488A-9A43-B5E7A1A72AD1}.Release|x86.Build.0 = Release|Any CPU {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Debug|Any CPU.Build.0 = Debug|Any CPU + {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Debug|x64.ActiveCfg = Debug|Any CPU + {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Debug|x64.Build.0 = Debug|Any CPU + {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Debug|x86.ActiveCfg = Debug|Any CPU + {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Debug|x86.Build.0 = Debug|Any CPU {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Release|Any CPU.ActiveCfg = Release|Any CPU {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Release|Any CPU.Build.0 = Release|Any CPU + {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Release|x64.ActiveCfg = Release|Any CPU + {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Release|x64.Build.0 = Release|Any CPU + {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Release|x86.ActiveCfg = Release|Any CPU + {95E452E3-9DC1-4D07-B238-A952FA6B6D96}.Release|x86.Build.0 = Release|Any CPU {E7302F39-7639-4C03-8A28-8855849B5DC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E7302F39-7639-4C03-8A28-8855849B5DC0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7302F39-7639-4C03-8A28-8855849B5DC0}.Debug|x64.ActiveCfg = Debug|Any CPU + {E7302F39-7639-4C03-8A28-8855849B5DC0}.Debug|x64.Build.0 = Debug|Any CPU + {E7302F39-7639-4C03-8A28-8855849B5DC0}.Debug|x86.ActiveCfg = Debug|Any CPU + {E7302F39-7639-4C03-8A28-8855849B5DC0}.Debug|x86.Build.0 = Debug|Any CPU {E7302F39-7639-4C03-8A28-8855849B5DC0}.Release|Any CPU.ActiveCfg = Release|Any CPU {E7302F39-7639-4C03-8A28-8855849B5DC0}.Release|Any CPU.Build.0 = Release|Any CPU + {E7302F39-7639-4C03-8A28-8855849B5DC0}.Release|x64.ActiveCfg = Release|Any CPU + {E7302F39-7639-4C03-8A28-8855849B5DC0}.Release|x64.Build.0 = Release|Any CPU + {E7302F39-7639-4C03-8A28-8855849B5DC0}.Release|x86.ActiveCfg = Release|Any CPU + {E7302F39-7639-4C03-8A28-8855849B5DC0}.Release|x86.Build.0 = Release|Any CPU {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Debug|x64.ActiveCfg = Debug|Any CPU + {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Debug|x64.Build.0 = Debug|Any CPU + {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Debug|x86.ActiveCfg = Debug|Any CPU + {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Debug|x86.Build.0 = Debug|Any CPU {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Release|Any CPU.ActiveCfg = Release|Any CPU {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Release|Any CPU.Build.0 = Release|Any CPU + {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Release|x64.ActiveCfg = Release|Any CPU + {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Release|x64.Build.0 = Release|Any CPU + {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Release|x86.ActiveCfg = Release|Any CPU + {3EE9598A-8464-450E-9BE4-C19E3FC1450D}.Release|x86.Build.0 = Release|Any CPU {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Debug|x64.ActiveCfg = Debug|Any CPU + {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Debug|x64.Build.0 = Debug|Any CPU + {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Debug|x86.ActiveCfg = Debug|Any CPU + {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Debug|x86.Build.0 = Debug|Any CPU {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Release|Any CPU.ActiveCfg = Release|Any CPU {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Release|Any CPU.Build.0 = Release|Any CPU + {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Release|x64.ActiveCfg = Release|Any CPU + {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Release|x64.Build.0 = Release|Any CPU + {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Release|x86.ActiveCfg = Release|Any CPU + {2D1EB4BD-E554-46B6-8FEE-73CC486341F2}.Release|x86.Build.0 = Release|Any CPU {85FF4115-0880-4DF6-816A-B314CA0432D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {85FF4115-0880-4DF6-816A-B314CA0432D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85FF4115-0880-4DF6-816A-B314CA0432D3}.Debug|x64.ActiveCfg = Debug|Any CPU + {85FF4115-0880-4DF6-816A-B314CA0432D3}.Debug|x64.Build.0 = Debug|Any CPU + {85FF4115-0880-4DF6-816A-B314CA0432D3}.Debug|x86.ActiveCfg = Debug|Any CPU + {85FF4115-0880-4DF6-816A-B314CA0432D3}.Debug|x86.Build.0 = Debug|Any CPU {85FF4115-0880-4DF6-816A-B314CA0432D3}.Release|Any CPU.ActiveCfg = Release|Any CPU {85FF4115-0880-4DF6-816A-B314CA0432D3}.Release|Any CPU.Build.0 = Release|Any CPU + {85FF4115-0880-4DF6-816A-B314CA0432D3}.Release|x64.ActiveCfg = Release|Any CPU + {85FF4115-0880-4DF6-816A-B314CA0432D3}.Release|x64.Build.0 = Release|Any CPU + {85FF4115-0880-4DF6-816A-B314CA0432D3}.Release|x86.ActiveCfg = Release|Any CPU + {85FF4115-0880-4DF6-816A-B314CA0432D3}.Release|x86.Build.0 = Release|Any CPU {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Debug|x64.ActiveCfg = Debug|Any CPU + {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Debug|x64.Build.0 = Debug|Any CPU + {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Debug|x86.ActiveCfg = Debug|Any CPU + {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Debug|x86.Build.0 = Debug|Any CPU {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Release|Any CPU.ActiveCfg = Release|Any CPU {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Release|Any CPU.Build.0 = Release|Any CPU + {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Release|x64.ActiveCfg = Release|Any CPU + {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Release|x64.Build.0 = Release|Any CPU + {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Release|x86.ActiveCfg = Release|Any CPU + {38C213E3-55D6-4FC7-87CA-E28C3EDD42EA}.Release|x86.Build.0 = Release|Any CPU {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Debug|x64.ActiveCfg = Debug|Any CPU + {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Debug|x64.Build.0 = Debug|Any CPU + {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Debug|x86.ActiveCfg = Debug|Any CPU + {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Debug|x86.Build.0 = Debug|Any CPU {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Release|Any CPU.ActiveCfg = Release|Any CPU {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Release|Any CPU.Build.0 = Release|Any CPU + {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Release|x64.ActiveCfg = Release|Any CPU + {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Release|x64.Build.0 = Release|Any CPU + {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Release|x86.ActiveCfg = Release|Any CPU + {1A320DE3-B14B-46EE-A0E6-C6783E585F73}.Release|x86.Build.0 = Release|Any CPU {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Debug|x64.ActiveCfg = Debug|Any CPU + {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Debug|x64.Build.0 = Debug|Any CPU + {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Debug|x86.ActiveCfg = Debug|Any CPU + {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Debug|x86.Build.0 = Debug|Any CPU {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Release|Any CPU.ActiveCfg = Release|Any CPU {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Release|Any CPU.Build.0 = Release|Any CPU + {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Release|x64.ActiveCfg = Release|Any CPU + {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Release|x64.Build.0 = Release|Any CPU + {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Release|x86.ActiveCfg = Release|Any CPU + {03C7B4AE-7905-4292-8133-3F4AA6EDCA3A}.Release|x86.Build.0 = Release|Any CPU {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Debug|x64.ActiveCfg = Debug|Any CPU + {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Debug|x64.Build.0 = Debug|Any CPU + {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Debug|x86.ActiveCfg = Debug|Any CPU + {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Debug|x86.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 + {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Release|x64.ActiveCfg = Release|Any CPU + {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Release|x64.Build.0 = Release|Any CPU + {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Release|x86.ActiveCfg = Release|Any CPU + {E09BD60D-6E8A-4210-9274-695A2DFFE976}.Release|x86.Build.0 = Release|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Debug|x64.ActiveCfg = Debug|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Debug|x64.Build.0 = Debug|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Debug|x86.ActiveCfg = Debug|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Debug|x86.Build.0 = Debug|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Release|Any CPU.Build.0 = Release|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Release|x64.ActiveCfg = Release|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Release|x64.Build.0 = Release|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Release|x86.ActiveCfg = Release|Any CPU + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E}.Release|x86.Build.0 = Release|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Debug|Any CPU.Build.0 = Debug|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Debug|x64.ActiveCfg = Debug|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Debug|x64.Build.0 = Debug|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Debug|x86.ActiveCfg = Debug|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Debug|x86.Build.0 = Debug|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Release|Any CPU.ActiveCfg = Release|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Release|Any CPU.Build.0 = Release|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Release|x64.ActiveCfg = Release|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Release|x64.Build.0 = Release|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Release|x86.ActiveCfg = Release|Any CPU + {255390E0-3B2B-481A-938E-82F359DC1D45}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -154,6 +330,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} + {4C28E18B-ABD6-4EBF-A986-9B3D05BA1B8E} = {8F3BA9BC-542C-450C-96C9-F0D72FECC930} + {255390E0-3B2B-481A-938E-82F359DC1D45} = {67253D97-9FC9-4749-80DC-A5D84339DC05} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C4B688C4-40EC-4577-9EB2-4CF2412DA0B1} 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 + { + /// + /// Gets the enumerator for the test data. + /// + /// An of []. + public IEnumerator GetEnumerator() + { + yield return new object[] { CultureInfo.InvariantCulture }; + yield return new object[] { new CultureInfo("en-US") }; + yield return new object[] { new CultureInfo("de-DE") }; + yield return new object[] { new CultureInfo("fr-FR") }; + yield return new object[] { new CultureInfo("ar-SA") }; + yield return new object[] { new CultureInfo("zh-CN") }; + yield return new object[] { new CultureInfo("ru-RU") }; + } + + /// + /// Gets the enumerator for the test data. + /// + /// An . + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} From 23cce502972eb861df7f80747d491942a512099a Mon Sep 17 00:00:00 2001 From: JustinCanton <67930245+JustinCanton@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:20:30 -0400 Subject: [PATCH 3/3] build: updating the sln file --- Geo.NET.sln | 16 +++++++++++++++- src/Geo.Core/DependencyInjection/BaseBuilder.cs | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) 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; } }