Skip to content

Commit

Permalink
add documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
DmyMi committed Aug 24, 2022
1 parent c66a0ee commit 785f9ad
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 77 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#nullable enable

using Microsoft.AspNetCore.Mvc;
using OutOfSchool.Common.Models;
using OutOfSchool.WebApi.Models.Geocoding;
Expand All @@ -11,30 +13,36 @@ public class GeocodingController : Controller
{
private readonly IGeocodingService geocodingService;

public GeocodingController(IGeocodingService geocodingService)
public GeocodingController(IGeocodingService? geocodingService)
{
this.geocodingService = geocodingService ?? throw new ArgumentNullException(nameof(geocodingService));
}

/// <summary>
/// Get geocoding or reverse geocoding information.
/// </summary>
/// <param name="request">Coordinates query.</param>
/// <returns> The geocoding information about address or coordinates. </returns>
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<GeocodingResponse>))]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[HttpPost]
public async Task<IActionResult> Geocoding(GeocodingRequest request)
{
var result = await Validate(request)
.FlatMapAsync(async r =>
.FlatMapAsync(r =>
r.IsReverse
? await geocodingService.GetReverseGeocodingInfo(r)
: await geocodingService.GetGeocodingInfo(r));
? geocodingService.GetReverseGeocodingInfo(r)
: geocodingService.GetGeocodingInfo(r));

return result.Match<IActionResult>(
error => error.HttpStatusCode switch
{
HttpStatusCode.BadRequest => BadRequest(error.Message),
_ => StatusCode((int)error.HttpStatusCode),
},
r => r is null ? NoContent() : Ok(r));
r => r is not null ? Ok(r) : NoContent());
}

private Either<ErrorResponse, GeocodingRequest> Validate(GeocodingRequest request)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using Newtonsoft.Json;

namespace OutOfSchool.WebApi.Models.Geocoding;

public class GeocodingRequest
Expand Down
162 changes: 93 additions & 69 deletions OutOfSchool/OutOfSchool.WebApi/Services/GeocodingService.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
#nullable enable

using AutoMapper;
using Microsoft.Extensions.Options;
using OutOfSchool.Common.Enums;
using OutOfSchool.Common.Models;
using OutOfSchool.WebApi.Models.Codeficator;
using OutOfSchool.WebApi.Models.Geocoding;

namespace OutOfSchool.WebApi.Services;

/// <summary>
/// Implements the interface with geocoding functionality.
/// </summary>
public class GeocodingService : CommunicationService, IGeocodingService
{
private const double SearchBoundsRadiusMeters = 50000.0;
Expand All @@ -13,88 +20,99 @@ public class GeocodingService : CommunicationService, IGeocodingService
private readonly IMapper mapper;

public GeocodingService(
IOptions<GeocodingConfig> options,
IOptions<GeocodingConfig>? options,
IHttpClientFactory httpClientFactory,
IOptions<CommunicationConfig> communicationConfig,
ICodeficatorService codeficatorService,
IMapper mapper,
ICodeficatorService? codeficatorService,
IMapper? mapper,
ILogger<GeocodingService> logger)
: base(httpClientFactory, communicationConfig.Value, logger)
{
config = options.Value;
this.mapper = mapper;
this.codeficatorService = codeficatorService;
config = options?.Value ?? throw new ArgumentNullException(nameof(options));
this.mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
this.codeficatorService = codeficatorService ?? throw new ArgumentNullException(nameof(codeficatorService));
}

/// <inheritdoc/>
public async Task<Either<ErrorResponse, GeocodingResponse?>> GetGeocodingInfo(GeocodingRequest request)
{
AllAddressPartsDto? address;
try
{
// Get codeficator from DB
var address = await codeficatorService.GetAllAddressPartsById(request.CATOTTGId);

if (address is null)
address = await codeficatorService.GetAllAddressPartsById(request.CATOTTGId);
}
catch (Exception e)
{
Logger.LogError(e, "Unable to retrieve codeficator");
return new ErrorResponse
{
return new ErrorResponse
{
HttpStatusCode = HttpStatusCode.BadRequest,
Message = "No city data available",
};
}

// If codeficator entry is a city's district - get it's parent (city) coordinates
// TODO: check if districts have their own coordinates
var lat = address.AddressParts.Category != "B" ? address.AddressParts.Latitude : address.AddressParts.Parent.Latitude;
var lon = address.AddressParts.Category != "B" ? address.AddressParts.Longitude : address.AddressParts.Parent.Longitude;

// Create bounds to limit the search are to certain coordinates square
// should help minimize response data for cities & streets with equal name
// TODO: 50km is 2x time more then enough for Kyiv
var bounds = new RectangularBounds(lat, lon, SearchBoundsRadiusMeters);

var req = new Request
Message = "Unable to retrieve codeficator data",
};
}

if (address is null)
{
return new ErrorResponse
{
HttpMethodType = HttpMethodType.Get,
Url = new Uri(config.BaseUrl),
Query = new Dictionary<string, string>
{
{"key", config.ApiKey},
{"categories", "adr_address"},
{"text", $"{address.AddressParts.Name}, {request.Street}, {request.BuildingNumber}"},
{ "contains", bounds.WKT },
},
HttpStatusCode = HttpStatusCode.BadRequest,
Message = "No city data available",
};
}

var response = await SendRequest<GeocodingApiResponse>(req);
// If codeficator entry is a city's district - get it's parent (city) coordinates
var lat = address.AddressParts.Category != CodeficatorCategory.CityDistrict.Name
? address.AddressParts.Latitude
: address.AddressParts.Parent.Latitude;
var lon = address.AddressParts.Category != CodeficatorCategory.CityDistrict.Name
? address.AddressParts.Longitude
: address.AddressParts.Parent.Longitude;

return response.Map(r =>
// Create bounds to limit the search are to certain coordinates square
// should help minimize response data for cities & streets with equal name
// 50km is 2x time more then enough for Kyiv
var bounds = new RectangularBounds(lat, lon, SearchBoundsRadiusMeters);

var req = new Request
{
HttpMethodType = HttpMethodType.Get,
Url = new Uri(config.BaseUrl),
Query = new Dictionary<string, string>
{
var result = new GeocodingResponse
{
CATOTTGId = request.CATOTTGId,
Settlement = address.AddressParts.Name,
};
{ "key", config.ApiKey },
{ "categories", "adr_address" },
{ "text", $"{address.AddressParts.Name}, {request.Street}, {request.BuildingNumber}" },
{ "contains", bounds.WKT },
},
};

return r switch
{
GeocodingSingleFeatureResponse single => mapper.Map(single, result),
var response = await SendRequest<GeocodingApiResponse>(req);

// TODO: for now take the most relevant (sorted by API) address, might need to change if frontend has issues
GeocodingListFeatureResponse multi => multi.Features.Any() ? mapper.Map(multi.Features.First(), result) : null,
GeocodingEmptyResponse => null,
_ => null
};
});
}
catch (Exception e)
return response.Map(r =>
{
Logger.LogError(e, e.Message);
// TODO: normal error
return new ErrorResponse();
}
var result = new GeocodingResponse
{
CATOTTGId = request.CATOTTGId,
Settlement = address.AddressParts.Name,
};

return r switch
{
GeocodingSingleFeatureResponse single => mapper.Map(single, result),

// for now take the most relevant (sorted by API) address,
// might need to change if frontend has issues
GeocodingListFeatureResponse multi => multi.Features.Any()
? mapper.Map(multi.Features.First(), result)
: null,
GeocodingEmptyResponse => null,
_ => null
};
});
}

