diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..04a1c0e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*.cs] +end_of_line = lf +insert_final_newline = true +indent_style = tab +tab_width = 4 +charset = utf-8-bom diff --git a/Geocoding.sln b/Geocoding.sln index e86f359..6cbe577 100644 --- a/Geocoding.sln +++ b/Geocoding.sln @@ -17,6 +17,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Geocoding.Yahoo", "src\Geoc EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Geocoding.Tests", "test\Geocoding.Tests\Geocoding.Tests.csproj", "{B87FBD8D-1E79-49F0-B06D-B4B15C1BA9C4}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Geocoding.Here", "src\Geocoding.Here\Geocoding.Here.csproj", "{41F9E0D3-2094-4CE7-A2CD-F3CAF585A048}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -47,6 +49,10 @@ Global {B87FBD8D-1E79-49F0-B06D-B4B15C1BA9C4}.Debug|Any CPU.Build.0 = Debug|Any CPU {B87FBD8D-1E79-49F0-B06D-B4B15C1BA9C4}.Release|Any CPU.ActiveCfg = Release|Any CPU {B87FBD8D-1E79-49F0-B06D-B4B15C1BA9C4}.Release|Any CPU.Build.0 = Release|Any CPU + {41F9E0D3-2094-4CE7-A2CD-F3CAF585A048}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {41F9E0D3-2094-4CE7-A2CD-F3CAF585A048}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41F9E0D3-2094-4CE7-A2CD-F3CAF585A048}.Release|Any CPU.ActiveCfg = Release|Any CPU + {41F9E0D3-2094-4CE7-A2CD-F3CAF585A048}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -56,6 +62,10 @@ Global {B46F0315-6CE4-414A-BDDA-186090F5062C} = {F742864D-9400-4CE2-957A-DBD0A0237277} {27F58640-D424-40F5-945E-42BF8BC872A0} = {F742864D-9400-4CE2-957A-DBD0A0237277} {9773C7DA-DD1A-490D-B4D8-CEA18804AD1E} = {F742864D-9400-4CE2-957A-DBD0A0237277} + {41F9E0D3-2094-4CE7-A2CD-F3CAF585A048} = {F742864D-9400-4CE2-957A-DBD0A0237277} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {085E97E6-56E0-4099-94E9-10F9080F2DD1} EndGlobalSection GlobalSection(MonoDevelopProperties) = preSolution StartupItem = Tests\Tests.csproj diff --git a/README.md b/README.md index bcec448..5fb2b3e 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Includes a model and interface for communicating with five popular Geocoding pro * [Bing Maps (aka Virtual Earth)](http://www.microsoft.com/maps/) - [docs](http://msdn.microsoft.com/en-us/library/ff701715.aspx) * :warning: MapQuest [(Commercial API)](http://www.mapquestapi.com/) - [docs](http://www.mapquestapi.com/geocoding/) * :warning: MapQuest [(OpenStreetMap)](http://open.mapquestapi.com/) - [docs](http://open.mapquestapi.com/geocoding/) + * [HERE](https://www.here.com/) - [docs](https://developer.here.com/documentation) The API returns latitude/longitude coordinates and normalized address information. This can be used to perform address validation, real time mapping of user-entered addresses, distance calculations, and much more. @@ -29,6 +30,7 @@ Install-Package Geocoding.Google Install-Package Geocoding.MapQuest Install-Package Geocoding.Microsoft Install-Package Geocoding.Yahoo +Install-Package Geocoding.Here ``` ## Example Usage @@ -36,7 +38,7 @@ Install-Package Geocoding.Yahoo ### Simple Example ```csharp -IGeocoder geocoder = new GoogleGeocoder() { ApiKey = "this-is-my-optional-google-api-key" }; +IGeocoder geocoder = new GoogleGeocoder() { ApiKey = "this-is-my-google-api-key" }; IEnumerable
addresses = await geocoder.GeocodeAsync("1600 pennsylvania ave washington dc"); Console.WriteLine("Formatted: " + addresses.First().FormattedAddress); //Formatted: 1600 Pennsylvania Ave SE, Washington, DC 20003, USA Console.WriteLine("Coordinates: " + addresses.First().Coordinates.Latitude + ", " + addresses.First().Coordinates.Longitude); //Coordinates: 38.8791981, -76.9818437 @@ -63,7 +65,7 @@ The Microsoft and Yahoo implementations each provide their own address class as ## API Keys -Google allows anonymous access to it's API, but if you start hitting rate limits, you must [sign up for a new Server API Key](https://developers.google.com/maps/documentation/javascript/tutorial#api_key). +Google [requires a new Server API Key](https://developers.google.com/maps/documentation/javascript/tutorial#api_key) to access its service. Bing [requires an API key](http://msdn.microsoft.com/en-us/library/ff428642.aspx) to access its service. @@ -71,6 +73,8 @@ You will need a [consumer secret and consumer key](http://developer.yahoo.com/bo MapQuest API requires a key. Sign up here: (http://developer.mapquest.com/web/products/open) +HERE requires an [app ID and app Code](https://developer.here.com/?create=Freemium-Basic&keepState=true&step=account) + ## How to Build from Source ``` diff --git a/src/Geocoding.Core/Bounds.cs b/src/Geocoding.Core/Bounds.cs index 2ecbec8..212ce48 100644 --- a/src/Geocoding.Core/Bounds.cs +++ b/src/Geocoding.Core/Bounds.cs @@ -1,4 +1,5 @@ -using System; +using Newtonsoft.Json; +using System; namespace Geocoding { @@ -20,6 +21,7 @@ public Location NorthEast public Bounds(double southWestLatitude, double southWestLongitude, double northEastLatitude, double northEastLongitude) : this(new Location(southWestLatitude, southWestLongitude), new Location(northEastLatitude, northEastLongitude)) { } + [JsonConstructor] public Bounds(Location southWest, Location northEast) { if (southWest == null) diff --git a/src/Geocoding.Core/Geocoding.Core.csproj b/src/Geocoding.Core/Geocoding.Core.csproj index 82b8790..aa73f73 100644 --- a/src/Geocoding.Core/Geocoding.Core.csproj +++ b/src/Geocoding.Core/Geocoding.Core.csproj @@ -5,7 +5,7 @@ Geocoding.net Core 4.0.0-beta1 chadly - netstandard1.3 + netstandard1.3;net46 Geocoding.Core Geocoding.Core geocoding;geocode;geocoder;maps;address;validation;normalization;google-maps;bing-maps;yahoo-placefinder;mapquest diff --git a/src/Geocoding.Core/GeocodingException.cs b/src/Geocoding.Core/GeocodingException.cs new file mode 100644 index 0000000..f3c853f --- /dev/null +++ b/src/Geocoding.Core/GeocodingException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Geocoding.Core +{ + public class GeocodingException : Exception + { + public GeocodingException(string message, Exception innerException = null) + : base(message, innerException) + { + } + } +} diff --git a/src/Geocoding.Core/IBatchGeocoder.cs b/src/Geocoding.Core/IBatchGeocoder.cs index 7395159..74081b4 100644 --- a/src/Geocoding.Core/IBatchGeocoder.cs +++ b/src/Geocoding.Core/IBatchGeocoder.cs @@ -1,11 +1,12 @@ using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace Geocoding { public interface IBatchGeocoder { - Task> GeocodeAsync(IEnumerable addresses); - Task> ReverseGeocodeAsync(IEnumerable locations); + Task> GeocodeAsync(IEnumerable addresses, CancellationToken cancellationToken = default(CancellationToken)); + Task> ReverseGeocodeAsync(IEnumerable locations, CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/src/Geocoding.Core/IGeocoder.cs b/src/Geocoding.Core/IGeocoder.cs index 08c9bee..10dda71 100644 --- a/src/Geocoding.Core/IGeocoder.cs +++ b/src/Geocoding.Core/IGeocoder.cs @@ -1,14 +1,15 @@ using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace Geocoding { public interface IGeocoder { - Task> GeocodeAsync(string address); - Task> GeocodeAsync(string street, string city, string state, string postalCode, string country); + Task> GeocodeAsync(string address, CancellationToken cancellationToken = default(CancellationToken)); + Task> GeocodeAsync(string street, string city, string state, string postalCode, string country, CancellationToken cancellationToken = default(CancellationToken)); - Task> ReverseGeocodeAsync(Location location); - Task> ReverseGeocodeAsync(double latitude, double longitude); + Task> ReverseGeocodeAsync(Location location, CancellationToken cancellationToken = default(CancellationToken)); + Task> ReverseGeocodeAsync(double latitude, double longitude, CancellationToken cancellationToken = default(CancellationToken)); } } \ No newline at end of file diff --git a/src/Geocoding.Google/Geocoding.Google.csproj b/src/Geocoding.Google/Geocoding.Google.csproj index 7e5c703..0d93df3 100644 --- a/src/Geocoding.Google/Geocoding.Google.csproj +++ b/src/Geocoding.Google/Geocoding.Google.csproj @@ -5,7 +5,7 @@ Geocoding.net Google 4.0.0-beta1 chadly - netstandard1.3 + netstandard1.3;net46 Geocoding.Google Geocoding.Google geocoding;geocode;geocoder;maps;address;validation;normalization;google-maps;bing-maps;yahoo-placefinder;mapquest @@ -21,9 +21,13 @@ - + + + + + diff --git a/src/Geocoding.Google/GoogleGeocoder.cs b/src/Geocoding.Google/GoogleGeocoder.cs index 0861f0b..7125758 100644 --- a/src/Geocoding.Google/GoogleGeocoder.cs +++ b/src/Geocoding.Google/GoogleGeocoder.cs @@ -6,6 +6,7 @@ using System.Net; using System.Net.Http; using System.Text; +using System.Threading; using System.Threading.Tasks; using System.Xml.XPath; @@ -124,27 +125,27 @@ public string ServiceUrl } } - public async Task> GeocodeAsync(string address) + public async Task> GeocodeAsync(string address, CancellationToken cancellationToken = default(CancellationToken)) { if (string.IsNullOrEmpty(address)) throw new ArgumentNullException("address"); var request = BuildWebRequest("address", WebUtility.UrlEncode(address)); - return await ProcessRequest(request).ConfigureAwait(false); + return await ProcessRequest(request, cancellationToken).ConfigureAwait(false); } - public async Task> ReverseGeocodeAsync(Location location) + public async Task> ReverseGeocodeAsync(Location location, CancellationToken cancellationToken = default(CancellationToken)) { if (location == null) throw new ArgumentNullException("location"); - return await ReverseGeocodeAsync(location.Latitude, location.Longitude).ConfigureAwait(false); + return await ReverseGeocodeAsync(location.Latitude, location.Longitude, cancellationToken).ConfigureAwait(false); } - public async Task> ReverseGeocodeAsync(double latitude, double longitude) + public async Task> ReverseGeocodeAsync(double latitude, double longitude, CancellationToken cancellationToken = default(CancellationToken)) { var request = BuildWebRequest("latlng", BuildGeolocation(latitude, longitude)); - return await ProcessRequest(request).ConfigureAwait(false); + return await ProcessRequest(request, cancellationToken).ConfigureAwait(false); } private string BuildAddress(string street, string city, string state, string postalCode, string country) @@ -157,13 +158,13 @@ private string BuildGeolocation(double latitude, double longitude) return string.Format(CultureInfo.InvariantCulture, "{0:0.00000000},{1:0.00000000}", latitude, longitude); } - private async Task> ProcessRequest(HttpRequestMessage request) + private async Task> ProcessRequest(HttpRequestMessage request, CancellationToken cancellationToken) { try { using (var client = BuildClient()) { - return await ProcessWebResponse(await client.SendAsync(request).ConfigureAwait(false)).ConfigureAwait(false); + return await ProcessWebResponse(await client.SendAsync(request, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); } } catch (GoogleGeocodingException) @@ -188,24 +189,24 @@ HttpClient BuildClient() return new HttpClient(handler); } - async Task> IGeocoder.GeocodeAsync(string address) + async Task> IGeocoder.GeocodeAsync(string address, CancellationToken cancellationToken) { - return await GeocodeAsync(address).ConfigureAwait(false); + return await GeocodeAsync(address, cancellationToken).ConfigureAwait(false); } - async Task> IGeocoder.GeocodeAsync(string street, string city, string state, string postalCode, string country) + async Task> IGeocoder.GeocodeAsync(string street, string city, string state, string postalCode, string country, CancellationToken cancellationToken) { - return await GeocodeAsync(BuildAddress(street, city, state, postalCode, country)).ConfigureAwait(false); + return await GeocodeAsync(BuildAddress(street, city, state, postalCode, country), cancellationToken).ConfigureAwait(false); } - async Task> IGeocoder.ReverseGeocodeAsync(Location location) + async Task> IGeocoder.ReverseGeocodeAsync(Location location, CancellationToken cancellationToken) { - return await ReverseGeocodeAsync(location).ConfigureAwait(false); + return await ReverseGeocodeAsync(location, cancellationToken).ConfigureAwait(false); } - async Task> IGeocoder.ReverseGeocodeAsync(double latitude, double longitude) + async Task> IGeocoder.ReverseGeocodeAsync(double latitude, double longitude, CancellationToken cancellationToken) { - return await ReverseGeocodeAsync(latitude, longitude).ConfigureAwait(false); + return await ReverseGeocodeAsync(latitude, longitude, cancellationToken).ConfigureAwait(false); } private HttpRequestMessage BuildWebRequest(string type, string value) diff --git a/src/Geocoding.Google/GoogleGeocodingException.cs b/src/Geocoding.Google/GoogleGeocodingException.cs index 16ed9c6..855e7d1 100644 --- a/src/Geocoding.Google/GoogleGeocodingException.cs +++ b/src/Geocoding.Google/GoogleGeocodingException.cs @@ -1,8 +1,9 @@ using System; +using Geocoding.Core; namespace Geocoding.Google { - public class GoogleGeocodingException : Exception + public class GoogleGeocodingException : GeocodingException { const string defaultMessage = "There was an error processing the geocoding request. See Status or InnerException for more information."; diff --git a/src/Geocoding.Here/Geocoding.Here.csproj b/src/Geocoding.Here/Geocoding.Here.csproj new file mode 100644 index 0000000..93b4da5 --- /dev/null +++ b/src/Geocoding.Here/Geocoding.Here.csproj @@ -0,0 +1,37 @@ + + + + Includes a model and interface for communicating with four popular Geocoding providers. Current implementations include: Google Maps, Yahoo! PlaceFinder, Bing Maps (aka Virtual Earth), and Mapquest. The API returns latitude/longitude coordinates and normalized address information. This can be used to perform address validation, real time mapping of user-entered addresses, distance calculations, and much more. + Geocoding.net Here + 4.0.0-beta1 + chadly + netstandard1.3;net46 + Geocoding.Here + Geocoding.Here + geocoding;geocode;geocoder;maps;address;validation;normalization;google-maps;bing-maps;yahoo-placefinder;mapquest + https://github.com/chadly/Geocoding.net/releases/latest + https://github.com/chadly/Geocoding.net + https://github.com/chadly/Geocoding.net/blob/master/LICENSE + 4.0.0 + https://github.com/chadly/Geocoding.net.git + git + + + + + + + + + + + + + + + + + + + + diff --git a/src/Geocoding.Here/HereAddress.cs b/src/Geocoding.Here/HereAddress.cs new file mode 100644 index 0000000..5067d5b --- /dev/null +++ b/src/Geocoding.Here/HereAddress.cs @@ -0,0 +1,58 @@ +using System; + +namespace Geocoding.Here +{ + public class HereAddress : Address + { + readonly string street, houseNumber, city, state, country, postalCode; + readonly HereLocationType type; + + public string AddressLine + { + get { return street ?? ""; } + } + + public string AdminDistrict + { + get { return houseNumber ?? ""; } + } + + public string AdminDistrict2 + { + get { return city ?? ""; } + } + + public string CountryRegion + { + get { return state ?? ""; } + } + + public string Neighborhood + { + get { return country ?? ""; } + } + + public string PostalCode + { + get { return postalCode ?? ""; } + } + + public HereLocationType Type + { + get { return type; } + } + + public HereAddress(string formattedAddress, Location coordinates, string street, string houseNumber, string city, + string state, string postalCode, string country, HereLocationType type) + : base(formattedAddress, coordinates, "HERE") + { + this.street = street; + this.houseNumber = houseNumber; + this.city = city; + this.state = state; + this.postalCode = postalCode; + this.country = country; + this.type = type; + } + } +} diff --git a/src/Geocoding.Here/HereGeocoder.cs b/src/Geocoding.Here/HereGeocoder.cs new file mode 100644 index 0000000..79f3dad --- /dev/null +++ b/src/Geocoding.Here/HereGeocoder.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Runtime.Serialization.Json; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Geocoding.Here +{ + /// + /// https://developer.here.com/documentation/geocoder/topics/request-constructing.html + /// + public class HereGeocoder : IGeocoder + { + const string GEOCODING_QUERY = "https://geocoder.api.here.com/6.2/geocode.json?app_id={0}&app_code={1}&{2}"; + const string REVERSE_GEOCODING_QUERY = "https://reverse.geocoder.api.here.com/6.2/reversegeocode.json?app_id={0}&app_code={1}&mode=retrieveAddresses&{2}"; + const string SEARCHTEXT = "searchtext={0}"; + const string PROX = "prox={0}"; + const string STREET = "street={0}"; + const string CITY = "city={0}"; + const string STATE = "state={0}"; + const string POSTAL_CODE = "postalcode={0}"; + const string COUNTRY = "country={0}"; + + readonly string appId; + readonly string appCode; + + public IWebProxy Proxy { get; set; } + public Location UserLocation { get; set; } + public Bounds UserMapView { get; set; } + public int? MaxResults { get; set; } + + public HereGeocoder(string appId, string appCode) + { + if (string.IsNullOrWhiteSpace(appId)) + throw new ArgumentException("appId can not be null or empty"); + + if (string.IsNullOrWhiteSpace(appCode)) + throw new ArgumentException("appCode can not be null or empty"); + + this.appId = appId; + this.appCode = appCode; + } + + private string GetQueryUrl(string address) + { + var parameters = new StringBuilder(); + var first = AppendParameter(parameters, address, SEARCHTEXT, true); + AppendGlobalParameters(parameters, first); + + return string.Format(GEOCODING_QUERY, appId, appCode, parameters.ToString()); + } + + private string GetQueryUrl(string street, string city, string state, string postalCode, string country) + { + var parameters = new StringBuilder(); + var first = AppendParameter(parameters, street, STREET, true); + first = AppendParameter(parameters, city, CITY, first); + first = AppendParameter(parameters, state, STATE, first); + first = AppendParameter(parameters, postalCode, POSTAL_CODE, first); + first = AppendParameter(parameters, country, COUNTRY, first); + AppendGlobalParameters(parameters, first); + + return string.Format(GEOCODING_QUERY, appId, appCode, parameters.ToString()); + } + + private string GetQueryUrl(double latitude, double longitude) + { + var parameters = new StringBuilder(); + var first = AppendParameter(parameters, string.Format(CultureInfo.InvariantCulture, "{0},{1}", latitude, longitude), PROX, true); + AppendGlobalParameters(parameters, first); + + return string.Format(REVERSE_GEOCODING_QUERY, appId, appCode, parameters.ToString()); + } + + private IEnumerable> GetGlobalParameters() + { + if (UserLocation != null) + yield return new KeyValuePair("prox", UserLocation.ToString()); + + if (UserMapView != null) + yield return new KeyValuePair("mapview", string.Concat(UserMapView.SouthWest.ToString(), ",", UserMapView.NorthEast.ToString())); + + if (MaxResults != null && MaxResults.Value > 0) + yield return new KeyValuePair("maxresults", MaxResults.Value.ToString(CultureInfo.InvariantCulture)); + } + + private bool AppendGlobalParameters(StringBuilder parameters, bool first) + { + var values = GetGlobalParameters().ToArray(); + + if (!first) parameters.Append("&"); + parameters.Append(BuildQueryString(values)); + + return first && !values.Any(); + } + + private string BuildQueryString(IEnumerable> parameters) + { + var builder = new StringBuilder(); + foreach (var pair in parameters) + { + if (builder.Length > 0) builder.Append("&"); + + builder.Append(UrlEncode(pair.Key)); + builder.Append("="); + builder.Append(UrlEncode(pair.Value)); + } + return builder.ToString(); + } + + public async Task> GeocodeAsync(string address, CancellationToken cancellationToken = default(CancellationToken)) + { + try + { + var url = GetQueryUrl(address); + var response = await GetResponse(url, cancellationToken).ConfigureAwait(false); + return ParseResponse(response); + } + catch (Exception ex) + { + throw new HereGeocodingException(ex); + } + } + + public async Task> GeocodeAsync(string street, string city, string state, string postalCode, string country, CancellationToken cancellationToken = default(CancellationToken)) + { + try + { + var url = GetQueryUrl(street, city, state, postalCode, country); + var response = await GetResponse(url, cancellationToken).ConfigureAwait(false); + return ParseResponse(response); + } + catch (Exception ex) + { + throw new HereGeocodingException(ex); + } + } + + public async Task> ReverseGeocodeAsync(Location location, CancellationToken cancellationToken = default(CancellationToken)) + { + if (location == null) + throw new ArgumentNullException(nameof(location)); + + return await ReverseGeocodeAsync(location.Latitude, location.Longitude, cancellationToken).ConfigureAwait(false); + } + + public async Task> ReverseGeocodeAsync(double latitude, double longitude, CancellationToken cancellationToken = default(CancellationToken)) + { + try + { + var url = GetQueryUrl(latitude, longitude); + var response = await GetResponse(url, cancellationToken).ConfigureAwait(false); + return ParseResponse(response); + } + catch (Exception ex) + { + throw new HereGeocodingException(ex); + } + } + + async Task> IGeocoder.GeocodeAsync(string address, CancellationToken cancellationToken) + { + return await GeocodeAsync(address, cancellationToken).ConfigureAwait(false); + } + + async Task> IGeocoder.GeocodeAsync(string street, string city, string state, string postalCode, string country, CancellationToken cancellationToken) + { + return await GeocodeAsync(street, city, state, postalCode, country, cancellationToken).ConfigureAwait(false); + } + + async Task> IGeocoder.ReverseGeocodeAsync(Location location, CancellationToken cancellationToken) + { + return await ReverseGeocodeAsync(location, cancellationToken).ConfigureAwait(false); + } + + async Task> IGeocoder.ReverseGeocodeAsync(double latitude, double longitude, CancellationToken cancellationToken) + { + return await ReverseGeocodeAsync(latitude, longitude, cancellationToken).ConfigureAwait(false); + } + + private bool AppendParameter(StringBuilder sb, string parameter, string format, bool first) + { + if (!string.IsNullOrEmpty(parameter)) + { + if (!first) + { + sb.Append('&'); + } + sb.Append(string.Format(format, UrlEncode(parameter))); + return false; + } + return first; + } + + private IEnumerable ParseResponse(Json.Response response) + { + foreach (var view in response.View) + { + foreach (var result in view.Result) + { + var location = result.Location; + yield return new HereAddress( + location.Address.Label, + new Location(location.DisplayPosition.Latitude, location.DisplayPosition.Longitude), + location.Address.Street, + location.Address.HouseNumber, + location.Address.City, + location.Address.State, + location.Address.PostalCode, + location.Address.Country, + (HereLocationType)Enum.Parse(typeof(HereLocationType), location.LocationType, true)); + } + } + } + + private HttpRequestMessage CreateRequest(string url) + { + return new HttpRequestMessage(HttpMethod.Get, url); + } + + private HttpClient BuildClient() + { + if (this.Proxy == null) + return new HttpClient(); + + var handler = new HttpClientHandler {Proxy = this.Proxy}; + return new HttpClient(handler); + } + + private async Task GetResponse(string queryURL, CancellationToken cancellationToken) + { + using (var client = BuildClient()) + { + var response = await client.SendAsync(CreateRequest(queryURL), cancellationToken).ConfigureAwait(false); + using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + { + var jsonSerializer = new DataContractJsonSerializer(typeof(Json.ServerResponse)); + var serverResponse = (Json.ServerResponse)jsonSerializer.ReadObject(stream); + + if (serverResponse.ErrorType != null) + { + throw new HereGeocodingException(serverResponse.Details, serverResponse.ErrorType, serverResponse.ErrorType); + } + + return serverResponse.Response; + } + } + } + + private string UrlEncode(string toEncode) + { + if (string.IsNullOrEmpty(toEncode)) + return string.Empty; + + return WebUtility.UrlEncode(toEncode); + } + } +} diff --git a/src/Geocoding.Here/HereGeocodingException.cs b/src/Geocoding.Here/HereGeocodingException.cs new file mode 100644 index 0000000..e9204bf --- /dev/null +++ b/src/Geocoding.Here/HereGeocodingException.cs @@ -0,0 +1,26 @@ +using System; +using Geocoding.Core; + +namespace Geocoding.Here +{ + public class HereGeocodingException : GeocodingException + { + const string defaultMessage = "There was an error processing the geocoding request. See InnerException for more information."; + + public string ErrorType { get; } + + public string ErrorSubtype { get; } + + public HereGeocodingException(Exception innerException) + : base(defaultMessage, innerException) + { + } + + public HereGeocodingException(string message, string errorType, string errorSubtype) + : base(message) + { + ErrorType = errorType; + ErrorSubtype = errorSubtype; + } + } +} diff --git a/src/Geocoding.Here/HereLocationType.cs b/src/Geocoding.Here/HereLocationType.cs new file mode 100644 index 0000000..9d40fc9 --- /dev/null +++ b/src/Geocoding.Here/HereLocationType.cs @@ -0,0 +1,39 @@ +namespace Geocoding.Here +{ + /// + /// https://developer.here.com/documentation/geocoder/topics/resource-type-response-geocode.html + /// + public enum HereLocationType + { + Unknown, + Point, + Area, + Address, + Trail, + Park, + Lake, + MountainPeak, + Volcano, + River, + GolfCourse, + IndustrialComplex, + Island, + Woodland, + Cemetery, + CanalWaterChannel, + BayHarbor, + Airport, + Hospital, + SportsComplex, + ShoppingCentre, + UniversityCollege, + NativeAmericanReservation, + Railroad, + MilitaryBase, + ParkingLot, + ParkingGarage, + AnimalPark, + Beach, + DistanceMarker + } +} diff --git a/src/Geocoding.Here/HereMatchType.cs b/src/Geocoding.Here/HereMatchType.cs new file mode 100644 index 0000000..4d3c2ec --- /dev/null +++ b/src/Geocoding.Here/HereMatchType.cs @@ -0,0 +1,12 @@ +namespace Geocoding.Here +{ + /// + /// https://developer.here.com/documentation/geocoder/topics/resource-type-response-geocode.html + /// + public enum HereMatchType + { + Unknown, + PointAddress, + Interpolated + } +} diff --git a/src/Geocoding.Here/HereViewport.cs b/src/Geocoding.Here/HereViewport.cs new file mode 100644 index 0000000..80229e5 --- /dev/null +++ b/src/Geocoding.Here/HereViewport.cs @@ -0,0 +1,8 @@ +namespace Geocoding.Here +{ + public class HereViewport + { + public Location Northeast { get; set; } + public Location Southwest { get; set; } + } +} diff --git a/src/Geocoding.Here/Json.cs b/src/Geocoding.Here/Json.cs new file mode 100644 index 0000000..9512514 --- /dev/null +++ b/src/Geocoding.Here/Json.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Geocoding.Here.Json +{ + [DataContract] + public class ServerResponse + { + [DataMember(Name = "Response")] + public Response Response { get; set; } + [DataMember(Name = "Details")] + public string Details { get; set; } + [DataMember(Name = "type")] + public string ErrorType { get; set; } + [DataMember(Name = "subtype")] + public string ErrorSubtype { get; set; } + } + + [DataContract] + public class Response + { + [DataMember(Name = "View")] + public View[] View { get; set; } + } + + [DataContract] + public class View + { + [DataMember(Name = "ViewId")] + public int ViewId { get; set; } + [DataMember(Name = "Result")] + public Result[] Result { get; set; } + } + + [DataContract] + public class Result + { + [DataMember(Name = "Relevance")] + public float Relevance { get; set; } + [DataMember(Name = "MatchLevel")] + public string MatchLevel { get; set; } + [DataMember(Name = "MatchType")] + public string MatchType { get; set; } + [DataMember(Name = "Location")] + public Location Location { get; set; } + } + + [DataContract] + public class Location + { + [DataMember(Name = "LocationId")] + public string LocationId { get; set; } + [DataMember(Name = "LocationType")] + public string LocationType { get; set; } + [DataMember(Name = "Name")] + public string Name { get; set; } + [DataMember(Name = "DisplayPosition")] + public GeoCoordinate DisplayPosition { get; set; } + [DataMember(Name = "NavigationPosition")] + public GeoCoordinate NavigationPosition { get; set; } + [DataMember(Name = "Address")] + public Address Address { get; set; } + } + + [DataContract] + public class GeoCoordinate + { + [DataMember(Name = "Latitude")] + public double Latitude { get; set; } + [DataMember(Name = "Longitude")] + public double Longitude { get; set; } + } + + [DataContract] + public class GeoBoundingBox + { + [DataMember(Name = "TopLeft")] + public GeoCoordinate TopLeft { get; set; } + [DataMember(Name = "BottomRight")] + public GeoCoordinate BottomRight { get; set; } + } + + [DataContract] + public class Address + { + [DataMember(Name = "Label")] + public string Label { get; set; } + [DataMember(Name = "Country")] + public string Country { get; set; } + [DataMember(Name = "State")] + public string State { get; set; } + [DataMember(Name = "County")] + public string County { get; set; } + [DataMember(Name = "City")] + public string City { get; set; } + [DataMember(Name = "District")] + public string District { get; set; } + [DataMember(Name = "Subdistrict")] + public string Subdistrict { get; set; } + [DataMember(Name = "Street")] + public string Street { get; set; } + [DataMember(Name = "HouseNumber")] + public string HouseNumber { get; set; } + [DataMember(Name = "PostalCode")] + public string PostalCode { get; set; } + [DataMember(Name = "Building")] + public string Building { get; set; } + } +} diff --git a/src/Geocoding.MapQuest/Geocoding.MapQuest.csproj b/src/Geocoding.MapQuest/Geocoding.MapQuest.csproj index b73cf90..ae6d998 100644 --- a/src/Geocoding.MapQuest/Geocoding.MapQuest.csproj +++ b/src/Geocoding.MapQuest/Geocoding.MapQuest.csproj @@ -5,7 +5,7 @@ Geocoding.net MapQuest 4.0.0-beta1 chadly - netstandard1.3 + netstandard1.3;net46 Geocoding.MapQuest Geocoding.MapQuest geocoding;geocode;geocoder;maps;address;validation;normalization;google-maps;bing-maps;yahoo-placefinder;mapquest @@ -21,7 +21,7 @@ - + diff --git a/src/Geocoding.MapQuest/MapQuestGeocoder.cs b/src/Geocoding.MapQuest/MapQuestGeocoder.cs index 94038ff..21c43f6 100644 --- a/src/Geocoding.MapQuest/MapQuestGeocoder.cs +++ b/src/Geocoding.MapQuest/MapQuestGeocoder.cs @@ -1,9 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Geocoding.MapQuest @@ -64,17 +65,17 @@ IEnumerable
HandleSingleResponse(IEnumerable locs) } } - public async Task> GeocodeAsync(string address) + public async Task> GeocodeAsync(string address, CancellationToken cancellationToken = default(CancellationToken)) { if (string.IsNullOrWhiteSpace(address)) throw new ArgumentException("address can not be null or empty!"); var f = new GeocodeRequest(key, address) { UseOSM = this.UseOSM }; - MapQuestResponse res = await Execute(f).ConfigureAwait(false); + MapQuestResponse res = await Execute(f, cancellationToken).ConfigureAwait(false); return HandleSingleResponse(res); } - public async Task> GeocodeAsync(string street, string city, string state, string postalCode, string country) + public async Task> GeocodeAsync(string street, string city, string state, string postalCode, string country, CancellationToken cancellationToken = default(CancellationToken)) { var sb = new StringBuilder(); if (!string.IsNullOrWhiteSpace(street)) @@ -98,28 +99,28 @@ public async Task> GeocodeAsync(string street, string city, if (s.Last() == ',') s = s.Remove(s.Length - 1); - return await GeocodeAsync(s).ConfigureAwait(false); + return await GeocodeAsync(s, cancellationToken).ConfigureAwait(false); } - public async Task> ReverseGeocodeAsync(Location location) + public async Task> ReverseGeocodeAsync(Location location, CancellationToken cancellationToken = default(CancellationToken)) { if (location == null) throw new ArgumentNullException("location"); var f = new ReverseGeocodeRequest(key, location) { UseOSM = this.UseOSM }; - MapQuestResponse res = await Execute(f).ConfigureAwait(false); + MapQuestResponse res = await Execute(f, cancellationToken).ConfigureAwait(false); return HandleSingleResponse(res); } - public async Task> ReverseGeocodeAsync(double latitude, double longitude) + public async Task> ReverseGeocodeAsync(double latitude, double longitude, CancellationToken cancellationToken = default(CancellationToken)) { - return await ReverseGeocodeAsync(new Location(latitude, longitude)).ConfigureAwait(false); + return await ReverseGeocodeAsync(new Location(latitude, longitude), cancellationToken).ConfigureAwait(false); } - public async Task Execute(BaseRequest f) + public async Task Execute(BaseRequest f, CancellationToken cancellationToken = default(CancellationToken)) { - HttpWebRequest request = await Send(f).ConfigureAwait(false); - MapQuestResponse r = await Parse(request).ConfigureAwait(false); + HttpWebRequest request = await Send(f, cancellationToken).ConfigureAwait(false); + MapQuestResponse r = await Parse(request, cancellationToken).ConfigureAwait(false); if (r != null && !r.Results.IsNullOrEmpty()) { foreach (MapQuestResult o in r.Results) @@ -142,7 +143,7 @@ public async Task Execute(BaseRequest f) return r; } - private async Task Send(BaseRequest f) + private async Task Send(BaseRequest f, CancellationToken cancellationToken) { if (f == null) throw new ArgumentNullException("f"); @@ -169,7 +170,7 @@ private async Task Send(BaseRequest f) break; } request.Method = f.RequestVerb; - request.ContentType = "application/" + f.InputFormat; + request.ContentType = "application/" + f.InputFormat + "; charset=utf-8"; if (Proxy != null) request.Proxy = Proxy; @@ -178,8 +179,10 @@ private async Task Send(BaseRequest f) { byte[] buffer = Encoding.UTF8.GetBytes(f.RequestBody); //request.Headers.ContentLength = buffer.Length; + using(cancellationToken.Register(request.Abort, false)) using (Stream rs = await request.GetRequestStreamAsync().ConfigureAwait(false)) { + cancellationToken.ThrowIfCancellationRequested(); rs.Write(buffer, 0, buffer.Length); rs.Flush(); } @@ -187,7 +190,7 @@ private async Task Send(BaseRequest f) return request; } - private async Task Parse(HttpWebRequest request) + private async Task Parse(HttpWebRequest request, CancellationToken cancellationToken) { if (request == null) throw new ArgumentNullException("request"); @@ -198,6 +201,7 @@ private async Task Parse(HttpWebRequest request) string json; using (HttpWebResponse response = await request.GetResponseAsync().ConfigureAwait(false) as HttpWebResponse) { + cancellationToken.ThrowIfCancellationRequested(); if ((int)response.StatusCode >= 300) //error throw new Exception((int)response.StatusCode + " " + response.StatusDescription); @@ -230,7 +234,7 @@ private async Task Parse(HttpWebRequest request) } } - public async Task> GeocodeAsync(IEnumerable addresses) + public async Task> GeocodeAsync(IEnumerable addresses, CancellationToken cancellationToken = default(CancellationToken)) { if (addresses == null) throw new ArgumentNullException("addresses"); @@ -243,7 +247,7 @@ group a by a into ag throw new ArgumentException("Atleast one none blank item is required in addresses"); var f = new BatchGeocodeRequest(key, adr) { UseOSM = this.UseOSM }; - MapQuestResponse res = await Execute(f).ConfigureAwait(false); + MapQuestResponse res = await Execute(f, cancellationToken).ConfigureAwait(false); return HandleBatchResponse(res); } @@ -261,9 +265,9 @@ ICollection HandleBatchResponse(MapQuestResponse res) return new ResultItem[0]; } - public Task> ReverseGeocodeAsync(IEnumerable locations) + public Task> ReverseGeocodeAsync(IEnumerable locations, CancellationToken cancellationToken = default(CancellationToken)) { throw new NotSupportedException("ReverseGeocodeAsync(...) is not available for MapQuestGeocoder."); } } -} \ No newline at end of file +} diff --git a/src/Geocoding.MapQuest/Quality.cs b/src/Geocoding.MapQuest/Quality.cs index e53cbb8..1940f8d 100644 --- a/src/Geocoding.MapQuest/Quality.cs +++ b/src/Geocoding.MapQuest/Quality.cs @@ -31,25 +31,25 @@ public enum Quality : int /// ZIP = 5, /// + /// A6 Admin area. For USA, a neighborhood. + /// + NEIGHBORHOOD = 6, + /// /// A5 Admin area. For USA, a city. /// - CITY = 6, + CITY = 7, /// /// A4 Admin area. For USA, a county. /// - COUNTY = 7, + COUNTY = 8, /// /// A3 Admin area. For USA, a state. /// - STATE = 8, + STATE = 9, /// /// A1 Admin area, largest. For USA, a country. /// - COUNTRY = 9, - /// - /// Admin area. For USA, a neighborhood. - /// - NEIGHBORHOOD = 10, + COUNTRY = 10, UNKNOWN = 11 } diff --git a/src/Geocoding.Microsoft/BingGeocodingException.cs b/src/Geocoding.Microsoft/BingGeocodingException.cs index 2a8554a..c90e2de 100644 --- a/src/Geocoding.Microsoft/BingGeocodingException.cs +++ b/src/Geocoding.Microsoft/BingGeocodingException.cs @@ -1,8 +1,9 @@ using System; +using Geocoding.Core; namespace Geocoding.Microsoft { - public class BingGeocodingException : Exception + public class BingGeocodingException : GeocodingException { const string defaultMessage = "There was an error processing the geocoding request. See InnerException for more information."; diff --git a/src/Geocoding.Microsoft/BingMapsGeocoder.cs b/src/Geocoding.Microsoft/BingMapsGeocoder.cs index e0d42bf..3b32c1b 100644 --- a/src/Geocoding.Microsoft/BingMapsGeocoder.cs +++ b/src/Geocoding.Microsoft/BingMapsGeocoder.cs @@ -6,6 +6,7 @@ using System.Net.Http; using System.Runtime.Serialization.Json; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Geocoding.Microsoft @@ -23,6 +24,7 @@ public class BingMapsGeocoder : IGeocoder const string ZIP = "postalCode={0}"; const string CITY = "locality={0}"; const string ADDRESS = "addressLine={0}"; + const int BINGMAXRESULTSVALUE = 20; readonly string bingKey; @@ -32,6 +34,7 @@ public class BingMapsGeocoder : IGeocoder public Bounds UserMapView { get; set; } public IPAddress UserIP { get; set; } public bool IncludeNeighborhood { get; set; } + public int? MaxResults { get; set; } public BingMapsGeocoder(string bingKey) { @@ -88,6 +91,9 @@ private IEnumerable> GetGlobalParameters() if (IncludeNeighborhood) yield return new KeyValuePair("inclnb", IncludeNeighborhood ? "1" : "0"); + + if (MaxResults != null && MaxResults.Value > 0) + yield return new KeyValuePair("maxResults", Math.Min(MaxResults.Value, BINGMAXRESULTSVALUE).ToString()); } private bool AppendGlobalParameters(StringBuilder parameters, bool first) @@ -114,12 +120,12 @@ private string BuildQueryString(IEnumerable> parame return builder.ToString(); } - public async Task> GeocodeAsync(string address) + public async Task> GeocodeAsync(string address, CancellationToken cancellationToken = default(CancellationToken)) { try { var url = GetQueryUrl(address); - var response = await GetResponse(url).ConfigureAwait(false); + var response = await GetResponse(url, cancellationToken).ConfigureAwait(false); return ParseResponse(response); } catch (Exception ex) @@ -128,12 +134,12 @@ public async Task> GeocodeAsync(string address) } } - public async Task> GeocodeAsync(string street, string city, string state, string postalCode, string country) + public async Task> GeocodeAsync(string street, string city, string state, string postalCode, string country, CancellationToken cancellationToken = default(CancellationToken)) { try { var url = GetQueryUrl(street, city, state, postalCode, country); - var response = await GetResponse(url).ConfigureAwait(false); + var response = await GetResponse(url, cancellationToken).ConfigureAwait(false); return ParseResponse(response); } catch (Exception ex) @@ -142,20 +148,20 @@ public async Task> GeocodeAsync(string street, string c } } - public async Task> ReverseGeocodeAsync(Location location) + public async Task> ReverseGeocodeAsync(Location location, CancellationToken cancellationToken = default(CancellationToken)) { if (location == null) throw new ArgumentNullException("location"); - return await ReverseGeocodeAsync(location.Latitude, location.Longitude).ConfigureAwait(false); + return await ReverseGeocodeAsync(location.Latitude, location.Longitude, cancellationToken).ConfigureAwait(false); } - public async Task> ReverseGeocodeAsync(double latitude, double longitude) + public async Task> ReverseGeocodeAsync(double latitude, double longitude, CancellationToken cancellationToken = default(CancellationToken)) { try { var url = GetQueryUrl(latitude, longitude); - var response = await GetResponse(url).ConfigureAwait(false); + var response = await GetResponse(url, cancellationToken).ConfigureAwait(false); return ParseResponse(response); } catch (Exception ex) @@ -164,24 +170,24 @@ public async Task> ReverseGeocodeAsync(double latitude, } } - async Task> IGeocoder.GeocodeAsync(string address) + async Task> IGeocoder.GeocodeAsync(string address, CancellationToken cancellationToken) { - return await GeocodeAsync(address).ConfigureAwait(false); + return await GeocodeAsync(address, cancellationToken).ConfigureAwait(false); } - async Task> IGeocoder.GeocodeAsync(string street, string city, string state, string postalCode, string country) + async Task> IGeocoder.GeocodeAsync(string street, string city, string state, string postalCode, string country, CancellationToken cancellationToken) { - return await GeocodeAsync(street, city, state, postalCode, country).ConfigureAwait(false); + return await GeocodeAsync(street, city, state, postalCode, country, cancellationToken).ConfigureAwait(false); } - async Task> IGeocoder.ReverseGeocodeAsync(Location location) + async Task> IGeocoder.ReverseGeocodeAsync(Location location, CancellationToken cancellationToken) { - return await ReverseGeocodeAsync(location).ConfigureAwait(false); + return await ReverseGeocodeAsync(location, cancellationToken).ConfigureAwait(false); } - async Task> IGeocoder.ReverseGeocodeAsync(double latitude, double longitude) + async Task> IGeocoder.ReverseGeocodeAsync(double latitude, double longitude, CancellationToken cancellationToken) { - return await ReverseGeocodeAsync(latitude, longitude).ConfigureAwait(false); + return await ReverseGeocodeAsync(latitude, longitude, cancellationToken).ConfigureAwait(false); } private bool AppendParameter(StringBuilder sb, string parameter, string format, bool first) @@ -237,11 +243,11 @@ HttpClient BuildClient() return new HttpClient(handler); } - private async Task GetResponse(string queryURL) + private async Task GetResponse(string queryURL, CancellationToken cancellationToken) { using (var client = BuildClient()) { - var response = await client.SendAsync(CreateRequest(queryURL)).ConfigureAwait(false); + var response = await client.SendAsync(CreateRequest(queryURL), cancellationToken).ConfigureAwait(false); using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) { DataContractJsonSerializer jsonSerializer = new DataContractJsonSerializer(typeof(Json.Response)); diff --git a/src/Geocoding.Microsoft/Geocoding.Microsoft.csproj b/src/Geocoding.Microsoft/Geocoding.Microsoft.csproj index 27c9f97..06e74be 100644 --- a/src/Geocoding.Microsoft/Geocoding.Microsoft.csproj +++ b/src/Geocoding.Microsoft/Geocoding.Microsoft.csproj @@ -5,7 +5,7 @@ Geocoding.net Microsoft 4.0.0-beta1 chadly - netstandard1.3 + netstandard1.3;net46 Geocoding.Microsoft Geocoding.Microsoft geocoding;geocode;geocoder;maps;address;validation;normalization;google-maps;bing-maps;yahoo-placefinder;mapquest @@ -23,8 +23,15 @@ + + + + + + + diff --git a/src/Geocoding.Yahoo/Geocoding.Yahoo.csproj b/src/Geocoding.Yahoo/Geocoding.Yahoo.csproj index a9ba228..dfa1e88 100644 --- a/src/Geocoding.Yahoo/Geocoding.Yahoo.csproj +++ b/src/Geocoding.Yahoo/Geocoding.Yahoo.csproj @@ -5,7 +5,7 @@ Geocoding.net Yahoo 4.0.0-beta1 chadly - netstandard1.3 + netstandard1.3;net46 Geocoding.Yahoo Geocoding.Yahoo geocoding;geocode;geocoder;maps;address;validation;normalization;google-maps;bing-maps;yahoo-placefinder;mapquest @@ -21,7 +21,7 @@ - + diff --git a/src/Geocoding.Yahoo/YahooGeocoder.cs b/src/Geocoding.Yahoo/YahooGeocoder.cs index 279d3dc..7d2a32b 100644 --- a/src/Geocoding.Yahoo/YahooGeocoder.cs +++ b/src/Geocoding.Yahoo/YahooGeocoder.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Net; +using System.Threading; using System.Threading.Tasks; using System.Xml.XPath; @@ -44,7 +45,7 @@ public YahooGeocoder(string consumerKey, string consumerSecret) this.consumerSecret = consumerSecret; } - public async Task> GeocodeAsync(string address) + public async Task> GeocodeAsync(string address, CancellationToken cancellationToken = default(CancellationToken)) { if (string.IsNullOrEmpty(address)) throw new ArgumentNullException("address"); @@ -52,39 +53,41 @@ public async Task> GeocodeAsync(string address) string url = string.Format(ServiceUrl, WebUtility.UrlEncode(address)); HttpWebRequest request = BuildWebRequest(url); - return await ProcessRequest(request).ConfigureAwait(false); + return await ProcessRequest(request, cancellationToken).ConfigureAwait(false); } - public async Task> GeocodeAsync(string street, string city, string state, string postalCode, string country) + public async Task> GeocodeAsync(string street, string city, string state, string postalCode, string country, CancellationToken cancellationToken = default(CancellationToken)) { string url = string.Format(ServiceUrlNormal, WebUtility.UrlEncode(street), WebUtility.UrlEncode(city), WebUtility.UrlEncode(state), WebUtility.UrlEncode(postalCode), WebUtility.UrlEncode(country)); HttpWebRequest request = BuildWebRequest(url); - return await ProcessRequest(request).ConfigureAwait(false); + return await ProcessRequest(request, cancellationToken).ConfigureAwait(false); } - public async Task> ReverseGeocodeAsync(Location location) + public async Task> ReverseGeocodeAsync(Location location, CancellationToken cancellationToken = default(CancellationToken)) { if (location == null) throw new ArgumentNullException("location"); - return await ReverseGeocodeAsync(location.Latitude, location.Longitude).ConfigureAwait(false); + return await ReverseGeocodeAsync(location.Latitude, location.Longitude, cancellationToken).ConfigureAwait(false); } - public async Task> ReverseGeocodeAsync(double latitude, double longitude) + public async Task> ReverseGeocodeAsync(double latitude, double longitude, CancellationToken cancellationToken = default(CancellationToken)) { string url = string.Format(ServiceUrlReverse, string.Format(CultureInfo.InvariantCulture, "{0} {1}", latitude, longitude)); HttpWebRequest request = BuildWebRequest(url); - return await ProcessRequest(request).ConfigureAwait(false); + return await ProcessRequest(request, cancellationToken).ConfigureAwait(false); } - private async Task> ProcessRequest(HttpWebRequest request) + private async Task> ProcessRequest(HttpWebRequest request, CancellationToken cancellationToken) { try { + using(cancellationToken.Register(request.Abort, false)) using (WebResponse response = await request.GetResponseAsync().ConfigureAwait(false)) { + cancellationToken.ThrowIfCancellationRequested(); return ProcessWebResponse(response); } } @@ -100,24 +103,24 @@ private async Task> ProcessRequest(HttpWebRequest requ } } - async Task> IGeocoder.GeocodeAsync(string address) + async Task> IGeocoder.GeocodeAsync(string address, CancellationToken cancellationToken) { - return await GeocodeAsync(address).ConfigureAwait(false); + return await GeocodeAsync(address, cancellationToken).ConfigureAwait(false); } - async Task> IGeocoder.GeocodeAsync(string street, string city, string state, string postalCode, string country) + async Task> IGeocoder.GeocodeAsync(string street, string city, string state, string postalCode, string country, CancellationToken cancellationToken) { - return await GeocodeAsync(street, city, state, postalCode, country).ConfigureAwait(false); + return await GeocodeAsync(street, city, state, postalCode, country, cancellationToken).ConfigureAwait(false); } - async Task> IGeocoder.ReverseGeocodeAsync(Location location) + async Task> IGeocoder.ReverseGeocodeAsync(Location location, CancellationToken cancellationToken) { - return await ReverseGeocodeAsync(location).ConfigureAwait(false); + return await ReverseGeocodeAsync(location, cancellationToken).ConfigureAwait(false); } - async Task> IGeocoder.ReverseGeocodeAsync(double latitude, double longitude) + async Task> IGeocoder.ReverseGeocodeAsync(double latitude, double longitude, CancellationToken cancellationToken) { - return await ReverseGeocodeAsync(latitude, longitude).ConfigureAwait(false); + return await ReverseGeocodeAsync(latitude, longitude, cancellationToken).ConfigureAwait(false); } private HttpWebRequest BuildWebRequest(string url) diff --git a/src/Geocoding.Yahoo/YahooGeocodingException.cs b/src/Geocoding.Yahoo/YahooGeocodingException.cs index 53dcb0c..3d79c10 100644 --- a/src/Geocoding.Yahoo/YahooGeocodingException.cs +++ b/src/Geocoding.Yahoo/YahooGeocodingException.cs @@ -1,8 +1,9 @@ using System; +using Geocoding.Core; namespace Geocoding.Yahoo { - public class YahooGeocodingException : Exception + public class YahooGeocodingException : GeocodingException { const string defaultMessage = "There was an error processing the geocoding request. See ErrorCode or InnerException for more information."; diff --git a/test/Geocoding.Tests/AsyncGeocoderTest.cs b/test/Geocoding.Tests/AsyncGeocoderTest.cs index 226f87b..513ac88 100644 --- a/test/Geocoding.Tests/AsyncGeocoderTest.cs +++ b/test/Geocoding.Tests/AsyncGeocoderTest.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.Globalization; +using System.Linq; using System.Threading.Tasks; using Xunit; @@ -11,7 +12,7 @@ public abstract class AsyncGeocoderTest public AsyncGeocoderTest() { - //Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("en-us"); + CultureInfo.CurrentCulture = new CultureInfo("en-us"); asyncGeocoder = CreateAsyncGeocoder(); } @@ -19,23 +20,17 @@ public AsyncGeocoderTest() protected abstract IGeocoder CreateAsyncGeocoder(); [Fact] - public void CanGeocodeAddress() + public async Task CanGeocodeAddress() { - asyncGeocoder.GeocodeAsync("1600 pennsylvania ave washington dc").ContinueWith(task => - { - Address[] addresses = task.Result.ToArray(); - addresses[0].AssertWhiteHouse(); - }); + var addresses = await asyncGeocoder.GeocodeAsync("1600 pennsylvania ave washington dc"); + addresses.First().AssertWhiteHouse(); } [Fact] - public void CanGeocodeNormalizedAddress() + public async Task CanGeocodeNormalizedAddress() { - asyncGeocoder.GeocodeAsync("1600 pennsylvania ave", "washington", "dc", null, null).ContinueWith(task => - { - Address[] addresses = task.Result.ToArray(); - addresses[0].AssertWhiteHouse(); - }); + var addresses = await asyncGeocoder.GeocodeAsync("1600 pennsylvania ave", "washington", "dc", null, null); + addresses.First().AssertWhiteHouse(); } [Theory] @@ -43,11 +38,10 @@ public void CanGeocodeNormalizedAddress() [InlineData("cs-CZ")] public async Task CanGeocodeAddressUnderDifferentCultures(string cultureName) { - //Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(cultureName); + CultureInfo.CurrentCulture = new CultureInfo(cultureName); - var result = await asyncGeocoder.GeocodeAsync("24 sussex drive ottawa, ontario"); - Address[] addresses = result.ToArray(); - addresses[0].AssertCanadianPrimeMinister(); + var addresses = await asyncGeocoder.GeocodeAsync("24 sussex drive ottawa, ontario"); + addresses.First().AssertCanadianPrimeMinister(); } [Theory] @@ -55,43 +49,38 @@ public async Task CanGeocodeAddressUnderDifferentCultures(string cultureName) [InlineData("cs-CZ")] public async Task CanReverseGeocodeAddressUnderDifferentCultures(string cultureName) { - //Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(cultureName); + CultureInfo.CurrentCulture = new CultureInfo(cultureName); - var result = await asyncGeocoder.ReverseGeocodeAsync(38.8976777, -77.036517); - Address[] addresses = result.ToArray(); - addresses[0].AssertWhiteHouseArea(); + var addresses = await asyncGeocoder.ReverseGeocodeAsync(38.8976777, -77.036517); + addresses.First().AssertWhiteHouseArea(); } [Fact] - public void ShouldNotBlowUpOnBadAddress() + public async Task ShouldNotBlowUpOnBadAddress() { - asyncGeocoder.GeocodeAsync("sdlkf;jasl;kjfldksjfasldf").ContinueWith(task => - { - var addresses = task.Result; - Assert.Empty(addresses); - }); + var addresses = await asyncGeocoder.GeocodeAsync("sdlkf;jasl;kjfldksjfasldf"); + Assert.Empty(addresses); } [Fact] - public void CanGeocodeWithSpecialCharacters() + public async Task CanGeocodeWithSpecialCharacters() { - asyncGeocoder.GeocodeAsync("Fried St & 2nd St, Gretna, LA 70053").ContinueWith(task => - { - var addresses = task.Result; + var addresses = await asyncGeocoder.GeocodeAsync("Fried St & 2nd St, Gretna, LA 70053"); + Assert.NotEmpty(addresses); + } - //asserting no exceptions are thrown and that we get something - Assert.NotEmpty(addresses); - }); + [Fact] + public async Task CanGeocodeWithUnicodeCharacters() + { + var addresses = await asyncGeocoder.GeocodeAsync("Étretat, France"); + Assert.NotEmpty(addresses); } [Fact] - public void CanReverseGeocodeAsync() + public async Task CanReverseGeocodeAsync() { - asyncGeocoder.ReverseGeocodeAsync(38.8976777, -77.036517).ContinueWith(task => - { - Address[] addresses = task.Result.ToArray(); - addresses[0].AssertWhiteHouseArea(); - }); + var addresses = await asyncGeocoder.ReverseGeocodeAsync(38.8976777, -77.036517); + addresses.First().AssertWhiteHouse(); } } } diff --git a/test/Geocoding.Tests/BingMapsTest.cs b/test/Geocoding.Tests/BingMapsTest.cs index a99af53..fe13771 100644 --- a/test/Geocoding.Tests/BingMapsTest.cs +++ b/test/Geocoding.Tests/BingMapsTest.cs @@ -38,7 +38,7 @@ public async Task ApplyUserLocation(string address, double userLatitude, double { geoCoder.UserLocation = new Location(userLatitude, userLongitude); BingAddress[] addresses = (await geoCoder.GeocodeAsync(address)).ToArray(); - Assert.Equal(country, addresses[0].CountryRegion); + Assert.Contains(addresses, x => x.CountryRegion == country); } [Theory] @@ -48,8 +48,9 @@ public async Task ApplyUserLocation(string address, double userLatitude, double public async Task ApplyUserMapView(string address, double userLatitude1, double userLongitude1, double userLatitude2, double userLongitude2, string country) { geoCoder.UserMapView = new Bounds(userLatitude1, userLongitude1, userLatitude2, userLongitude2); + geoCoder.MaxResults = 20; BingAddress[] addresses = (await geoCoder.GeocodeAsync(address)).ToArray(); - Assert.Equal(country, addresses[0].CountryRegion); + Assert.Contains(addresses, x => x.CountryRegion == country); } [Theory] diff --git a/test/Geocoding.Tests/GeocoderTest.cs b/test/Geocoding.Tests/GeocoderTest.cs index 82be0a9..5b0a4e9 100644 --- a/test/Geocoding.Tests/GeocoderTest.cs +++ b/test/Geocoding.Tests/GeocoderTest.cs @@ -70,6 +70,7 @@ public virtual async Task ShouldNotBlowUpOnBadAddress() [InlineData("40 1/2 Road")] [InlineData("B's Farm RD")] [InlineData("Wilshire & Bundy Plaza, Los Angeles")] + [InlineData("Étretat, France")] public virtual async Task CanGeocodeWithSpecialCharacters(string address) { Address[] addresses = (await geocoder.GeocodeAsync(address)).ToArray(); @@ -106,4 +107,4 @@ public virtual async Task CanGeocodeInvalidZipCodes(string address) Assert.NotEmpty(addresses); } } -} \ No newline at end of file +} diff --git a/test/Geocoding.Tests/Geocoding.Tests.csproj b/test/Geocoding.Tests/Geocoding.Tests.csproj index 9a37e78..7bda7e6 100644 --- a/test/Geocoding.Tests/Geocoding.Tests.csproj +++ b/test/Geocoding.Tests/Geocoding.Tests.csproj @@ -3,11 +3,11 @@ Geocoding.net Tests 4.0.0-beta1 - netcoreapp1.1 + netcoreapp2.1 Geocoding.Tests Geocoding.Tests true - 1.1.1 + 2.1.0 4.0.0 Tests @@ -20,6 +20,7 @@ + diff --git a/test/Geocoding.Tests/GoogleAsyncGeocoderTest.cs b/test/Geocoding.Tests/GoogleAsyncGeocoderTest.cs index 4f99a21..1e317a1 100644 --- a/test/Geocoding.Tests/GoogleAsyncGeocoderTest.cs +++ b/test/Geocoding.Tests/GoogleAsyncGeocoderTest.cs @@ -9,14 +9,8 @@ namespace Geocoding.Tests [Collection("Settings")] public class GoogleAsyncGeocoderTest : AsyncGeocoderTest { - readonly SettingsFixture settings; GoogleGeocoder geoCoder; - public GoogleAsyncGeocoderTest(SettingsFixture settings) - { - this.settings = settings; - } - protected override IGeocoder CreateAsyncGeocoder() { string apiKey = settings.GoogleApiKey; @@ -38,7 +32,7 @@ protected override IGeocoder CreateAsyncGeocoder() [InlineData("Illinois, US", GoogleAddressType.AdministrativeAreaLevel1)] [InlineData("New York, New York", GoogleAddressType.Locality)] [InlineData("90210, US", GoogleAddressType.PostalCode)] - [InlineData("1600 pennsylvania ave washington dc", GoogleAddressType.StreetAddress)] + [InlineData("1600 pennsylvania ave washington dc", GoogleAddressType.Establishment)] public async Task CanParseAddressTypes(string address, GoogleAddressType type) { var result = await geoCoder.GeocodeAsync(address); diff --git a/test/Geocoding.Tests/GoogleGeocoderTest.cs b/test/Geocoding.Tests/GoogleGeocoderTest.cs index ae80190..8cdcc6c 100644 --- a/test/Geocoding.Tests/GoogleGeocoderTest.cs +++ b/test/Geocoding.Tests/GoogleGeocoderTest.cs @@ -36,7 +36,7 @@ protected override IGeocoder CreateGeocoder() [InlineData("Illinois, US", GoogleAddressType.AdministrativeAreaLevel1)] [InlineData("New York, New York", GoogleAddressType.Locality)] [InlineData("90210, US", GoogleAddressType.PostalCode)] - [InlineData("1600 pennsylvania ave washington dc", GoogleAddressType.StreetAddress)] + [InlineData("1600 pennsylvania ave washington dc", GoogleAddressType.Establishment)] [InlineData("muswellbrook 2 New South Wales Australia", GoogleAddressType.Unknown)] public async Task CanParseAddressTypes(string address, GoogleAddressType type) { diff --git a/test/Geocoding.Tests/HereAsyncGeocoderTest.cs b/test/Geocoding.Tests/HereAsyncGeocoderTest.cs new file mode 100644 index 0000000..7bbded6 --- /dev/null +++ b/test/Geocoding.Tests/HereAsyncGeocoderTest.cs @@ -0,0 +1,16 @@ +using Geocoding.Here; +using Xunit; + +namespace Geocoding.Tests +{ + [Collection("Settings")] + public class HereAsyncGeocoderTest : AsyncGeocoderTest + { + HereGeocoder geoCoder; + + protected override IGeocoder CreateAsyncGeocoder() + { + return new HereGeocoder(settings.HereAppId, settings.HereAppCode); + } + } +} diff --git a/test/Geocoding.Tests/MapQuestAsyncGeocoderTest.cs b/test/Geocoding.Tests/MapQuestAsyncGeocoderTest.cs new file mode 100644 index 0000000..0c29046 --- /dev/null +++ b/test/Geocoding.Tests/MapQuestAsyncGeocoderTest.cs @@ -0,0 +1,17 @@ +using Geocoding.MapQuest; +using Xunit; + +namespace Geocoding.Tests +{ + [Collection("Settings")] + public class MapQuestAsyncGeocoderTest : AsyncGeocoderTest + { + protected override IGeocoder CreateAsyncGeocoder() + { + return new MapQuestGeocoder(settings.MapQuestKey) + { + UseOSM = false + }; + } + } +} diff --git a/test/Geocoding.Tests/MapQuestGeocoderTest.cs b/test/Geocoding.Tests/MapQuestGeocoderTest.cs index 688270c..f6cb806 100644 --- a/test/Geocoding.Tests/MapQuestGeocoderTest.cs +++ b/test/Geocoding.Tests/MapQuestGeocoderTest.cs @@ -1,4 +1,7 @@ -using Geocoding.MapQuest; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Geocoding.MapQuest; using Xunit; namespace Geocoding.Tests @@ -6,15 +9,27 @@ namespace Geocoding.Tests [Collection("Settings")] public class MapQuestGeocoderTest : GeocoderTest { + MapQuestGeocoder geocoder; + public MapQuestGeocoderTest(SettingsFixture settings) : base(settings) { } protected override IGeocoder CreateGeocoder() { - return new MapQuestGeocoder(settings.MapQuestKey) + geocoder = new MapQuestGeocoder(settings.MapQuestKey) { UseOSM = false }; + return geocoder; } + + [Fact] + public virtual async Task CanGeocodeNeighborhood() + { + // Regression test: Addresses with Quality=NEIGHBORHOOD are not returned + Address[] addresses = (await geocoder.GeocodeAsync("North Sydney, New South Wales, Australia")).ToArray(); + Assert.NotEmpty(addresses); + } + } } diff --git a/test/Geocoding.Tests/SettingsFixture.cs b/test/Geocoding.Tests/SettingsFixture.cs index 74fdfca..ef8f78e 100644 --- a/test/Geocoding.Tests/SettingsFixture.cs +++ b/test/Geocoding.Tests/SettingsFixture.cs @@ -39,6 +39,16 @@ public string MapQuestKey { get { return config.GetValue("mapQuestKey"); } } + + public string HereAppId + { + get { return config.GetValue("hereAppId"); } + } + + public string HereAppCode + { + get { return config.GetValue("hereAppCode"); } + } } [CollectionDefinition("Settings")] diff --git a/test/Geocoding.Tests/settings.json b/test/Geocoding.Tests/settings.json index 1e5608d..47e5f12 100644 --- a/test/Geocoding.Tests/settings.json +++ b/test/Geocoding.Tests/settings.json @@ -6,5 +6,8 @@ "googleApiKey": "", - "mapQuestKey": "" + "mapQuestKey": "", + + "hereAppId": "", + "hereAppCode": "" }