diff --git a/.gitignore b/.gitignore index 389b389..a5834e8 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,6 @@ test-results*/ .idea/* packages/* -!packages/repositories.config \ No newline at end of file +!packages/repositories.config +.vs +TestResults diff --git a/OsmSharp.IO.API.Tests/NonAuthTests.cs b/OsmSharp.IO.API.Tests/NonAuthTests.cs index a1417da..c1d63e9 100644 --- a/OsmSharp.IO.API.Tests/NonAuthTests.cs +++ b/OsmSharp.IO.API.Tests/NonAuthTests.cs @@ -213,7 +213,7 @@ public void TestToStringCulture() const string doubleString = "-77.06719208"; var cultures = CultureInfo.GetCultures(CultureTypes.AllCultures); - var clientAsChild = client as NonAuthClient; + var clientAsChild = client as OsmClient; foreach (var culture in cultures) { diff --git a/OsmSharp.IO.API.Tests/BasicAuthClientTests.cs b/OsmSharp.IO.API.Tests/OAuth2ClientTests.cs similarity index 92% rename from OsmSharp.IO.API.Tests/BasicAuthClientTests.cs rename to OsmSharp.IO.API.Tests/OAuth2ClientTests.cs index a87ac93..a9de3e8 100644 --- a/OsmSharp.IO.API.Tests/BasicAuthClientTests.cs +++ b/OsmSharp.IO.API.Tests/OAuth2ClientTests.cs @@ -3,7 +3,6 @@ using OsmSharp.API; using OsmSharp.Changesets; using OsmSharp.Tags; -using System; using System.IO; using System.Linq; using System.Net.Http; @@ -12,8 +11,7 @@ namespace OsmSharp.IO.API.Tests { [TestClass] - [Ignore("Should only be ran manually - comment-out for testing, do not check-in")] - public class BasicAuthClientTests + public class OAuth2ClientTests { private IAuthClient client; @@ -26,7 +24,7 @@ public class BasicAuthClientTests private static readonly TagsCollection ChangeSetTags = new TagsCollection() { - new Tag("comment", "Running a functional test of an automated system."), + new Tag("comment", "Running a functional test of an automation tool."), new Tag("created_by", "https://github.com/OsmSharp/osm-api-client/"), new Tag("bot", "yes") }; @@ -37,13 +35,12 @@ public void TestInitialize() using var loggerFactory = LoggerFactory.Create(b => b.AddConsole()); var logger = loggerFactory.CreateLogger("Tests"); IClientsFactory clientFactory = new ClientsFactory(logger, new HttpClient(), ClientsFactory.DEVELOPMENT_URL); - // Enter your user name and password here or OAuth credential below - do not check-in! - client = clientFactory.CreateBasicAuthClient("user-email", "password"); - //client = clientFactory.CreateOAuthClient("customerkey", "customerSecret", "token", "tokenSecret"); - //client = clientFactory.CreateOAuth2Client("token"); + // Enter your OAuth2 credential below - do not check-in! + client = clientFactory.CreateOAuth2Client("oAuth-token"); } [TestMethod] + [Ignore("Should only be ran manually - comment-out for testing, do not check-in")] public async Task TestChangesetLifeCycle() { var user = await client.GetUserDetails(); @@ -101,6 +98,7 @@ public async Task TestChangesetLifeCycle() } [TestMethod] + [Ignore("Should only be ran manually - comment-out for testing, do not check-in")] public async Task TestPreferences() { var preferences = await client.GetUserPreferences(); @@ -122,6 +120,7 @@ public async Task TestPreferences() } [TestMethod] + [Ignore("Should only be ran manually - comment-out for testing, do not check-in")] public async Task TestNotes() { var permissions = await client.GetPermissions(); @@ -162,6 +161,7 @@ public async Task TestNotes() } [TestMethod] + [Ignore("Should only be ran manually - comment-out for testing, do not check-in")] public async Task TestTraces() { using var gpxStream = File.Open("test.gpx", FileMode.Open); @@ -171,6 +171,7 @@ public async Task TestTraces() NewGpx.Description += updatedText; await client.UpdateTrace(NewGpx); var myTraces = await client.GetTraces(); + var originalLength = myTraces.Length; Assert.IsTrue(myTraces?.Length > 0); var gpxDetails = await client.GetTraceDetails(NewGpx.Id); Assert.IsNotNull(gpxDetails); @@ -179,12 +180,9 @@ public async Task TestTraces() Assert.IsNotNull(gpxStreamBack.Stream); Assert.IsNotNull(gpxStreamBack.FileName); Assert.IsNotNull(gpxStreamBack.ContentType); - foreach (var trace in myTraces) - { - await client.DeleteTrace(trace.Id); - } + await client.DeleteTrace(NewGpx.Id); myTraces = await client.GetTraces(); - Assert.AreEqual(0 ,myTraces?.Length); + Assert.AreEqual(originalLength - 1, myTraces?.Length); } } } diff --git a/OsmSharp.IO.API.Tests/OsmSharp.IO.API.Tests.csproj b/OsmSharp.IO.API.Tests/OsmSharp.IO.API.Tests.csproj index a4b111a..132ecfb 100644 --- a/OsmSharp.IO.API.Tests/OsmSharp.IO.API.Tests.csproj +++ b/OsmSharp.IO.API.Tests/OsmSharp.IO.API.Tests.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 false diff --git a/OsmSharp.IO.API/AuthClient.cs b/OsmSharp.IO.API/AuthClient.cs deleted file mode 100644 index 060a913..0000000 --- a/OsmSharp.IO.API/AuthClient.cs +++ /dev/null @@ -1,309 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using OsmSharp.API; -using OsmSharp.Changesets; -using OsmSharp.Tags; -using OsmSharp.Complete; -using OsmSharp.IO.Xml; -using System.Web; -using Microsoft.Extensions.Logging; - -namespace OsmSharp.IO.API -{ - public abstract class AuthClient : NonAuthClient, IAuthClient - { - /// - /// Creates an instance of an AuthClient which can make - /// authenticated (read and write) calls to the OSM API. - /// - /// The base address for the OSM API (for example: 'https://www.openstreetmap.org/api/0.6/') - /// An HttpClient - /// For logging out details of requests. Optional. - protected AuthClient(string baseAddress, HttpClient httpClient, ILogger logger = null) - : base(baseAddress, httpClient, logger) - { } - - #region Users - /// - public async Task GetPermissions() - { - var address = BaseAddress + "0.6/permissions"; - var osm = await Get(address, c => AddAuthentication(c, address)); - return osm.Permissions; - } - - /// - public async Task GetUserDetails() - { - var address = BaseAddress + "0.6/user/details"; - var osm = await Get(address, c => AddAuthentication(c, address)); - return osm.User; - } - - /// - public async Task GetUserPreferences() - { - var address = BaseAddress + "0.6/user/preferences"; - var osm = await Get(address, c => AddAuthentication(c, address)); - return osm.Preferences.UserPreferences; - } - - /// - public async Task SetUserPreferences(Preferences preferences) - { - var address = BaseAddress + "0.6/user/preferences"; - var osm = new Osm() { Preferences = preferences }; - var content = new StringContent(osm.SerializeToXml()); - await SendAuthRequest(HttpMethod.Put, address, content); - } - - /// - public async Task GetUserPreference(string key) - { - var address = BaseAddress + $"0.6/user/preferences/{Encode(key)}"; - var content = await Get(address, c => AddAuthentication(c, address)); - var value = await content.ReadAsStringAsync(); - return value; - } - - /// - public async Task SetUserPreference(string key, string value) - { - var address = BaseAddress + $"0.6/user/preferences/{Encode(key)}"; - var content = new StringContent(value); - await SendAuthRequest(HttpMethod.Put, address, content); - } - - /// - public async Task DeleteUserPreference(string key) - { - var address = BaseAddress + $"0.6/user/preferences/{Encode(key)}"; - await SendAuthRequest(HttpMethod.Delete, address, null); - } - #endregion - - #region Changesets and Element Changes - /// - public async Task CreateChangeset(TagsCollectionBase tags) - { - Validate.ContainsTags(tags, "comment", "created_by"); - var address = BaseAddress + "0.6/changeset/create"; - var changeSet = new Osm { Changesets = new[] { new Changeset { Tags = tags } } }; - var content = new StringContent(changeSet.SerializeToXml()); - var resultContent = await SendAuthRequest(HttpMethod.Put, address, content); - var id = await resultContent.ReadAsStringAsync(); - return long.Parse(id); - } - - /// - public async Task UpdateChangeset(long changesetId, TagsCollectionBase tags) - { - Validate.ContainsTags(tags, "comment", "created_by"); - // TODO: Validate change meets OsmSharp.API.Capabilities? - var address = BaseAddress + $"0.6/changeset/{changesetId}"; - var changeSet = new Osm { Changesets = new[] { new Changeset { Tags = tags } } }; - var content = new StringContent(changeSet.SerializeToXml()); - var osm = await Put(address, content); - return osm.Changesets[0]; - } - - /// - public async Task UploadChangeset(long changesetId, OsmChange osmChange) - { - var elements = new OsmGeo[][] { osmChange.Create, osmChange.Modify, osmChange.Delete } - .Where(c => c != null).SelectMany(c => c); - - foreach (var osmGeo in elements) - { - osmGeo.ChangeSetId = changesetId; - } - - var address = BaseAddress + $"0.6/changeset/{changesetId}/upload"; - var request = new StringContent(osmChange.SerializeToXml()); - - return await Post(address, request); - } - - /// - public async Task CreateElement(long changesetId, OsmGeo osmGeo) - { - var address = BaseAddress + $"0.6/{osmGeo.Type.ToString().ToLower()}/create"; - var osmRequest = GetOsmRequest(changesetId, osmGeo); - var content = new StringContent(osmRequest.SerializeToXml()); - var response = await SendAuthRequest(HttpMethod.Put, address, content); - var id = await response.ReadAsStringAsync(); - return long.Parse(id); - } - - /// - public async Task UpdateElement(long changesetId, ICompleteOsmGeo osmGeo) - { - switch (osmGeo.Type) - { - case OsmGeoType.Node: - return await UpdateElement(changesetId, osmGeo as OsmGeo); - case OsmGeoType.Way: - return await UpdateElement(changesetId, ((CompleteWay)osmGeo).ToSimple()); - case OsmGeoType.Relation: - return await UpdateElement(changesetId, ((CompleteRelation)osmGeo).ToSimple()); - default: - throw new Exception($"Invalid OSM geometry type: {osmGeo.Type}"); - } - } - - /// - public async Task UpdateElement(long changesetId, OsmGeo osmGeo) - { - Validate.ElementHasAVersion(osmGeo); - var address = BaseAddress + $"0.6/{osmGeo.Type.ToString().ToLower()}/{osmGeo.Id}"; - var osmRequest = GetOsmRequest(changesetId, osmGeo); - var content = new StringContent(osmRequest.SerializeToXml()); - var responseContent = await SendAuthRequest(HttpMethod.Put, address, content); - var newVersionNumber = await responseContent.ReadAsStringAsync(); - return long.Parse(newVersionNumber); - } - - /// - public async Task DeleteElement(long changesetId, OsmGeo osmGeo) - { - Validate.ElementHasAVersion(osmGeo); - var address = BaseAddress + $"0.6/{osmGeo.Type.ToString().ToLower()}/{osmGeo.Id}"; - var osmRequest = GetOsmRequest(changesetId, osmGeo); - var content = new StringContent(osmRequest.SerializeToXml()); - var responseContent = await SendAuthRequest(HttpMethod.Delete, address, content); - var newVersionNumber = await responseContent.ReadAsStringAsync(); - return long.Parse(newVersionNumber); - } - - /// - public async Task CloseChangeset(long changesetId) - { - var address = BaseAddress + $"0.6/changeset/{changesetId}/close"; - await SendAuthRequest(HttpMethod.Put, address, new StringContent("")); - } - - /// - public async Task AddChangesetComment(long changesetId, string text) - { - var address = BaseAddress + $"0.6/changeset/{changesetId}/comment"; - var content = new MultipartFormDataContent() { { new StringContent(text), "text" } }; - var osm = await Post(address, content); - return osm.Changesets[0]; - } - - /// - public async Task ChangesetSubscribe(long changesetId) - { - var address = BaseAddress + $"0.6/changeset/{changesetId}/subscribe"; - await SendAuthRequest(HttpMethod.Post, address, new StringContent("")); - } - - /// - public async Task ChangesetUnsubscribe(long changesetId) - { - var address = BaseAddress + $"0.6/changeset/{changesetId}/unsubscribe"; - await SendAuthRequest(HttpMethod.Post, address, new StringContent("")); - } - #endregion - - #region Traces - /// - public async Task GetTraces() - { - var address = BaseAddress + "0.6/user/gpx_files"; - var osm = await Get(address, c => AddAuthentication(c, address)); - return osm.GpxFiles ?? new GpxFile[0]; - } - - /// - public async Task CreateTrace(GpxFile gpx, Stream fileStream) - { - var address = BaseAddress + "0.6/gpx/create"; - var form = new MultipartFormDataContent(); - form.Add(new StringContent(gpx.Description), "\"description\""); - form.Add(new StringContent(gpx.Visibility.ToString().ToLower()), "\"visibility\""); - var tags = string.Join(",", gpx.Tags ?? new string[0]); - form.Add(new StringContent(tags), "\"tags\""); - var stream = new StreamContent(fileStream); - var cleanName = Encoding.ASCII.GetString(Encoding.ASCII.GetBytes(gpx.Name)); - form.Add(stream, "file", cleanName); - var content = await SendAuthRequest(HttpMethod.Post, address, form); - var id = await content.ReadAsStringAsync(); - return long.Parse(id); - } - - /// - public async Task UpdateTrace(GpxFile trace) - { - var address = BaseAddress + $"0.6/gpx/{trace.Id}"; - var osm = new Osm { GpxFiles = new[] { trace } }; - var content = new StringContent(osm.SerializeToXml()); - await SendAuthRequest(HttpMethod.Put, address, content); - } - - /// - public async Task DeleteTrace(long traceId) - { - var address = BaseAddress + $"0.6/gpx/{traceId}"; - await SendAuthRequest(HttpMethod.Delete, address, null); - } - #endregion - - #region Notes - /// - public async Task CommentNote(long noteId, string text) - { - var query = HttpUtility.ParseQueryString(string.Empty); - query["text"] = text; - var address = BaseAddress + $"0.6/notes/{noteId}/comment?{query}"; - // Can be with Auth or without. - var osm = await Post(address); - return osm.Notes[0]; - } - - /// - public async Task CloseNote(long noteId, string text) - { - var query = HttpUtility.ParseQueryString(string.Empty); - query["text"] = text; - var address = BaseAddress + $"0.6/notes/{noteId}/close?{query}"; - var osm = await Post(address); - return osm.Notes[0]; - } - - /// - public async Task ReOpenNote(long noteId, string text) - { - var query = HttpUtility.ParseQueryString(string.Empty); - query["text"] = text; - var address = BaseAddress + $"0.6/notes/{noteId}/reopen?{query}"; - var osm = await Post(address); - return osm.Notes[0]; - } - #endregion - - private Osm GetOsmRequest(long changesetId, OsmGeo osmGeo) - { - osmGeo.ChangeSetId = changesetId; - var osm = new Osm(); - switch (osmGeo.Type) - { - case OsmGeoType.Node: - osm.Nodes = new[] { osmGeo as Node }; - break; - case OsmGeoType.Way: - osm.Ways = new[] { osmGeo as Way }; - break; - case OsmGeoType.Relation: - osm.Relations = new[] { osmGeo as Relation }; - break; - } - return osm; - } - } -} - diff --git a/OsmSharp.IO.API/BasicAuthClient.cs b/OsmSharp.IO.API/BasicAuthClient.cs deleted file mode 100644 index 066ef3e..0000000 --- a/OsmSharp.IO.API/BasicAuthClient.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.Extensions.Logging; -using System; -using System.Net.Http; -using System.Text; - -namespace OsmSharp.IO.API -{ - /// - /// Use of basic auth is discouraged. Use OAuth when practical. - /// - public class BasicAuthClient : AuthClient - { - private readonly string Username; - private readonly string Password; - - public BasicAuthClient(HttpClient httpClient, - ILogger logger, - string baseAddress, - string username, - string password) - : base (baseAddress, httpClient, logger) - { - Username = username; - Password = password; - } - - protected override void AddAuthentication(HttpRequestMessage request, string url, string method = "GET") - { - var auth = Convert.ToBase64String(Encoding.ASCII.GetBytes(Username + ":" + Password)); - request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", auth); - } - } -} diff --git a/OsmSharp.IO.API/ClientsFactory.cs b/OsmSharp.IO.API/ClientsFactory.cs index 4f36d12..ebdcdba 100644 --- a/OsmSharp.IO.API/ClientsFactory.cs +++ b/OsmSharp.IO.API/ClientsFactory.cs @@ -1,8 +1,5 @@ using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; using System.Net.Http; -using System.Text; namespace OsmSharp.IO.API { @@ -33,25 +30,13 @@ public ClientsFactory(ILogger logger, HttpClient httpClient, string baseAddress) /// public INonAuthClient CreateNonAuthClient() { - return new NonAuthClient(_baseAddress, _httpClient, _logger); + return new OsmClient(_baseAddress, _httpClient, null, _logger); } - /// - public IAuthClient CreateBasicAuthClient(string username, string password) - { - return new BasicAuthClient(_httpClient, _logger, _baseAddress, username, password); - } - - /// - public IAuthClient CreateOAuthClient(string consumerKey, string consumerSecret, string token, string tokenSecret) - { - return new OAuthClient(_httpClient, _logger, _baseAddress, consumerKey, consumerSecret, token, tokenSecret); - } - /// public IAuthClient CreateOAuth2Client(string token) { - return new OAuth2Client(_httpClient, _logger, _baseAddress, token); + return new OsmClient(_baseAddress, _httpClient, token, _logger); } } } diff --git a/OsmSharp.IO.API/IClientsFactory.cs b/OsmSharp.IO.API/IClientsFactory.cs index bbcf96c..31965f3 100644 --- a/OsmSharp.IO.API/IClientsFactory.cs +++ b/OsmSharp.IO.API/IClientsFactory.cs @@ -11,22 +11,6 @@ public interface IClientsFactory /// /// INonAuthClient CreateNonAuthClient(); - /// - /// Creates a client that needs user name and password credentials - /// - /// - /// - /// - IAuthClient CreateBasicAuthClient(string username, string password); - /// - /// Creates a client that will use OAuth 1.0 credentials provided from the OAuth OSM page - /// - /// - /// - /// - /// - /// - IAuthClient CreateOAuthClient(string consumerKey, string consumerSecret, string token, string tokenSecret); /// /// Creates a client that will use OAuth 2.0 credentials provided from the OAuth OSM page diff --git a/OsmSharp.IO.API/INonAuthClient.cs b/OsmSharp.IO.API/INonAuthClient.cs index 6a754d2..ef6246e 100644 --- a/OsmSharp.IO.API/INonAuthClient.cs +++ b/OsmSharp.IO.API/INonAuthClient.cs @@ -5,50 +5,243 @@ using OsmSharp.API; using OsmSharp.Changesets; using OsmSharp.Complete; -using OsmSharp.Db; namespace OsmSharp.IO.API { public interface INonAuthClient { + /// + /// Available API versions + /// + /// GET /api/versions. + /// Task GetVersions(); + /// + /// API Capabilities + /// + /// GET /api/capabilities. + /// Task GetCapabilities(); + /// + /// Retrieving map data by bounding box + /// + /// GET /api/0.6/map. + /// Task GetMap(Bounds bounds); Task GetUser(long id); Task GetUsers(params long[] ids); + /// + /// Gets a Way, including the details of each Node in it + /// + /// GET /api/0.6/way/#id/full. + /// Task GetCompleteWay(long id); + /// + /// Gats a Relation, including the details of each Element in it + /// + /// GET /api/0.6/relation/#id/full. + /// Task GetCompleteRelation(long id); + /// + /// Gets a Node and its details + /// + /// GET /api/0.6/node/#id. + /// Task GetNode(long id); + /// + /// Gets a Way and its details (but not the details of its Nodes) + /// + /// GET /api/0.6/way/#id. + /// Task GetWay(long id); + /// + /// Gets a Relation and its details (but not the details of its elements) + /// + /// GET /api/0.6/relation/#id. + /// Task GetRelation(long id); + /// + /// Gets a Node's history + /// + /// GET /api/0.6/node/#id/history. + /// Task GetNodeHistory(long id); + /// + /// Gets a Way's history + /// + /// GET /api/0.6/way/#id/history. + /// Task GetWayHistory(long id); + /// + /// Gets a Relation's history + /// + /// GET /api/0.6/relation/#id/history. + /// Task GetRelationHistory(long id); + /// + /// Gets a Node's version + /// + /// GET /api/0.6/node/#id/#version. + /// Task GetNodeVersion(long id, long version); + /// + /// Gets a Way's version + /// + /// GET /api/0.6/way/#id/#version. + /// Task GetWayVersion(long id, long version); + /// + /// Gets a Relation's version + /// + /// GET /api/0.6/relation/#id/#version. + /// Task GetRelationVersion(long id, long version); + /// + /// Gets many Nodes + /// + /// GET /api/0.6/nodes?#parameters. + /// Task GetNodes(params long[] ids); + /// + /// Gets many Ways + /// + /// GET /api/0.6/ways?#parameters. + /// Task GetWays(params long[] ids); + /// + /// Gets many Relations + /// + /// GET /api/0.6/relations?#parameters. + /// Task GetRelations(params long[] ids); Task GetElements(params OsmGeoKey[] elementKeys); + /// + /// Elements Multifetch + /// + /// GET /api/0.6/[nodes|ways|relations]?#parameters. + /// Task GetElements(Dictionary elementKeyVersions); + /// + /// Gets many Nodes at specific versions + /// + /// GET /api/0.6/nodes?#parameters. + /// Task GetNodes(IEnumerable> idVersions); + /// + /// Gets many Ways at specific versions + /// + /// GET /api/0.6/ways?#parameters. + /// Task GetWays(IEnumerable> idVersions); + /// + /// Gets many Relations at specific versions + /// + /// GET /api/0.6/relations?#parameters. + /// Task GetRelations(IEnumerable> idVersions); + /// + /// Gets the Relations containing a specific Node + /// + /// GET /api/0.6/node/#id/relations. + /// Task GetNodeRelations(long id); + /// + /// Gets the Relations containing a specific Way + /// + /// GET /api/0.6/way/#id/relations. + /// Task GetWayRelations(long id); + /// + /// Gets the Relations containing a specific Relation + /// + /// GET /api/0.6/relation/#id/relations. + /// Task GetRelationRelations(long id); + /// + /// Node Ways + /// + /// GET /api/0.6/node/#id/ways. + /// Task GetNodeWays(long id); + /// + /// Changeset Read + /// + /// GET /api/0.6/changeset/#id?include_discussion=true. + /// Task GetChangeset(long changesetId, bool includeDiscussion = false); + /// + /// Changeset Query + /// + /// GET /api/0.6/changesets + /// Task QueryChangesets(Bounds bounds, long? userId, string userName, DateTime? minClosedDate, DateTime? maxOpenedDate, bool openOnly, bool closedOnly, long[] ids); + /// + /// Changeset Download + /// + /// GET /api/0.6/changeset/#id/download + /// Task GetChangesetDownload(long changesetId); + /// + /// Get GPS Points + /// + /// Get /api/0.6/trackpoints?bbox=left,bottom,right,top&page=pageNumber. + /// Retrieve the GPS track points that are inside a given bounding box (formatted in a GPX format). + /// Warning: GPX version 1.0 is not the current version. Your tools might not support it. + /// + /// A stream of a GPX (version 1.0) file. Task GetTrackPoints(Bounds bounds, int pageNumber = 0); + /// + /// Download Metadata + /// + /// GET /api/0.6/gpx/#id/details. + /// Task GetTraceDetails(long id); + /// + /// Download Data + /// + /// GET /api/0.6/gpx/#id/data. + /// This will return exactly what was uploaded, which might not be a gpx file (it could be a zip etc.) + /// + /// A stream of a GPX (version 1.0) file. Task GetTraceData(long id); + /// + /// Gets a Note + /// + /// GET /api/0.6/notes/#id. + /// Task GetNote(long id); + /// + /// Gets many a Notes in a box and with the spcified time since closed + /// + /// GET /api/0.6/notes?[parameters]. + /// + /// Must be between 1 and 10,000. + /// 0 means only open notes. -1 mean all (open and closed) notes. Task GetNotes(Bounds bounds, int limit = 100, int maxClosedDays = 7); + /// + /// Gets an RSS feed of Notes in an area + /// + /// GET /api/0.6/notes/feed. + /// Task GetNotesRssFeed(Bounds bounds); + /// + /// Search for Notes + /// + /// GET /api/0.6/notes/search. + /// + /// Specifies the search query. This is the only required field. + /// Specifies the creator of the returned notes by the id of the user. Does not work together with the display_name parameter + /// Specifies the creator of the returned notes by the display name. Does not work together with the user parameter + /// Must be between 1 and 10,000. 100 is default if null. + /// 0 means only open notes. -1 mean all (open and closed) notes. 7 is default if null. + /// Specifies the beginning of a date range to search in for a note + /// Specifies the end of a date range to search in for a note Task QueryNotes(string searchText, long? userId, string userName, int? limit, int? maxClosedDays, DateTime? fromDate, DateTime? toDate); + /// + /// Creates a new Note + /// + /// POST /api/0.6/notes. + /// Task CreateNote(double latitude, double longitude, string text); } } \ No newline at end of file diff --git a/OsmSharp.IO.API/OAuth2Client.cs b/OsmSharp.IO.API/OAuth2Client.cs deleted file mode 100644 index d13387f..0000000 --- a/OsmSharp.IO.API/OAuth2Client.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Net.Http; -using Microsoft.Extensions.Logging; - -namespace OsmSharp.IO.API -{ - public class OAuth2Client : AuthClient - { - private readonly string Token; - public OAuth2Client(HttpClient httpClient, ILogger logger, string baseAddress, string token) : base(baseAddress, httpClient, logger) - { - Token = token; - } - - protected override void AddAuthentication(HttpRequestMessage message, string url, string method = "GET") - { - message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", this.Token); - } - } -} \ No newline at end of file diff --git a/OsmSharp.IO.API/OAuthClient.cs b/OsmSharp.IO.API/OAuthClient.cs deleted file mode 100644 index 8e8a887..0000000 --- a/OsmSharp.IO.API/OAuthClient.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Microsoft.Extensions.Logging; -using System; -using System.Net.Http; - -namespace OsmSharp.IO.API -{ - public class OAuthClient : AuthClient - { - /// - /// The OSM consumer key that was generated from OSM site - /// - private readonly string ConsumerKey; - /// - /// The OSM consumer secret that was generated from OSM site - /// - private readonly string ConsumerSecret; - private readonly string Token; - private readonly string TokenSecret; - - public OAuthClient(HttpClient httpClient, - ILogger logger, - string baseAddress, - string consumerKey, - string consumerSecret, - string token, - string tokenSecret) - : base (baseAddress, httpClient, logger) - { - ConsumerKey = consumerKey; - ConsumerSecret = consumerSecret; - Token = token; - TokenSecret = tokenSecret; - } - - protected override void AddAuthentication(HttpRequestMessage message, string url, string method = "GET") - { - var request = new OAuth.OAuthRequest - { - ConsumerKey = ConsumerKey, - ConsumerSecret = ConsumerSecret, - Token = Token, - TokenSecret = TokenSecret, - Type = OAuth.OAuthRequestType.ProtectedResource, - SignatureMethod = OAuth.OAuthSignatureMethod.HmacSha1, - RequestUrl = url, - Version = "1.0", - Method = method - }; - var auth = request.GetAuthorizationHeader().Replace("OAuth ", ""); - message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("OAuth", auth); - } - } -} \ No newline at end of file diff --git a/OsmSharp.IO.API/NonAuthClient.cs b/OsmSharp.IO.API/OsmClient.cs similarity index 55% rename from OsmSharp.IO.API/NonAuthClient.cs rename to OsmSharp.IO.API/OsmClient.cs index 5b1eeae..e2d435e 100644 --- a/OsmSharp.IO.API/NonAuthClient.cs +++ b/OsmSharp.IO.API/OsmClient.cs @@ -14,12 +14,13 @@ using System.IO; using System.Web; using Microsoft.Extensions.Logging; -using OsmSharp.Db; using System.Globalization; +using OsmSharp.IO.Xml; +using OsmSharp.Tags; namespace OsmSharp.IO.API { - public class NonAuthClient : INonAuthClient + public class OsmClient : IAuthClient { /// /// The OSM base address @@ -28,13 +29,18 @@ public class NonAuthClient : INonAuthClient /// "https://master.apis.dev.openstreetmap.org/api/" /// "https://www.openstreetmap.org/api/" /// - protected readonly string BaseAddress; + private readonly string _baseAddress; + /// + /// The OSM OAuth2 bearer token used for authentication + /// + private readonly string _token; + // Prevent scientific notation in a url. - protected string OsmMaxPrecision = "0.########"; + private const string OsmMaxPrecision = "0.########"; private readonly HttpClient _httpClient; - protected readonly ILogger _logger; + private readonly ILogger _logger; /// /// Creates an instance of a NonAuthClient which can make @@ -42,92 +48,52 @@ public class NonAuthClient : INonAuthClient /// /// The base address for the OSM API (for example: 'https://www.openstreetmap.org/api/0.6/') /// An HttpClient + /// OAuth2 bearer token /// For logging out details of requests. Optional. - public NonAuthClient(string baseAddress, + public OsmClient(string baseAddress, HttpClient httpClient, + string token = null, ILogger logger = null) { - BaseAddress = baseAddress; + _baseAddress = baseAddress; _httpClient = httpClient; _logger = logger; + _token = token; } #region Miscellaneous - /// - /// Available API versions - /// - /// GET /api/versions. - /// + /// public async Task GetVersions() { - var osm = await Get(BaseAddress + "versions"); + var osm = await Get(_baseAddress + "versions"); return osm.Api.Version.Maximum; } - /// - /// API Capabilities - /// - /// GET /api/capabilities. - /// + /// public async Task GetCapabilities() { - return await Get(BaseAddress + "0.6/capabilities"); + return await Get(_baseAddress + "0.6/capabilities"); } - /// - /// Retrieving map data by bounding box - /// - /// GET /api/0.6/map. - /// + /// public async Task GetMap(Bounds bounds) { Validate.BoundLimits(bounds); - var address = BaseAddress + $"0.6/map?bbox={ToString(bounds)}"; + var address = _baseAddress + $"0.6/map?bbox={ToString(bounds)}"; return await Get(address); } - - /// - /// Details of a User - /// - /// GET /api/0.6/user/#id. - /// - public async Task GetUser(long id) - { - var address = BaseAddress + $"0.6/user/{id}"; - var osm = await Get(address); - return osm.User; - } - - /// - /// Details of multiple Users - /// - /// GET /api/0.6/users?users=#id1,#id2,...,#idn. - /// - public async Task GetUsers(params long[] ids) - { - var address = BaseAddress + $"0.6/users?users={string.Join(",", ids)}"; - var osm = await Get(address); - return osm.Users; - } + #endregion #region Elements - /// - /// Gets a Way, including the details of each Node in it - /// - /// GET /api/0.6/way/#id/full. - /// + /// public Task GetCompleteWay(long id) { return GetCompleteElement(id); } - /// - /// Gats a Relation, including the details of each Element in it - /// - /// GET /api/0.6/relation/#id/full. - /// + /// public Task GetCompleteRelation(long id) { return GetCompleteElement(id); @@ -141,7 +107,7 @@ public Task GetCompleteRelation(long id) private async Task GetCompleteElement(long id) where TCompleteOsmGeo : ICompleteOsmGeo, new() { var type = new TCompleteOsmGeo().Type.ToString().ToLower(); - var address = BaseAddress + $"0.6/{type}/{id}/full"; + var address = _baseAddress + $"0.6/{type}/{id}/full"; var content = await Get(address); var stream = await content.ReadAsStreamAsync(); var streamSource = new XmlOsmStreamSource(stream); @@ -150,31 +116,19 @@ public Task GetCompleteRelation(long id) return element; } - /// - /// Gets a Node and its details - /// - /// GET /api/0.6/node/#id. - /// + /// public async Task GetNode(long id) { return await GetElement(id); } - /// - /// Gets a Way and its details (but not the details of its Nodes) - /// - /// GET /api/0.6/way/#id. - /// + /// public async Task GetWay(long id) { return await GetElement(id); } - /// - /// Gets a Relation and its details (but not the details of its elements) - /// - /// GET /api/0.6/relation/#id. - /// + /// public async Task GetRelation(long id) { return await GetElement(id); @@ -188,36 +142,24 @@ public async Task GetRelation(long id) private async Task GetElement(long id) where TOsmGeo : OsmGeo, new() { var type = new TOsmGeo().Type.ToString().ToLower(); - var address = BaseAddress + $"0.6/{type}/{id}"; + var address = _baseAddress + $"0.6/{type}/{id}"; var elements = await GetOfType(address); return elements.FirstOrDefault(); } - /// - /// Gets a Node's history - /// - /// GET /api/0.6/node/#id/history. - /// + /// public async Task GetNodeHistory(long id) { return await GetElementHistory(id); } - /// - /// Gets a Way's history - /// - /// GET /api/0.6/way/#id/history. - /// + /// public async Task GetWayHistory(long id) { return await GetElementHistory(id); } - /// - /// Gets a Relation's history - /// - /// GET /api/0.6/relation/#id/history. - /// + /// public async Task GetRelationHistory(long id) { return await GetElementHistory(id); @@ -231,36 +173,24 @@ public async Task GetRelationHistory(long id) private async Task GetElementHistory(long id) where TOsmGeo : OsmGeo, new() { var type = new TOsmGeo().Type.ToString().ToLower(); - var address = BaseAddress + $"0.6/{type}/{id}/history"; + var address = _baseAddress + $"0.6/{type}/{id}/history"; var elements = await GetOfType(address); return elements.ToArray(); } - /// - /// Gets a Node's version - /// - /// GET /api/0.6/node/#id/#version. - /// + /// public async Task GetNodeVersion(long id, long version) { return await GetElementVersion(id, version); } - /// - /// Gets a Way's version - /// - /// GET /api/0.6/way/#id/#version. - /// + /// public async Task GetWayVersion(long id, long version) { return await GetElementVersion(id, version); } - /// - /// Gets a Relation's version - /// - /// GET /api/0.6/relation/#id/#version. - /// + /// public async Task GetRelationVersion(long id, long version) { return await GetElementVersion(id, version); @@ -274,36 +204,24 @@ public async Task GetRelationVersion(long id, long version) private async Task GetElementVersion(long id, long version) where TOsmGeo : OsmGeo, new() { var type = new TOsmGeo().Type.ToString().ToLower(); - var address = BaseAddress + $"0.6/{type}/{id}/{version}"; + var address = _baseAddress + $"0.6/{type}/{id}/{version}"; var elements = await GetOfType(address); return elements.FirstOrDefault(); } - /// - /// Gets many Nodes - /// - /// GET /api/0.6/nodes?#parameters. - /// + /// public async Task GetNodes(params long[] ids) { return await GetElements(ids); } - /// - /// Gets many Ways - /// - /// GET /api/0.6/ways?#parameters. - /// + /// public async Task GetWays(params long[] ids) { return await GetElements(ids); } - /// - /// Gets many Relations - /// - /// GET /api/0.6/relations?#parameters. - /// + /// public async Task GetRelations(params long[] ids) { return await GetElements(ids); @@ -315,31 +233,19 @@ public async Task GetRelations(params long[] ids) return await GetElements(idVersions); } - /// - /// Gets many Nodes at specific versions - /// - /// GET /api/0.6/nodes?#parameters. - /// + /// public async Task GetNodes(IEnumerable> idVersions) { return await GetElements(idVersions); } - /// - /// Gets many Ways at specific versions - /// - /// GET /api/0.6/ways?#parameters. - /// + /// public async Task GetWays(IEnumerable> idVersions) { return await GetElements(idVersions); } - /// - /// Gets many Relations at specific versions - /// - /// GET /api/0.6/relations?#parameters. - /// + /// public async Task GetRelations(IEnumerable> idVersions) { return await GetElements(idVersions); @@ -349,7 +255,8 @@ public async Task GetElements(params OsmGeoKey[] elementKeys) { return await GetElements(elementKeys.ToDictionary(ek => ek, ek => (long?)null)); } - + + /// public async Task GetElements(Dictionary elementKeyVersions) { var elements = new List(); @@ -368,11 +275,7 @@ public async Task GetElements(Dictionary elementKeyV return elements.ToArray(); } - /// - /// Elements Multifetch - /// - /// GET /api/0.6/[nodes|ways|relations]?#parameters. - /// + private async Task GetElements(IEnumerable> idVersions) where TOsmGeo : OsmGeo, new() { var tasks = new List>>(); @@ -382,7 +285,7 @@ public async Task GetElements(Dictionary elementKeyV var type = new TOsmGeo().Type.ToString().ToLower(); // For exmple: "12,13,14v1,15v1" var parameters = string.Join(",", chunk.Select(e => e.Value.HasValue ? $"{e.Key}v{e.Value}" : e.Key.ToString())); - var address = BaseAddress + $"0.6/{type}s?{type}s={parameters}"; + var address = _baseAddress + $"0.6/{type}s?{type}s={parameters}"; tasks.Add(GetOfType(address)); } @@ -398,31 +301,19 @@ private IEnumerable Chunks(IEnumerable elements, int chunkSize) .Select(g => g.Select(ei => ei.e).ToArray()); } - /// - /// Gets the Relations containing a specific Node - /// - /// GET /api/0.6/node/#id/relations. - /// + /// public async Task GetNodeRelations(long id) { return await GetElementRelations(id); } - /// - /// Gets the Relations containing a specific Way - /// - /// GET /api/0.6/way/#id/relations. - /// + /// public async Task GetWayRelations(long id) { return await GetElementRelations(id); } - /// - /// Gets the Relations containing a specific Relation - /// - /// GET /api/0.6/relation/#id/relations. - /// + /// public async Task GetRelationRelations(long id) { return await GetElementRelations(id); @@ -436,33 +327,25 @@ public async Task GetRelationRelations(long id) private async Task GetElementRelations(long id) where TOsmGeo : OsmGeo, new() { var type = new TOsmGeo().Type.ToString().ToLower(); - var address = BaseAddress + $"0.6/{type}/{id}/relations"; + var address = _baseAddress + $"0.6/{type}/{id}/relations"; var elements = await GetOfType(address); return elements.ToArray(); } - /// - /// Node Ways - /// - /// GET /api/0.6/node/#id/ways. - /// + /// public async Task GetNodeWays(long id) { - var address = BaseAddress + $"0.6/node/{id}/ways"; + var address = _baseAddress + $"0.6/node/{id}/ways"; var elements = await GetOfType(address); return elements.ToArray(); } #endregion #region Changesets - /// - /// Changeset Read - /// - /// GET /api/0.6/changeset/#id?include_discussion=true. - /// + /// public async Task GetChangeset(long changesetId, bool includeDiscussion = false) { - var address = BaseAddress + $"0.6/changeset/{changesetId}"; + var address = _baseAddress + $"0.6/changeset/{changesetId}"; if (includeDiscussion) { address += "?include_discussion=true"; @@ -471,11 +354,7 @@ public async Task GetChangeset(long changesetId, bool includeDiscussi return osm.Changesets[0]; } - /// - /// Changeset Query - /// - /// GET /api/0.6/changesets - /// + /// public async Task QueryChangesets(Bounds bounds, long? userId, string userName, DateTime? minClosedDate, DateTime? maxOpenedDate, bool openOnly, bool closedOnly, long[] ids) @@ -497,119 +376,238 @@ public async Task QueryChangesets(Bounds bounds, long? userId, stri if (closedOnly) query["closed"] = "true"; if (ids != null) query["changesets"] = string.Join(",", ids); - var address = BaseAddress + "0.6/changesets?" + query.ToString(); + var address = _baseAddress + "0.6/changesets?" + query.ToString(); var osm = await Get(address); return osm.Changesets; } - /// - /// Changeset Download - /// - /// GET /api/0.6/changeset/#id/download - /// + /// public async Task GetChangesetDownload(long changesetId) { - return await Get(BaseAddress + $"0.6/changeset/{changesetId}/download"); + return await Get(_baseAddress + $"0.6/changeset/{changesetId}/download"); } #endregion + #region Changesets and Element Changes + /// + public async Task CreateChangeset(TagsCollectionBase tags) + { + Validate.ContainsTags(tags, "comment", "created_by"); + var address = _baseAddress + "0.6/changeset/create"; + var changeSet = new Osm { Changesets = new[] { new Changeset { Tags = tags } } }; + var content = new StringContent(changeSet.SerializeToXml()); + var resultContent = await SendAuthRequest(HttpMethod.Put, address, content); + var id = await resultContent.ReadAsStringAsync(); + return long.Parse(id); + } + + /// + public async Task UpdateChangeset(long changesetId, TagsCollectionBase tags) + { + Validate.ContainsTags(tags, "comment", "created_by"); + // TODO: Validate change meets OsmSharp.API.Capabilities? + var address = _baseAddress + $"0.6/changeset/{changesetId}"; + var changeSet = new Osm { Changesets = new[] { new Changeset { Tags = tags } } }; + var content = new StringContent(changeSet.SerializeToXml()); + var osm = await Put(address, content); + return osm.Changesets[0]; + } + + /// + public async Task UploadChangeset(long changesetId, OsmChange osmChange) + { + var elements = new OsmGeo[][] { osmChange.Create, osmChange.Modify, osmChange.Delete } + .Where(c => c != null).SelectMany(c => c); + + foreach (var osmGeo in elements) + { + osmGeo.ChangeSetId = changesetId; + } + + var address = _baseAddress + $"0.6/changeset/{changesetId}/upload"; + var request = new StringContent(osmChange.SerializeToXml()); + + return await Post(address, request); + } + + /// + public async Task CreateElement(long changesetId, OsmGeo osmGeo) + { + var address = _baseAddress + $"0.6/{osmGeo.Type.ToString().ToLower()}/create"; + var osmRequest = GetOsmRequest(changesetId, osmGeo); + var content = new StringContent(osmRequest.SerializeToXml()); + var response = await SendAuthRequest(HttpMethod.Put, address, content); + var id = await response.ReadAsStringAsync(); + return long.Parse(id); + } + + /// + public async Task UpdateElement(long changesetId, ICompleteOsmGeo osmGeo) + { + switch (osmGeo.Type) + { + case OsmGeoType.Node: + return await UpdateElement(changesetId, osmGeo as OsmGeo); + case OsmGeoType.Way: + return await UpdateElement(changesetId, ((CompleteWay)osmGeo).ToSimple()); + case OsmGeoType.Relation: + return await UpdateElement(changesetId, ((CompleteRelation)osmGeo).ToSimple()); + default: + throw new Exception($"Invalid OSM geometry type: {osmGeo.Type}"); + } + } + + /// + public async Task UpdateElement(long changesetId, OsmGeo osmGeo) + { + Validate.ElementHasAVersion(osmGeo); + var address = _baseAddress + $"0.6/{osmGeo.Type.ToString().ToLower()}/{osmGeo.Id}"; + var osmRequest = GetOsmRequest(changesetId, osmGeo); + var content = new StringContent(osmRequest.SerializeToXml()); + var responseContent = await SendAuthRequest(HttpMethod.Put, address, content); + var newVersionNumber = await responseContent.ReadAsStringAsync(); + return long.Parse(newVersionNumber); + } + + /// + public async Task DeleteElement(long changesetId, OsmGeo osmGeo) + { + Validate.ElementHasAVersion(osmGeo); + var address = _baseAddress + $"0.6/{osmGeo.Type.ToString().ToLower()}/{osmGeo.Id}"; + var osmRequest = GetOsmRequest(changesetId, osmGeo); + var content = new StringContent(osmRequest.SerializeToXml()); + var responseContent = await SendAuthRequest(HttpMethod.Delete, address, content); + var newVersionNumber = await responseContent.ReadAsStringAsync(); + return long.Parse(newVersionNumber); + } + + /// + public async Task CloseChangeset(long changesetId) + { + var address = _baseAddress + $"0.6/changeset/{changesetId}/close"; + await SendAuthRequest(HttpMethod.Put, address, new StringContent("")); + } + + /// + public async Task AddChangesetComment(long changesetId, string text) + { + var address = _baseAddress + $"0.6/changeset/{changesetId}/comment"; + var content = new MultipartFormDataContent() { { new StringContent(text), "text" } }; + var osm = await Post(address, content); + return osm.Changesets[0]; + } + + /// + public async Task ChangesetSubscribe(long changesetId) + { + var address = _baseAddress + $"0.6/changeset/{changesetId}/subscribe"; + await SendAuthRequest(HttpMethod.Post, address, new StringContent("")); + } + + /// + public async Task ChangesetUnsubscribe(long changesetId) + { + var address = _baseAddress + $"0.6/changeset/{changesetId}/unsubscribe"; + await SendAuthRequest(HttpMethod.Post, address, new StringContent("")); + } + #endregion + #region Traces - /// - /// Get GPS Points - /// - /// Get /api/0.6/trackpoints?bbox=left,bottom,right,top&page=pageNumber. - /// Retrieve the GPS track points that are inside a given bounding box (formatted in a GPX format). - /// Warning: GPX version 1.0 is not the current version. Your tools might not support it. - /// - /// A stream of a GPX (version 1.0) file. - public virtual async Task GetTrackPoints(Bounds bounds, int pageNumber = 0) + /// + public async Task GetTrackPoints(Bounds bounds, int pageNumber = 0) { - var address = BaseAddress + $"0.6/trackpoints?bbox={ToString(bounds)}&page={pageNumber}"; + var address = _baseAddress + $"0.6/trackpoints?bbox={ToString(bounds)}&page={pageNumber}"; var content = await Get(address); var stream = await content.ReadAsStreamAsync(); return stream; } - /// - /// Download Metadata - /// - /// GET /api/0.6/gpx/#id/details. - /// + /// public async Task GetTraceDetails(long id) { - var address = BaseAddress + $"0.6/gpx/{id}/details"; + var address = _baseAddress + $"0.6/gpx/{id}/details"; var osm = await Get(address, c => AddAuthentication(c, address)); return osm.GpxFiles[0]; } - /// - /// Download Data - /// - /// GET /api/0.6/gpx/#id/data. - /// This will return exactly what was uploaded, which might not be a gpx file (it could be a zip etc.) - /// - /// A stream of a GPX (version 1.0) file. + /// public async Task GetTraceData(long id) { - var address = BaseAddress + $"0.6/gpx/{id}/data"; + var address = _baseAddress + $"0.6/gpx/{id}/data"; var content = await Get(address, c => AddAuthentication(c, address)); return await TypedStream.Create(content); } + + /// + public async Task GetTraces() + { + var address = _baseAddress + "0.6/user/gpx_files"; + var osm = await Get(address, c => AddAuthentication(c, address)); + return osm.GpxFiles ?? Array.Empty(); + } + + /// + public async Task CreateTrace(GpxFile gpx, Stream fileStream) + { + var address = _baseAddress + "0.6/gpx/create"; + var form = new MultipartFormDataContent(); + form.Add(new StringContent(gpx.Description), "\"description\""); + form.Add(new StringContent(gpx.Visibility.ToString().ToLower()), "\"visibility\""); + var tags = string.Join(",", gpx.Tags ?? Array.Empty()); + form.Add(new StringContent(tags), "\"tags\""); + var stream = new StreamContent(fileStream); + var cleanName = Encoding.ASCII.GetString(Encoding.ASCII.GetBytes(gpx.Name)); + form.Add(stream, "file", cleanName); + var content = await SendAuthRequest(HttpMethod.Post, address, form); + var id = await content.ReadAsStringAsync(); + return long.Parse(id); + } + + /// + public async Task UpdateTrace(GpxFile trace) + { + var address = _baseAddress + $"0.6/gpx/{trace.Id}"; + var osm = new Osm { GpxFiles = new[] { trace } }; + var content = new StringContent(osm.SerializeToXml()); + await SendAuthRequest(HttpMethod.Put, address, content); + } + + /// + public async Task DeleteTrace(long traceId) + { + var address = _baseAddress + $"0.6/gpx/{traceId}"; + await SendAuthRequest(HttpMethod.Delete, address, null); + } #endregion #region Notes - /// - /// Gets a Note - /// - /// GET /api/0.6/notes/#id. - /// + /// public async Task GetNote(long id) { - var address = BaseAddress + $"0.6/notes/{id}"; + var address = _baseAddress + $"0.6/notes/{id}"; var osm = await Get(address); return osm.Notes?.FirstOrDefault(); } - /// - /// Gets many a Notes in a box and with the spcified time since closed - /// - /// GET /api/0.6/notes?[parameters]. - /// - /// Must be between 1 and 10,000. - /// 0 means only open notes. -1 mean all (open and closed) notes. + /// public async Task GetNotes(Bounds bounds, int limit = 100, int maxClosedDays = 7) { string format = ".xml"; - var address = BaseAddress + $"0.6/notes{format}?bbox={ToString(bounds)}&limit={limit}&closed={maxClosedDays}"; + var address = _baseAddress + $"0.6/notes{format}?bbox={ToString(bounds)}&limit={limit}&closed={maxClosedDays}"; var osm = await Get(address); return osm.Notes; } - /// - /// Gets an RSS feed of Notes in an area - /// - /// GET /api/0.6/notes/feed. - /// + /// public async Task GetNotesRssFeed(Bounds bounds) { - var address = BaseAddress + $"0.6/notes/feed?bbox={ToString(bounds)}"; + var address = _baseAddress + $"0.6/notes/feed?bbox={ToString(bounds)}"; var content = await Get(address); var stream = await content.ReadAsStreamAsync(); return stream; } - /// - /// Search for Notes - /// - /// GET /api/0.6/notes/search. - /// - /// Specifies the search query. This is the only required field. - /// Specifies the creator of the returned notes by the id of the user. Does not work together with the display_name parameter - /// Specifies the creator of the returned notes by the display name. Does not work together with the user parameter - /// Must be between 1 and 10,000. 100 is default if null. - /// 0 means only open notes. -1 mean all (open and closed) notes. 7 is default if null. - /// Specifies the beginning of a date range to search in for a note - /// Specifies the end of a date range to search in for a note + /// public async Task QueryNotes(string searchText, long? userId, string userName, int? limit, int? maxClosedDays, DateTime? fromDate, DateTime? toDate) { @@ -630,7 +628,7 @@ public async Task QueryNotes(string searchText, long? userId, string use if (toDate != null) query["to"] = FormatNoteDate(toDate.Value); string format = ".xml"; - var address = BaseAddress + $"0.6/notes/search{format}?{query}"; + var address = _baseAddress + $"0.6/notes/search{format}?{query}"; var osm = await Get(address); return osm.Notes; } @@ -641,11 +639,7 @@ private static string FormatNoteDate(DateTime date) return date.ToString("yyyy-MM-dd HH:mm:ss") + " UTC"; } - /// - /// Creates a new Note - /// - /// POST /api/0.6/notes. - /// + /// public async Task CreateNote(double latitude, double longitude, string text) { var query = HttpUtility.ParseQueryString(string.Empty); @@ -653,52 +647,56 @@ public async Task CreateNote(double latitude, double longitude, string tex query["lat"] = ToString(latitude); query["lon"] = ToString(longitude); - var address = BaseAddress + $"0.6/notes?{query}"; + var address = _baseAddress + $"0.6/notes?{query}"; // Can be with Auth or without. var osm = await Post(address); return osm.Notes[0]; } - #endregion - - protected async Task> GetOfType(string address, Action auth = null) where T : class + + /// + public async Task CommentNote(long noteId, string text) { - var content = await Get(address, auth); - var streamSource = new XmlOsmStreamSource(await content.ReadAsStreamAsync()); - var elements = streamSource.OfType(); - return elements; + var query = HttpUtility.ParseQueryString(string.Empty); + query["text"] = text; + var address = _baseAddress + $"0.6/notes/{noteId}/comment?{query}"; + // Can be with Auth or without. + var osm = await Post(address); + return osm.Notes[0]; } - [EditorBrowsable(EditorBrowsableState.Never)] - public string ToString(Bounds bounds) + /// + public async Task CloseNote(long noteId, string text) { - StringBuilder x = new StringBuilder(); - x.Append(bounds.MinLongitude.Value.ToString(OsmMaxPrecision, CultureInfo.InvariantCulture)); - x.Append(','); - x.Append(bounds.MinLatitude.Value.ToString(OsmMaxPrecision, CultureInfo.InvariantCulture)); - x.Append(','); - x.Append(bounds.MaxLongitude.Value.ToString(OsmMaxPrecision, CultureInfo.InvariantCulture)); - x.Append(','); - x.Append(bounds.MaxLatitude.Value.ToString(OsmMaxPrecision, CultureInfo.InvariantCulture)); - - return x.ToString(); + var query = HttpUtility.ParseQueryString(string.Empty); + query["text"] = text; + var address = _baseAddress + $"0.6/notes/{noteId}/close?{query}"; + var osm = await Post(address); + return osm.Notes[0]; } - [EditorBrowsable(EditorBrowsableState.Never)] - public string ToString(float number) + /// + public async Task ReOpenNote(long noteId, string text) { - return number.ToString(OsmMaxPrecision, CultureInfo.InvariantCulture); + var query = HttpUtility.ParseQueryString(string.Empty); + query["text"] = text; + var address = _baseAddress + $"0.6/notes/{noteId}/reopen?{query}"; + var osm = await Post(address); + return osm.Notes[0]; } + #endregion - [EditorBrowsable(EditorBrowsableState.Never)] - public string ToString(double number) + #region Http + private static readonly Func Encode = HttpUtility.UrlEncode; + + private void AddAuthentication(HttpRequestMessage message, string url, string method = "GET") { - return number.ToString(OsmMaxPrecision, CultureInfo.InvariantCulture); + if (!string.IsNullOrEmpty(_token)) + { + message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", this._token); + } } - - #region Http - protected static readonly Func Encode = HttpUtility.UrlEncode; - - protected async Task Get(string address, Action auth = null) where T : class + + private async Task Get(string address, Action auth = null) where T : class { var content = await Get(address, auth); var stream = await content.ReadAsStreamAsync(); @@ -707,18 +705,18 @@ protected async Task Get(string address, Action auth = return element; } - protected async Task Get(string address, Action auth = null) + private async Task Get(string address, Action auth = null) { using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, address)) { auth?.Invoke(request); var response = await _httpClient.SendAsync(request); - await VerifyAndLogReponse(response, $"{GetType().Name} GET: {address}"); + await VerifyAndLogResponse(response, $"{GetType().Name} GET: {address}"); return response.Content; } } - protected async Task Post(string address, HttpContent requestContent = null) where T : class + private async Task Post(string address, HttpContent requestContent = null) where T : class { var responseContent = await SendAuthRequest(HttpMethod.Post, address, requestContent); var stream = await responseContent.ReadAsStreamAsync(); @@ -727,7 +725,7 @@ protected async Task Post(string address, HttpContent requestContent = nul return content; } - protected async Task Put(string address, HttpContent requestContent = null) where T : class + private async Task Put(string address, HttpContent requestContent = null) where T : class { var content = await SendAuthRequest(HttpMethod.Put, address, requestContent); var stream = await content.ReadAsStreamAsync(); @@ -736,27 +734,19 @@ protected async Task Put(string address, HttpContent requestContent = null return element; } - /// - /// For GetTraceDetails() and GetTraceData(), which may be authenticated or not. - /// - /// - /// - /// - protected virtual void AddAuthentication(HttpRequestMessage message, string url, string method = "GET") { } - - protected async Task SendAuthRequest(HttpMethod method, string address, HttpContent requestContent) + private async Task SendAuthRequest(HttpMethod method, string address, HttpContent requestContent) { using (HttpRequestMessage request = new HttpRequestMessage(method, address)) { AddAuthentication(request, address, method.ToString()); request.Content = requestContent; var response = await _httpClient.SendAsync(request); - await VerifyAndLogReponse(response, $"{GetType().Name} {method}: {address}"); + await VerifyAndLogResponse(response, $"{GetType().Name} {method}: {address}"); return response.Content; } } - protected async Task VerifyAndLogReponse(HttpResponseMessage response, string logMessage) + private async Task VerifyAndLogResponse(HttpResponseMessage response, string logMessage) { if (!response.IsSuccessStatusCode) { @@ -770,6 +760,143 @@ protected async Task VerifyAndLogReponse(HttpResponseMessage response, string lo } } #endregion + + #region Users + /// + /// Details of a User + /// + /// GET /api/0.6/user/#id. + /// + public async Task GetUser(long id) + { + var address = _baseAddress + $"0.6/user/{id}"; + var osm = await Get(address); + return osm.User; + } + + /// + /// Details of multiple Users + /// + /// GET /api/0.6/users?users=#id1,#id2,...,#idn. + /// + public async Task GetUsers(params long[] ids) + { + var address = _baseAddress + $"0.6/users?users={string.Join(",", ids)}"; + var osm = await Get(address); + return osm.Users; + } + + /// + public async Task GetPermissions() + { + var address = _baseAddress + "0.6/permissions"; + var osm = await Get(address, c => AddAuthentication(c, address)); + return osm.Permissions; + } + + /// + public async Task GetUserDetails() + { + var address = _baseAddress + "0.6/user/details"; + var osm = await Get(address, c => AddAuthentication(c, address)); + return osm.User; + } + + /// + public async Task GetUserPreferences() + { + var address = _baseAddress + "0.6/user/preferences"; + var osm = await Get(address, c => AddAuthentication(c, address)); + return osm.Preferences.UserPreferences; + } + + /// + public async Task SetUserPreferences(Preferences preferences) + { + var address = _baseAddress + "0.6/user/preferences"; + var osm = new Osm() { Preferences = preferences }; + var content = new StringContent(osm.SerializeToXml()); + await SendAuthRequest(HttpMethod.Put, address, content); + } + + /// + public async Task GetUserPreference(string key) + { + var address = _baseAddress + $"0.6/user/preferences/{Encode(key)}"; + var content = await Get(address, c => AddAuthentication(c, address)); + var value = await content.ReadAsStringAsync(); + return value; + } + + /// + public async Task SetUserPreference(string key, string value) + { + var address = _baseAddress + $"0.6/user/preferences/{Encode(key)}"; + var content = new StringContent(value); + await SendAuthRequest(HttpMethod.Put, address, content); + } + + /// + public async Task DeleteUserPreference(string key) + { + var address = _baseAddress + $"0.6/user/preferences/{Encode(key)}"; + await SendAuthRequest(HttpMethod.Delete, address, null); + } + #endregion + + private Osm GetOsmRequest(long changesetId, OsmGeo osmGeo) + { + osmGeo.ChangeSetId = changesetId; + var osm = new Osm(); + switch (osmGeo.Type) + { + case OsmGeoType.Node: + osm.Nodes = new[] { osmGeo as Node }; + break; + case OsmGeoType.Way: + osm.Ways = new[] { osmGeo as Way }; + break; + case OsmGeoType.Relation: + osm.Relations = new[] { osmGeo as Relation }; + break; + } + return osm; + } + + private async Task> GetOfType(string address, Action auth = null) where T : class + { + var content = await Get(address, auth); + var streamSource = new XmlOsmStreamSource(await content.ReadAsStreamAsync()); + var elements = streamSource.OfType(); + return elements; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public string ToString(Bounds bounds) + { + StringBuilder x = new StringBuilder(); + x.Append(bounds.MinLongitude.Value.ToString(OsmMaxPrecision, CultureInfo.InvariantCulture)); + x.Append(','); + x.Append(bounds.MinLatitude.Value.ToString(OsmMaxPrecision, CultureInfo.InvariantCulture)); + x.Append(','); + x.Append(bounds.MaxLongitude.Value.ToString(OsmMaxPrecision, CultureInfo.InvariantCulture)); + x.Append(','); + x.Append(bounds.MaxLatitude.Value.ToString(OsmMaxPrecision, CultureInfo.InvariantCulture)); + + return x.ToString(); + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public string ToString(float number) + { + return number.ToString(OsmMaxPrecision, CultureInfo.InvariantCulture); + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public string ToString(double number) + { + return number.ToString(OsmMaxPrecision, CultureInfo.InvariantCulture); + } } } diff --git a/OsmSharp.IO.API/OsmSharp.IO.API.csproj b/OsmSharp.IO.API/OsmSharp.IO.API.csproj index 6dc65da..df30474 100644 --- a/OsmSharp.IO.API/OsmSharp.IO.API.csproj +++ b/OsmSharp.IO.API/OsmSharp.IO.API.csproj @@ -3,9 +3,9 @@ netstandard2.0 OsmSharp.IO.API - 1.0.4 - 1.0.4 - 1.0.4 + 1.0.5 + 1.0.5 + 1.0.5 https://github.com/OsmSharp/io-api https://github.com/OsmSharp/io-api https://www.osmsharp.com/logos/osmsharp_logo64.png @@ -15,7 +15,7 @@ true true snupkg - Copyright 2023 Alex Hennings & Harel Mazor + Copyright 2025 Alex Hennings & Harel Mazor Alex Hennings & Harel Mazor git OsmSharp.IO.API @@ -24,14 +24,13 @@ - + - diff --git a/README.md b/README.md index 24e9cb9..0cd6757 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Pull requests are welcome. You will need VisualStudio, VS Code or Rider to modif ### Features - Supports Logging using ILogger -- Supports Authentication with username and password, OAuth 1 and OAuth 2 +- Supports Authentication with OAuth 2 - Supports every documented operation of the Osm Api v0.6 - Is thread safe @@ -26,9 +26,9 @@ var client = clientFactory.CreateNonAuthClient(); var node = await client.GetNode(100); ``` -### Delete a Node (map changes require BasicAuth or OAuth) +### Delete a Node (map changes require OAuth) ```c# -var authClient = clientFactory.CreateBasicAuthClient("username", "password"); +var authClient = clientFactory.CreateOAuth2Client("oAuth-token"); var changeSetTags = new TagsCollection() { new Tag("comment", "Deleting a node.") }; var changeSetId = await client.CreateChangeset(changeSetTags); node.Version = await client.DeleteElement(changeSetId, node);