public async Task<Either<ErrorResponse, GeocodingResponse>> GetReverseGeocodingInfo(
/// <inheritdoc/>
public async Task<Either<ErrorResponse, GeocodingResponse?>> GetReverseGeocodingInfo(
GeocodingRequest request)
{
var req = new Request
Expand All @@ -103,23 +121,24 @@ public async Task<Either<ErrorResponse, GeocodingResponse>> GetReverseGeocodingI
Url = new Uri(config.BaseUrl),
Query = new Dictionary<string, string>
{
{"key", config.ApiKey},
{"categories", "adr_address"},
{"radius", $"{config.Radius}"},
{"near", $"{request.Lon},{request.Lat}"}, // it has to be lng first, lat second as per api
{"order", "distance"},
{ "key", config.ApiKey },
{ "categories", "adr_address" },
{ "radius", $"{config.Radius}" },
{ "near", $"{request.Lon},{request.Lat}" }, // it has to be lng first, lat second as per api
{ "order", "distance" },
},
};
var response = await SendRequest<GeocodingApiResponse>(req).ConfigureAwait(false);
return await response
.Map(r => r switch
{
GeocodingSingleFeatureResponse single => mapper.Map<GeocodingResponse>(single),
GeocodingListFeatureResponse multi => multi.Features.Select(mapper.Map<GeocodingResponse>).FirstOrDefault(),
GeocodingListFeatureResponse multi => multi.Features.Select(mapper.Map<GeocodingResponse>)
.FirstOrDefault(),
GeocodingEmptyResponse => null,
_ => null
})
.FlatMapAsync<GeocodingResponse>(async r =>
.FlatMapAsync<GeocodingResponse?>(async r =>
{
if (r is null)
{
Expand Down Expand Up @@ -147,14 +166,19 @@ public async Task<Either<ErrorResponse, GeocodingResponse>> GetReverseGeocodingI
}
catch (Exception e)
{
Logger.LogError(e, e.Message);
// TODO: normal error
return new ErrorResponse();
Logger.LogError(e, "Unable to retrieve codeficator");
return new ErrorResponse
{
Message = "Unable to retrieve codeficator data",
};
}
});
}
}

/// <summary>
/// Container for well known text representation of square bound around a point.
/// </summary>
public class RectangularBounds
{
private const double EarthRadius = 6378.137;
Expand Down
17 changes: 16 additions & 1 deletion OutOfSchool/OutOfSchool.WebApi/Services/IGeocodingService.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
#nullable enable

using OutOfSchool.Common.Models;
using OutOfSchool.WebApi.Models.Geocoding;

namespace OutOfSchool.WebApi.Services;

/// <summary>
/// Defines interface for geocoding functionality.
/// </summary>
public interface IGeocodingService
{
/// <summary>
/// Get coordinates by address.
/// </summary>
/// <param name="request">Geocoding request. </param>
/// <returns>The task result contains the <see cref="Either{ErrorResponse, GeocodingResponse}"/>.</returns>
public Task<Either<ErrorResponse, GeocodingResponse?>> GetGeocodingInfo(GeocodingRequest request);

public Task<Either<ErrorResponse, GeocodingResponse>> GetReverseGeocodingInfo(
/// <summary>
/// Get address by coordinates.
/// </summary>
/// <param name="request">Geocoding request. </param>
/// <returns>The task result contains the <see cref="Either{ErrorResponse, GeocodingResponse}"/>.</returns>
public Task<Either<ErrorResponse, GeocodingResponse?>> GetReverseGeocodingInfo(
GeocodingRequest request);
}

0 comments on commit 785f9ad

Please sign in to comment.