-
Notifications
You must be signed in to change notification settings - Fork 31
/
PointsOfInterestController.cs
280 lines (266 loc) · 12.7 KB
/
PointsOfInterestController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
using IsraelHiking.API.Executors;
using IsraelHiking.API.Services;
using IsraelHiking.API.Services.Poi;
using IsraelHiking.Common;
using IsraelHiking.Common.Api;
using IsraelHiking.Common.Configuration;
using IsraelHiking.Common.Extensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NetTopologySuite.Features;
using OsmSharp.IO.API;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using IsraelHiking.API.Services.Osm;
namespace IsraelHiking.API.Controllers
{
/// <summary>
/// This controller allows viewing, editing and filtering of points of interest (POI)
/// </summary>
[Route("api/points")]
public class PointsOfInterestController : ControllerBase
{
private readonly IClientsFactory _clientsFactory;
private readonly ITagsHelper _tagsHelper;
private readonly IPointsOfInterestProvider _pointsOfInterestProvider;
private readonly IImagesUrlsStorageExecutor _imageUrlStoreExecutor;
private readonly ISimplePointAdderExecutor _simplePointAdderExecutor;
private readonly IDistributedCache _persistentCache;
private readonly ILogger _logger;
private readonly ConfigurationData _options;
/// <summary>
/// Controller's constructor
/// </summary>
/// <param name="clientsFactory"></param>
/// <param name="tagsHelper"></param>
/// <param name="pointsOfInterestProvider"></param>
/// <param name="imageUrlStoreExecutor"></param>
/// <param name="simplePointAdderExecutor"></param>
/// <param name="persistentCache"></param>
/// <param name="logger"></param>
/// <param name="options"></param>
public PointsOfInterestController(IClientsFactory clientsFactory,
ITagsHelper tagsHelper,
IPointsOfInterestProvider pointsOfInterestProvider,
IImagesUrlsStorageExecutor imageUrlStoreExecutor,
ISimplePointAdderExecutor simplePointAdderExecutor,
IDistributedCache persistentCache,
ILogger logger,
IOptions<ConfigurationData> options)
{
_clientsFactory = clientsFactory;
_tagsHelper = tagsHelper;
_imageUrlStoreExecutor = imageUrlStoreExecutor;
_pointsOfInterestProvider = pointsOfInterestProvider;
_simplePointAdderExecutor = simplePointAdderExecutor;
_persistentCache = persistentCache;
_logger = logger;
_options = options.Value;
}
/// <summary>
/// Gets the available categories for the specified type
/// </summary>
/// <param name="categoriesGroup">The categories' type</param>
/// <returns></returns>
[Route("categories/{categoriesGroup}")]
[HttpGet]
public IEnumerable<Category> GetCategoriesByGroup(string categoriesGroup)
{
return _tagsHelper.GetCategoriesByGroup(categoriesGroup);
}
/// <summary>
/// Get points of interest in a bounding box.
/// </summary>
/// <param name="northEast">North east bounding box corner</param>
/// <param name="southWest">South west bounding box corner</param>
/// <param name="categories">The relevant categories to include</param>
/// <param name="language">The required language</param>
/// <returns>A list of GeoJSON features</returns>
[Route("")]
[HttpGet]
public async Task<IFeature[]> GetPointsOfInterest(string northEast, string southWest, string categories,
string language = "")
{
if (string.IsNullOrWhiteSpace(categories))
{
return Array.Empty<IFeature>();
}
var categoriesArray = categories.Split(',').Select(f => f.Trim()).ToArray();
var northEastCoordinate = northEast.ToCoordinate();
var southWestCoordinate = southWest.ToCoordinate();
return await _pointsOfInterestProvider.GetFeatures(northEastCoordinate, southWestCoordinate, categoriesArray, language);
}
/// <summary>
/// Get a POI by id and source
/// </summary>
/// <param name="source">The source</param>
/// <param name="id">The ID</param>
/// <returns></returns>
[Route("{source}/{id}")]
[HttpGet]
public async Task<IActionResult> GetPointOfInterest(string source, string id)
{
var poiItem = await _pointsOfInterestProvider.GetFeatureById(source, id);
if (poiItem == null)
{
return NotFound();
}
return Ok(poiItem);
}
/// <summary>
/// Creates a POI by id and source, upload the image to wikimedia commons if needed.
/// </summary>
/// <param name="feature"></param>
/// <param name="language">The language code</param>
/// <returns></returns>
[Route("")]
[HttpPost]
[Authorize]
public async Task<IActionResult> CreatePointOfInterest([FromBody]Feature feature,
[FromQuery] string language)
{
_logger.LogInformation("Processing create point of interest request");
var validationResults = ValidateFeature(feature, language);
if (!string.IsNullOrEmpty(validationResults))
{
_logger.LogWarning("Create request validation failed: " + validationResults);
return BadRequest(validationResults);
}
if (feature.Attributes.Exists(FeatureAttributes.POI_IS_SIMPLE))
{
await AddSimplePoint(new AddSimplePointOfInterestRequest
{
Guid = feature.GetId(),
LatLng = new LatLng(feature.GetLocation().Y, feature.GetLocation().X),
PointType = Enum.Parse<SimplePointType>(feature.Attributes[FeatureAttributes.POI_TYPE].ToString(), true)
});
return Ok();
}
var mappedId = _persistentCache.GetString(feature.GetId());
if (!string.IsNullOrEmpty(mappedId))
{
var featureFromDatabase = await _pointsOfInterestProvider.GetFeatureById(feature.Attributes[FeatureAttributes.POI_SOURCE].ToString(), mappedId);
if (featureFromDatabase == null)
{
return BadRequest("Feature is still in process please try again later...");
}
return Ok(featureFromDatabase);
}
_persistentCache.SetString(feature.GetId(), "In process", new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(30) });
var osmGateway = OsmAuthFactoryWrapper.ClientFromUser(User, _clientsFactory, _options);
var newFeature = await _pointsOfInterestProvider.AddFeature(feature, osmGateway, language);
_persistentCache.SetString(feature.GetId(), newFeature.Attributes[FeatureAttributes.ID].ToString(), new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(30) });
return Ok(newFeature);
}
/// <summary>
/// Creates a POI by id and source, upload the image to wikimedia commons if needed.
/// </summary>
/// <param name="id">The feature ID</param>
/// <param name="feature"></param>
/// <param name="language">The language code</param>
/// <returns></returns>
[Route("{id}")]
[HttpPut]
[Authorize]
public async Task<IActionResult> UpdatePointOfInterest(string id, [FromBody]Feature feature,
[FromQuery] string language)
{
_logger.LogInformation("Processing update point of interest request");
var validationResults = ValidateFeature(feature, language);
if (!string.IsNullOrEmpty(validationResults))
{
_logger.LogWarning("Update request validation failed: " + validationResults);
return BadRequest(validationResults);
}
if (feature.GetId() != id) {
return BadRequest("Feature ID and supplied id do not match...");
}
var osmGateway = OsmAuthFactoryWrapper.ClientFromUser(User, _clientsFactory, _options);
return Ok(await _pointsOfInterestProvider.UpdateFeature(feature, osmGateway, language));
}
private string ValidateFeature(Feature feature, string language)
{
if (!feature.Attributes[FeatureAttributes.POI_SOURCE].ToString().Equals(Sources.OSM, StringComparison.InvariantCultureIgnoreCase))
{
return "OSM is the only supported source for this action...";
}
if (feature.GetDescription(language).Length > 255)
{
return "Description must not be more than 255 characters...";
}
if (feature.GetTitle(language).Length > 255)
{
return "Title must not be more than 255 characters...";
}
var invalidWebsites = feature.Attributes.GetNames()
.Where(n => n.StartsWith(FeatureAttributes.WEBSITE))
.Select(n => feature.Attributes[n].ToString())
.Any(w => w.Length > 255);
var invalidAddedWebsites = feature.Attributes.GetNames()
.Where(n => n == FeatureAttributes.POI_ADDED_URLS)
.SelectMany(n => feature.Attributes[n] as IEnumerable<object>)
.Any(w => w.ToString().Length > 255);
if (invalidWebsites || invalidAddedWebsites)
{
return "Website address length must not be more than 255 characters...";
}
return string.Empty;
}
/// <summary>
/// Gets the closest point to a given location.
/// </summary>
/// <param name="location">The location string "lat,lon" to search around</param>
/// <param name="source">Optional, if given this is the only source this methosd will use</param>
/// <param name="language">Optional, if given this is the only language this method will use</param>
/// <returns></returns>
[HttpGet]
[Route("closest")]
public Task<IFeature> GetClosestPoint(string location, string source, string language)
{
return _pointsOfInterestProvider.GetClosestPoint(location.ToCoordinate(), source, language);
}
/// <summary>
/// Get a POI by id and source
/// </summary>
/// <param name="lastModified">Start date for updates</param>
/// <param name="modifiedUntil">End date for updates</param>
/// <returns></returns>
[Route("updates/{lastModified}/{modifiedUntil?}")]
[HttpGet]
public async Task<UpdatesResponse> GetPointOfInterestUpdates(DateTime lastModified, DateTime? modifiedUntil)
{
var response = await _pointsOfInterestProvider.GetUpdates(lastModified, modifiedUntil ?? DateTime.Now);
var imageUrls = new List<string>();
foreach (var feature in response.Features)
{
var currentImageUrls = feature.Attributes.GetNames()
.Where(a => a.StartsWith(FeatureAttributes.IMAGE_URL))
.Select(k => feature.Attributes[k].ToString());
imageUrls.AddRange(currentImageUrls.ToList());
}
response.Images = await _imageUrlStoreExecutor.GetAllImagesForUrls(imageUrls.ToArray());
_logger.LogInformation($"Finished getting POIs updates for {lastModified} - {modifiedUntil}. Features: {response.Features.Length}, Images: {response.Images.Length}");
return response;
}
/// <summary>
/// Creates a simple POI
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private async Task AddSimplePoint(AddSimplePointOfInterestRequest request)
{
if (!string.IsNullOrEmpty(_persistentCache.GetString(request.Guid))) {
return;
}
_persistentCache.SetString(request.Guid, "In process", new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(30) });
_logger.LogInformation($"Adding a simple POI of type {request.PointType} at {request.LatLng.Lat}, {request.LatLng.Lng}");
var osmGateway = OsmAuthFactoryWrapper.ClientFromUser(User, _clientsFactory, _options);
await _simplePointAdderExecutor.Add(osmGateway, request);
}
}
}