From e2135b7e308a6317874c6e18f793e12e939aec39 Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Tue, 21 Mar 2017 15:08:24 +0100 Subject: [PATCH 01/35] start working on multithreaded/multitasked FileSource --- src/Mono/HTTPRequest.cs | 53 ++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/src/Mono/HTTPRequest.cs b/src/Mono/HTTPRequest.cs index 8d95bb5..4a1643a 100644 --- a/src/Mono/HTTPRequest.cs +++ b/src/Mono/HTTPRequest.cs @@ -4,65 +4,52 @@ // //----------------------------------------------------------------------- -namespace Mapbox.Mono -{ +namespace Mapbox.Mono { using System; using System.Net.Http; using System.Threading.Tasks; - internal sealed class HTTPRequest : IAsyncRequest - { + internal sealed class HTTPRequest : IAsyncRequest { private static readonly HttpClient Client = new HttpClient(); - private Task task; - private Action callback; + private Task _task; + private Action _callback; - public HTTPRequest(string url, Action callback) - { - this.callback = callback; - this.task = this.DoRequestAsync(url); + public HTTPRequest(string url, Action callback) { + _callback = callback; + _task = DoRequestAsync(url); } - public void Cancel() - { + public void Cancel() { // FIXME: CancellationTokenSource not available on Mono? // We should use it when it gets available. - this.callback = null; + _callback = null; } - public bool Wait() - { - if (this.task.IsCompleted) - { - if (this.callback != null) - { - this.callback(this.task.Result); - this.callback = null; + public bool Wait() { + if (_task.IsCompleted) { + if (_callback != null) { + _callback(_task.Result); + _callback = null; } } - return this.callback == null; + return _callback == null; } - private async Task DoRequestAsync(string url) - { + private async Task DoRequestAsync(string url) { var response = new Response(); - try - { + try { var message = await Client.GetAsync(url); - if (message.IsSuccessStatusCode) - { + if (message.IsSuccessStatusCode) { response.Data = await message.Content.ReadAsByteArrayAsync(); - } - else - { + } else { response.Error = message.StatusCode.ToString(); } } - catch (Exception exception) - { + catch (Exception exception) { response.Error = exception.Message; } From 92d67f1e44aa52002c4fc1a8249318d94786b38a Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Tue, 21 Mar 2017 20:11:27 +0100 Subject: [PATCH 02/35] rewor FileSource --- src/Mono/FileSource.cs | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/src/Mono/FileSource.cs b/src/Mono/FileSource.cs index 451d81d..53decc5 100644 --- a/src/Mono/FileSource.cs +++ b/src/Mono/FileSource.cs @@ -4,8 +4,7 @@ // //----------------------------------------------------------------------- -namespace Mapbox.Mono -{ +namespace Mapbox.Mono { using System; using System.Collections.Generic; using System.Threading; @@ -19,10 +18,10 @@ namespace Mapbox.Mono /// This implementation requires .NET 4.5 and later. The access token is expected to /// be exported to the environment as MAPBOX_ACCESS_TOKEN. /// - public sealed class FileSource : IFileSource - { - private readonly List requests = new List(); - private readonly string accessToken = Environment.GetEnvironmentVariable("MAPBOX_ACCESS_TOKEN"); + public sealed class FileSource : IFileSource { + + private readonly List _requests = new List(); + private readonly string _accessToken = Environment.GetEnvironmentVariable("MAPBOX_ACCESS_TOKEN"); /// Performs a request asynchronously. /// The HTTP/HTTPS url. @@ -32,15 +31,13 @@ public sealed class FileSource : IFileSource /// request. This handle can be completely ignored if there is no intention of ever /// canceling the request. /// - public IAsyncRequest Request(string url, Action callback) - { - if (this.accessToken != null) - { - url += "?access_token=" + this.accessToken; + public IAsyncRequest Request(string url, Action callback) { + if (_accessToken != null) { + url += "?access_token=" + _accessToken; } var request = new HTTPRequest(url, callback); - this.requests.Add(request); + _requests.Add(request); return request; } @@ -48,21 +45,16 @@ public IAsyncRequest Request(string url, Action callback) /// /// Block until all the requests are processed. /// - public void WaitForAllRequests() - { - while (true) - { + public void WaitForAllRequests() { + while (true) { // Reverse for safely removing while iterating. - for (int i = this.requests.Count - 1; i >= 0; i--) - { - if (this.requests[i].Wait()) - { - this.requests.RemoveAt(i); + for (int i = _requests.Count - 1; i >= 0; i--) { + if (_requests[i].Wait()) { + _requests.RemoveAt(i); } } - if (this.requests.Count == 0) - { + if (_requests.Count == 0) { break; } From 73bc07c8e19be9e37afbed170a1934d9ba6389f3 Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Thu, 20 Apr 2017 16:12:41 +0200 Subject: [PATCH 03/35] [wip] backport new FileSource and HttpRequest --- MapboxSdkCs.sln | 34 ---- src/Map/Map.csproj | 16 -- src/Map/Map.project.json | 2 +- src/Map/Tile.cs | 61 +++--- src/Mono/HTTPRequest.cs | 60 ------ src/Mono/Mono.csproj | 64 ------- src/Mono/Mono.project.json | 4 - src/Mono/MonoUWP.csproj | 144 --------------- src/Mono/Properties/AssemblyInfo.cs | 23 --- src/Mono/Properties/MonoUWP.rd.xml | 33 ---- src/Mono/packages.config | 5 - src/Mono/project.json | 16 -- src/{Mono => Platform}/FileSource.cs | 54 +++++- src/Platform/HTTPRequest.cs | 267 +++++++++++++++++++++++++++ src/Platform/Platform.csproj | 2 + src/Platform/PlatformUWP.csproj | 2 + src/Platform/Response.cs | 51 ++++- test/UnitTest/CompressionTest.cs | 46 ++--- test/UnitTest/DirectionsTest.cs | 3 +- test/UnitTest/FileSourceTest.cs | 195 +++++++++---------- test/UnitTest/GeocoderTest.cs | 33 ++-- test/UnitTest/MapTest.cs | 44 ++--- test/UnitTest/TileTest.cs | 5 +- test/UnitTest/UnitTest.csproj | 4 - test/UnitTest/VectorTileTest.cs | 4 +- 25 files changed, 534 insertions(+), 638 deletions(-) delete mode 100644 src/Mono/HTTPRequest.cs delete mode 100644 src/Mono/Mono.csproj delete mode 100644 src/Mono/Mono.project.json delete mode 100644 src/Mono/MonoUWP.csproj delete mode 100644 src/Mono/Properties/AssemblyInfo.cs delete mode 100644 src/Mono/Properties/MonoUWP.rd.xml delete mode 100644 src/Mono/packages.config delete mode 100644 src/Mono/project.json rename src/{Mono => Platform}/FileSource.cs (57%) create mode 100644 src/Platform/HTTPRequest.cs diff --git a/MapboxSdkCs.sln b/MapboxSdkCs.sln index 6559738..98d1a8c 100644 --- a/MapboxSdkCs.sln +++ b/MapboxSdkCs.sln @@ -10,8 +10,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Geocoding", "src\Geocoding\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Directions", "src\Directions\Directions.csproj", "{08214364-755E-4D6F-B7FF-9D2D49011C20}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mono", "src\Mono\Mono.csproj", "{346B1208-1587-490B-A7DE-A96B86E81CD6}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTest", "test\UnitTest\UnitTest.csproj", "{F04D4384-62B7-4F73-ACD9-C9A5112FC53F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Map", "src\Map\Map.csproj", "{6AECAE3C-A1F3-4B94-976B-D27AA4610879}" @@ -26,8 +24,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeocodingUWP", "src\Geocodi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DirectionsUWP", "src\Directions\DirectionsUWP.csproj", "{44852FAF-423D-446D-BFDC-8A11DDA0C1C9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoUWP", "src\Mono\MonoUWP.csproj", "{CD90BB97-AFF1-4699-96D6-2E9823FA7505}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapUWP", "src\Map\MapUWP.csproj", "{B01D8D98-78E8-4419-8138-4BE47BDD985B}" EndProject Global @@ -102,21 +98,6 @@ Global {08214364-755E-4D6F-B7FF-9D2D49011C20}.DebugUWP|x64.Build.0 = DebugUWP|Any CPU {08214364-755E-4D6F-B7FF-9D2D49011C20}.DebugUWP|x86.ActiveCfg = DebugUWP|Any CPU {08214364-755E-4D6F-B7FF-9D2D49011C20}.DebugUWP|x86.Build.0 = DebugUWP|Any CPU - {346B1208-1587-490B-A7DE-A96B86E81CD6}.DebugNet|Any CPU.ActiveCfg = DebugNet|Any CPU - {346B1208-1587-490B-A7DE-A96B86E81CD6}.DebugNet|Any CPU.Build.0 = DebugNet|Any CPU - {346B1208-1587-490B-A7DE-A96B86E81CD6}.DebugNet|ARM.ActiveCfg = DebugNet|Any CPU - {346B1208-1587-490B-A7DE-A96B86E81CD6}.DebugNet|ARM.Build.0 = DebugNet|Any CPU - {346B1208-1587-490B-A7DE-A96B86E81CD6}.DebugNet|x64.ActiveCfg = DebugNet|Any CPU - {346B1208-1587-490B-A7DE-A96B86E81CD6}.DebugNet|x64.Build.0 = DebugNet|Any CPU - {346B1208-1587-490B-A7DE-A96B86E81CD6}.DebugNet|x86.ActiveCfg = DebugNet|Any CPU - {346B1208-1587-490B-A7DE-A96B86E81CD6}.DebugNet|x86.Build.0 = DebugNet|Any CPU - {346B1208-1587-490B-A7DE-A96B86E81CD6}.DebugUWP|Any CPU.ActiveCfg = DebugUWP|Any CPU - {346B1208-1587-490B-A7DE-A96B86E81CD6}.DebugUWP|ARM.ActiveCfg = DebugUWP|Any CPU - {346B1208-1587-490B-A7DE-A96B86E81CD6}.DebugUWP|ARM.Build.0 = DebugUWP|Any CPU - {346B1208-1587-490B-A7DE-A96B86E81CD6}.DebugUWP|x64.ActiveCfg = DebugUWP|Any CPU - {346B1208-1587-490B-A7DE-A96B86E81CD6}.DebugUWP|x64.Build.0 = DebugUWP|Any CPU - {346B1208-1587-490B-A7DE-A96B86E81CD6}.DebugUWP|x86.ActiveCfg = DebugUWP|Any CPU - {346B1208-1587-490B-A7DE-A96B86E81CD6}.DebugUWP|x86.Build.0 = DebugUWP|Any CPU {F04D4384-62B7-4F73-ACD9-C9A5112FC53F}.DebugNet|Any CPU.ActiveCfg = DebugNet|Any CPU {F04D4384-62B7-4F73-ACD9-C9A5112FC53F}.DebugNet|Any CPU.Build.0 = DebugNet|Any CPU {F04D4384-62B7-4F73-ACD9-C9A5112FC53F}.DebugNet|ARM.ActiveCfg = DebugNet|Any CPU @@ -222,21 +203,6 @@ Global {44852FAF-423D-446D-BFDC-8A11DDA0C1C9}.DebugUWP|x64.Build.0 = DebugUWP|x64 {44852FAF-423D-446D-BFDC-8A11DDA0C1C9}.DebugUWP|x86.ActiveCfg = DebugUWP|x86 {44852FAF-423D-446D-BFDC-8A11DDA0C1C9}.DebugUWP|x86.Build.0 = DebugUWP|x86 - {CD90BB97-AFF1-4699-96D6-2E9823FA7505}.DebugNet|Any CPU.ActiveCfg = DebugUWP|Any CPU - {CD90BB97-AFF1-4699-96D6-2E9823FA7505}.DebugNet|ARM.ActiveCfg = DebugNet|ARM - {CD90BB97-AFF1-4699-96D6-2E9823FA7505}.DebugNet|ARM.Build.0 = DebugNet|ARM - {CD90BB97-AFF1-4699-96D6-2E9823FA7505}.DebugNet|x64.ActiveCfg = DebugNet|x64 - {CD90BB97-AFF1-4699-96D6-2E9823FA7505}.DebugNet|x64.Build.0 = DebugNet|x64 - {CD90BB97-AFF1-4699-96D6-2E9823FA7505}.DebugNet|x86.ActiveCfg = DebugNet|x86 - {CD90BB97-AFF1-4699-96D6-2E9823FA7505}.DebugNet|x86.Build.0 = DebugNet|x86 - {CD90BB97-AFF1-4699-96D6-2E9823FA7505}.DebugUWP|Any CPU.ActiveCfg = DebugUWP|Any CPU - {CD90BB97-AFF1-4699-96D6-2E9823FA7505}.DebugUWP|Any CPU.Build.0 = DebugUWP|Any CPU - {CD90BB97-AFF1-4699-96D6-2E9823FA7505}.DebugUWP|ARM.ActiveCfg = DebugUWP|ARM - {CD90BB97-AFF1-4699-96D6-2E9823FA7505}.DebugUWP|ARM.Build.0 = DebugUWP|ARM - {CD90BB97-AFF1-4699-96D6-2E9823FA7505}.DebugUWP|x64.ActiveCfg = DebugUWP|x64 - {CD90BB97-AFF1-4699-96D6-2E9823FA7505}.DebugUWP|x64.Build.0 = DebugUWP|x64 - {CD90BB97-AFF1-4699-96D6-2E9823FA7505}.DebugUWP|x86.ActiveCfg = DebugUWP|x86 - {CD90BB97-AFF1-4699-96D6-2E9823FA7505}.DebugUWP|x86.Build.0 = DebugUWP|x86 {B01D8D98-78E8-4419-8138-4BE47BDD985B}.DebugNet|Any CPU.ActiveCfg = DebugUWP|Any CPU {B01D8D98-78E8-4419-8138-4BE47BDD985B}.DebugNet|ARM.ActiveCfg = DebugNet|ARM {B01D8D98-78E8-4419-8138-4BE47BDD985B}.DebugNet|ARM.Build.0 = DebugNet|ARM diff --git a/src/Map/Map.csproj b/src/Map/Map.csproj index fdcd842..4424047 100644 --- a/src/Map/Map.csproj +++ b/src/Map/Map.csproj @@ -34,22 +34,6 @@ MinimumRecommendedRules.ruleset - - ..\..\packages\Mapbox.VectorTile.1.0.3-alpha1\lib\net35\Mapbox.VectorTile.ExtensionMethods.dll - True - - - ..\..\packages\Mapbox.VectorTile.1.0.3-alpha1\lib\net35\Mapbox.VectorTile.Geometry.dll - True - - - ..\..\packages\Mapbox.VectorTile.1.0.3-alpha1\lib\net35\Mapbox.VectorTile.PbfReader.dll - True - - - ..\..\packages\Mapbox.VectorTile.1.0.3-alpha1\lib\net35\Mapbox.VectorTile.VectorTileReader.dll - True - diff --git a/src/Map/Map.project.json b/src/Map/Map.project.json index 8ceedc2..a078c50 100644 --- a/src/Map/Map.project.json +++ b/src/Map/Map.project.json @@ -6,6 +6,6 @@ "win": {} }, "dependencies": { - "Mapbox.VectorTile": "1.0.3-alpha2" + "Mapbox.VectorTile": "1.0.4-alpha1" } } \ No newline at end of file diff --git a/src/Map/Tile.cs b/src/Map/Tile.cs index d92ed0a..7850693 100644 --- a/src/Map/Tile.cs +++ b/src/Map/Tile.cs @@ -4,18 +4,17 @@ // //----------------------------------------------------------------------- -namespace Mapbox.Map -{ +namespace Mapbox.Map { using System; using Mapbox.Platform; + using System.Linq; /// /// A Map tile, a square with vector or raster data representing a geographic /// bounding box. More info /// here . /// - public abstract class Tile - { + public abstract class Tile { private CanonicalTileId id; private string error; @@ -24,8 +23,7 @@ public abstract class Tile private Action callback; /// Tile state. - public enum State - { + public enum State { /// New tile, not yet initialized. New, /// Loading data. @@ -38,24 +36,19 @@ public enum State /// Gets the identifier. /// The canonical tile identifier. - public CanonicalTileId Id - { - get - { + public CanonicalTileId Id { + get { return this.id; } - set - { + set { this.id = value; } } /// Gets the error message if any. /// The error string. - public string Error - { - get - { + public string Error { + get { return this.error; } } @@ -64,8 +57,7 @@ public string Error /// Sets the error message. /// /// - public void SetError(string errorMessage) - { + public void SetError(string errorMessage) { error = errorMessage; } @@ -75,10 +67,8 @@ public void SetError(string errorMessage) /// is accusing any error. /// /// The tile state. - public State CurrentState - { - get - { + public State CurrentState { + get { return this.state; } } @@ -89,8 +79,7 @@ public State CurrentState /// /// Initialization parameters. /// The completion callback. - public void Initialize(Parameters param, Action callback) - { + public void Initialize(Parameters param, Action callback) { this.Cancel(); this.state = State.Loading; @@ -107,8 +96,7 @@ public void Initialize(Parameters param, Action callback) /// A that represents the current /// . /// - public override string ToString() - { + public override string ToString() { return this.Id.ToString(); } @@ -136,10 +124,8 @@ public override string ToString() /// }); /// /// - public void Cancel() - { - if (this.request != null) - { + public void Cancel() { + if (this.request != null) { this.request.Cancel(); this.request = null; } @@ -159,14 +145,10 @@ public void Cancel() // a Worker class to abstract this, so on platforms that support threads (like Unity // on the desktop, Android, etc) we can use worker threads and when building for // the browser, we keep it single-threaded. - private void HandleTileResponse(Response response) - { - if (!string.IsNullOrEmpty(response.Error)) - { - this.error = response.Error; - } - else if (this.ParseTileData(response.Data) == false) - { + private void HandleTileResponse(Response response) { + if (response.HasError) { + this.error = string.Join(Environment.NewLine, response.Exceptions.Select(e => e.Message).ToArray()); + } else if (this.ParseTileData(response.Data) == false) { this.error = "ParseError"; } @@ -185,8 +167,7 @@ private void HandleTileResponse(Response response) /// parameters.MapId = "mapbox.mapbox-streets-v7"; /// /// - public struct Parameters - { + public struct Parameters { /// The tile id. public CanonicalTileId Id; diff --git a/src/Mono/HTTPRequest.cs b/src/Mono/HTTPRequest.cs deleted file mode 100644 index a53fd1b..0000000 --- a/src/Mono/HTTPRequest.cs +++ /dev/null @@ -1,60 +0,0 @@ -//----------------------------------------------------------------------- -// -// Copyright (c) 2016 Mapbox. All rights reserved. -// -//----------------------------------------------------------------------- - -namespace Mapbox.Mono { - using System; - using System.Net.Http; - using System.Threading.Tasks; - using Mapbox.Platform; - - internal sealed class HTTPRequest : IAsyncRequest { - private static readonly HttpClient Client = new HttpClient(); - - private Task _task; - private Action _callback; - - public HTTPRequest(string url, Action callback) { - _callback = callback; - _task = DoRequestAsync(url); - } - - public void Cancel() { - // FIXME: CancellationTokenSource not available on Mono? - // We should use it when it gets available. - _callback = null; - } - - public bool Wait() { - if (_task.IsCompleted) { - if (_callback != null) { - _callback(_task.Result); - _callback = null; - } - } - - return _callback == null; - } - - private async Task DoRequestAsync(string url) { - var response = new Response(); - - try { - var message = await Client.GetAsync(url); - - if (message.IsSuccessStatusCode) { - response.Data = await message.Content.ReadAsByteArrayAsync(); - } else { - response.Error = message.StatusCode.ToString(); - } - } - catch (Exception exception) { - response.Error = exception.Message; - } - - return response; - } - } -} diff --git a/src/Mono/Mono.csproj b/src/Mono/Mono.csproj deleted file mode 100644 index c8adc04..0000000 --- a/src/Mono/Mono.csproj +++ /dev/null @@ -1,64 +0,0 @@ - - - - - Debug - AnyCPU - {346B1208-1587-490B-A7DE-A96B86E81CD6} - Library - Mapbox.Mono - Mapbox.Mono - 0.0.1 - v4.5 - - - true - full - false - ..\..\bin\Debug\net35\ - DEBUG; - prompt - 4 - false - - - ..\..\data\Settings.StyleCop - - - true - ..\..\bin\Debug\net35\ - DEBUG; - full - AnyCPU - prompt - MinimumRecommendedRules.ruleset - - - - - - - - Properties\SharedAssemblyInfo.cs - - - - - - - - {FE49745C-01F6-4A3F-BF08-828113D3E19F} - Platform - - - {48BE9D66-3A19-4248-BBDD-4DB4A52B3FE5} - Utils - - - - - - - - - \ No newline at end of file diff --git a/src/Mono/Mono.project.json b/src/Mono/Mono.project.json deleted file mode 100644 index e9e6723..0000000 --- a/src/Mono/Mono.project.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "frameworks": { "net45": {} }, - "runtimes": { "win": {} } -} \ No newline at end of file diff --git a/src/Mono/MonoUWP.csproj b/src/Mono/MonoUWP.csproj deleted file mode 100644 index f72fa36..0000000 --- a/src/Mono/MonoUWP.csproj +++ /dev/null @@ -1,144 +0,0 @@ - - - - - Debug - AnyCPU - {CD90BB97-AFF1-4699-96D6-2E9823FA7505} - Library - Properties - Mapbox.Mono - Mapbox.Mono - en-US - UAP - 10.0.14393.0 - 10.0.10586.0 - 14 - 512 - {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - - - AnyCPU - true - full - false - ..\..\bin\Debug\uap10\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP - prompt - 4 - - - x86 - true - bin\x86\Debug\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP - ;2008 - full - x86 - false - prompt - - - ARM - true - bin\ARM\Debug\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP - ;2008 - full - ARM - false - prompt - - - x64 - true - bin\x64\Debug\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP - ;2008 - full - x64 - false - prompt - - - - - - - - Properties\SharedAssemblyInfo.cs - - - - - - - - - {74dca811-e397-491d-b8af-6b4cf91a42fa} - PlatformUWP - - - {fe92b261-daeb-4548-906f-7d2daac98ee4} - UtilsUWP - - - - 14.0 - - - true - ..\..\bin\Debug\uap10\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP - true - full - AnyCPU - false - prompt - MinimumRecommendedRules.ruleset - - - true - bin\x86\DebugUWP\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP - ;2008 - true - full - x86 - false - prompt - MinimumRecommendedRules.ruleset - - - true - bin\ARM\DebugUWP\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP - ;2008 - true - full - ARM - false - prompt - MinimumRecommendedRules.ruleset - - - true - bin\x64\DebugUWP\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP - ;2008 - true - full - x64 - false - prompt - MinimumRecommendedRules.ruleset - - - - \ No newline at end of file diff --git a/src/Mono/Properties/AssemblyInfo.cs b/src/Mono/Properties/AssemblyInfo.cs deleted file mode 100644 index 8bbb600..0000000 --- a/src/Mono/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,23 +0,0 @@ -// - -using System.Reflection; -using System.Runtime.CompilerServices; - -// Information about this assembly is defined by the following attributes. -// Change them to the values specific to your project. - -[assembly: AssemblyTitle("Mono")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("")] -[assembly: AssemblyCopyright("")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - - -// The following attributes are used to specify the signing key for the assembly, -// if desired. See the Mono documentation for more information about signing. - -//[assembly: AssemblyDelaySign(false)] -//[assembly: AssemblyKeyFile("")] diff --git a/src/Mono/Properties/MonoUWP.rd.xml b/src/Mono/Properties/MonoUWP.rd.xml deleted file mode 100644 index 95c4ac8..0000000 --- a/src/Mono/Properties/MonoUWP.rd.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - diff --git a/src/Mono/packages.config b/src/Mono/packages.config deleted file mode 100644 index f22d165..0000000 --- a/src/Mono/packages.config +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/Mono/project.json b/src/Mono/project.json deleted file mode 100644 index 92d1456..0000000 --- a/src/Mono/project.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "dependencies": { - "Microsoft.NETCore.UniversalWindowsPlatform": "5.1.0" - }, - "frameworks": { - "uap10.0": {} - }, - "runtimes": { - "win10-arm": {}, - "win10-arm-aot": {}, - "win10-x86": {}, - "win10-x86-aot": {}, - "win10-x64": {}, - "win10-x64-aot": {} - } -} \ No newline at end of file diff --git a/src/Mono/FileSource.cs b/src/Platform/FileSource.cs similarity index 57% rename from src/Mono/FileSource.cs rename to src/Platform/FileSource.cs index 8fc9b98..a41ff0d 100644 --- a/src/Mono/FileSource.cs +++ b/src/Platform/FileSource.cs @@ -4,11 +4,18 @@ // //----------------------------------------------------------------------- -namespace Mapbox.Mono { - using System; - using System.Collections.Generic; - using System.Threading; - using Mapbox.Platform; +namespace Mapbox.Platform { + + + using System; + using System.Collections.Generic; + using System.Net; + using System.Net.Security; +#if !NETFX_CORE + using System.Security.Cryptography.X509Certificates; +#endif + using System.Threading; + /// /// Mono implementation of the FileSource class. It will use Mono's @@ -21,9 +28,19 @@ namespace Mapbox.Mono { /// public sealed class FileSource : IFileSource { + private readonly List _requests = new List(); private readonly string _accessToken = Environment.GetEnvironmentVariable("MAPBOX_ACCESS_TOKEN"); + + /// Length of rate-limiting interval in seconds. https://www.mapbox.com/api-documentation/#rate-limits + private int? XRateLimitInterval; + /// Maximum number of requests you may make in the current interval before reaching the limit. https://www.mapbox.com/api-documentation/#rate-limits + private long? XRateLimitLimit; + /// Timestamp of when the current interval will end and the ratelimit counter is reset. https://www.mapbox.com/api-documentation/#rate-limits + private DateTime? XRateLimitReset; + + /// Performs a request asynchronously. /// The HTTP/HTTPS url. /// Callback to be called after the request is completed. @@ -37,12 +54,28 @@ public IAsyncRequest Request(string url, Action callback) { url += "?access_token=" + _accessToken; } + // TODO: + // * add queue for requests + // * evaluate rate limits (headers and status code) + // * throttle requests accordingly + + //var request = new HTTPRequest_v2(url, proxyResponse); var request = new HTTPRequest(url, callback); _requests.Add(request); return request; } + + // TODO: look at requests and implement throttling if needed + private void proxyResponse(Response response) { + if (response.XRateLimitInterval.HasValue) { XRateLimitInterval = response.XRateLimitInterval; } + if (response.XRateLimitLimit.HasValue) { XRateLimitLimit = response.XRateLimitLimit; } + if (response.XRateLimitReset.HasValue) { XRateLimitReset = response.XRateLimitReset; } + //callback(response); + } + + /// /// Block until all the requests are processed. /// @@ -50,7 +83,7 @@ public void WaitForAllRequests() { while (true) { // Reverse for safely removing while iterating. for (int i = _requests.Count - 1; i >= 0; i--) { - if (_requests[i].Wait()) { + if (_requests[i].IsCompleted) { _requests.RemoveAt(i); } } @@ -60,11 +93,16 @@ public void WaitForAllRequests() { } #if !WINDOWS_UWP - Thread.Sleep(10); + Thread.Sleep(50); #else - System.Threading.Tasks.Task.Delay(5).Wait(); + System.Threading.Tasks.Task.Delay(50).Wait(); #endif } } + + + + + } } diff --git a/src/Platform/HTTPRequest.cs b/src/Platform/HTTPRequest.cs new file mode 100644 index 0000000..18646ce --- /dev/null +++ b/src/Platform/HTTPRequest.cs @@ -0,0 +1,267 @@ +#if UNITY_EDITOR || UNITY_STANDALONE || UNITY_ANDROID || UNITY_WP_8_1 || UNITY_WSA || UNITY_WEBGL || UNITY_IOS || UNITY_PS4 || UNITY_SAMSUNGTV || UNITY_XBOXONE || UNITY_TIZEN || UNITY_TVOS +#define UNITY +#endif +//----------------------------------------------------------------------- +// +// Copyright (c) 2016 Mapbox. All rights reserved. +// Based on http://stackoverflow.com/a/12606963 and http://wiki.unity3d.com/index.php/WebAsync +// +//----------------------------------------------------------------------- + +namespace Mapbox.Platform { + + + using System; + using System.Net; +#if !UNITY && !NETFX_CORE + using System.Net.Cache; +#endif + using System.IO; + using System.Collections.Generic; + using System.Threading; + using System.ComponentModel; + + //using System.Windows.Threading; + + internal sealed class HTTPRequest : IAsyncRequest { + + + public bool IsCompleted = false; + + + private Action _callback; + private HttpWebRequest _hwr; + private int _timeOut; +#if !UNITY + private SynchronizationContext _sync = AsyncOperationManager.SynchronizationContext; +#endif + + + /// + /// + /// + /// + /// + /// seconds + public HTTPRequest(string url, Action callback, int timeOut = 10) { + + _callback = callback; + _timeOut = timeOut; + + _hwr = WebRequest.Create(url) as HttpWebRequest; + _hwr.Method = "GET"; +#if !UNITY && !NETFX_CORE + _hwr.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; +#endif +#if !NETFX_CORE + _hwr.UserAgent = "mapbox-sdk-cs"; + //_hwr.CachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore); + _hwr.CachePolicy = new RequestCachePolicy(RequestCacheLevel.CacheIfAvailable); +#endif + //_hwr.Timeout = timeOut * 1000; doesn't work in async calls, see below + + getResponseAsync(_hwr, EvaluateResponse); + } + +#region oldversion + + //private void GetResponseAsync(HttpWebRequest request, Action gotResponse) { + // //private void GetResponseAsync(HttpWebRequest request) { + + // // create an additional action wrapper, because of: + // // https://msdn.microsoft.com/en-us/library/system.net.httpwebrequest.begingetresponse.aspx + // // The BeginGetResponse method requires some synchronous setup tasks to complete (DNS resolution, + // //proxy detection, and TCP socket connection, for example) before this method becomes asynchronous. + // // As a result, this method should never be called on a user interface (UI) thread because it might + // // take considerable time(up to several minutes depending on network settings) to complete the + // // initial synchronous setup tasks before an exception for an error is thrown or the method succeeds. + + // Action actionWrapper = () => { + // request.BeginGetResponse((r) => { + // try { // there's a try/catch here because execution path is different from invokation one, exception here may cause a crash + // HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(r); + // System.Diagnostics.Debug.WriteLine(string.Format("HTTPRequest before 'gotResponse', thread id:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId)); + // gotResponse(response, null); + // } + // // EndGetResponse() throws on on some status codes, try to get response anyway (and status codes) + // catch (WebException wex) { + // HttpWebResponse hwr = wex.Response as HttpWebResponse; + // if (null == hwr) { + // throw; + // } + // gotResponse(hwr, wex); + // } + // catch (Exception ex) { + // gotResponse(null, ex); + // } + // } + // , request); + // }; + + // //http://www.cshandler.com/2011/07/delegate-and-async-programming-part-ii.html + // //http://www.yoda.arachsys.com/csharp/threads/printable.shtml + // //https://msdn.microsoft.com/en-us/library/2e08f6yc(v=vs.110).aspx + // //http://www.c-sharpcorner.com/UploadFile/vendettamit/delegate-and-async-programming-C-Sharp-asynccallback-and-object-state/ + // //http://xcalibursystems.com/2011/10/c-how-to-get-a-return-value-from-an-action/ + // //https://stackoverflow.com/questions/8099631/how-to-return-value-from-action + // //http://blog.aggregatedintelligence.com/2010/06/c-asynchronous-programming-using.html + // //http://csharpindepth.com/Articles/Chapter2/Events.aspx?printable=true + + + // // !!!!BeginInvoke runs on a thread of the thread pool (!= main thread)!!!! + // // TODO: how to influence threadpool: nr of threads etc. + // System.Diagnostics.Debug.WriteLine(string.Format("HTTPRequest before 'BeginInvoke', thread id:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId)); + // actionWrapper.BeginInvoke(new AsyncCallback((iASyncResult) => { + // System.Diagnostics.Debug.WriteLine(string.Format("HTTPRequest within 'BeginInvoke', thread id:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId)); + // var action = (Action)iASyncResult.AsyncState; + // action.EndInvoke(iASyncResult); + // System.Diagnostics.Debug.WriteLine(string.Format("HTTPRequest after 'EndInvoke', thread id:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId)); + // }) + // , actionWrapper + // ); + + // System.Diagnostics.Debug.WriteLine(string.Format("HTTPRequest past 'BeginInvoke', thread id:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId)); + //} + + +#endregion + + private void getResponseAsync(HttpWebRequest request, Action gotResponse) { + + // create an additional action wrapper, because of: + // https://msdn.microsoft.com/en-us/library/system.net.httpwebrequest.begingetresponse.aspx + // The BeginGetResponse method requires some synchronous setup tasks to complete (DNS resolution, + //proxy detection, and TCP socket connection, for example) before this method becomes asynchronous. + // As a result, this method should never be called on a user interface (UI) thread because it might + // take considerable time(up to several minutes depending on network settings) to complete the + // initial synchronous setup tasks before an exception for an error is thrown or the method succeeds. + + Action actionWrapper = () => { + // BeginInvoke runs on a thread of the thread pool (!= main/UI thread) + // that's why we need SynchronizationContext when + // TODO: how to influence threadpool: nr of threads etc. + request.BeginGetResponse((asycnResult) => { + try { // there's a try/catch here because execution path is different from invokation one, exception here may cause a crash + HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asycnResult); + gotResponse(response, null); + } + // EndGetResponse() throws on on some status codes, try to get response anyway (and status codes) + catch (WebException wex) { + HttpWebResponse hwr = wex.Response as HttpWebResponse; + if (null == hwr) { + throw; + } + gotResponse(hwr, wex); + } + catch (Exception ex) { + gotResponse(null, ex); + } + } + , null); + }; + + try { + actionWrapper.BeginInvoke(new AsyncCallback((iAsyncResult) => { + var action = (Action)iAsyncResult.AsyncState; + action.EndInvoke(iAsyncResult); + }) + , actionWrapper); + } + catch (Exception ex) { + gotResponse(null, ex); + } + } + + + + private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { + + var response = new Response(); + + if (null != apiEx) { + response.AddException(apiEx); + } + + // timeout: API response is null + if (null == apiResponse) { + response.AddException(new Exception("No Reponse.")); + } else { + // TODO: evaluate headers and add custom exception, eg if rate limit is exceeded + // https://www.mapbox.com/api-documentation/#rate-limits + // X-Rate-Limit-Interval + // X-Rate-Limit-Limit + // X-Rate-Limit-Reset + if (null != apiResponse.Headers) { + response.Headers = new Dictionary(); + for (int i = 0; i < apiResponse.Headers.Count; i++) { +// TODO: implement .Net Core / UWP implementation +#if !NETFX_CORE + string key = apiResponse.Headers.Keys[i]; + string val = apiResponse.Headers[i]; + response.Headers.Add(key, val); + if (key.Equals("X-Rate-Limit-Interval", StringComparison.InvariantCultureIgnoreCase)) { + int limitInterval; + if (int.TryParse(val, out limitInterval)) { response.XRateLimitInterval = limitInterval; } + } else if (key.Equals("X-Rate-Limit-Limit", StringComparison.InvariantCultureIgnoreCase)) { + long limitLimit; + if (long.TryParse(val, out limitLimit)) { response.XRateLimitLimit = limitLimit; } + } else if (key.Equals("X-Rate-Limit-Reset", StringComparison.InvariantCultureIgnoreCase)) { + double unixTimestamp; + if (double.TryParse(val, out unixTimestamp)) { + DateTime beginningOfTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + response.XRateLimitReset = beginningOfTime.AddSeconds(unixTimestamp).ToLocalTime(); + } + } else if (key.Equals("Content-Type", StringComparison.InvariantCultureIgnoreCase)) { + response.ContentType = val; + } +#endif + } + } + + if (apiResponse.StatusCode != HttpStatusCode.OK) { + response.AddException(new Exception(string.Format("{0}: {1}", apiResponse.StatusCode, apiResponse.StatusDescription))); + } + int statusCode = (int)apiResponse.StatusCode; + response.StatusCode = statusCode; + if (429 == statusCode) { + response.AddException(new Exception("Rate limit hit")); + } + + if (null != apiResponse) { + using (Stream responseStream = apiResponse.GetResponseStream()) { + byte[] buffer = new byte[0x1000]; + int bytesRead; + using (MemoryStream ms = new MemoryStream()) { + while (0 != (bytesRead = responseStream.Read(buffer, 0, buffer.Length))) { + ms.Write(buffer, 0, bytesRead); + } + response.Data = ms.ToArray(); + } + } + } + } + + // post (async) callback back to the main/UI thread + // Unity: SynchronizationContext doesn't do anything + // use the Dispatcher +#if !UNITY + _sync.Post(delegate { _callback(response); }, null); +#else + UnityMainThreadDispatcher.Instance().Enqueue(() => _callback(response)); +#endif + IsCompleted = true; + } + + + public void Cancel() { + + _callback = null; + if (null != _hwr) { + _hwr.Abort(); + } + IsCompleted = true; + } + + + } +} diff --git a/src/Platform/Platform.csproj b/src/Platform/Platform.csproj index 792866b..70b9759 100644 --- a/src/Platform/Platform.csproj +++ b/src/Platform/Platform.csproj @@ -41,6 +41,8 @@ Properties\SharedAssemblyInfo.cs + + diff --git a/src/Platform/PlatformUWP.csproj b/src/Platform/PlatformUWP.csproj index 0f4452e..689e868 100644 --- a/src/Platform/PlatformUWP.csproj +++ b/src/Platform/PlatformUWP.csproj @@ -68,6 +68,8 @@ Properties\SharedAssemblyInfo.cs + + diff --git a/src/Platform/Response.cs b/src/Platform/Response.cs index 613a620..bdb3bf4 100644 --- a/src/Platform/Response.cs +++ b/src/Platform/Response.cs @@ -4,20 +4,61 @@ // //----------------------------------------------------------------------- +using System; using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Mapbox.Platform { + -namespace Mapbox.Platform -{ /// A response from a request. - public struct Response - { + public struct Response { + + + public bool RateLimitHit { + get { return StatusCode.HasValue ? 429 == StatusCode.Value : false; } + } + + public bool HasError { + get { + return _exceptions == null ? false : _exceptions.Count > 0; + } + } + + public int? StatusCode; + + + public string ContentType; + + + /// Length of rate-limiting interval in seconds. https://www.mapbox.com/api-documentation/#rate-limits + public int? XRateLimitInterval; + + /// Maximum number of requests you may make in the current interval before reaching the limit. https://www.mapbox.com/api-documentation/#rate-limits + public long? XRateLimitLimit; + + /// Timestamp of when the current interval will end and the ratelimit counter is reset. https://www.mapbox.com/api-documentation/#rate-limits + public DateTime? XRateLimitReset; + + private List _exceptions; /// Error description, set on error, empty otherwise. - public string Error; + public ReadOnlyCollection Exceptions { + get { return null == _exceptions ? null : _exceptions.AsReadOnly(); } + } + /// Headers of the response. public Dictionary Headers; + /// Raw data fetched from the request. public byte[] Data; + + public void AddException(Exception ex) { + if (null == _exceptions) { _exceptions = new List(); } + _exceptions.Add(ex); + } + + } } \ No newline at end of file diff --git a/test/UnitTest/CompressionTest.cs b/test/UnitTest/CompressionTest.cs index f455180..9d5e3cc 100644 --- a/test/UnitTest/CompressionTest.cs +++ b/test/UnitTest/CompressionTest.cs @@ -4,43 +4,37 @@ // //----------------------------------------------------------------------- -namespace Mapbox.UnitTest -{ - using System.Text; - using Mapbox.Platform; - using Mapbox.Utils; - using NUnit.Framework; +namespace Mapbox.UnitTest { + using System.Text; + using Mapbox.Platform; + using Mapbox.Utils; + using NUnit.Framework; - [TestFixture] - internal class CompressionTest - { + [TestFixture] + internal class CompressionTest { [Test] - public void Empty() - { + public void Empty() { var buffer = new byte[] { }; Assert.AreEqual(buffer, Compression.Decompress(buffer)); } [Test] - public void NotCompressed() - { + public void NotCompressed() { var buffer = Encoding.ASCII.GetBytes("foobar"); Assert.AreEqual(buffer, Compression.Decompress(buffer)); } [Test] - public void Corrupt() - { - var fs = new Mono.FileSource(); + public void Corrupt() { + var fs = new FileSource(); var buffer = new byte[] { }; // Vector tiles are compressed. fs.Request( "https://api.mapbox.com/v4/mapbox.mapbox-streets-v7/0/0/0.vector.pbf", - (Response res) => - { - buffer = res.Data; - }); + (Response res) => { + buffer = res.Data; + }); fs.WaitForAllRequests(); @@ -54,18 +48,16 @@ public void Corrupt() } [Test] - public void Decompress() - { - var fs = new Mono.FileSource(); + public void Decompress() { + var fs = new FileSource(); var buffer = new byte[] { }; // Vector tiles are compressed. fs.Request( "https://api.mapbox.com/v4/mapbox.mapbox-streets-v7/0/0/0.vector.pbf", - (Response res) => - { - buffer = res.Data; - }); + (Response res) => { + buffer = res.Data; + }); fs.WaitForAllRequests(); diff --git a/test/UnitTest/DirectionsTest.cs b/test/UnitTest/DirectionsTest.cs index 2d4b2da..cd6b13c 100644 --- a/test/UnitTest/DirectionsTest.cs +++ b/test/UnitTest/DirectionsTest.cs @@ -7,6 +7,7 @@ namespace Mapbox.UnitTest { using Directions; using Mapbox.Json; + using Mapbox.Platform; using NUnit.Framework; /// @@ -16,7 +17,7 @@ namespace Mapbox.UnitTest { internal class DirectionsTest { private string basicResponse = "{\"routes\":[{\"legs\":[{\"steps\":[],\"summary\":\"\",\"duration\":214.4,\"distance\":1318.2}],\"geometry\":\"_urwFt}qbMuLp_@jWzPoHhRMK\",\"duration\":214.4,\"distance\":1318.2}],\"waypoints\":[{\"name\":\"East 13th Street\",\"location\":[-73.988909,40.733122]},{\"name\":\"6th Avenue\",\"location\":[-74.00001,40.733004]}],\"code\":\"Ok\"}"; private string responseWithSteps = "{\"routes\":[{\"legs\":[{\"steps\":[{\"intersections\":[{\"out\":0,\"entry\":[true],\"bearings\":[299],\"location\":[-73.988909,40.733122]},{\"out\":3,\"location\":[-73.989868,40.733528],\"bearings\":[15,120,195,300],\"entry\":[true,false,false,true],\"in\":1},{\"out\":3,\"location\":[-73.990945,40.733978],\"bearings\":[15,120,195,300],\"entry\":[false,false,true,true],\"in\":1},{\"out\":3,\"location\":[-73.992266,40.734532],\"bearings\":[30,120,210,300],\"entry\":[true,false,false,true],\"in\":1}],\"geometry\":\"_urwFt}qbMqA~DyAvEmBfG{CpJ\",\"maneuver\":{\"bearing_after\":299,\"type\":\"depart\",\"modifier\":\"left\",\"bearing_before\":0,\"location\":[-73.988909,40.733122],\"instruction\":\"Head northwest on East 13th Street\"},\"duration\":90.5,\"distance\":502.1,\"name\":\"East 13th Street\",\"mode\":\"driving\"},{\"intersections\":[{\"out\":2,\"location\":[-73.994118,40.735313],\"bearings\":[30,120,210,300],\"entry\":[false,false,true,true],\"in\":1},{\"out\":2,\"location\":[-73.994585,40.734672],\"bearings\":[30,120,210,300],\"entry\":[false,true,true,false],\"in\":0},{\"out\":2,\"location\":[-73.99505,40.734034],\"bearings\":[30,120,210,300],\"entry\":[false,false,true,true],\"in\":0},{\"out\":2,\"location\":[-73.995489,40.733437],\"bearings\":[30,120,210,300],\"entry\":[false,true,true,false],\"in\":0},{\"out\":2,\"location\":[-73.995914,40.732847],\"bearings\":[30,120,210,300],\"entry\":[false,false,true,true],\"in\":0},{\"out\":2,\"location\":[-73.996351,40.732255],\"bearings\":[30,120,210,300],\"entry\":[false,true,true,false],\"in\":0}],\"geometry\":\"ubswFf~rbM~B|A~BzAtBvAtBrAtBvAh@Vd@`@lAx@JH\",\"maneuver\":{\"bearing_after\":209,\"type\":\"turn\",\"modifier\":\"left\",\"bearing_before\":299,\"location\":[-73.994118,40.735313],\"instruction\":\"Turn left onto 5th Avenue\"},\"duration\":67.8,\"distance\":496.3,\"name\":\"5th Avenue\",\"mode\":\"driving\"},{\"intersections\":[{\"out\":2,\"location\":[-73.996976,40.731414],\"bearings\":[30,120,300],\"entry\":[false,true,true],\"in\":0}],\"geometry\":\"ijrwFbpsbMKPoChHEH\",\"maneuver\":{\"bearing_after\":305,\"type\":\"end of road\",\"modifier\":\"right\",\"bearing_before\":212,\"location\":[-73.996976,40.731414],\"instruction\":\"Turn right onto Washington Square North\"},\"duration\":21,\"distance\":164.2,\"name\":\"Washington Square North\",\"mode\":\"driving\"},{\"intersections\":[{\"out\":3,\"location\":[-73.998612,40.732215],\"bearings\":[30,120,210,300],\"entry\":[false,false,true,true],\"in\":1}],\"geometry\":\"korwFhzsbMmCbH\",\"maneuver\":{\"bearing_after\":303,\"type\":\"new name\",\"modifier\":\"straight\",\"bearing_before\":303,\"location\":[-73.998612,40.732215],\"instruction\":\"Continue straight onto Waverly Place\"},\"duration\":34.5,\"distance\":146,\"name\":\"Waverly Place\",\"mode\":\"driving\"},{\"intersections\":[{\"out\":0,\"location\":[-74.000066,40.732929],\"bearings\":[30,120,210,300],\"entry\":[true,false,false,true],\"in\":1}],\"geometry\":\"ysrwFlctbMMK\",\"maneuver\":{\"bearing_after\":30,\"type\":\"turn\",\"modifier\":\"right\",\"bearing_before\":303,\"location\":[-74.000066,40.732929],\"instruction\":\"Turn right onto 6th Avenue\"},\"duration\":0.6,\"distance\":9.6,\"name\":\"6th Avenue\",\"mode\":\"driving\"},{\"intersections\":[{\"in\":0,\"entry\":[true],\"bearings\":[210],\"location\":[-74.00001,40.733004]}],\"geometry\":\"gtrwF`ctbM\",\"maneuver\":{\"bearing_after\":0,\"location\":[-74.000066,40.732929],\"bearing_before\":30,\"type\":\"arrive\",\"instruction\":\"You have arrived at your destination\"},\"duration\":0,\"distance\":0,\"name\":\"6th Avenue\",\"mode\":\"driving\"}],\"summary\":\"East 13th Street, 5th Avenue\",\"duration\":214.4,\"distance\":1318.2}],\"geometry\":\"_urwFt}qbMuLp_@jWzPoHhRMK\",\"duration\":214.4,\"distance\":1318.2}],\"waypoints\":[{\"name\":\"East 13th Street\",\"location\":[-73.988909,40.733122]},{\"name\":\"6th Avenue\",\"location\":[-74.00001,40.733004]}],\"code\":\"Ok\"}"; - private Directions directions = new Directions(new Mono.FileSource()); + private Directions directions = new Directions(new FileSource()); [Test] public void SerializesAndDeserializesBasic() { diff --git a/test/UnitTest/FileSourceTest.cs b/test/UnitTest/FileSourceTest.cs index f755f7a..0360e81 100644 --- a/test/UnitTest/FileSourceTest.cs +++ b/test/UnitTest/FileSourceTest.cs @@ -4,109 +4,94 @@ // //----------------------------------------------------------------------- -namespace Mapbox.UnitTest -{ - using System; - using Mapbox.Platform; - using NUnit.Framework; - - [TestFixture] - internal class FileSourceTest - { - private const string Uri = "https://api.mapbox.com/geocoding/v5/mapbox.places/helsinki.json"; - private Mono.FileSource fs; - - [SetUp] - public void SetUp() - { - this.fs = new Mono.FileSource(); - } - - [Test] - public void AccessTokenSet() - { - Assert.IsNotNull( - Environment.GetEnvironmentVariable("MAPBOX_ACCESS_TOKEN"), - "MAPBOX_ACCESS_TOKEN not set in the environment."); - } - - [Test] - public void Request() - { - this.fs.Request( - Uri, - (Response res) => - { - Assert.IsNotNull(res.Data, "No data received from the servers."); - }); - - this.fs.WaitForAllRequests(); - } - - [Test] - public void MultipleRequests() - { - int count = 0; - - this.fs.Request(Uri, (Response res) => ++count); - this.fs.Request(Uri, (Response res) => ++count); - this.fs.Request(Uri, (Response res) => ++count); - - this.fs.WaitForAllRequests(); - - Assert.AreEqual(count, 3, "Should have received 3 replies."); - } - - [Test] - public void RequestCancel() - { - var request = this.fs.Request( - Uri, - (Response res) => - { - Assert.Fail("Should never happen."); - }); - - request.Cancel(); - - this.fs.WaitForAllRequests(); - } - - [Test] - public void RequestDnsError() - { - this.fs.Request( - "https://dnserror.shouldnotwork", - (Response res) => - { - // Do no assume any error message. Mono != .NET. - Assert.NotNull(res.Error); - }); - - this.fs.WaitForAllRequests(); - } - - [Test] - public void RequestForbidden() - { - // Mapbox servers will return a forbidden when attempting - // to access a page outside the API space with a token - // on the query. Let's hope the behaviour stay like this. - this.fs.Request( - "https://mapbox.com/forbidden", - (Response res) => - { - Assert.AreEqual(res.Error, "Forbidden"); - }); - - this.fs.WaitForAllRequests(); - } - - [Test] - public void WaitWithNoRequests() - { - // This should simply not block. - this.fs.WaitForAllRequests(); - } - } +namespace Mapbox.UnitTest { + using System; + using Mapbox.Platform; + using NUnit.Framework; + + [TestFixture] + internal class FileSourceTest { + private const string Uri = "https://api.mapbox.com/geocoding/v5/mapbox.places/helsinki.json"; + private FileSource fs; + + [SetUp] + public void SetUp() { + this.fs = new FileSource(); + } + + [Test] + public void AccessTokenSet() { + Assert.IsNotNull( + Environment.GetEnvironmentVariable("MAPBOX_ACCESS_TOKEN"), + "MAPBOX_ACCESS_TOKEN not set in the environment."); + } + + [Test] + public void Request() { + this.fs.Request( + Uri, + (Response res) => { + Assert.IsNotNull(res.Data, "No data received from the servers."); + }); + + this.fs.WaitForAllRequests(); + } + + [Test] + public void MultipleRequests() { + int count = 0; + + this.fs.Request(Uri, (Response res) => ++count); + this.fs.Request(Uri, (Response res) => ++count); + this.fs.Request(Uri, (Response res) => ++count); + + this.fs.WaitForAllRequests(); + + Assert.AreEqual(count, 3, "Should have received 3 replies."); + } + + [Test] + public void RequestCancel() { + var request = this.fs.Request( + Uri, + (Response res) => { + Assert.Fail("Should never happen."); + }); + + request.Cancel(); + + this.fs.WaitForAllRequests(); + } + + [Test] + public void RequestDnsError() { + this.fs.Request( + "https://dnserror.shouldnotwork", + (Response res) => { + Assert.IsTrue(res.HasError); + }); + + this.fs.WaitForAllRequests(); + } + + [Test] + public void RequestForbidden() { + // Mapbox servers will return a forbidden when attempting + // to access a page outside the API space with a token + // on the query. Let's hope the behaviour stay like this. + this.fs.Request( + "https://mapbox.com/forbidden", + (Response res) => { + Assert.IsTrue(res.HasError, "Forbidden"); + }); + + this.fs.WaitForAllRequests(); + } + + [Test] + public void WaitWithNoRequests() { + // This should simply not block. + this.fs.WaitForAllRequests(); + } + } } \ No newline at end of file diff --git a/test/UnitTest/GeocoderTest.cs b/test/UnitTest/GeocoderTest.cs index 017c2e6..9cddd4d 100644 --- a/test/UnitTest/GeocoderTest.cs +++ b/test/UnitTest/GeocoderTest.cs @@ -4,26 +4,24 @@ // //----------------------------------------------------------------------- -namespace Mapbox.UnitTest -{ - using Geocoding; - using Mapbox.Json; - using Mapbox.Utils.JsonConverters; - using NUnit.Framework; - - /// - /// Test that Geocoder serializes and deserializes responses correctly. - /// - [TestFixture] - internal class GeocoderTest - { - private readonly Geocoder geocoder = new Geocoder(new Mono.FileSource()); +namespace Mapbox.UnitTest { + using Geocoding; + using Mapbox.Json; + using Mapbox.Platform; + using Mapbox.Utils.JsonConverters; + using NUnit.Framework; + + /// + /// Test that Geocoder serializes and deserializes responses correctly. + /// + [TestFixture] + internal class GeocoderTest { + private readonly Geocoder geocoder = new Geocoder(new FileSource()); private string forwardResponse = "{\"type\":\"FeatureCollection\",\"query\":[\"minneapolis\"],\"features\":[{\"id\":\"place.12871500125885940\",\"type\":\"Feature\",\"text\":\"Minneapolis\",\"place_name\":\"Minneapolis, Minnesota, United States\",\"relevance\":0.99,\"properties\":{\"wikidata\":\"Q36091\"},\"bbox\":[-93.5226520099878,44.7853029900244,-93.1424209928836,45.2129100099882],\"center\":[-93.2655,44.9773],\"geometry\":{\"type\":\"Point\",\"coordinates\":[-93.2655,44.9773]},\"context\":[{\"id\":\"postcode.11389548391063390\",\"text\":\"55415\"},{\"id\":\"region.12225983719702200\",\"text\":\"Minnesota\",\"short_code\":\"US-MN\",\"wikidata\":\"Q1527\"},{\"id\":\"country.12862386939497690\",\"text\":\"United States\",\"short_code\":\"us\",\"wikidata\":\"Q30\"}]},{\"id\":\"poi.15555644443768740\",\"type\":\"Feature\",\"text\":\"Minneapolis City Hall\",\"place_name\":\"Minneapolis City Hall, Minneapolis, Minnesota 55415, United States\",\"relevance\":0.99,\"properties\":{\"wikidata\":\"Q1384874\",\"landmark\":true,\"tel\":null,\"address\":null,\"category\":\"other\"},\"center\":[-93.265277777778,44.977222222222],\"geometry\":{\"type\":\"Point\",\"coordinates\":[-93.265277777778,44.977222222222]},\"context\":[{\"id\":\"neighborhood.13081559486410050\",\"text\":\"Greater Central\"},{\"id\":\"place.12871500125885940\",\"text\":\"Minneapolis\",\"wikidata\":\"Q36091\"},{\"id\":\"postcode.11389548391063390\",\"text\":\"55415\"},{\"id\":\"region.12225983719702200\",\"text\":\"Minnesota\",\"short_code\":\"US-MN\",\"wikidata\":\"Q1527\"},{\"id\":\"country.12862386939497690\",\"text\":\"United States\",\"short_code\":\"us\",\"wikidata\":\"Q30\"}]},{\"id\":\"poi.6527299549845510\",\"type\":\"Feature\",\"text\":\"Minneapolis Grain Exchange\",\"place_name\":\"Minneapolis Grain Exchange, Minneapolis, Minnesota 55415, United States\",\"relevance\":0.99,\"properties\":{\"wikidata\":\"Q1540984\",\"landmark\":true,\"tel\":null,\"address\":null,\"category\":\"other\"},\"center\":[-93.2636,44.9775],\"geometry\":{\"type\":\"Point\",\"coordinates\":[-93.2636,44.9775]},\"context\":[{\"id\":\"neighborhood.13081559486410050\",\"text\":\"Greater Central\"},{\"id\":\"place.12871500125885940\",\"text\":\"Minneapolis\",\"wikidata\":\"Q36091\"},{\"id\":\"postcode.11389548391063390\",\"text\":\"55415\"},{\"id\":\"region.12225983719702200\",\"text\":\"Minnesota\",\"short_code\":\"US-MN\",\"wikidata\":\"Q1527\"},{\"id\":\"country.12862386939497690\",\"text\":\"United States\",\"short_code\":\"us\",\"wikidata\":\"Q30\"}]},{\"id\":\"poi.12655750184890630\",\"type\":\"Feature\",\"text\":\"Minneapolis Armory\",\"place_name\":\"Minneapolis Armory, Minneapolis, Minnesota 55415, United States\",\"relevance\":0.99,\"properties\":{\"wikidata\":\"Q745327\",\"landmark\":true,\"tel\":null,\"address\":null,\"category\":\"other\"},\"center\":[-93.263278,44.975092],\"geometry\":{\"type\":\"Point\",\"coordinates\":[-93.263278,44.975092]},\"context\":[{\"id\":\"neighborhood.13081559486410050\",\"text\":\"Greater Central\"},{\"id\":\"place.12871500125885940\",\"text\":\"Minneapolis\",\"wikidata\":\"Q36091\"},{\"id\":\"postcode.11389548391063390\",\"text\":\"55415\"},{\"id\":\"region.12225983719702200\",\"text\":\"Minnesota\",\"short_code\":\"US-MN\",\"wikidata\":\"Q1527\"},{\"id\":\"country.12862386939497690\",\"text\":\"United States\",\"short_code\":\"us\",\"wikidata\":\"Q30\"}]},{\"id\":\"poi.4855757554573390\",\"type\":\"Feature\",\"text\":\"Minneapolis Chain of Lakes Park\",\"place_name\":\"Minneapolis Chain of Lakes Park, Minneapolis, Minnesota 55405, United States\",\"relevance\":0.99,\"properties\":{\"wikidata\":null,\"landmark\":true,\"tel\":null,\"address\":null,\"category\":\"park\",\"maki\":\"picnic-site\"},\"bbox\":[-93.330260720104,44.9504758437682,-93.3013567328453,44.969400319872],\"center\":[-93.310259,44.959942],\"geometry\":{\"type\":\"Point\",\"coordinates\":[-93.310259,44.959942]},\"context\":[{\"id\":\"neighborhood.12530456224376080\",\"text\":\"Kenwood\"},{\"id\":\"place.12871500125885940\",\"text\":\"Minneapolis\",\"wikidata\":\"Q36091\"},{\"id\":\"postcode.10829535691218220\",\"text\":\"55405\"},{\"id\":\"region.12225983719702200\",\"text\":\"Minnesota\",\"short_code\":\"US-MN\",\"wikidata\":\"Q1527\"},{\"id\":\"country.12862386939497690\",\"text\":\"United States\",\"short_code\":\"us\",\"wikidata\":\"Q30\"}]}],\"attribution\":\"NOTICE: \u00A9 2016 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained.\"}"; private string reverseResponse = "{\"type\":\"FeatureCollection\",\"query\":[-77.0268808,38.925326999999996],\"features\":[{\"id\":\"address.5375777428110760\",\"type\":\"Feature\",\"text\":\"11th St NW\",\"place_name\":\"2717 11th St NW, Washington, District of Columbia 20001, United States\",\"relevance\":1.0,\"properties\":{},\"center\":[-77.026824,38.925306],\"geometry\":{\"type\":\"Point\",\"coordinates\":[-77.026824,38.925306]},\"address\":\"2717\",\"context\":[{\"id\":\"neighborhood.11736072639395000\",\"text\":\"Pleasant Plains\"},{\"id\":\"place.12334081418246050\",\"text\":\"Washington\",\"wikidata\":\"Q61\"},{\"id\":\"postcode.3526019892841050\",\"text\":\"20001\"},{\"id\":\"region.6884744206035790\",\"text\":\"District of Columbia\",\"short_code\":\"US-DC\",\"wikidata\":\"Q61\"},{\"id\":\"country.12862386939497690\",\"text\":\"United States\",\"wikidata\":\"Q30\",\"short_code\":\"us\"}]},{\"id\":\"neighborhood.11736072639395000\",\"type\":\"Feature\",\"text\":\"Pleasant Plains\",\"place_name\":\"Pleasant Plains, Washington, 20001, District of Columbia, United States\",\"relevance\":1.0,\"properties\":{},\"bbox\":[-77.0367101373528,38.9177500315001,-77.0251464843832,38.9273657639],\"center\":[-77.0303,38.9239],\"geometry\":{\"type\":\"Point\",\"coordinates\":[-77.0303,38.9239]},\"context\":[{\"id\":\"place.12334081418246050\",\"text\":\"Washington\",\"wikidata\":\"Q61\"},{\"id\":\"postcode.3526019892841050\",\"text\":\"20001\"},{\"id\":\"region.6884744206035790\",\"text\":\"District of Columbia\",\"short_code\":\"US-DC\",\"wikidata\":\"Q61\"},{\"id\":\"country.12862386939497690\",\"text\":\"United States\",\"wikidata\":\"Q30\",\"short_code\":\"us\"}]},{\"id\":\"place.12334081418246050\",\"type\":\"Feature\",\"text\":\"Washington\",\"place_name\":\"Washington, District of Columbia, United States\",\"relevance\":1.0,\"properties\":{\"wikidata\":\"Q61\"},\"bbox\":[-77.1197590084041,38.8031129900659,-76.90939299,38.9955480080759],\"center\":[-77.0366,38.895],\"geometry\":{\"type\":\"Point\",\"coordinates\":[-77.0366,38.895]},\"context\":[{\"id\":\"postcode.3526019892841050\",\"text\":\"20001\"},{\"id\":\"region.6884744206035790\",\"text\":\"District of Columbia\",\"short_code\":\"US-DC\",\"wikidata\":\"Q61\"},{\"id\":\"country.12862386939497690\",\"text\":\"United States\",\"wikidata\":\"Q30\",\"short_code\":\"us\"}]},{\"id\":\"postcode.3526019892841050\",\"type\":\"Feature\",\"text\":\"20001\",\"place_name\":\"20001, District of Columbia, United States\",\"relevance\":1.0,\"properties\":{},\"bbox\":[-77.028082,38.890834,-77.007177,38.929058],\"center\":[-77.018017,38.909197],\"geometry\":{\"type\":\"Point\",\"coordinates\":[-77.018017,38.909197]},\"context\":[{\"id\":\"region.6884744206035790\",\"text\":\"District of Columbia\",\"short_code\":\"US-DC\",\"wikidata\":\"Q61\"},{\"id\":\"country.12862386939497690\",\"text\":\"United States\",\"wikidata\":\"Q30\",\"short_code\":\"us\"}]},{\"id\":\"region.6884744206035790\",\"type\":\"Feature\",\"text\":\"District of Columbia\",\"place_name\":\"District of Columbia, United States\",\"relevance\":1.0,\"properties\":{\"short_code\":\"US-DC\",\"wikidata\":\"Q61\"},\"bbox\":[-77.2081379659453,38.7177026348658,-76.909393,38.995548],\"center\":[-76.990661,38.89657],\"geometry\":{\"type\":\"Point\",\"coordinates\":[-76.990661,38.89657]},\"context\":[{\"id\":\"country.12862386939497690\",\"text\":\"United States\",\"wikidata\":\"Q30\",\"short_code\":\"us\"}]},{\"id\":\"country.12862386939497690\",\"type\":\"Feature\",\"text\":\"United States\",\"place_name\":\"United States\",\"relevance\":1.0,\"properties\":{\"wikidata\":\"Q30\",\"short_code\":\"us\"},\"bbox\":[-179.330950579,18.765563302,179.959578044,71.540723637],\"center\":[-97.922211,39.381266],\"geometry\":{\"type\":\"Point\",\"coordinates\":[-97.922211,39.381266]}}],\"attribution\":\"NOTICE: © 2016 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained.\"}"; [Test] - public void SerializesAndDeserializesReverse() - { + public void SerializesAndDeserializesReverse() { // First, deserialize the example response ReverseGeocodeResponse reverseResp = this.geocoder.Deserialize(this.reverseResponse); @@ -35,8 +33,7 @@ public void SerializesAndDeserializesReverse() } [Test] - public void SerializesAndDeserializesForward() - { + public void SerializesAndDeserializesForward() { // First, deserialize the example response ForwardGeocodeResponse forwardResp = this.geocoder.Deserialize(this.forwardResponse); diff --git a/test/UnitTest/MapTest.cs b/test/UnitTest/MapTest.cs index f0b4a06..4b4671d 100644 --- a/test/UnitTest/MapTest.cs +++ b/test/UnitTest/MapTest.cs @@ -4,27 +4,24 @@ // //----------------------------------------------------------------------- -namespace Mapbox.UnitTest -{ - using System.Drawing; - using Mapbox.Map; - using Mapbox.Utils; - using NUnit.Framework; - - [TestFixture] - internal class MapTest - { - private Mono.FileSource fs; +namespace Mapbox.UnitTest { + using System.Drawing; + using Mapbox.Map; + using Mapbox.Platform; + using Mapbox.Utils; + using NUnit.Framework; + + [TestFixture] + internal class MapTest { + private FileSource fs; [SetUp] - public void SetUp() - { - this.fs = new Mono.FileSource(); + public void SetUp() { + this.fs = new FileSource(); } [Test] - public void World() - { + public void World() { var map = new Map(this.fs); map.Vector2dBounds = Vector2dBounds.World(); @@ -42,8 +39,7 @@ public void World() } [Test] - public void RasterHelsinki() - { + public void RasterHelsinki() { var map = new Map(this.fs); map.Center = new Vector2d(60.163200, 24.937700); @@ -64,8 +60,7 @@ public void RasterHelsinki() } [Test] - public void ChangeMapId() - { + public void ChangeMapId() { var map = new Map(this.fs); var mapObserver = new Utils.ClassicRasterMapObserver(); @@ -98,8 +93,7 @@ public void ChangeMapId() } [Test] - public void SetVector2dBoundsZoom() - { + public void SetVector2dBoundsZoom() { var map1 = new Map(this.fs); var map2 = new Map(this.fs); @@ -112,8 +106,7 @@ public void SetVector2dBoundsZoom() } [Test] - public void TileMax() - { + public void TileMax() { var map = new Map(this.fs); map.SetVector2dBoundsZoom(Vector2dBounds.World(), 2); @@ -127,8 +120,7 @@ public void TileMax() } [Test] - public void Zoom() - { + public void Zoom() { var map = new Map(this.fs); map.Zoom = 50; diff --git a/test/UnitTest/TileTest.cs b/test/UnitTest/TileTest.cs index 9d4f34d..3b6d30a 100644 --- a/test/UnitTest/TileTest.cs +++ b/test/UnitTest/TileTest.cs @@ -7,17 +7,18 @@ namespace Mapbox.UnitTest { using Mapbox.Map; + using Mapbox.Platform; using NUnit.Framework; [TestFixture] internal class TileTest { - private Mono.FileSource fs; + private FileSource fs; [SetUp] public void SetUp() { - this.fs = new Mono.FileSource(); + this.fs = new FileSource(); } [Test] diff --git a/test/UnitTest/UnitTest.csproj b/test/UnitTest/UnitTest.csproj index 0c75b1e..1e6a915 100644 --- a/test/UnitTest/UnitTest.csproj +++ b/test/UnitTest/UnitTest.csproj @@ -90,10 +90,6 @@ {89F8BAB2-2E7A-425E-B715-2CC4519D561F} Geocoding - - {346B1208-1587-490B-A7DE-A96B86E81CD6} - Mono - {FE49745C-01F6-4A3F-BF08-828113D3E19F} Platform diff --git a/test/UnitTest/VectorTileTest.cs b/test/UnitTest/VectorTileTest.cs index feda959..3da0167 100644 --- a/test/UnitTest/VectorTileTest.cs +++ b/test/UnitTest/VectorTileTest.cs @@ -15,11 +15,11 @@ namespace Mapbox.UnitTest { [TestFixture] internal class VectorTileTest { - private Mono.FileSource fs; + private FileSource fs; [SetUp] public void SetUp() { - this.fs = new Mono.FileSource(); + this.fs = new FileSource(); } [Test] From 015d58a9297f008b37f50ec3c952c8ad481b14fb Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Thu, 20 Apr 2017 16:44:16 +0200 Subject: [PATCH 04/35] test hang locally, let's see what AppVeyor does --- test/UnitTest/CompressionTest.cs | 5 +++++ test/UnitTest/FileSourceTest.cs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/test/UnitTest/CompressionTest.cs b/test/UnitTest/CompressionTest.cs index 9d5e3cc..948d1b8 100644 --- a/test/UnitTest/CompressionTest.cs +++ b/test/UnitTest/CompressionTest.cs @@ -61,7 +61,12 @@ public void Decompress() { fs.WaitForAllRequests(); + //tiles are automatically decompressed during HttpRequest on full .Net framework +#if NETFX_CORE Assert.Less(buffer.Length, Compression.Decompress(buffer).Length); +#else + Assert.AreEqual(buffer.Length, Compression.Decompress(buffer).Length); +#endif } } } diff --git a/test/UnitTest/FileSourceTest.cs b/test/UnitTest/FileSourceTest.cs index 0360e81..6e683e5 100644 --- a/test/UnitTest/FileSourceTest.cs +++ b/test/UnitTest/FileSourceTest.cs @@ -82,7 +82,7 @@ public void RequestForbidden() { this.fs.Request( "https://mapbox.com/forbidden", (Response res) => { - Assert.IsTrue(res.HasError, "Forbidden"); + Assert.IsTrue(res.HasError); }); this.fs.WaitForAllRequests(); From f5e5c519f5edafcc0efc51c5b121245f0207af94 Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Thu, 20 Apr 2017 17:56:06 +0200 Subject: [PATCH 05/35] AppVeyor: try tests again --- src/Platform/HTTPRequest.cs | 63 ++++++++++++++++++++------------- test/UnitTest/FileSourceTest.cs | 6 +++- 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/src/Platform/HTTPRequest.cs b/src/Platform/HTTPRequest.cs index 18646ce..5a52092 100644 --- a/src/Platform/HTTPRequest.cs +++ b/src/Platform/HTTPRequest.cs @@ -63,7 +63,7 @@ public HTTPRequest(string url, Action callback, int timeOut = 10) { getResponseAsync(_hwr, EvaluateResponse); } -#region oldversion + #region oldversion //private void GetResponseAsync(HttpWebRequest request, Action gotResponse) { // //private void GetResponseAsync(HttpWebRequest request) { @@ -124,7 +124,7 @@ public HTTPRequest(string url, Action callback, int timeOut = 10) { //} -#endregion + #endregion private void getResponseAsync(HttpWebRequest request, Action gotResponse) { @@ -137,27 +137,33 @@ private void getResponseAsync(HttpWebRequest request, Action { - // BeginInvoke runs on a thread of the thread pool (!= main/UI thread) - // that's why we need SynchronizationContext when - // TODO: how to influence threadpool: nr of threads etc. - request.BeginGetResponse((asycnResult) => { - try { // there's a try/catch here because execution path is different from invokation one, exception here may cause a crash - HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asycnResult); - gotResponse(response, null); - } - // EndGetResponse() throws on on some status codes, try to get response anyway (and status codes) - catch (WebException wex) { - HttpWebResponse hwr = wex.Response as HttpWebResponse; - if (null == hwr) { - throw; + try { + // BeginInvoke runs on a thread of the thread pool (!= main/UI thread) + // that's why we need SynchronizationContext when + // TODO: how to influence threadpool: nr of threads etc. + request.BeginGetResponse((asycnResult) => { + try { // there's a try/catch here because execution path is different from invokation one, exception here may cause a crash + HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asycnResult); + gotResponse(response, null); + } + // EndGetResponse() throws on on some status codes, try to get response anyway (and status codes) + catch (WebException wex) { + HttpWebResponse hwr = wex.Response as HttpWebResponse; + if (null == hwr) { + throw; + } + gotResponse(hwr, wex); + } + catch (Exception ex) { + gotResponse(null, ex); } - gotResponse(hwr, wex); - } - catch (Exception ex) { - gotResponse(null, ex); } + , null); + } + catch (Exception ex) { + //catch exception from HttpWebRequest.Abort + gotResponse(null, ex); } - , null); }; try { @@ -194,7 +200,7 @@ private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { if (null != apiResponse.Headers) { response.Headers = new Dictionary(); for (int i = 0; i < apiResponse.Headers.Count; i++) { -// TODO: implement .Net Core / UWP implementation + // TODO: implement .Net Core / UWP implementation #if !NETFX_CORE string key = apiResponse.Headers.Keys[i]; string val = apiResponse.Headers[i]; @@ -245,17 +251,24 @@ private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { // Unity: SynchronizationContext doesn't do anything // use the Dispatcher #if !UNITY - _sync.Post(delegate { _callback(response); }, null); + _sync.Post(delegate { + _callback(response); + IsCompleted = true; + _callback = null; + }, null); #else - UnityMainThreadDispatcher.Instance().Enqueue(() => _callback(response)); + UnityMainThreadDispatcher.Instance().Enqueue(() => { + _callback(response); + IsCompleted = true; + _callback = null; + }); #endif - IsCompleted = true; + } public void Cancel() { - _callback = null; if (null != _hwr) { _hwr.Abort(); } diff --git a/test/UnitTest/FileSourceTest.cs b/test/UnitTest/FileSourceTest.cs index 6e683e5..5997771 100644 --- a/test/UnitTest/FileSourceTest.cs +++ b/test/UnitTest/FileSourceTest.cs @@ -8,6 +8,7 @@ namespace Mapbox.UnitTest { using System; using Mapbox.Platform; using NUnit.Framework; + using System.Net; [TestFixture] internal class FileSourceTest { @@ -55,7 +56,10 @@ public void RequestCancel() { var request = this.fs.Request( Uri, (Response res) => { - Assert.Fail("Should never happen."); + Assert.IsTrue(res.HasError); + WebException wex = res.Exceptions[0] as WebException; + Assert.IsNotNull(wex); + Assert.AreEqual(wex.Status, WebExceptionStatus.RequestCanceled); }); request.Cancel(); From 7e8d90c52c4740150354d854f038869fdd876859 Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Thu, 20 Apr 2017 18:04:10 +0200 Subject: [PATCH 06/35] more safe measures for HttpWebRequest.Abort --- src/Platform/HTTPRequest.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Platform/HTTPRequest.cs b/src/Platform/HTTPRequest.cs index 5a52092..adafe27 100644 --- a/src/Platform/HTTPRequest.cs +++ b/src/Platform/HTTPRequest.cs @@ -148,11 +148,16 @@ private void getResponseAsync(HttpWebRequest request, Action Date: Fri, 21 Apr 2017 17:37:43 +0200 Subject: [PATCH 07/35] [wip] Action.BeginInvoke does not work on UWP --- src/Platform/HTTPRequest.cs | 74 ++++++------------------------------- 1 file changed, 12 insertions(+), 62 deletions(-) diff --git a/src/Platform/HTTPRequest.cs b/src/Platform/HTTPRequest.cs index adafe27..840832b 100644 --- a/src/Platform/HTTPRequest.cs +++ b/src/Platform/HTTPRequest.cs @@ -20,6 +20,9 @@ namespace Mapbox.Platform { using System.Collections.Generic; using System.Threading; using System.ComponentModel; +#if NETFX_CORE + using System.Net.Http; +#endif //using System.Windows.Threading; @@ -63,71 +66,17 @@ public HTTPRequest(string url, Action callback, int timeOut = 10) { getResponseAsync(_hwr, EvaluateResponse); } - #region oldversion - - //private void GetResponseAsync(HttpWebRequest request, Action gotResponse) { - // //private void GetResponseAsync(HttpWebRequest request) { - - // // create an additional action wrapper, because of: - // // https://msdn.microsoft.com/en-us/library/system.net.httpwebrequest.begingetresponse.aspx - // // The BeginGetResponse method requires some synchronous setup tasks to complete (DNS resolution, - // //proxy detection, and TCP socket connection, for example) before this method becomes asynchronous. - // // As a result, this method should never be called on a user interface (UI) thread because it might - // // take considerable time(up to several minutes depending on network settings) to complete the - // // initial synchronous setup tasks before an exception for an error is thrown or the method succeeds. - - // Action actionWrapper = () => { - // request.BeginGetResponse((r) => { - // try { // there's a try/catch here because execution path is different from invokation one, exception here may cause a crash - // HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(r); - // System.Diagnostics.Debug.WriteLine(string.Format("HTTPRequest before 'gotResponse', thread id:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId)); - // gotResponse(response, null); - // } - // // EndGetResponse() throws on on some status codes, try to get response anyway (and status codes) - // catch (WebException wex) { - // HttpWebResponse hwr = wex.Response as HttpWebResponse; - // if (null == hwr) { - // throw; - // } - // gotResponse(hwr, wex); - // } - // catch (Exception ex) { - // gotResponse(null, ex); - // } - // } - // , request); - // }; - - // //http://www.cshandler.com/2011/07/delegate-and-async-programming-part-ii.html - // //http://www.yoda.arachsys.com/csharp/threads/printable.shtml - // //https://msdn.microsoft.com/en-us/library/2e08f6yc(v=vs.110).aspx - // //http://www.c-sharpcorner.com/UploadFile/vendettamit/delegate-and-async-programming-C-Sharp-asynccallback-and-object-state/ - // //http://xcalibursystems.com/2011/10/c-how-to-get-a-return-value-from-an-action/ - // //https://stackoverflow.com/questions/8099631/how-to-return-value-from-action - // //http://blog.aggregatedintelligence.com/2010/06/c-asynchronous-programming-using.html - // //http://csharpindepth.com/Articles/Chapter2/Events.aspx?printable=true - - - // // !!!!BeginInvoke runs on a thread of the thread pool (!= main thread)!!!! - // // TODO: how to influence threadpool: nr of threads etc. - // System.Diagnostics.Debug.WriteLine(string.Format("HTTPRequest before 'BeginInvoke', thread id:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId)); - // actionWrapper.BeginInvoke(new AsyncCallback((iASyncResult) => { - // System.Diagnostics.Debug.WriteLine(string.Format("HTTPRequest within 'BeginInvoke', thread id:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId)); - // var action = (Action)iASyncResult.AsyncState; - // action.EndInvoke(iASyncResult); - // System.Diagnostics.Debug.WriteLine(string.Format("HTTPRequest after 'EndInvoke', thread id:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId)); - // }) - // , actionWrapper - // ); - - // System.Diagnostics.Debug.WriteLine(string.Format("HTTPRequest past 'BeginInvoke', thread id:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId)); - //} - - - #endregion private void getResponseAsync(HttpWebRequest request, Action gotResponse) { +#if NETFX_CORE + using (var client = new HttpClient()) { + using (var response = await client.GetAsync(_hwr.RequestUri)) { + string result = await response.Content.ReadAsStringAsync(); + } + } +#else + // create an additional action wrapper, because of: // https://msdn.microsoft.com/en-us/library/system.net.httpwebrequest.begingetresponse.aspx // The BeginGetResponse method requires some synchronous setup tasks to complete (DNS resolution, @@ -181,6 +130,7 @@ private void getResponseAsync(HttpWebRequest request, Action Date: Mon, 24 Apr 2017 14:00:21 +0200 Subject: [PATCH 08/35] [wip] UWP HTTPRequest --- src/Platform/HTTPRequest.cs | 97 +++++++++++++++++++++++++++++++++---- 1 file changed, 88 insertions(+), 9 deletions(-) diff --git a/src/Platform/HTTPRequest.cs b/src/Platform/HTTPRequest.cs index 840832b..7bbc1f2 100644 --- a/src/Platform/HTTPRequest.cs +++ b/src/Platform/HTTPRequest.cs @@ -22,6 +22,7 @@ namespace Mapbox.Platform { using System.ComponentModel; #if NETFX_CORE using System.Net.Http; + using System.Linq; #endif //using System.Windows.Threading; @@ -55,11 +56,11 @@ public HTTPRequest(string url, Action callback, int timeOut = 10) { _hwr.Method = "GET"; #if !UNITY && !NETFX_CORE _hwr.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + _hwr.CachePolicy = new RequestCachePolicy(RequestCacheLevel.CacheIfAvailable); #endif #if !NETFX_CORE _hwr.UserAgent = "mapbox-sdk-cs"; //_hwr.CachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore); - _hwr.CachePolicy = new RequestCachePolicy(RequestCacheLevel.CacheIfAvailable); #endif //_hwr.Timeout = timeOut * 1000; doesn't work in async calls, see below @@ -67,15 +68,95 @@ public HTTPRequest(string url, Action callback, int timeOut = 10) { } - private void getResponseAsync(HttpWebRequest request, Action gotResponse) { - #if NETFX_CORE + private async void getResponseAsync(HttpWebRequest request, Action gotResponse) { + + //http://rextester.com/discussion/XPKY90132/async-example-with-HttpClient using (var client = new HttpClient()) { - using (var response = await client.GetAsync(_hwr.RequestUri)) { - string result = await response.Content.ReadAsStringAsync(); + var response = await client.GetAsync(_hwr.RequestUri); + gotResponse(response, null); + } + } + + + private async void EvaluateResponse(HttpResponseMessage apiResponse, Exception apiEx) { + + var response = new Response(); + + if (null != apiEx) { + response.AddException(apiEx); + } + + // timeout: API response is null + if (null == apiResponse) { + response.AddException(new Exception("No Reponse.")); + } else { + // TODO: evaluate headers and add custom exception, eg if rate limit is exceeded + // https://www.mapbox.com/api-documentation/#rate-limits + // X-Rate-Limit-Interval + // X-Rate-Limit-Limit + // X-Rate-Limit-Reset + if (null != apiResponse.Headers) { + response.Headers = new Dictionary(); + foreach (var hdr in apiResponse.Headers) { + string key = hdr.Key; + string val = hdr.Value.FirstOrDefault(); + response.Headers.Add(key, val); + if (key.Equals("X-Rate-Limit-Interval", StringComparison.OrdinalIgnoreCase)) { + int limitInterval; + if (int.TryParse(val, out limitInterval)) { response.XRateLimitInterval = limitInterval; } + } else if (key.Equals("X-Rate-Limit-Limit", StringComparison.OrdinalIgnoreCase)) { + long limitLimit; + if (long.TryParse(val, out limitLimit)) { response.XRateLimitLimit = limitLimit; } + } else if (key.Equals("X-Rate-Limit-Reset", StringComparison.OrdinalIgnoreCase)) { + double unixTimestamp; + if (double.TryParse(val, out unixTimestamp)) { + DateTime beginningOfTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + response.XRateLimitReset = beginningOfTime.AddSeconds(unixTimestamp).ToLocalTime(); + } + } else if (key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase)) { + response.ContentType = val; + } + } + } + + if (apiResponse.StatusCode != HttpStatusCode.OK) { + response.AddException(new Exception(string.Format("{0}: {1}", apiResponse.StatusCode, apiResponse.ReasonPhrase))); + } + int statusCode = (int)apiResponse.StatusCode; + response.StatusCode = statusCode; + if (429 == statusCode) { + response.AddException(new Exception("Rate limit hit")); + } + + if (null != apiResponse) { + response.Data = await apiResponse.Content.ReadAsByteArrayAsync(); } } + + // post (async) callback back to the main/UI thread + // Unity: SynchronizationContext doesn't do anything + // use the Dispatcher +#if !UNITY + _sync.Post(delegate { + _callback(response); + IsCompleted = true; + _callback = null; + }, null); #else + UnityMainThreadDispatcher.Instance().Enqueue(() => { + _callback(response); + IsCompleted = true; + _callback = null; + }); +#endif + } + +#endif + + +#if !NETFX_CORE + private void getResponseAsync(HttpWebRequest request, Action gotResponse) { // create an additional action wrapper, because of: // https://msdn.microsoft.com/en-us/library/system.net.httpwebrequest.begingetresponse.aspx @@ -130,7 +211,6 @@ private void getResponseAsync(HttpWebRequest request, Action(); for (int i = 0; i < apiResponse.Headers.Count; i++) { // TODO: implement .Net Core / UWP implementation -#if !NETFX_CORE string key = apiResponse.Headers.Keys[i]; string val = apiResponse.Headers[i]; response.Headers.Add(key, val); @@ -175,7 +254,6 @@ private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { } else if (key.Equals("Content-Type", StringComparison.InvariantCultureIgnoreCase)) { response.ContentType = val; } -#endif } } @@ -218,8 +296,9 @@ private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { _callback = null; }); #endif - } +#endif + public void Cancel() { From 78dec8ceaaad770800fc3f40261ac05fbb5053fc Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Tue, 25 Apr 2017 09:15:00 +0200 Subject: [PATCH 09/35] new methods (UnityMainThreadDispatcher.Enqueue(()) --- src/Platform/HTTPRequest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Platform/HTTPRequest.cs b/src/Platform/HTTPRequest.cs index 7bbc1f2..1e442ea 100644 --- a/src/Platform/HTTPRequest.cs +++ b/src/Platform/HTTPRequest.cs @@ -290,7 +290,7 @@ private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { _callback = null; }, null); #else - UnityMainThreadDispatcher.Instance().Enqueue(() => { + UnityMainThreadDispatcher.Enqueue(() => { _callback(response); IsCompleted = true; _callback = null; From 034e52557eb2a59361466b7810e00b3989b18c03 Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Wed, 26 Apr 2017 13:31:54 +0200 Subject: [PATCH 10/35] speed up HTTPWebRequest --- src/Platform/HTTPRequest.cs | 48 +++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/src/Platform/HTTPRequest.cs b/src/Platform/HTTPRequest.cs index 1e442ea..ebfbdcd 100644 --- a/src/Platform/HTTPRequest.cs +++ b/src/Platform/HTTPRequest.cs @@ -48,18 +48,42 @@ internal sealed class HTTPRequest : IAsyncRequest { /// /// seconds public HTTPRequest(string url, Action callback, int timeOut = 10) { - + //UnityEngine.Debug.Log(url); _callback = callback; _timeOut = timeOut; + //The answer is changing HttpWebRequest / HttpWebResponse to WebRequest/ WebResponse only.That fixed the problem. + _hwr = WebRequest.Create(url) as HttpWebRequest; + _hwr.Credentials = CredentialCache.DefaultCredentials; + _hwr.KeepAlive = true; + + //_hwr.ProtocolVersion = HttpVersion.Version11; //that's it!!! + + //that' it !!!! + // https://msdn.microsoft.com/en-us/library/system.net.httpwebrequest(v=vs.90).aspx#Remarks + // use a value that is 12 times the number of CPUs on the local computer + + _hwr.ServicePoint.ConnectionLimit = Environment.ProcessorCount * 6; + + _hwr.ServicePoint.UseNagleAlgorithm = true; + _hwr.ServicePoint.Expect100Continue = false; + _hwr.ServicePoint.MaxIdleTime = 2000; + //System.Net.ServicePointManager.SetTcpKeepAlive(false, 0, 0); + + + //UnityEngine.Debug.Log("CurrentConnections: " + _hwr.ServicePoint.CurrentConnections); + //UnityEngine.Debug.Log("ConnectionLimit: " + _hwr.ServicePoint.ConnectionLimit); + //UnityEngine.Debug.Log("ConnectionName: " + _hwr.ServicePoint.ConnectionName); _hwr.Method = "GET"; -#if !UNITY && !NETFX_CORE + _hwr.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip,deflate"); _hwr.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; - _hwr.CachePolicy = new RequestCachePolicy(RequestCacheLevel.CacheIfAvailable); +#if !UNITY && !NETFX_CORE + _hwr.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.CacheIfAvailable); #endif #if !NETFX_CORE - _hwr.UserAgent = "mapbox-sdk-cs"; + //_hwr.UserAgent = "mapbox-sdk-cs"; + _hwr.UserAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36"; //_hwr.CachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore); #endif //_hwr.Timeout = timeOut * 1000; doesn't work in async calls, see below @@ -171,9 +195,17 @@ private void getResponseAsync(HttpWebRequest request, Action { try { // there's a try/catch here because execution path is different from invokation one, exception here may cause a crash + long beforeEndGet = DateTime.Now.Ticks; HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asycnResult); + //long finished = DateTime.Now.Ticks; + //long duration = finished - startTicks; + //TimeSpan ts = TimeSpan.FromTicks(duration); + //TimeSpan tsEndGet = TimeSpan.FromTicks(finished - beforeEndGet); + //TimeSpan tsBeginGet = TimeSpan.FromTicks(beforeEndGet - startTicks); + //UnityEngine.Debug.Log("received response - " + ts.Milliseconds + "ms" + " BeginGet: " + tsBeginGet.Milliseconds + " EndGet: " + tsEndGet.Milliseconds + " CompletedSynchronously: " + asycnResult.CompletedSynchronously); gotResponse(response, null); } // EndGetResponse() throws on on some status codes, try to get response anyway (and status codes) @@ -277,6 +309,7 @@ private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { response.Data = ms.ToArray(); } } + apiResponse.Close(); } } @@ -290,7 +323,12 @@ private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { _callback = null; }, null); #else - UnityMainThreadDispatcher.Enqueue(() => { + //UnityMainThreadDispatcher.Enqueue(() => { + // _callback(response); + // IsCompleted = true; + // _callback = null; + //}); + UnityToolbag.Dispatcher.InvokeAsync(() => { _callback(response); IsCompleted = true; _callback = null; From 5fdada49a5e70adc34aef046cd93153f054029d9 Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Thu, 27 Apr 2017 12:52:25 +0200 Subject: [PATCH 11/35] [wip] backporting latest HTTPRequest improments --- src/Platform/FileSource.cs | 60 +++++++++++++++++++++++++------------ src/Platform/HTTPRequest.cs | 8 +++-- src/Platform/Response.cs | 1 + 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/src/Platform/FileSource.cs b/src/Platform/FileSource.cs index a41ff0d..b821e80 100644 --- a/src/Platform/FileSource.cs +++ b/src/Platform/FileSource.cs @@ -29,9 +29,9 @@ namespace Mapbox.Platform { public sealed class FileSource : IFileSource { - private readonly List _requests = new List(); + private readonly Dictionary _requests = new Dictionary(); private readonly string _accessToken = Environment.GetEnvironmentVariable("MAPBOX_ACCESS_TOKEN"); - + private readonly object _lock = new object(); /// Length of rate-limiting interval in seconds. https://www.mapbox.com/api-documentation/#rate-limits private int? XRateLimitInterval; @@ -58,21 +58,33 @@ public IAsyncRequest Request(string url, Action callback) { // * add queue for requests // * evaluate rate limits (headers and status code) // * throttle requests accordingly - - //var request = new HTTPRequest_v2(url, proxyResponse); - var request = new HTTPRequest(url, callback); - _requests.Add(request); + //var request = new HTTPRequest(url, callback); + IEnumerator proxy = proxyResponse(url, callback); + proxy.MoveNext(); + HTTPRequest request = proxy.Current; return request; } // TODO: look at requests and implement throttling if needed - private void proxyResponse(Response response) { - if (response.XRateLimitInterval.HasValue) { XRateLimitInterval = response.XRateLimitInterval; } - if (response.XRateLimitLimit.HasValue) { XRateLimitLimit = response.XRateLimitLimit; } - if (response.XRateLimitReset.HasValue) { XRateLimitReset = response.XRateLimitReset; } - //callback(response); + private IEnumerator proxyResponse(string url, Action callback) { + + // TODO: plugin caching somewhere around here + + var request = new HTTPRequest(url, (Response response) => { + if (response.XRateLimitInterval.HasValue) { XRateLimitInterval = response.XRateLimitInterval; } + if (response.XRateLimitLimit.HasValue) { XRateLimitLimit = response.XRateLimitLimit; } + if (response.XRateLimitReset.HasValue) { XRateLimitReset = response.XRateLimitReset; } + callback(response); + lock (_lock) { + _requests.Remove(response.Request); + } + }); + lock (_lock) { + _requests.Add(request, 0); + } + yield return request; } @@ -80,20 +92,30 @@ private void proxyResponse(Response response) { /// Block until all the requests are processed. /// public void WaitForAllRequests() { - while (true) { + while (_requests.Count > 0) { // Reverse for safely removing while iterating. - for (int i = _requests.Count - 1; i >= 0; i--) { - if (_requests[i].IsCompleted) { - _requests.RemoveAt(i); + lock (_lock) { + foreach (var req in _requests) { + if (((HTTPRequest)req.Key).IsCompleted) { + _requests.Remove(req.Key); + } } } - if (_requests.Count == 0) { - break; - } - #if !WINDOWS_UWP Thread.Sleep(50); + // TODO: get rid of DoEvents!!! and find non-blocking wait that works for Net3.5 + System.Windows.Forms.Application.DoEvents(); + + //var resetEvent = new ManualResetEvent(false); + //ThreadPool.QueueUserWorkItem(new WaitCallback(delegate { + // Thread.Sleep(2500); + // resetEvent.Set(); + //}), null); + //resetEvent.WaitOne(); + //resetEvent.Close(); + //resetEvent = null; + #else System.Threading.Tasks.Task.Delay(50).Wait(); #endif diff --git a/src/Platform/HTTPRequest.cs b/src/Platform/HTTPRequest.cs index ebfbdcd..7078352 100644 --- a/src/Platform/HTTPRequest.cs +++ b/src/Platform/HTTPRequest.cs @@ -79,11 +79,12 @@ public HTTPRequest(string url, Action callback, int timeOut = 10) { _hwr.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip,deflate"); _hwr.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; #if !UNITY && !NETFX_CORE - _hwr.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.CacheIfAvailable); + //_hwr.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.CacheIfAvailable); + _hwr.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.NoCacheNoStore); #endif #if !NETFX_CORE - //_hwr.UserAgent = "mapbox-sdk-cs"; - _hwr.UserAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36"; + _hwr.UserAgent = "mapbox-sdk-cs"; + //_hwr.UserAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36"; //_hwr.CachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore); #endif //_hwr.Timeout = timeOut * 1000; doesn't work in async calls, see below @@ -250,6 +251,7 @@ private void getResponseAsync(HttpWebRequest request, Action A response from a request. public struct Response { + public IAsyncRequest Request; public bool RateLimitHit { get { return StatusCode.HasValue ? 429 == StatusCode.Value : false; } From 79e80417d2a921cee6f0b4bc93543a43e4c5a850 Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Thu, 27 Apr 2017 12:57:27 +0200 Subject: [PATCH 12/35] comment Application.DoEvents --- src/Platform/FileSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Platform/FileSource.cs b/src/Platform/FileSource.cs index b821e80..78b04a0 100644 --- a/src/Platform/FileSource.cs +++ b/src/Platform/FileSource.cs @@ -105,7 +105,7 @@ public void WaitForAllRequests() { #if !WINDOWS_UWP Thread.Sleep(50); // TODO: get rid of DoEvents!!! and find non-blocking wait that works for Net3.5 - System.Windows.Forms.Application.DoEvents(); + //System.Windows.Forms.Application.DoEvents(); //var resetEvent = new ManualResetEvent(false); //ThreadPool.QueueUserWorkItem(new WaitCallback(delegate { From 213fc856b4034fe29e130f208cdb50b43dcab082 Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Fri, 28 Apr 2017 13:33:52 +0200 Subject: [PATCH 13/35] AppVeyor tests: how are you doing? --- src/Platform/FileSource.cs | 16 ++++++++++++++-- src/Platform/HTTPRequest.cs | 1 - 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Platform/FileSource.cs b/src/Platform/FileSource.cs index 78b04a0..a7fb42a 100644 --- a/src/Platform/FileSource.cs +++ b/src/Platform/FileSource.cs @@ -78,7 +78,13 @@ private IEnumerator proxyResponse(string url, Action call if (response.XRateLimitReset.HasValue) { XRateLimitReset = response.XRateLimitReset; } callback(response); lock (_lock) { - _requests.Remove(response.Request); + //another place to catch if request has been cancelled + try { + _requests.Remove(response.Request); + } + catch (Exception ex) { + System.Diagnostics.Debug.WriteLine(ex); + } } }); lock (_lock) { @@ -97,7 +103,13 @@ public void WaitForAllRequests() { lock (_lock) { foreach (var req in _requests) { if (((HTTPRequest)req.Key).IsCompleted) { - _requests.Remove(req.Key); + // another place to watch out if request has been cancelled + try { + _requests.Remove(req.Key); + } + catch(Exception ex) { + System.Diagnostics.Debug.WriteLine(ex); + } } } } diff --git a/src/Platform/HTTPRequest.cs b/src/Platform/HTTPRequest.cs index 7078352..22dbcee 100644 --- a/src/Platform/HTTPRequest.cs +++ b/src/Platform/HTTPRequest.cs @@ -346,7 +346,6 @@ public void Cancel() { if (null != _hwr) { _hwr.Abort(); } - IsCompleted = true; } From 3ac685e4073965525e7d3a6a22dc70fd2fcda81a Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Fri, 28 Apr 2017 13:54:08 +0200 Subject: [PATCH 14/35] quick fix for UWP --- src/Platform/HTTPRequest.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Platform/HTTPRequest.cs b/src/Platform/HTTPRequest.cs index 22dbcee..d37ae0f 100644 --- a/src/Platform/HTTPRequest.cs +++ b/src/Platform/HTTPRequest.cs @@ -55,6 +55,8 @@ public HTTPRequest(string url, Action callback, int timeOut = 10) { //The answer is changing HttpWebRequest / HttpWebResponse to WebRequest/ WebResponse only.That fixed the problem. _hwr = WebRequest.Create(url) as HttpWebRequest; + +#if !NETFX_CORE _hwr.Credentials = CredentialCache.DefaultCredentials; _hwr.KeepAlive = true; @@ -78,6 +80,8 @@ public HTTPRequest(string url, Action callback, int timeOut = 10) { _hwr.Method = "GET"; _hwr.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip,deflate"); _hwr.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; +#endif + #if !UNITY && !NETFX_CORE //_hwr.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.CacheIfAvailable); _hwr.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.NoCacheNoStore); From 8ed1f32538021f98766ba078020e62cabdf7233b Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Fri, 28 Apr 2017 15:00:37 +0200 Subject: [PATCH 15/35] MockApi first try with https://github.com/richardszalay/mockhttp --- test/UnitTest/MockApiTest.cs | 50 +++++++++++++++++++++++++++++++++++ test/UnitTest/UnitTest.csproj | 6 +++++ test/UnitTest/packages.config | 2 ++ 3 files changed, 58 insertions(+) create mode 100644 test/UnitTest/MockApiTest.cs diff --git a/test/UnitTest/MockApiTest.cs b/test/UnitTest/MockApiTest.cs new file mode 100644 index 0000000..bc6c7a1 --- /dev/null +++ b/test/UnitTest/MockApiTest.cs @@ -0,0 +1,50 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) 2017 Mapbox. All rights reserved. +// +//----------------------------------------------------------------------- + + +namespace Mapbox.UnitTest { + using Mapbox.Json; + using Mapbox.Utils; + using Mapbox.Utils.JsonConverters; + using NUnit.Framework; + using Platform; + using RichardSzalay.MockHttp; + using System.Net; + + + [TestFixture] + internal class MockApiTest { + + + private static readonly string _mockBaseUrl="http://localhost:2345"; + private MockHttpMessageHandler _mockHttp = new MockHttpMessageHandler(); + private FileSource _fs = new FileSource(); + + + [SetUp] + public void SetupMockHttp() { + _mockHttp + .When(_mockBaseUrl + "/*") + .Respond(HttpStatusCode.OK, "application/json", "{}"); + } + + + [Test] + public void First() { + _fs.Request( + _mockBaseUrl + "/bla/bla" + , (Response r)=> { + System.Diagnostics.Debug.WriteLine(r.StatusCode); + } + ); + + _fs.WaitForAllRequests(); + } + + + + } +} \ No newline at end of file diff --git a/test/UnitTest/UnitTest.csproj b/test/UnitTest/UnitTest.csproj index 1e6a915..8c4a208 100644 --- a/test/UnitTest/UnitTest.csproj +++ b/test/UnitTest/UnitTest.csproj @@ -53,13 +53,19 @@ ..\..\packages\Mapbox.VectorTile.1.0.3-alpha2\lib\net35\Mapbox.VectorTile.VectorTileReader.dll True + + ..\..\packages\RichardSzalay.MockHttp.1.5.0\lib\net45\RichardSzalay.MockHttp.dll + True + ..\..\packages\NUnit.3.5.0\lib\net45\nunit.framework.dll + + diff --git a/test/UnitTest/packages.config b/test/UnitTest/packages.config index 3546b28..79b93b2 100644 --- a/test/UnitTest/packages.config +++ b/test/UnitTest/packages.config @@ -10,4 +10,6 @@ + + \ No newline at end of file From 3655d6767e8b5e0afb2e33f9fefa90a5e57e694c Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Fri, 28 Apr 2017 16:21:19 +0200 Subject: [PATCH 16/35] MockApi try with https://github.com/hibri/HttpMock --- src/Platform/HTTPRequest.cs | 3 ++- src/Platform/IAsyncRequest.cs | 12 ++++++---- src/Platform/Platform.csproj | 2 +- test/UnitTest/MockApiTest.cs | 43 +++++++++++++++++++++++++++-------- test/UnitTest/UnitTest.csproj | 17 ++++++++++---- test/UnitTest/app.config | 11 +++++++++ test/UnitTest/packages.config | 5 ++-- 7 files changed, 72 insertions(+), 21 deletions(-) create mode 100644 test/UnitTest/app.config diff --git a/src/Platform/HTTPRequest.cs b/src/Platform/HTTPRequest.cs index d37ae0f..82af0ed 100644 --- a/src/Platform/HTTPRequest.cs +++ b/src/Platform/HTTPRequest.cs @@ -30,7 +30,7 @@ namespace Mapbox.Platform { internal sealed class HTTPRequest : IAsyncRequest { - public bool IsCompleted = false; + public bool IsCompleted { get; private set; } private Action _callback; @@ -48,6 +48,7 @@ internal sealed class HTTPRequest : IAsyncRequest { /// /// seconds public HTTPRequest(string url, Action callback, int timeOut = 10) { + IsCompleted = false; //UnityEngine.Debug.Log(url); _callback = callback; _timeOut = timeOut; diff --git a/src/Platform/IAsyncRequest.cs b/src/Platform/IAsyncRequest.cs index 1e85342..f07786c 100644 --- a/src/Platform/IAsyncRequest.cs +++ b/src/Platform/IAsyncRequest.cs @@ -4,11 +4,15 @@ // //----------------------------------------------------------------------- -namespace Mapbox.Platform -{ +namespace Mapbox.Platform { + + /// A handle to an asynchronous request. - public interface IAsyncRequest - { + public interface IAsyncRequest { + + ///// True after the request has finished. + //bool IsCompleted { get; } + /// Cancel the ongoing request, preventing it from firing a callback. void Cancel(); } diff --git a/src/Platform/Platform.csproj b/src/Platform/Platform.csproj index 70b9759..16ddea4 100644 --- a/src/Platform/Platform.csproj +++ b/src/Platform/Platform.csproj @@ -41,8 +41,8 @@ Properties\SharedAssemblyInfo.cs - + diff --git a/test/UnitTest/MockApiTest.cs b/test/UnitTest/MockApiTest.cs index bc6c7a1..5c16a7a 100644 --- a/test/UnitTest/MockApiTest.cs +++ b/test/UnitTest/MockApiTest.cs @@ -6,12 +6,13 @@ namespace Mapbox.UnitTest { + using HttpMock; using Mapbox.Json; using Mapbox.Utils; using Mapbox.Utils.JsonConverters; using NUnit.Framework; using Platform; - using RichardSzalay.MockHttp; + using System; using System.Net; @@ -19,29 +20,53 @@ namespace Mapbox.UnitTest { internal class MockApiTest { - private static readonly string _mockBaseUrl="http://localhost:2345"; - private MockHttpMessageHandler _mockHttp = new MockHttpMessageHandler(); + private static readonly string _mockBaseUrl = "http://localhost:2345"; private FileSource _fs = new FileSource(); + IHttpServer _mockApi; + + + [TearDown] + public void Finished() { + if (null == _mockApi) { return; } + _mockApi.Dispose(); + _mockApi = null; + } [SetUp] public void SetupMockHttp() { - _mockHttp - .When(_mockBaseUrl + "/*") - .Respond(HttpStatusCode.OK, "application/json", "{}"); + + //_mockApi = new HttpServer(new Uri(_mockBaseUrl)); + + var serverFactory = new HttpServerFactory(); + _mockApi = serverFactory.Get(new Uri(_mockBaseUrl)).WithNewContext(); + + _mockApi.Start(); + + _mockApi.Stub(r => r.Get("/*")) + .Return("{}") + .AsContentType("application/json") + .OK(); + } [Test] public void First() { + + Response resp = new Response(); _fs.Request( _mockBaseUrl + "/bla/bla" - , (Response r)=> { - System.Diagnostics.Debug.WriteLine(r.StatusCode); - } + , (Response r) => { + // hm why doesn't nunit work in here? maybe blocking 'WaitForAllRequests' + resp = r; + } ); _fs.WaitForAllRequests(); + + System.Diagnostics.Debug.WriteLine(resp.StatusCode); + Assert.AreEqual(200, resp.StatusCode, "wrong status code"); } diff --git a/test/UnitTest/UnitTest.csproj b/test/UnitTest/UnitTest.csproj index 8c4a208..ee81222 100644 --- a/test/UnitTest/UnitTest.csproj +++ b/test/UnitTest/UnitTest.csproj @@ -34,6 +34,18 @@ MinimumRecommendedRules.ruleset + + ..\..\packages\HttpMock.2.0.1\lib\net45\HttpMock.dll + True + + + ..\..\packages\Kayak.0.7.2\lib\Kayak.dll + True + + + ..\..\packages\log4net.2.0.7\lib\net45-full\log4net.dll + True + ..\..\3rdparty\Json.Net.Unity3D\Net35\Mapbox.Json.dll @@ -53,10 +65,6 @@ ..\..\packages\Mapbox.VectorTile.1.0.3-alpha2\lib\net35\Mapbox.VectorTile.VectorTileReader.dll True - - ..\..\packages\RichardSzalay.MockHttp.1.5.0\lib\net45\RichardSzalay.MockHttp.dll - True - ..\..\packages\NUnit.3.5.0\lib\net45\nunit.framework.dll @@ -110,6 +118,7 @@ + diff --git a/test/UnitTest/app.config b/test/UnitTest/app.config new file mode 100644 index 0000000..b0571f9 --- /dev/null +++ b/test/UnitTest/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/test/UnitTest/packages.config b/test/UnitTest/packages.config index 79b93b2..2850921 100644 --- a/test/UnitTest/packages.config +++ b/test/UnitTest/packages.config @@ -1,5 +1,8 @@  + + + @@ -10,6 +13,4 @@ - - \ No newline at end of file From 41f1b7b34cf2a321f88e0f9580e2223389aac0cb Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Fri, 28 Apr 2017 16:45:11 +0200 Subject: [PATCH 17/35] mock api: hibri/HttpMock seems to work --- test/UnitTest/MockApiTest.cs | 61 +++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/test/UnitTest/MockApiTest.cs b/test/UnitTest/MockApiTest.cs index 5c16a7a..d1f3d75 100644 --- a/test/UnitTest/MockApiTest.cs +++ b/test/UnitTest/MockApiTest.cs @@ -24,6 +24,11 @@ internal class MockApiTest { private FileSource _fs = new FileSource(); IHttpServer _mockApi; + private struct _testUrl { + public static string testMockServer1 = "/testmock1"; + public static string testMockServer2 = "/testmock2"; + public static string rateLimitHit = "/ratelimithit"; + } [TearDown] public void Finished() { @@ -36,27 +41,48 @@ public void Finished() { [SetUp] public void SetupMockHttp() { - //_mockApi = new HttpServer(new Uri(_mockBaseUrl)); - var serverFactory = new HttpServerFactory(); _mockApi = serverFactory.Get(new Uri(_mockBaseUrl)).WithNewContext(); _mockApi.Start(); - _mockApi.Stub(r => r.Get("/*")) - .Return("{}") + _mockApi.Stub(r => r.Get(_testUrl.testMockServer1)) + .Return(@"{""name"":""first test""}") .AsContentType("application/json") .OK(); + // test status code ('Unavailable For Legal Reasons') not available in .NET HttpStatusCode enum + _mockApi.Stub(r => r.Get(_testUrl.testMockServer1)).WithStatus((HttpStatusCode)451); + + _mockApi.Stub(r => r.Get(_testUrl.rateLimitHit)) + .Return(string.Empty) + .WithStatus((HttpStatusCode)429); + } [Test] - public void First() { + public void TestMockServer() { Response resp = new Response(); _fs.Request( - _mockBaseUrl + "/bla/bla" + _mockBaseUrl + _testUrl.testMockServer1 + , (Response r) => { + // hm why doesn't nunit work in here? maybe blocking 'WaitForAllRequests' + resp = r; + } + ); + + _fs.WaitForAllRequests(); + + Assert.IsTrue(resp.StatusCode.HasValue, "mock api did not set status code"); + Assert.AreEqual(200, resp.StatusCode, "mock api returned wrong status code"); + Assert.AreEqual("application/json", resp.ContentType, "mock api didn't set correct content type"); + Assert.AreEqual(@"{""name"":""first test""}", System.Text.Encoding.UTF8.GetString(resp.Data), "mock api returned wrong response"); + + + _fs.Request( + _mockBaseUrl + _testUrl.testMockServer2 , (Response r) => { // hm why doesn't nunit work in here? maybe blocking 'WaitForAllRequests' resp = r; @@ -65,11 +91,30 @@ public void First() { _fs.WaitForAllRequests(); - System.Diagnostics.Debug.WriteLine(resp.StatusCode); - Assert.AreEqual(200, resp.StatusCode, "wrong status code"); + Assert.IsTrue(resp.StatusCode.HasValue, "mock api did not set status code"); + Assert.AreEqual(451, resp.StatusCode, "mock api returned wrong status code"); + } + [Test] + public void RateLimitHit() { + + Response resp = new Response(); + _fs.Request( + _mockBaseUrl + _testUrl.rateLimitHit + , (Response r) => { + // hm why doesn't nunit work in here? maybe blocking 'WaitForAllRequests' + resp = r; + } + ); + + _fs.WaitForAllRequests(); + + Assert.IsTrue(resp.StatusCode.HasValue, "mock api did not set status code"); + Assert.AreEqual(429, resp.StatusCode, "rate limit status code not set"); + } + } } \ No newline at end of file From f4352c1d66336777024b4adc89ba8940b1a879ff Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Fri, 28 Apr 2017 17:01:17 +0200 Subject: [PATCH 18/35] hm, mock api tests failing on AppVeyor --- src/Platform/FileSource.cs | 22 +++++++++++----------- test/UnitTest/MockApiTest.cs | 6 +++--- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Platform/FileSource.cs b/src/Platform/FileSource.cs index a7fb42a..78cc224 100644 --- a/src/Platform/FileSource.cs +++ b/src/Platform/FileSource.cs @@ -98,8 +98,8 @@ private IEnumerator proxyResponse(string url, Action call /// Block until all the requests are processed. /// public void WaitForAllRequests() { + int waitTimeMs = 150; while (_requests.Count > 0) { - // Reverse for safely removing while iterating. lock (_lock) { foreach (var req in _requests) { if (((HTTPRequest)req.Key).IsCompleted) { @@ -115,21 +115,21 @@ public void WaitForAllRequests() { } #if !WINDOWS_UWP - Thread.Sleep(50); + //Thread.Sleep(50); // TODO: get rid of DoEvents!!! and find non-blocking wait that works for Net3.5 //System.Windows.Forms.Application.DoEvents(); - //var resetEvent = new ManualResetEvent(false); - //ThreadPool.QueueUserWorkItem(new WaitCallback(delegate { - // Thread.Sleep(2500); - // resetEvent.Set(); - //}), null); - //resetEvent.WaitOne(); - //resetEvent.Close(); - //resetEvent = null; + var resetEvent = new ManualResetEvent(false); + ThreadPool.QueueUserWorkItem(new WaitCallback(delegate { + Thread.Sleep(waitTimeMs); + resetEvent.Set(); + }), null); + resetEvent.WaitOne(); + resetEvent.Close(); + resetEvent = null; #else - System.Threading.Tasks.Task.Delay(50).Wait(); + System.Threading.Tasks.Task.Delay(waitTimeMs).Wait(); #endif } } diff --git a/test/UnitTest/MockApiTest.cs b/test/UnitTest/MockApiTest.cs index d1f3d75..7193ddb 100644 --- a/test/UnitTest/MockApiTest.cs +++ b/test/UnitTest/MockApiTest.cs @@ -30,7 +30,7 @@ private struct _testUrl { public static string rateLimitHit = "/ratelimithit"; } - [TearDown] + [OneTimeTearDown] public void Finished() { if (null == _mockApi) { return; } _mockApi.Dispose(); @@ -38,7 +38,7 @@ public void Finished() { } - [SetUp] + [OneTimeSetUp] public void SetupMockHttp() { var serverFactory = new HttpServerFactory(); @@ -52,7 +52,7 @@ public void SetupMockHttp() { .OK(); // test status code ('Unavailable For Legal Reasons') not available in .NET HttpStatusCode enum - _mockApi.Stub(r => r.Get(_testUrl.testMockServer1)).WithStatus((HttpStatusCode)451); + _mockApi.Stub(r => r.Get(_testUrl.testMockServer2)).WithStatus((HttpStatusCode)451); _mockApi.Stub(r => r.Get(_testUrl.rateLimitHit)) .Return(string.Empty) From 06f5b5facf128be70e6387d9f8183fe187fdf5d9 Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Fri, 28 Apr 2017 18:14:25 +0200 Subject: [PATCH 19/35] more mock api tests --- src/Platform/HTTPRequest.cs | 4 +- src/Utils/UnixTimestampUtils.cs | 30 +++++++++ src/Utils/Utils.csproj | 1 + src/Utils/UtilsUWP.csproj | 1 + ...ockApiTest.cs => FileSourceMockApiTest.cs} | 61 +++++++++++++------ test/UnitTest/UnitTest.csproj | 2 +- 6 files changed, 76 insertions(+), 23 deletions(-) create mode 100644 src/Utils/UnixTimestampUtils.cs rename test/UnitTest/{MockApiTest.cs => FileSourceMockApiTest.cs} (50%) diff --git a/src/Platform/HTTPRequest.cs b/src/Platform/HTTPRequest.cs index 82af0ed..3d3084b 100644 --- a/src/Platform/HTTPRequest.cs +++ b/src/Platform/HTTPRequest.cs @@ -20,6 +20,7 @@ namespace Mapbox.Platform { using System.Collections.Generic; using System.Threading; using System.ComponentModel; + using Utils; #if NETFX_CORE using System.Net.Http; using System.Linq; @@ -287,8 +288,7 @@ private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { } else if (key.Equals("X-Rate-Limit-Reset", StringComparison.InvariantCultureIgnoreCase)) { double unixTimestamp; if (double.TryParse(val, out unixTimestamp)) { - DateTime beginningOfTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); - response.XRateLimitReset = beginningOfTime.AddSeconds(unixTimestamp).ToLocalTime(); + response.XRateLimitReset = UnixTimestampUtils.From(unixTimestamp); } } else if (key.Equals("Content-Type", StringComparison.InvariantCultureIgnoreCase)) { response.ContentType = val; diff --git a/src/Utils/UnixTimestampUtils.cs b/src/Utils/UnixTimestampUtils.cs new file mode 100644 index 0000000..34a8b64 --- /dev/null +++ b/src/Utils/UnixTimestampUtils.cs @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) 2016 Mapbox. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace Mapbox.Utils { + using System; + using System.Collections.Generic; + using System.Text; + + + /// + /// A set of Unix Timestamp utils. + /// + public static class UnixTimestampUtils { + + public static double To(DateTime date) { + //return date.ToLocalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds; + return date.Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds; + } + + public static DateTime From(double timestamp) { + //return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).Add(TimeSpan.FromSeconds(timestamp)).ToLocalTime(); + return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).Add(TimeSpan.FromSeconds(timestamp)); + } + + + } +} diff --git a/src/Utils/Utils.csproj b/src/Utils/Utils.csproj index 842b086..2fd4058 100644 --- a/src/Utils/Utils.csproj +++ b/src/Utils/Utils.csproj @@ -49,6 +49,7 @@ Properties\SharedAssemblyInfo.cs + diff --git a/src/Utils/UtilsUWP.csproj b/src/Utils/UtilsUWP.csproj index 072f13b..8f00d43 100644 --- a/src/Utils/UtilsUWP.csproj +++ b/src/Utils/UtilsUWP.csproj @@ -77,6 +77,7 @@ + diff --git a/test/UnitTest/MockApiTest.cs b/test/UnitTest/FileSourceMockApiTest.cs similarity index 50% rename from test/UnitTest/MockApiTest.cs rename to test/UnitTest/FileSourceMockApiTest.cs index 7193ddb..120ecfa 100644 --- a/test/UnitTest/MockApiTest.cs +++ b/test/UnitTest/FileSourceMockApiTest.cs @@ -17,7 +17,7 @@ namespace Mapbox.UnitTest { [TestFixture] - internal class MockApiTest { + internal class FileSourceMockApiTest { private static readonly string _mockBaseUrl = "http://localhost:2345"; @@ -28,6 +28,7 @@ private struct _testUrl { public static string testMockServer1 = "/testmock1"; public static string testMockServer2 = "/testmock2"; public static string rateLimitHit = "/ratelimithit"; + public static string xrateheader = "/xrateheader"; } [OneTimeTearDown] @@ -58,63 +59,83 @@ public void SetupMockHttp() { .Return(string.Empty) .WithStatus((HttpStatusCode)429); + double unixTimestamp = UnixTimestampUtils.To(new DateTime(1981, 12, 2)); + _mockApi.Stub(r => r.Get(_testUrl.xrateheader)) + .AddHeader("X-Rate-Limit-Interval", "60") + .AddHeader("X-Rate-Limit-Limit", "100000") + .AddHeader("X-Rate-Limit-Reset", unixTimestamp.ToString()) + .OK(); + } [Test] public void TestMockServer() { - Response resp = new Response(); _fs.Request( _mockBaseUrl + _testUrl.testMockServer1 , (Response r) => { - // hm why doesn't nunit work in here? maybe blocking 'WaitForAllRequests' - resp = r; + Assert.IsTrue(r.StatusCode.HasValue, "mock api did not set status code"); + Assert.AreEqual(200, r.StatusCode, "mock api returned wrong status code"); + Assert.AreEqual("application/json", r.ContentType, "mock api didn't set correct content type"); + Assert.AreEqual(@"{""name"":""first test""}", System.Text.Encoding.UTF8.GetString(r.Data), "mock api returned wrong response"); } ); _fs.WaitForAllRequests(); - Assert.IsTrue(resp.StatusCode.HasValue, "mock api did not set status code"); - Assert.AreEqual(200, resp.StatusCode, "mock api returned wrong status code"); - Assert.AreEqual("application/json", resp.ContentType, "mock api didn't set correct content type"); - Assert.AreEqual(@"{""name"":""first test""}", System.Text.Encoding.UTF8.GetString(resp.Data), "mock api returned wrong response"); - _fs.Request( _mockBaseUrl + _testUrl.testMockServer2 , (Response r) => { - // hm why doesn't nunit work in here? maybe blocking 'WaitForAllRequests' - resp = r; + Assert.IsTrue(r.StatusCode.HasValue, "mock api did not set status code"); + Assert.AreEqual(451, r.StatusCode, "mock api returned wrong status code"); } ); _fs.WaitForAllRequests(); - - Assert.IsTrue(resp.StatusCode.HasValue, "mock api did not set status code"); - Assert.AreEqual(451, resp.StatusCode, "mock api returned wrong status code"); - } [Test] public void RateLimitHit() { - Response resp = new Response(); _fs.Request( _mockBaseUrl + _testUrl.rateLimitHit , (Response r) => { - // hm why doesn't nunit work in here? maybe blocking 'WaitForAllRequests' - resp = r; + Assert.IsTrue(r.StatusCode.HasValue, "request did not set status code"); + Assert.AreEqual(429, r.StatusCode, "request did not set rate limit status code correctly"); + Assert.IsTrue(r.HasError, "request did not set HasError"); + Assert.NotNull(r.Exceptions, "request did not set any exceptions"); + Assert.GreaterOrEqual(1, r.Exceptions.Count, "request did not set enough exceptions"); } ); _fs.WaitForAllRequests(); + } + + + [Test] + public void XRateHeaders() { + + _fs.Request( + _mockBaseUrl + _testUrl.xrateheader + , (Response r) => { + Assert.IsTrue(r.XRateLimitInterval.HasValue, "request did not set XRateLimitInterval"); + Assert.IsTrue(r.XRateLimitLimit.HasValue, "request did not set XRateLimitLimit"); + Assert.IsTrue(r.XRateLimitReset.HasValue, "request did not set XRateLimitReset"); + + Assert.AreEqual(60, r.XRateLimitInterval.Value, "request did not set XRateLimitInterval value correctly"); + Assert.AreEqual(100000, r.XRateLimitLimit.Value, "request did not set XRateLimitLimit value correctly"); + DateTime dt = new DateTime(1981, 12, 2); + Assert.AreEqual(dt, r.XRateLimitReset.Value, "request did not set XRateLimitReset value correctly"); + } + ); - Assert.IsTrue(resp.StatusCode.HasValue, "mock api did not set status code"); - Assert.AreEqual(429, resp.StatusCode, "rate limit status code not set"); + _fs.WaitForAllRequests(); } + } } \ No newline at end of file diff --git a/test/UnitTest/UnitTest.csproj b/test/UnitTest/UnitTest.csproj index ee81222..bffaa4a 100644 --- a/test/UnitTest/UnitTest.csproj +++ b/test/UnitTest/UnitTest.csproj @@ -73,7 +73,7 @@ - + From 6cd195df95d8f0727906abb4ad0ca75ec400e895 Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Wed, 3 May 2017 10:51:16 +0200 Subject: [PATCH 20/35] fix 'RateLimitHit' test --- src/Utils/UnixTimestampUtils.cs | 13 +++++++++++++ test/UnitTest/FileSourceMockApiTest.cs | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Utils/UnixTimestampUtils.cs b/src/Utils/UnixTimestampUtils.cs index 34a8b64..db6754b 100644 --- a/src/Utils/UnixTimestampUtils.cs +++ b/src/Utils/UnixTimestampUtils.cs @@ -15,11 +15,24 @@ namespace Mapbox.Utils { /// public static class UnixTimestampUtils { + // http://gigi.nullneuron.net/gigilabs/converting-tofrom-unix-timestamp-in-c/ + + /// + /// Convert from DateTime to Unix timestamp + /// + /// + /// public static double To(DateTime date) { //return date.ToLocalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds; return date.Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds; } + + /// + /// Convert from Unitx timestamp to DateTime + /// + /// + /// public static DateTime From(double timestamp) { //return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).Add(TimeSpan.FromSeconds(timestamp)).ToLocalTime(); return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).Add(TimeSpan.FromSeconds(timestamp)); diff --git a/test/UnitTest/FileSourceMockApiTest.cs b/test/UnitTest/FileSourceMockApiTest.cs index 120ecfa..3e0b4c7 100644 --- a/test/UnitTest/FileSourceMockApiTest.cs +++ b/test/UnitTest/FileSourceMockApiTest.cs @@ -107,7 +107,7 @@ public void RateLimitHit() { Assert.AreEqual(429, r.StatusCode, "request did not set rate limit status code correctly"); Assert.IsTrue(r.HasError, "request did not set HasError"); Assert.NotNull(r.Exceptions, "request did not set any exceptions"); - Assert.GreaterOrEqual(1, r.Exceptions.Count, "request did not set enough exceptions"); + Assert.GreaterOrEqual(r.Exceptions.Count, 1, "request did not set enough exceptions"); } ); From 32f1809445c140ade7aef79b0973ed72ee5b2d9a Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Wed, 3 May 2017 11:01:46 +0200 Subject: [PATCH 21/35] add HTTPRequest for 404 --- test/UnitTest/FileSourceMockApiTest.cs | 33 +++++++++++++++++++------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/test/UnitTest/FileSourceMockApiTest.cs b/test/UnitTest/FileSourceMockApiTest.cs index 3e0b4c7..8ca6eb2 100644 --- a/test/UnitTest/FileSourceMockApiTest.cs +++ b/test/UnitTest/FileSourceMockApiTest.cs @@ -25,8 +25,8 @@ internal class FileSourceMockApiTest { IHttpServer _mockApi; private struct _testUrl { - public static string testMockServer1 = "/testmock1"; - public static string testMockServer2 = "/testmock2"; + public static string simpleJson = "/testmock1"; + public static string customStatusCode = "/testmock2"; public static string rateLimitHit = "/ratelimithit"; public static string xrateheader = "/xrateheader"; } @@ -47,13 +47,13 @@ public void SetupMockHttp() { _mockApi.Start(); - _mockApi.Stub(r => r.Get(_testUrl.testMockServer1)) + _mockApi.Stub(r => r.Get(_testUrl.simpleJson)) .Return(@"{""name"":""first test""}") .AsContentType("application/json") .OK(); // test status code ('Unavailable For Legal Reasons') not available in .NET HttpStatusCode enum - _mockApi.Stub(r => r.Get(_testUrl.testMockServer2)).WithStatus((HttpStatusCode)451); + _mockApi.Stub(r => r.Get(_testUrl.customStatusCode)).WithStatus((HttpStatusCode)451); _mockApi.Stub(r => r.Get(_testUrl.rateLimitHit)) .Return(string.Empty) @@ -70,10 +70,10 @@ public void SetupMockHttp() { [Test] - public void TestMockServer() { + public void SimpleJson() { _fs.Request( - _mockBaseUrl + _testUrl.testMockServer1 + _mockBaseUrl + _testUrl.simpleJson , (Response r) => { Assert.IsTrue(r.StatusCode.HasValue, "mock api did not set status code"); Assert.AreEqual(200, r.StatusCode, "mock api returned wrong status code"); @@ -86,7 +86,7 @@ public void TestMockServer() { _fs.Request( - _mockBaseUrl + _testUrl.testMockServer2 + _mockBaseUrl + _testUrl.customStatusCode , (Response r) => { Assert.IsTrue(r.StatusCode.HasValue, "mock api did not set status code"); Assert.AreEqual(451, r.StatusCode, "mock api returned wrong status code"); @@ -105,7 +105,7 @@ public void RateLimitHit() { , (Response r) => { Assert.IsTrue(r.StatusCode.HasValue, "request did not set status code"); Assert.AreEqual(429, r.StatusCode, "request did not set rate limit status code correctly"); - Assert.IsTrue(r.HasError, "request did not set HasError"); + Assert.IsTrue(r.HasError, "request did not set 'HasError'"); Assert.NotNull(r.Exceptions, "request did not set any exceptions"); Assert.GreaterOrEqual(r.Exceptions.Count, 1, "request did not set enough exceptions"); } @@ -136,6 +136,23 @@ public void XRateHeaders() { } + [Test] + public void DoesNotExist404() { + + _fs.Request( + _mockBaseUrl + "/doesnotexist/mvt.pbf" + , (Response r) => { + Assert.IsTrue(r.StatusCode.HasValue, "request did not set status code"); + Assert.AreEqual(404, r.StatusCode, "request did not set 404 status code correctly"); + Assert.IsTrue(r.HasError, "request did not set 'HasError'"); + Assert.NotNull(r.Exceptions, "request did not set any exceptions"); + Assert.GreaterOrEqual(r.Exceptions.Count, 1, "request did not set enough exceptions"); + } + ); + + _fs.WaitForAllRequests(); + } + } } \ No newline at end of file From a18230650981773b03268b8dad2e7f93a8d1e38c Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Wed, 3 May 2017 11:34:11 +0200 Subject: [PATCH 22/35] add 'HasError' and 'Exceptions' to Mapbox.Map.Tile --- src/Map/Tile.cs | 95 ++++++++++++++++++++------------- src/Map/VectorTile.cs | 53 +++++++----------- src/Platform/Response.cs | 4 +- test/UnitTest/FileSourceTest.cs | 47 +++++++++------- test/UnitTest/Utils.cs | 20 +++---- test/UnitTest/VectorTileTest.cs | 44 ++++++++------- 6 files changed, 145 insertions(+), 118 deletions(-) diff --git a/src/Map/Tile.cs b/src/Map/Tile.cs index 7850693..788ec82 100644 --- a/src/Map/Tile.cs +++ b/src/Map/Tile.cs @@ -8,6 +8,9 @@ namespace Mapbox.Map { using System; using Mapbox.Platform; using System.Linq; + using System.Collections.Generic; + using System.Collections.ObjectModel; + /// /// A Map tile, a square with vector or raster data representing a geographic @@ -16,11 +19,13 @@ namespace Mapbox.Map { /// public abstract class Tile { - private CanonicalTileId id; - private string error; - private State state = State.New; - private IAsyncRequest request; - private Action callback; + + private CanonicalTileId _id; + private List _exceptions; + private State _state = State.New; + private IAsyncRequest _request; + private Action _callback; + /// Tile state. public enum State { @@ -37,30 +42,35 @@ public enum State { /// Gets the identifier. /// The canonical tile identifier. public CanonicalTileId Id { - get { - return this.id; - } - set { - this.id = value; - } + get { return _id; } + set { _id = value; } } - /// Gets the error message if any. - /// The error string. - public string Error { + + /// Flag to indicate if the request was successful + public bool HasError { get { - return this.error; + return _exceptions == null ? false : _exceptions.Count > 0; } } + + /// Exceptions that might have occured during creation of the tile. + public ReadOnlyCollection Exceptions { + get { return null == _exceptions ? null : _exceptions.AsReadOnly(); } + } + + /// /// Sets the error message. /// /// - public void SetError(string errorMessage) { - error = errorMessage; + internal void AddException(Exception ex) { + if (null == _exceptions) { _exceptions = new List(); } + _exceptions.Add(ex); } + /// /// Gets the current state. When fully loaded, you must /// check if the data actually arrived and if the tile @@ -69,10 +79,11 @@ public void SetError(string errorMessage) { /// The tile state. public State CurrentState { get { - return this.state; + return _state; } } + /// /// Initializes the object. It will /// start a network request and fire the callback when completed. @@ -80,14 +91,15 @@ public State CurrentState { /// Initialization parameters. /// The completion callback. public void Initialize(Parameters param, Action callback) { - this.Cancel(); + Cancel(); - this.state = State.Loading; - this.id = param.Id; - this.request = param.Fs.Request(this.MakeTileResource(param.MapId).GetUrl(), this.HandleTileResponse); - this.callback = callback; + _state = State.Loading; + _id = param.Id; + _request = param.Fs.Request(MakeTileResource(param.MapId).GetUrl(), HandleTileResponse); + _callback = callback; } + /// /// Returns a that represents the current /// . @@ -97,9 +109,10 @@ public void Initialize(Parameters param, Action callback) { /// . /// public override string ToString() { - return this.Id.ToString(); + return Id.ToString(); } + /// /// Cancels the request for the object. /// It will stop a network request and set the tile's state to Canceled. @@ -109,7 +122,7 @@ public override string ToString() { /// // Do not request tiles that we are already requesting /// // but at the same time exclude the ones we don't need /// // anymore, cancelling the network request. - /// this.tiles.RemoveWhere((T tile) => + /// tiles.RemoveWhere((T tile) => /// { /// if (cover.Remove(tile.Id)) /// { @@ -118,44 +131,52 @@ public override string ToString() { /// else /// { /// tile.Cancel(); - /// this.NotifyNext(tile); + /// NotifyNext(tile); /// return true; /// } /// }); /// /// public void Cancel() { - if (this.request != null) { - this.request.Cancel(); - this.request = null; + if (_request != null) { + _request.Cancel(); + _request = null; } - this.state = State.Canceled; + _state = State.Canceled; } - public void SetState(State state) { this.state = state; } // Get the tile resource (raster/vector/etc). internal abstract TileResource MakeTileResource(string mapid); + // Decode the tile. internal abstract bool ParseTileData(byte[] data); + // TODO: Currently the tile decoding is done on the main thread. We must implement // a Worker class to abstract this, so on platforms that support threads (like Unity // on the desktop, Android, etc) we can use worker threads and when building for // the browser, we keep it single-threaded. private void HandleTileResponse(Response response) { + if (response.HasError) { - this.error = string.Join(Environment.NewLine, response.Exceptions.Select(e => e.Message).ToArray()); - } else if (this.ParseTileData(response.Data) == false) { - this.error = "ParseError"; + response.Exceptions.ToList().ForEach(e => AddException(e)); + } else { + // only try to parse if request was successful + + // current implementation doesn't need to check if parsing is successful: + // * Mapbox.Map.VectorTile.ParseTileData() already adds any exception to the list + // * Mapbox.Map.RasterTile.ParseTileData() doesn't do any parsing + ParseTileData(response.Data); } - this.state = State.Loaded; - this.callback(); + _state = State.Loaded; + _callback(); } + /// /// Parameters for initializing a Tile object. /// @@ -181,5 +202,7 @@ public struct Parameters { /// The data source abstraction. public IFileSource Fs; } + + } } diff --git a/src/Map/VectorTile.cs b/src/Map/VectorTile.cs index 93e5bd7..144c313 100644 --- a/src/Map/VectorTile.cs +++ b/src/Map/VectorTile.cs @@ -4,8 +4,7 @@ // //----------------------------------------------------------------------- -namespace Mapbox.Map -{ +namespace Mapbox.Map { using System.Collections.ObjectModel; using Mapbox.Utils; using Mapbox.VectorTile; @@ -40,8 +39,7 @@ namespace Mapbox.Map /// })); /// /// - public sealed class VectorTile : Tile, IDisposable - { + public sealed class VectorTile : Tile, IDisposable { // FIXME: Namespace here is very confusing and conflicts (sematically) // with his class. Something has to be renamed here. private Mapbox.VectorTile.VectorTile data; @@ -50,10 +48,8 @@ public sealed class VectorTile : Tile, IDisposable /// Gets the vector decoded using Mapbox.VectorTile library. /// The GeoJson data. - public Mapbox.VectorTile.VectorTile Data - { - get - { + public Mapbox.VectorTile.VectorTile Data { + get { return this.data; } } @@ -67,23 +63,18 @@ public Mapbox.VectorTile.VectorTile Data //} - public void Dispose() - { + public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } //TODO: change signature if 'VectorTile' class changes from 'sealed' //protected override void Dispose(bool disposeManagedResources) - public void Dispose(bool disposeManagedResources) - { - if (!isDisposed) - { - if (disposeManagedResources) - { + public void Dispose(bool disposeManagedResources) { + if (!isDisposed) { + if (disposeManagedResources) { //TODO implement IDisposable with Mapbox.VectorTile.VectorTile - if (null != data) - { + if (null != data) { data = null; } } @@ -105,10 +96,8 @@ public void Dispose(bool disposeManagedResources) /// Console.Write("GeoJson: " + json); /// /// - public string GeoJson - { - get - { + public string GeoJson { + get { return this.data.ToGeoJson((ulong)Id.Z, (ulong)Id.X, (ulong)Id.Y, 0); } } @@ -129,8 +118,7 @@ public string GeoJson /// } /// /// - public ReadOnlyCollection LayerNames() - { + public ReadOnlyCollection LayerNames() { return this.data.LayerNames(); } @@ -151,30 +139,25 @@ public ReadOnlyCollection LayerNames() /// } /// /// - public VectorTileLayer GetLayer(string layerName) - { + public VectorTileLayer GetLayer(string layerName) { return this.data.GetLayer(layerName); } - internal override TileResource MakeTileResource(string mapId) - { + internal override TileResource MakeTileResource(string mapId) { return TileResource.MakeVector(Id, mapId); } - internal override bool ParseTileData(byte[] data) - { - try - { + internal override bool ParseTileData(byte[] data) { + try { var decompressed = Compression.Decompress(data); this.data = new Mapbox.VectorTile.VectorTile(decompressed); return true; } - catch (Exception ex) - { - SetError("VectorTile parsing failed: " + ex.ToString()); + catch (Exception ex) { + AddException(ex); return false; } } diff --git a/src/Platform/Response.cs b/src/Platform/Response.cs index f32e12e..990b262 100644 --- a/src/Platform/Response.cs +++ b/src/Platform/Response.cs @@ -20,6 +20,8 @@ public bool RateLimitHit { get { return StatusCode.HasValue ? 429 == StatusCode.Value : false; } } + + /// Flag to indicate if the request was successful public bool HasError { get { return _exceptions == null ? false : _exceptions.Count > 0; @@ -42,7 +44,7 @@ public bool HasError { public DateTime? XRateLimitReset; private List _exceptions; - /// Error description, set on error, empty otherwise. + /// Exceptions that might have occured during the request. public ReadOnlyCollection Exceptions { get { return null == _exceptions ? null : _exceptions.AsReadOnly(); } } diff --git a/test/UnitTest/FileSourceTest.cs b/test/UnitTest/FileSourceTest.cs index 5997771..f187d1f 100644 --- a/test/UnitTest/FileSourceTest.cs +++ b/test/UnitTest/FileSourceTest.cs @@ -10,16 +10,19 @@ namespace Mapbox.UnitTest { using NUnit.Framework; using System.Net; + [TestFixture] internal class FileSourceTest { - private const string Uri = "https://api.mapbox.com/geocoding/v5/mapbox.places/helsinki.json"; - private FileSource fs; + private const string _url = "https://api.mapbox.com/geocoding/v5/mapbox.places/helsinki.json"; + private FileSource _fs; + [SetUp] public void SetUp() { - this.fs = new FileSource(); + _fs = new FileSource(); } + [Test] public void AccessTokenSet() { Assert.IsNotNull( @@ -27,34 +30,37 @@ public void AccessTokenSet() { "MAPBOX_ACCESS_TOKEN not set in the environment."); } + [Test] public void Request() { - this.fs.Request( - Uri, + _fs.Request( + _url, (Response res) => { Assert.IsNotNull(res.Data, "No data received from the servers."); }); - this.fs.WaitForAllRequests(); + _fs.WaitForAllRequests(); } + [Test] public void MultipleRequests() { int count = 0; - this.fs.Request(Uri, (Response res) => ++count); - this.fs.Request(Uri, (Response res) => ++count); - this.fs.Request(Uri, (Response res) => ++count); + _fs.Request(_url, (Response res) => ++count); + _fs.Request(_url, (Response res) => ++count); + _fs.Request(_url, (Response res) => ++count); - this.fs.WaitForAllRequests(); + _fs.WaitForAllRequests(); Assert.AreEqual(count, 3, "Should have received 3 replies."); } + [Test] public void RequestCancel() { - var request = this.fs.Request( - Uri, + var request = _fs.Request( + _url, (Response res) => { Assert.IsTrue(res.HasError); WebException wex = res.Exceptions[0] as WebException; @@ -64,38 +70,43 @@ public void RequestCancel() { request.Cancel(); - this.fs.WaitForAllRequests(); + _fs.WaitForAllRequests(); } + [Test] public void RequestDnsError() { - this.fs.Request( + _fs.Request( "https://dnserror.shouldnotwork", (Response res) => { Assert.IsTrue(res.HasError); }); - this.fs.WaitForAllRequests(); + _fs.WaitForAllRequests(); } + [Test] public void RequestForbidden() { // Mapbox servers will return a forbidden when attempting // to access a page outside the API space with a token // on the query. Let's hope the behaviour stay like this. - this.fs.Request( + _fs.Request( "https://mapbox.com/forbidden", (Response res) => { Assert.IsTrue(res.HasError); }); - this.fs.WaitForAllRequests(); + _fs.WaitForAllRequests(); } + [Test] public void WaitWithNoRequests() { // This should simply not block. - this.fs.WaitForAllRequests(); + _fs.WaitForAllRequests(); } + + } } \ No newline at end of file diff --git a/test/UnitTest/Utils.cs b/test/UnitTest/Utils.cs index 5cf0cac..e541f47 100644 --- a/test/UnitTest/Utils.cs +++ b/test/UnitTest/Utils.cs @@ -5,14 +5,14 @@ //----------------------------------------------------------------------- namespace Mapbox.UnitTest { - using System; - using System.Collections.Generic; - using System.Drawing; - using System.IO; - using Mapbox.Map; - using Mapbox.Platform; - - internal static class Utils { + using System; + using System.Collections.Generic; + using System.Drawing; + using System.IO; + using Mapbox.Map; + using Mapbox.Platform; + + internal static class Utils { internal class VectorMapObserver : Mapbox.Utils.IObserver { private List tiles = new List(); @@ -39,7 +39,7 @@ public List Tiles { } public void OnNext(RasterTile tile) { - if (tile.CurrentState == Tile.State.Loaded && string.IsNullOrEmpty(tile.Error)) { + if (tile.CurrentState == Tile.State.Loaded && !tile.HasError) { var image = Image.FromStream(new MemoryStream(tile.Data)); tiles.Add(image); } @@ -56,7 +56,7 @@ public List Tiles { } public void OnNext(ClassicRasterTile tile) { - if (tile.CurrentState == Tile.State.Loaded && string.IsNullOrEmpty(tile.Error)) { + if (tile.CurrentState == Tile.State.Loaded && !tile.HasError) { var image = Image.FromStream(new MemoryStream(tile.Data)); tiles.Add(image); } diff --git a/test/UnitTest/VectorTileTest.cs b/test/UnitTest/VectorTileTest.cs index 3da0167..411f01b 100644 --- a/test/UnitTest/VectorTileTest.cs +++ b/test/UnitTest/VectorTileTest.cs @@ -5,26 +5,31 @@ //----------------------------------------------------------------------- namespace Mapbox.UnitTest { - using System; - using System.Collections.Generic; - using System.Linq; - using Mapbox.Map; - using Mapbox.Platform; - using Mapbox.Utils; - using NUnit.Framework; - - [TestFixture] + using System; + using System.Collections.Generic; + using System.Linq; + using Mapbox.Map; + using Mapbox.Platform; + using Mapbox.Utils; + using NUnit.Framework; + + + [TestFixture] internal class VectorTileTest { - private FileSource fs; + + + private FileSource _fs; + [SetUp] public void SetUp() { - this.fs = new FileSource(); + _fs = new FileSource(); } + [Test] public void ParseSuccess() { - var map = new Map(this.fs); + var map = new Map(_fs); var mapObserver = new Utils.VectorMapObserver(); map.Subscribe(mapObserver); @@ -35,7 +40,7 @@ public void ParseSuccess() { for (int zoom = 0; zoom < 15; ++zoom) { map.Zoom = zoom; map.Update(); - this.fs.WaitForAllRequests(); + _fs.WaitForAllRequests(); } // We must have all the tiles for Helsinki from 0-15. @@ -54,6 +59,7 @@ public void ParseSuccess() { map.Unsubscribe(mapObserver); } + [Test] public void ParseFailure() { var resource = TileResource.MakeVector(new CanonicalTileId(13, 5465, 2371), null); @@ -82,9 +88,10 @@ public void ParseFailure() { map.Unsubscribe(mapObserver); } + [Test] public void SeveralTiles() { - var map = new Map(this.fs); + var map = new Map(_fs); var mapObserver = new Utils.VectorMapObserver(); map.Subscribe(mapObserver); @@ -93,20 +100,21 @@ public void SeveralTiles() { map.Zoom = 3; // 64 tiles. map.Update(); - this.fs.WaitForAllRequests(); + _fs.WaitForAllRequests(); Assert.AreEqual(64, mapObserver.Tiles.Count); foreach (var tile in mapObserver.Tiles) { - if (string.IsNullOrEmpty(tile.Error)) { + if (!tile.HasError) { Assert.Greater(tile.GeoJson.Length, 41); } else { - // NotFound is fine. - Assert.AreNotEqual("ParseError", tile.Error); + Assert.GreaterOrEqual(tile.Exceptions.Count, 1, "not set enough exceptions set on 'Tile'"); } } map.Unsubscribe(mapObserver); } + + } } From c4c25a47cd881e704a3719d9fa695b59e2e61c18 Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Wed, 3 May 2017 12:46:07 +0200 Subject: [PATCH 23/35] HTTPRequest clean up and some changes for UWP --- src/Platform/HTTPRequest.cs | 145 +++++++++++++++++++++++------------- 1 file changed, 93 insertions(+), 52 deletions(-) diff --git a/src/Platform/HTTPRequest.cs b/src/Platform/HTTPRequest.cs index 3d3084b..8b35a99 100644 --- a/src/Platform/HTTPRequest.cs +++ b/src/Platform/HTTPRequest.cs @@ -35,11 +35,18 @@ internal sealed class HTTPRequest : IAsyncRequest { private Action _callback; - private HttpWebRequest _hwr; - private int _timeOut; +#if !NETFX_CORE + private HttpWebRequest _request; +#else + private HttpClient _request; + private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); +#endif #if !UNITY private SynchronizationContext _sync = AsyncOperationManager.SynchronizationContext; #endif + private int _timeOut; + private string _requestUrl; + private readonly string _userAgent = "mapbox-sdk-cs"; /// @@ -49,64 +56,83 @@ internal sealed class HTTPRequest : IAsyncRequest { /// /// seconds public HTTPRequest(string url, Action callback, int timeOut = 10) { + IsCompleted = false; - //UnityEngine.Debug.Log(url); _callback = callback; _timeOut = timeOut; + _requestUrl = url; - //The answer is changing HttpWebRequest / HttpWebResponse to WebRequest/ WebResponse only.That fixed the problem. + setupRequest(); + getResponseAsync(_request, EvaluateResponse); + } - _hwr = WebRequest.Create(url) as HttpWebRequest; -#if !NETFX_CORE - _hwr.Credentials = CredentialCache.DefaultCredentials; - _hwr.KeepAlive = true; + private void setupRequest() { - //_hwr.ProtocolVersion = HttpVersion.Version11; //that's it!!! +#if !NETFX_CORE + _request = WebRequest.Create(_requestUrl) as HttpWebRequest; + _request.UserAgent = _userAgent; + //_hwr.UserAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36"; + //_hwr.CachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore); + _request.Credentials = CredentialCache.DefaultCredentials; + _request.KeepAlive = true; + _request.ProtocolVersion = HttpVersion.Version11; // improved performance - //that' it !!!! + // improved performance. + // ServicePointManager.DefaultConnectionLimit doesn't seem to change anything + // set ConnectionLimit per request // https://msdn.microsoft.com/en-us/library/system.net.httpwebrequest(v=vs.90).aspx#Remarks // use a value that is 12 times the number of CPUs on the local computer + _request.ServicePoint.ConnectionLimit = Environment.ProcessorCount * 6; + + _request.ServicePoint.UseNagleAlgorithm = true; + _request.ServicePoint.Expect100Continue = false; + _request.ServicePoint.MaxIdleTime = 2000; + _request.Method = "GET"; + _request.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip,deflate"); + _request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + //_hwr.Timeout = timeOut * 1000; doesn't work in async calls, see below - _hwr.ServicePoint.ConnectionLimit = Environment.ProcessorCount * 6; - - _hwr.ServicePoint.UseNagleAlgorithm = true; - _hwr.ServicePoint.Expect100Continue = false; - _hwr.ServicePoint.MaxIdleTime = 2000; - //System.Net.ServicePointManager.SetTcpKeepAlive(false, 0, 0); +#else + HttpClientHandler handler = new HttpClientHandler() { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, + AllowAutoRedirect = true, + UseDefaultCredentials = true + }; + _request = new HttpClient(handler); + _request.DefaultRequestHeaders.Add("User-Agent", _userAgent); + _request.Timeout = TimeSpan.FromSeconds(_timeOut); - //UnityEngine.Debug.Log("CurrentConnections: " + _hwr.ServicePoint.CurrentConnections); - //UnityEngine.Debug.Log("ConnectionLimit: " + _hwr.ServicePoint.ConnectionLimit); - //UnityEngine.Debug.Log("ConnectionName: " + _hwr.ServicePoint.ConnectionName); - _hwr.Method = "GET"; - _hwr.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip,deflate"); - _hwr.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + // TODO: how to set ConnectionLimit? ServicePoint.ConnectionLimit doesn't seem to be available. #endif #if !UNITY && !NETFX_CORE - //_hwr.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.CacheIfAvailable); - _hwr.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.NoCacheNoStore); + // 'NoCacheNoStore' greatly reduced the number of faulty request + // seems that .Net caching and Mapbox API don't play well together + _request.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.NoCacheNoStore); #endif -#if !NETFX_CORE - _hwr.UserAgent = "mapbox-sdk-cs"; - //_hwr.UserAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36"; - //_hwr.CachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore); -#endif - //_hwr.Timeout = timeOut * 1000; doesn't work in async calls, see below - - getResponseAsync(_hwr, EvaluateResponse); } + #if NETFX_CORE - private async void getResponseAsync(HttpWebRequest request, Action gotResponse) { - //http://rextester.com/discussion/XPKY90132/async-example-with-HttpClient - using (var client = new HttpClient()) { - var response = await client.GetAsync(_hwr.RequestUri); + private async void getResponseAsync(HttpClient request, Action gotResponse) { + + // TODO: implement a strategy similar to the full .Net one to avoid blocking of 'GetAsync()' + // see 'Remarks' https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.timeout?view=netcore-1.1#System_Net_Http_HttpClient_Timeout + // "A Domain Name System (DNS) query may take up to 15 seconds to return or time out." + + HttpResponseMessage response = null; + try { + response = await request.GetAsync(_requestUrl, _cancellationTokenSource.Token); gotResponse(response, null); } + catch (Exception ex) { + gotResponse(response, ex); + } + } @@ -122,11 +148,7 @@ private async void EvaluateResponse(HttpResponseMessage apiResponse, Exception a if (null == apiResponse) { response.AddException(new Exception("No Reponse.")); } else { - // TODO: evaluate headers and add custom exception, eg if rate limit is exceeded // https://www.mapbox.com/api-documentation/#rate-limits - // X-Rate-Limit-Interval - // X-Rate-Limit-Limit - // X-Rate-Limit-Reset if (null != apiResponse.Headers) { response.Headers = new Dictionary(); foreach (var hdr in apiResponse.Headers) { @@ -173,12 +195,24 @@ private async void EvaluateResponse(HttpResponseMessage apiResponse, Exception a _callback(response); IsCompleted = true; _callback = null; +#if NETFX_CORE + if (null != _request) { + _request.Dispose(); + _request = null; + } +#endif }, null); #else - UnityMainThreadDispatcher.Instance().Enqueue(() => { + UnityToolbag.Dispatcher.InvokeAsync(() => { _callback(response); IsCompleted = true; _callback = null; +#if NETFX_CORE + if (null != _request) { + _request.Dispose(); + _request = null; + } +#endif }); #endif } @@ -267,11 +301,7 @@ private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { if (null == apiResponse) { response.AddException(new Exception("No Reponse.")); } else { - // TODO: evaluate headers and add custom exception, eg if rate limit is exceeded // https://www.mapbox.com/api-documentation/#rate-limits - // X-Rate-Limit-Interval - // X-Rate-Limit-Limit - // X-Rate-Limit-Reset if (null != apiResponse.Headers) { response.Headers = new Dictionary(); for (int i = 0; i < apiResponse.Headers.Count; i++) { @@ -328,17 +358,24 @@ private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { _callback(response); IsCompleted = true; _callback = null; +#if NETFX_CORE + if (null != _request) { + _request.Dispose(); + _request = null; + } +#endif }, null); #else - //UnityMainThreadDispatcher.Enqueue(() => { - // _callback(response); - // IsCompleted = true; - // _callback = null; - //}); UnityToolbag.Dispatcher.InvokeAsync(() => { _callback(response); IsCompleted = true; _callback = null; +#if NETFX_CORE + if (null != _request) { + _request.Dispose(); + _request = null; + } +#endif }); #endif } @@ -348,9 +385,13 @@ private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { public void Cancel() { - if (null != _hwr) { - _hwr.Abort(); +#if !NETFX_CORE + if (null != _request) { + _request.Abort(); } +#else + _cancellationTokenSource.Cancel(); +#endif } From d2f632ef8ddc65d4217bf8b0679de9f9f876151f Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Thu, 4 May 2017 14:37:35 +0200 Subject: [PATCH 24/35] progress and finished callbacks on IFileSource --- src/Platform/FileSource.cs | 26 +++++++++++++++++---- src/Platform/IFileSource.cs | 8 +++---- test/UnitTest/FileSourceMockApiTest.cs | 31 ++++++++++++++++++++++++++ test/UnitTest/Utils.cs | 2 +- 4 files changed, 57 insertions(+), 10 deletions(-) diff --git a/src/Platform/FileSource.cs b/src/Platform/FileSource.cs index 78cc224..e51d9b2 100644 --- a/src/Platform/FileSource.cs +++ b/src/Platform/FileSource.cs @@ -49,7 +49,12 @@ public sealed class FileSource : IFileSource { /// request. This handle can be completely ignored if there is no intention of ever /// canceling the request. /// - public IAsyncRequest Request(string url, Action callback) { + public IAsyncRequest Request( + string url + , Action callback + , Action progressCallback = null + , Action finishedCallback = null + ) { if (_accessToken != null) { url += "?access_token=" + _accessToken; } @@ -59,7 +64,7 @@ public IAsyncRequest Request(string url, Action callback) { // * evaluate rate limits (headers and status code) // * throttle requests accordingly //var request = new HTTPRequest(url, callback); - IEnumerator proxy = proxyResponse(url, callback); + IEnumerator proxy = proxyResponse(url, callback, progressCallback, finishedCallback); proxy.MoveNext(); HTTPRequest request = proxy.Current; @@ -68,7 +73,12 @@ public IAsyncRequest Request(string url, Action callback) { // TODO: look at requests and implement throttling if needed - private IEnumerator proxyResponse(string url, Action callback) { + private IEnumerator proxyResponse( + string url + , Action callback + , Action progressCallback + , Action finishedCallback + ) { // TODO: plugin caching somewhere around here @@ -80,7 +90,15 @@ private IEnumerator proxyResponse(string url, Action call lock (_lock) { //another place to catch if request has been cancelled try { + //remove requests as they come back to be gentle on memory _requests.Remove(response.Request); + int reqsLeft = _requests.Count; + if (null != progressCallback) { + progressCallback(reqsLeft); + } + if (0 == reqsLeft && null != finishedCallback) { + finishedCallback(); + } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); @@ -107,7 +125,7 @@ public void WaitForAllRequests() { try { _requests.Remove(req.Key); } - catch(Exception ex) { + catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } } diff --git a/src/Platform/IFileSource.cs b/src/Platform/IFileSource.cs index 2698201..f2d6571 100644 --- a/src/Platform/IFileSource.cs +++ b/src/Platform/IFileSource.cs @@ -4,8 +4,7 @@ // //----------------------------------------------------------------------- -namespace Mapbox.Platform -{ +namespace Mapbox.Platform { using System; /// @@ -14,8 +13,7 @@ namespace Mapbox.Platform /// IFileSource could fetch the data from the network, disk cache or even generate /// the data at runtime. /// - public interface IFileSource - { + public interface IFileSource { /// Performs a request asynchronously. /// The resource description in the URI format. /// Callback to be called after the request is completed. @@ -24,6 +22,6 @@ public interface IFileSource /// request. This handle can be completely ignored if there is no intention of ever /// canceling the request. /// - IAsyncRequest Request(string uri, Action callback); + IAsyncRequest Request(string uri, Action callback, Action progressCallback = null, Action finishedCallback = null); } } \ No newline at end of file diff --git a/test/UnitTest/FileSourceMockApiTest.cs b/test/UnitTest/FileSourceMockApiTest.cs index 8ca6eb2..bfcc225 100644 --- a/test/UnitTest/FileSourceMockApiTest.cs +++ b/test/UnitTest/FileSourceMockApiTest.cs @@ -154,5 +154,36 @@ public void DoesNotExist404() { } + [Test] + public void ProgressAndFinishedCallback() { + + int responsesCount = 0; + int progressCount = 0; + bool finishReceived = false; + + + for (int i = 0; i < 3; i++) { + _fs.Request( + _mockBaseUrl + _testUrl.simpleJson + , (Response r) => { + responsesCount++; + } + , (int requestsLeft) => { + progressCount++; + } + , () => { + finishReceived = true; + } + ); + } + + _fs.WaitForAllRequests(); + + Assert.AreEqual(3, progressCount, "number of progress callbacks does not match"); + Assert.AreEqual(3, responsesCount, "number of responses does not match"); + Assert.IsTrue(finishReceived, "finished callback did not fire"); + } + + } } \ No newline at end of file diff --git a/test/UnitTest/Utils.cs b/test/UnitTest/Utils.cs index e541f47..b003e9f 100644 --- a/test/UnitTest/Utils.cs +++ b/test/UnitTest/Utils.cs @@ -67,7 +67,7 @@ internal class MockFileSource : IFileSource { private Dictionary responses = new Dictionary(); private List requests = new List(); - public IAsyncRequest Request(string uri, Action callback) { + public IAsyncRequest Request(string uri, Action callback, Action progress = null, Action finished = null) { var response = new Response(); if (this.responses.ContainsKey(uri)) { response = this.responses[uri]; From 2df9131ab7d8fee50c791d1cef43d32475db5200 Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Thu, 4 May 2017 16:38:07 +0200 Subject: [PATCH 25/35] [wip ]non threaded HTTPRequest --- src/Platform/FileSource.cs | 21 ++++++++++--- src/Platform/HTTPRequest.cs | 43 +++++++++++++++++++++----- src/Platform/IFileSource.cs | 2 +- test/UnitTest/FileSourceMockApiTest.cs | 15 +++++++++ test/UnitTest/Utils.cs | 2 +- 5 files changed, 68 insertions(+), 15 deletions(-) diff --git a/src/Platform/FileSource.cs b/src/Platform/FileSource.cs index e51d9b2..5398434 100644 --- a/src/Platform/FileSource.cs +++ b/src/Platform/FileSource.cs @@ -54,6 +54,8 @@ string url , Action callback , Action progressCallback = null , Action finishedCallback = null + , int timeout = 10 + , bool threaded = true ) { if (_accessToken != null) { url += "?access_token=" + _accessToken; @@ -64,10 +66,11 @@ string url // * evaluate rate limits (headers and status code) // * throttle requests accordingly //var request = new HTTPRequest(url, callback); - IEnumerator proxy = proxyResponse(url, callback, progressCallback, finishedCallback); + IEnumerator proxy = proxyResponse(url, callback, progressCallback, finishedCallback, timeout, threaded); proxy.MoveNext(); HTTPRequest request = proxy.Current; - + proxy.Dispose(); + proxy = null; return request; } @@ -78,9 +81,11 @@ string url , Action callback , Action progressCallback , Action finishedCallback + , int timeout + , bool threaded ) { - // TODO: plugin caching somewhere around here + // TODO: insert caching somewhere around here var request = new HTTPRequest(url, (Response response) => { if (response.XRateLimitInterval.HasValue) { XRateLimitInterval = response.XRateLimitInterval; } @@ -104,9 +109,15 @@ string url System.Diagnostics.Debug.WriteLine(ex); } } - }); + } + , timeout + , threaded + ); lock (_lock) { - _requests.Add(request, 0); + //if the request is fast and has a light callback only it may have finished before we get here + if (!request.IsCompleted) { + _requests.Add(request, 0); + } } yield return request; } diff --git a/src/Platform/HTTPRequest.cs b/src/Platform/HTTPRequest.cs index 8b35a99..9ceeebd 100644 --- a/src/Platform/HTTPRequest.cs +++ b/src/Platform/HTTPRequest.cs @@ -44,6 +44,7 @@ internal sealed class HTTPRequest : IAsyncRequest { #if !UNITY private SynchronizationContext _sync = AsyncOperationManager.SynchronizationContext; #endif + private bool _threaded; private int _timeOut; private string _requestUrl; private readonly string _userAgent = "mapbox-sdk-cs"; @@ -55,15 +56,21 @@ internal sealed class HTTPRequest : IAsyncRequest { /// /// /// seconds - public HTTPRequest(string url, Action callback, int timeOut = 10) { + public HTTPRequest(string url, Action callback, int timeOut = 10, bool threaded = true) { IsCompleted = false; + _threaded = threaded; _callback = callback; _timeOut = timeOut; _requestUrl = url; setupRequest(); - getResponseAsync(_request, EvaluateResponse); + + if (_threaded) { + getResponseThreaded(_request, EvaluateResponse); + } else { + getResponseNonThreaded(_request, EvaluateResponse); + } } @@ -83,7 +90,7 @@ private void setupRequest() { // set ConnectionLimit per request // https://msdn.microsoft.com/en-us/library/system.net.httpwebrequest(v=vs.90).aspx#Remarks // use a value that is 12 times the number of CPUs on the local computer - _request.ServicePoint.ConnectionLimit = Environment.ProcessorCount * 6; + _request.ServicePoint.ConnectionLimit = Environment.ProcessorCount * 6; _request.ServicePoint.UseNagleAlgorithm = true; _request.ServicePoint.Expect100Continue = false; @@ -117,7 +124,6 @@ private void setupRequest() { #if NETFX_CORE - private async void getResponseAsync(HttpClient request, Action gotResponse) { // TODO: implement a strategy similar to the full .Net one to avoid blocking of 'GetAsync()' @@ -221,7 +227,21 @@ private async void EvaluateResponse(HttpResponseMessage apiResponse, Exception a #if !NETFX_CORE - private void getResponseAsync(HttpWebRequest request, Action gotResponse) { + + private void getResponseNonThreaded(HttpWebRequest request, Action gotResponse) { + + HttpWebResponse response = null; + try { + response = (HttpWebResponse)request.GetResponse(); + gotResponse(response, null); + } + catch (Exception ex) { + gotResponse(response, ex); + } + } + + + private void getResponseThreaded(HttpWebRequest request, Action gotResponse) { // create an additional action wrapper, because of: // https://msdn.microsoft.com/en-us/library/system.net.httpwebrequest.begingetresponse.aspx @@ -353,18 +373,24 @@ private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { // post (async) callback back to the main/UI thread // Unity: SynchronizationContext doesn't do anything // use the Dispatcher -#if !UNITY - _sync.Post(delegate { + if (!_threaded) { _callback(response); IsCompleted = true; _callback = null; + } else { + +#if !UNITY + _sync.Post(delegate { + _callback(response); + IsCompleted = true; + _callback = null; #if NETFX_CORE if (null != _request) { _request.Dispose(); _request = null; } #endif - }, null); + }, null); #else UnityToolbag.Dispatcher.InvokeAsync(() => { _callback(response); @@ -378,6 +404,7 @@ private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { #endif }); #endif + } } #endif diff --git a/src/Platform/IFileSource.cs b/src/Platform/IFileSource.cs index f2d6571..d531281 100644 --- a/src/Platform/IFileSource.cs +++ b/src/Platform/IFileSource.cs @@ -22,6 +22,6 @@ public interface IFileSource { /// request. This handle can be completely ignored if there is no intention of ever /// canceling the request. /// - IAsyncRequest Request(string uri, Action callback, Action progressCallback = null, Action finishedCallback = null); + IAsyncRequest Request(string uri, Action callback, Action progressCallback = null, Action finishedCallback = null, int timeout = 10, bool threaded = true); } } \ No newline at end of file diff --git a/test/UnitTest/FileSourceMockApiTest.cs b/test/UnitTest/FileSourceMockApiTest.cs index bfcc225..2531bfd 100644 --- a/test/UnitTest/FileSourceMockApiTest.cs +++ b/test/UnitTest/FileSourceMockApiTest.cs @@ -185,5 +185,20 @@ public void ProgressAndFinishedCallback() { } + [Test] + public void NonThreaded() { + + _fs.Request( + _mockBaseUrl + _testUrl.simpleJson + , (Response r) => { + Assert.AreEqual(200, r.StatusCode); + } + , threaded: false + ); + + _fs.WaitForAllRequests(); + } + + } } \ No newline at end of file diff --git a/test/UnitTest/Utils.cs b/test/UnitTest/Utils.cs index b003e9f..15f45bd 100644 --- a/test/UnitTest/Utils.cs +++ b/test/UnitTest/Utils.cs @@ -67,7 +67,7 @@ internal class MockFileSource : IFileSource { private Dictionary responses = new Dictionary(); private List requests = new List(); - public IAsyncRequest Request(string uri, Action callback, Action progress = null, Action finished = null) { + public IAsyncRequest Request(string uri, Action callback, Action progress = null, Action finished = null, int timeout = 10, bool threaded = false) { var response = new Response(); if (this.responses.ContainsKey(uri)) { response = this.responses[uri]; From c81d5706c3cfe647be1d59494df4ae1e7ce6605d Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Fri, 5 May 2017 07:17:35 +0200 Subject: [PATCH 26/35] [wip] multiple implementations of IASynyRequest --- src/Platform/FileSource.cs | 59 ++- src/Platform/HTTPRequestNonThreaded.cs | 356 ++++++++++++++++++ ...{HTTPRequest.cs => HTTPRequestThreaded.cs} | 40 +- src/Platform/IAsyncRequest.cs | 9 +- src/Platform/IFileSource.cs | 2 +- src/Platform/Platform.csproj | 3 +- src/Platform/PlatformUWP.csproj | 3 +- test/UnitTest/FileSourceMockApiTest.cs | 1 - test/UnitTest/Utils.cs | 103 ++--- 9 files changed, 471 insertions(+), 105 deletions(-) create mode 100644 src/Platform/HTTPRequestNonThreaded.cs rename src/Platform/{HTTPRequest.cs => HTTPRequestThreaded.cs} (93%) diff --git a/src/Platform/FileSource.cs b/src/Platform/FileSource.cs index 5398434..3028678 100644 --- a/src/Platform/FileSource.cs +++ b/src/Platform/FileSource.cs @@ -49,14 +49,13 @@ public sealed class FileSource : IFileSource { /// request. This handle can be completely ignored if there is no intention of ever /// canceling the request. /// - public IAsyncRequest Request( + public IAsyncRequest Request( string url , Action callback , Action progressCallback = null , Action finishedCallback = null , int timeout = 10 - , bool threaded = true - ) { + ) where T : IAsyncRequest, new() { if (_accessToken != null) { url += "?access_token=" + _accessToken; } @@ -66,9 +65,9 @@ string url // * evaluate rate limits (headers and status code) // * throttle requests accordingly //var request = new HTTPRequest(url, callback); - IEnumerator proxy = proxyResponse(url, callback, progressCallback, finishedCallback, timeout, threaded); + IEnumerator proxy = proxyResponse(url, callback, progressCallback, finishedCallback, timeout); proxy.MoveNext(); - HTTPRequest request = proxy.Current; + IAsyncRequest request = proxy.Current; proxy.Dispose(); proxy = null; return request; @@ -76,18 +75,47 @@ string url // TODO: look at requests and implement throttling if needed - private IEnumerator proxyResponse( + private IEnumerator proxyResponse( string url , Action callback , Action progressCallback , Action finishedCallback , int timeout - , bool threaded - ) { + ) where T : IAsyncRequest, new() { // TODO: insert caching somewhere around here - var request = new HTTPRequest(url, (Response response) => { + //IAsyncRequest request = new T(url, (Response response) => { + // if (response.XRateLimitInterval.HasValue) { XRateLimitInterval = response.XRateLimitInterval; } + // if (response.XRateLimitLimit.HasValue) { XRateLimitLimit = response.XRateLimitLimit; } + // if (response.XRateLimitReset.HasValue) { XRateLimitReset = response.XRateLimitReset; } + // callback(response); + // lock (_lock) { + // //another place to catch if request has been cancelled + // try { + // //remove requests as they come back to be gentle on memory + // _requests.Remove(response.Request); + // int reqsLeft = _requests.Count; + // if (null != progressCallback) { + // progressCallback(reqsLeft); + // } + // if (0 == reqsLeft && null != finishedCallback) { + // finishedCallback(); + // } + // } + // catch (Exception ex) { + // System.Diagnostics.Debug.WriteLine(ex); + // } + // } + //} + //, timeout + //); + + + IAsyncRequest request = new T(); + yield return request; + + request.Get(url, (Response response) => { if (response.XRateLimitInterval.HasValue) { XRateLimitInterval = response.XRateLimitInterval; } if (response.XRateLimitLimit.HasValue) { XRateLimitLimit = response.XRateLimitLimit; } if (response.XRateLimitReset.HasValue) { XRateLimitReset = response.XRateLimitReset; } @@ -111,10 +139,11 @@ string url } } , timeout - , threaded - ); + ); + + lock (_lock) { - //if the request is fast and has a light callback only it may have finished before we get here + //if the request is fast and has a light callback it may have finished before we get here if (!request.IsCompleted) { _requests.Add(request, 0); } @@ -123,15 +152,13 @@ string url } - /// - /// Block until all the requests are processed. - /// + /// Block until all the requests are processed. public void WaitForAllRequests() { int waitTimeMs = 150; while (_requests.Count > 0) { lock (_lock) { foreach (var req in _requests) { - if (((HTTPRequest)req.Key).IsCompleted) { + if (((HTTPRequestThreaded)req.Key).IsCompleted) { // another place to watch out if request has been cancelled try { _requests.Remove(req.Key); diff --git a/src/Platform/HTTPRequestNonThreaded.cs b/src/Platform/HTTPRequestNonThreaded.cs new file mode 100644 index 0000000..0b69662 --- /dev/null +++ b/src/Platform/HTTPRequestNonThreaded.cs @@ -0,0 +1,356 @@ +#if UNITY_EDITOR || UNITY_STANDALONE || UNITY_ANDROID || UNITY_WP_8_1 || UNITY_WSA || UNITY_WEBGL || UNITY_IOS || UNITY_PS4 || UNITY_SAMSUNGTV || UNITY_XBOXONE || UNITY_TIZEN || UNITY_TVOS +#define UNITY +#endif +//----------------------------------------------------------------------- +// +// Copyright (c) 2016 Mapbox. All rights reserved. +// Based on http://stackoverflow.com/a/12606963 and http://wiki.unity3d.com/index.php/WebAsync +// +//----------------------------------------------------------------------- + +namespace Mapbox.Platform { + + + using System; + using System.Net; +#if !UNITY && !NETFX_CORE + using System.Net.Cache; +#endif + using System.IO; + using System.Collections.Generic; + using System.Threading; + using System.ComponentModel; + using Utils; +#if NETFX_CORE + using System.Net.Http; + using System.Linq; +#endif + + //using System.Windows.Threading; + + internal sealed class HTTPRequestNonThreaded : IAsyncRequest { + + + public bool IsCompleted { get; private set; } + + + private Action _callback; +#if !NETFX_CORE + private HttpWebRequest _request; +#else + private HttpClient _request; + private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); +#endif +#if !UNITY + private SynchronizationContext _sync = AsyncOperationManager.SynchronizationContext; +#endif + private bool _threaded; + private int _timeOut; + private string _requestUrl; + private readonly string _userAgent = "mapbox-sdk-cs"; + + + /// + /// + /// + /// + /// + /// seconds + public HTTPRequestNonThreaded() { + setupRequest(); + } + + + private void setupRequest() { + +#if !NETFX_CORE + _request = WebRequest.Create(_requestUrl) as HttpWebRequest; + _request.UserAgent = _userAgent; + //_hwr.UserAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36"; + //_hwr.CachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore); + _request.Credentials = CredentialCache.DefaultCredentials; + _request.KeepAlive = true; + _request.ProtocolVersion = HttpVersion.Version11; // improved performance + + // improved performance. + // ServicePointManager.DefaultConnectionLimit doesn't seem to change anything + // set ConnectionLimit per request + // https://msdn.microsoft.com/en-us/library/system.net.httpwebrequest(v=vs.90).aspx#Remarks + // use a value that is 12 times the number of CPUs on the local computer + _request.ServicePoint.ConnectionLimit = Environment.ProcessorCount * 6; + + _request.ServicePoint.UseNagleAlgorithm = true; + _request.ServicePoint.Expect100Continue = false; + _request.ServicePoint.MaxIdleTime = 2000; + _request.Method = "GET"; + _request.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip,deflate"); + _request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + //_hwr.Timeout = timeOut * 1000; doesn't work in async calls, see below + +#else + HttpClientHandler handler = new HttpClientHandler() { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, + AllowAutoRedirect = true, + UseDefaultCredentials = true + + }; + _request = new HttpClient(handler); + _request.DefaultRequestHeaders.Add("User-Agent", _userAgent); + _request.Timeout = TimeSpan.FromSeconds(_timeOut); + + // TODO: how to set ConnectionLimit? ServicePoint.ConnectionLimit doesn't seem to be available. +#endif + +#if !UNITY && !NETFX_CORE + // 'NoCacheNoStore' greatly reduced the number of faulty request + // seems that .Net caching and Mapbox API don't play well together + _request.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.NoCacheNoStore); +#endif + } + + + +#if NETFX_CORE + private async void getResponseNonThreaded(HttpClient request, Action gotResponse) { + + // TODO: implement a strategy similar to the full .Net one to avoid blocking of 'GetAsync()' + // see 'Remarks' https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.timeout?view=netcore-1.1#System_Net_Http_HttpClient_Timeout + // "A Domain Name System (DNS) query may take up to 15 seconds to return or time out." + + HttpResponseMessage response = null; + try { + response = await request.GetAsync(_requestUrl, _cancellationTokenSource.Token); + gotResponse(response, null); + } + catch (Exception ex) { + gotResponse(response, ex); + } + + } + + + private async void EvaluateResponse(HttpResponseMessage apiResponse, Exception apiEx) { + + var response = new Response(); + + if (null != apiEx) { + response.AddException(apiEx); + } + + // timeout: API response is null + if (null == apiResponse) { + response.AddException(new Exception("No Reponse.")); + } else { + // https://www.mapbox.com/api-documentation/#rate-limits + if (null != apiResponse.Headers) { + response.Headers = new Dictionary(); + foreach (var hdr in apiResponse.Headers) { + string key = hdr.Key; + string val = hdr.Value.FirstOrDefault(); + response.Headers.Add(key, val); + if (key.Equals("X-Rate-Limit-Interval", StringComparison.OrdinalIgnoreCase)) { + int limitInterval; + if (int.TryParse(val, out limitInterval)) { response.XRateLimitInterval = limitInterval; } + } else if (key.Equals("X-Rate-Limit-Limit", StringComparison.OrdinalIgnoreCase)) { + long limitLimit; + if (long.TryParse(val, out limitLimit)) { response.XRateLimitLimit = limitLimit; } + } else if (key.Equals("X-Rate-Limit-Reset", StringComparison.OrdinalIgnoreCase)) { + double unixTimestamp; + if (double.TryParse(val, out unixTimestamp)) { + DateTime beginningOfTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + response.XRateLimitReset = beginningOfTime.AddSeconds(unixTimestamp).ToLocalTime(); + } + } else if (key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase)) { + response.ContentType = val; + } + } + } + + if (apiResponse.StatusCode != HttpStatusCode.OK) { + response.AddException(new Exception(string.Format("{0}: {1}", apiResponse.StatusCode, apiResponse.ReasonPhrase))); + } + int statusCode = (int)apiResponse.StatusCode; + response.StatusCode = statusCode; + if (429 == statusCode) { + response.AddException(new Exception("Rate limit hit")); + } + + if (null != apiResponse) { + response.Data = await apiResponse.Content.ReadAsByteArrayAsync(); + } + } + + // post (async) callback back to the main/UI thread + // Unity: SynchronizationContext doesn't do anything + // use the Dispatcher +#if !UNITY + _sync.Post(delegate { + _callback(response); + IsCompleted = true; + _callback = null; +#if NETFX_CORE + if (null != _request) { + _request.Dispose(); + _request = null; + } +#endif + }, null); +#else + UnityToolbag.Dispatcher.InvokeAsync(() => { + _callback(response); + IsCompleted = true; + _callback = null; +#if NETFX_CORE + if (null != _request) { + _request.Dispose(); + _request = null; + } +#endif + }); +#endif + } + +#endif + + +#if !NETFX_CORE + + public void Get(string url, Action callback, int timeOut = 10) { + + IsCompleted = false; + _callback = callback; + _timeOut = timeOut; + _requestUrl = url; + + getResponseNonThreaded(_request, EvaluateResponse); + } + + private void getResponseNonThreaded(HttpWebRequest request, Action gotResponse) { + + HttpWebResponse response = null; + try { + response = (HttpWebResponse)request.GetResponse(); + gotResponse(response, null); + } + catch (Exception ex) { + gotResponse(response, ex); + } + } + + + private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { + + var response = new Response(); + response.Request = this; + + if (null != apiEx) { + response.AddException(apiEx); + } + + // timeout: API response is null + if (null == apiResponse) { + response.AddException(new Exception("No Reponse.")); + } else { + // https://www.mapbox.com/api-documentation/#rate-limits + if (null != apiResponse.Headers) { + response.Headers = new Dictionary(); + for (int i = 0; i < apiResponse.Headers.Count; i++) { + // TODO: implement .Net Core / UWP implementation + string key = apiResponse.Headers.Keys[i]; + string val = apiResponse.Headers[i]; + response.Headers.Add(key, val); + if (key.Equals("X-Rate-Limit-Interval", StringComparison.InvariantCultureIgnoreCase)) { + int limitInterval; + if (int.TryParse(val, out limitInterval)) { response.XRateLimitInterval = limitInterval; } + } else if (key.Equals("X-Rate-Limit-Limit", StringComparison.InvariantCultureIgnoreCase)) { + long limitLimit; + if (long.TryParse(val, out limitLimit)) { response.XRateLimitLimit = limitLimit; } + } else if (key.Equals("X-Rate-Limit-Reset", StringComparison.InvariantCultureIgnoreCase)) { + double unixTimestamp; + if (double.TryParse(val, out unixTimestamp)) { + response.XRateLimitReset = UnixTimestampUtils.From(unixTimestamp); + } + } else if (key.Equals("Content-Type", StringComparison.InvariantCultureIgnoreCase)) { + response.ContentType = val; + } + } + } + + if (apiResponse.StatusCode != HttpStatusCode.OK) { + response.AddException(new Exception(string.Format("{0}: {1}", apiResponse.StatusCode, apiResponse.StatusDescription))); + } + int statusCode = (int)apiResponse.StatusCode; + response.StatusCode = statusCode; + if (429 == statusCode) { + response.AddException(new Exception("Rate limit hit")); + } + + if (null != apiResponse) { + using (Stream responseStream = apiResponse.GetResponseStream()) { + byte[] buffer = new byte[0x1000]; + int bytesRead; + using (MemoryStream ms = new MemoryStream()) { + while (0 != (bytesRead = responseStream.Read(buffer, 0, buffer.Length))) { + ms.Write(buffer, 0, bytesRead); + } + response.Data = ms.ToArray(); + } + } + apiResponse.Close(); + } + } + + // post (async) callback back to the main/UI thread + // Unity: SynchronizationContext doesn't do anything + // use the Dispatcher + if (!_threaded) { + _callback(response); + IsCompleted = true; + _callback = null; + } else { + +#if !UNITY + _sync.Post(delegate { + _callback(response); + IsCompleted = true; + _callback = null; +#if NETFX_CORE + if (null != _request) { + _request.Dispose(); + _request = null; + } +#endif + }, null); +#else + UnityToolbag.Dispatcher.InvokeAsync(() => { + _callback(response); + IsCompleted = true; + _callback = null; +#if NETFX_CORE + if (null != _request) { + _request.Dispose(); + _request = null; + } +#endif + }); +#endif + } + } +#endif + + + + public void Cancel() { + +#if !NETFX_CORE + if (null != _request) { + _request.Abort(); + } +#else + _cancellationTokenSource.Cancel(); +#endif + } + + + } +} diff --git a/src/Platform/HTTPRequest.cs b/src/Platform/HTTPRequestThreaded.cs similarity index 93% rename from src/Platform/HTTPRequest.cs rename to src/Platform/HTTPRequestThreaded.cs index 9ceeebd..d97ccaf 100644 --- a/src/Platform/HTTPRequest.cs +++ b/src/Platform/HTTPRequestThreaded.cs @@ -28,7 +28,7 @@ namespace Mapbox.Platform { //using System.Windows.Threading; - internal sealed class HTTPRequest : IAsyncRequest { + internal sealed class HTTPRequestThreaded : IAsyncRequest { public bool IsCompleted { get; private set; } @@ -44,7 +44,6 @@ internal sealed class HTTPRequest : IAsyncRequest { #if !UNITY private SynchronizationContext _sync = AsyncOperationManager.SynchronizationContext; #endif - private bool _threaded; private int _timeOut; private string _requestUrl; private readonly string _userAgent = "mapbox-sdk-cs"; @@ -56,21 +55,15 @@ internal sealed class HTTPRequest : IAsyncRequest { /// /// /// seconds - public HTTPRequest(string url, Action callback, int timeOut = 10, bool threaded = true) { + public HTTPRequestThreaded(string url, Action callback, int timeOut = 10) { IsCompleted = false; - _threaded = threaded; _callback = callback; _timeOut = timeOut; _requestUrl = url; setupRequest(); - - if (_threaded) { - getResponseThreaded(_request, EvaluateResponse); - } else { - getResponseNonThreaded(_request, EvaluateResponse); - } + getResponseThreaded(_request, EvaluateResponse); } @@ -124,7 +117,7 @@ private void setupRequest() { #if NETFX_CORE - private async void getResponseAsync(HttpClient request, Action gotResponse) { + private async void getResponseThreaded(HttpClient request, Action gotResponse) { // TODO: implement a strategy similar to the full .Net one to avoid blocking of 'GetAsync()' // see 'Remarks' https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.timeout?view=netcore-1.1#System_Net_Http_HttpClient_Timeout @@ -228,18 +221,6 @@ private async void EvaluateResponse(HttpResponseMessage apiResponse, Exception a #if !NETFX_CORE - private void getResponseNonThreaded(HttpWebRequest request, Action gotResponse) { - - HttpWebResponse response = null; - try { - response = (HttpWebResponse)request.GetResponse(); - gotResponse(response, null); - } - catch (Exception ex) { - gotResponse(response, ex); - } - } - private void getResponseThreaded(HttpWebRequest request, Action gotResponse) { @@ -373,24 +354,18 @@ private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { // post (async) callback back to the main/UI thread // Unity: SynchronizationContext doesn't do anything // use the Dispatcher - if (!_threaded) { +#if !UNITY + _sync.Post(delegate { _callback(response); IsCompleted = true; _callback = null; - } else { - -#if !UNITY - _sync.Post(delegate { - _callback(response); - IsCompleted = true; - _callback = null; #if NETFX_CORE if (null != _request) { _request.Dispose(); _request = null; } #endif - }, null); + }, null); #else UnityToolbag.Dispatcher.InvokeAsync(() => { _callback(response); @@ -404,7 +379,6 @@ private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { #endif }); #endif - } } #endif diff --git a/src/Platform/IAsyncRequest.cs b/src/Platform/IAsyncRequest.cs index f07786c..c6e6642 100644 --- a/src/Platform/IAsyncRequest.cs +++ b/src/Platform/IAsyncRequest.cs @@ -6,14 +6,19 @@ namespace Mapbox.Platform { + using System; /// A handle to an asynchronous request. public interface IAsyncRequest { - ///// True after the request has finished. - //bool IsCompleted { get; } + /// True after the request has finished. + bool IsCompleted { get; } /// Cancel the ongoing request, preventing it from firing a callback. void Cancel(); + + + /// Issue a GET request. + void Get(string url, Action callback, int timeOut = 10); } } \ No newline at end of file diff --git a/src/Platform/IFileSource.cs b/src/Platform/IFileSource.cs index d531281..73c9e06 100644 --- a/src/Platform/IFileSource.cs +++ b/src/Platform/IFileSource.cs @@ -22,6 +22,6 @@ public interface IFileSource { /// request. This handle can be completely ignored if there is no intention of ever /// canceling the request. /// - IAsyncRequest Request(string uri, Action callback, Action progressCallback = null, Action finishedCallback = null, int timeout = 10, bool threaded = true); + IAsyncRequest Request(string uri, Action callback, Action progressCallback = null, Action finishedCallback = null, int timeout = 10) where T : IAsyncRequest, new(); } } \ No newline at end of file diff --git a/src/Platform/Platform.csproj b/src/Platform/Platform.csproj index 16ddea4..a8201d1 100644 --- a/src/Platform/Platform.csproj +++ b/src/Platform/Platform.csproj @@ -41,7 +41,8 @@ Properties\SharedAssemblyInfo.cs - + + diff --git a/src/Platform/PlatformUWP.csproj b/src/Platform/PlatformUWP.csproj index 689e868..5e08341 100644 --- a/src/Platform/PlatformUWP.csproj +++ b/src/Platform/PlatformUWP.csproj @@ -69,7 +69,8 @@ Properties\SharedAssemblyInfo.cs - + + diff --git a/test/UnitTest/FileSourceMockApiTest.cs b/test/UnitTest/FileSourceMockApiTest.cs index 2531bfd..ac77b2b 100644 --- a/test/UnitTest/FileSourceMockApiTest.cs +++ b/test/UnitTest/FileSourceMockApiTest.cs @@ -193,7 +193,6 @@ public void NonThreaded() { , (Response r) => { Assert.AreEqual(200, r.StatusCode); } - , threaded: false ); _fs.WaitForAllRequests(); diff --git a/test/UnitTest/Utils.cs b/test/UnitTest/Utils.cs index 15f45bd..4e4815c 100644 --- a/test/UnitTest/Utils.cs +++ b/test/UnitTest/Utils.cs @@ -63,55 +63,58 @@ public void OnNext(ClassicRasterTile tile) { } } - internal class MockFileSource : IFileSource { - private Dictionary responses = new Dictionary(); - private List requests = new List(); - - public IAsyncRequest Request(string uri, Action callback, Action progress = null, Action finished = null, int timeout = 10, bool threaded = false) { - var response = new Response(); - if (this.responses.ContainsKey(uri)) { - response = this.responses[uri]; - } - - var request = new MockRequest(response, callback); - this.requests.Add(request); - - return request; - } - - public void SetReponse(string uri, Response response) { - this.responses[uri] = response; - } - - public void WaitForAllRequests() { - while (this.requests.Count > 0) { - var req = this.requests[0]; - this.requests.RemoveAt(0); - - req.Run(); - } - } - - public class MockRequest : IAsyncRequest { - private Response response; - private Action callback; - - public MockRequest(Response response, Action callback) { - this.response = response; - this.callback = callback; - } - - public void Run() { - if (this.callback != null) { - this.callback(this.response); - this.callback = null; - } - } - - public void Cancel() { - this.callback = null; - } - } - } + //internal class MockFileSource : IFileSource { + // private Dictionary responses = new Dictionary(); + // private List requests = new List(); + + // public IAsyncRequest Request(string uri, Action callback, Action progress = null, Action finished = null, int timeout = 10) { + // var response = new Response(); + // if (this.responses.ContainsKey(uri)) { + // response = this.responses[uri]; + // } + + // var request = new MockRequest(response, callback); + // this.requests.Add(request); + + // return request; + // } + + // public void SetReponse(string uri, Response response) { + // this.responses[uri] = response; + // } + + // public void WaitForAllRequests() { + // while (this.requests.Count > 0) { + // var req = this.requests[0]; + // this.requests.RemoveAt(0); + + // req.Run(); + // } + // } + + // public class MockRequest : IAsyncRequest { + // public bool IsCompleted { get; private set; } + // private Response response; + // private Action callback; + + // public MockRequest(Response response, Action callback) { + // this.response = response; + // this.callback = callback; + // } + + // public void Run() { + // if (this.callback != null) { + // this.callback(this.response); + // this.callback = null; + // IsCompleted = true; + // } + // } + + // public void Cancel() { + // this.callback = null; + // IsCompleted = true; + // } + // } + //} } } From 0937d97962cbfaa6a3b1f37204273133f07ec6dd Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Mon, 8 May 2017 11:58:14 +0200 Subject: [PATCH 27/35] try AppVeyor: HTTPRequestNonThreaded --- src/Platform/FileSource.cs | 93 ++---- src/Platform/HTTPRequestNonThreaded.cs | 54 ++-- src/Platform/HTTPRequestThreaded-2.cs | 400 +++++++++++++++++++++++++ src/Platform/HTTPRequestThreaded.cs | 54 ++-- src/Platform/IAsyncRequest.cs | 5 - src/Platform/IAsyncRequestFactory.cs | 30 ++ src/Platform/IFileSource.cs | 2 +- src/Platform/Platform.csproj | 3 +- src/Platform/PlatformUWP.csproj | 1 + test/UnitTest/FileSourceMockApiTest.cs | 31 -- test/UnitTest/VectorTileTest.cs | 38 +-- 11 files changed, 538 insertions(+), 173 deletions(-) create mode 100644 src/Platform/HTTPRequestThreaded-2.cs create mode 100644 src/Platform/IAsyncRequestFactory.cs diff --git a/src/Platform/FileSource.cs b/src/Platform/FileSource.cs index 3028678..5f94d9e 100644 --- a/src/Platform/FileSource.cs +++ b/src/Platform/FileSource.cs @@ -49,13 +49,7 @@ public sealed class FileSource : IFileSource { /// request. This handle can be completely ignored if there is no intention of ever /// canceling the request. /// - public IAsyncRequest Request( - string url - , Action callback - , Action progressCallback = null - , Action finishedCallback = null - , int timeout = 10 - ) where T : IAsyncRequest, new() { + public IAsyncRequest Request(string url, Action callback, int timeout = 10) { if (_accessToken != null) { url += "?access_token=" + _accessToken; } @@ -65,57 +59,23 @@ string url // * evaluate rate limits (headers and status code) // * throttle requests accordingly //var request = new HTTPRequest(url, callback); - IEnumerator proxy = proxyResponse(url, callback, progressCallback, finishedCallback, timeout); - proxy.MoveNext(); - IAsyncRequest request = proxy.Current; - proxy.Dispose(); - proxy = null; - return request; + //IEnumerator proxy = proxyResponse(url, callback); + //proxy.MoveNext(); + //IAsyncRequest request = proxy.Current; + + //return request; + + return proxyResponse(url, callback); } // TODO: look at requests and implement throttling if needed - private IEnumerator proxyResponse( - string url - , Action callback - , Action progressCallback - , Action finishedCallback - , int timeout - ) where T : IAsyncRequest, new() { - - // TODO: insert caching somewhere around here - - //IAsyncRequest request = new T(url, (Response response) => { - // if (response.XRateLimitInterval.HasValue) { XRateLimitInterval = response.XRateLimitInterval; } - // if (response.XRateLimitLimit.HasValue) { XRateLimitLimit = response.XRateLimitLimit; } - // if (response.XRateLimitReset.HasValue) { XRateLimitReset = response.XRateLimitReset; } - // callback(response); - // lock (_lock) { - // //another place to catch if request has been cancelled - // try { - // //remove requests as they come back to be gentle on memory - // _requests.Remove(response.Request); - // int reqsLeft = _requests.Count; - // if (null != progressCallback) { - // progressCallback(reqsLeft); - // } - // if (0 == reqsLeft && null != finishedCallback) { - // finishedCallback(); - // } - // } - // catch (Exception ex) { - // System.Diagnostics.Debug.WriteLine(ex); - // } - // } - //} - //, timeout - //); - - - IAsyncRequest request = new T(); - yield return request; - - request.Get(url, (Response response) => { + //private IEnumerator proxyResponse(string url, Action callback) { + private IAsyncRequest proxyResponse(string url, Action callback) { + + // TODO: plugin caching somewhere around here + + var request = IAsyncRequestFactory.CreateRequest(url, (Response response) => { if (response.XRateLimitInterval.HasValue) { XRateLimitInterval = response.XRateLimitInterval; } if (response.XRateLimitLimit.HasValue) { XRateLimitLimit = response.XRateLimitLimit; } if (response.XRateLimitReset.HasValue) { XRateLimitReset = response.XRateLimitReset; } @@ -123,42 +83,33 @@ string url lock (_lock) { //another place to catch if request has been cancelled try { - //remove requests as they come back to be gentle on memory _requests.Remove(response.Request); - int reqsLeft = _requests.Count; - if (null != progressCallback) { - progressCallback(reqsLeft); - } - if (0 == reqsLeft && null != finishedCallback) { - finishedCallback(); - } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } } - } - , timeout - ); - - + }); lock (_lock) { - //if the request is fast and has a light callback it may have finished before we get here + //sometimes we get here after the request has already finished if (!request.IsCompleted) { _requests.Add(request, 0); } } - yield return request; + //yield return request; + return request; } - /// Block until all the requests are processed. + /// + /// Block until all the requests are processed. + /// public void WaitForAllRequests() { int waitTimeMs = 150; while (_requests.Count > 0) { lock (_lock) { foreach (var req in _requests) { - if (((HTTPRequestThreaded)req.Key).IsCompleted) { + if (((IAsyncRequest)req.Key).IsCompleted) { // another place to watch out if request has been cancelled try { _requests.Remove(req.Key); diff --git a/src/Platform/HTTPRequestNonThreaded.cs b/src/Platform/HTTPRequestNonThreaded.cs index 0b69662..ed02560 100644 --- a/src/Platform/HTTPRequestNonThreaded.cs +++ b/src/Platform/HTTPRequestNonThreaded.cs @@ -44,7 +44,6 @@ internal sealed class HTTPRequestNonThreaded : IAsyncRequest { #if !UNITY private SynchronizationContext _sync = AsyncOperationManager.SynchronizationContext; #endif - private bool _threaded; private int _timeOut; private string _requestUrl; private readonly string _userAgent = "mapbox-sdk-cs"; @@ -56,8 +55,15 @@ internal sealed class HTTPRequestNonThreaded : IAsyncRequest { /// /// /// seconds - public HTTPRequestNonThreaded() { + public HTTPRequestNonThreaded(string url, Action callback, int timeOut = 10) { + + IsCompleted = false; + _callback = callback; + _timeOut = timeOut; + _requestUrl = url; + setupRequest(); + getResponseNonThreaded(_request, EvaluateResponse); } @@ -214,17 +220,6 @@ private async void EvaluateResponse(HttpResponseMessage apiResponse, Exception a #if !NETFX_CORE - - public void Get(string url, Action callback, int timeOut = 10) { - - IsCompleted = false; - _callback = callback; - _timeOut = timeOut; - _requestUrl = url; - - getResponseNonThreaded(_request, EvaluateResponse); - } - private void getResponseNonThreaded(HttpWebRequest request, Action gotResponse) { HttpWebResponse response = null; @@ -232,6 +227,19 @@ private void getResponseNonThreaded(HttpWebRequest request, Action { _callback(response); @@ -334,7 +337,6 @@ private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { #endif }); #endif - } } #endif diff --git a/src/Platform/HTTPRequestThreaded-2.cs b/src/Platform/HTTPRequestThreaded-2.cs new file mode 100644 index 0000000..d97ccaf --- /dev/null +++ b/src/Platform/HTTPRequestThreaded-2.cs @@ -0,0 +1,400 @@ +#if UNITY_EDITOR || UNITY_STANDALONE || UNITY_ANDROID || UNITY_WP_8_1 || UNITY_WSA || UNITY_WEBGL || UNITY_IOS || UNITY_PS4 || UNITY_SAMSUNGTV || UNITY_XBOXONE || UNITY_TIZEN || UNITY_TVOS +#define UNITY +#endif +//----------------------------------------------------------------------- +// +// Copyright (c) 2016 Mapbox. All rights reserved. +// Based on http://stackoverflow.com/a/12606963 and http://wiki.unity3d.com/index.php/WebAsync +// +//----------------------------------------------------------------------- + +namespace Mapbox.Platform { + + + using System; + using System.Net; +#if !UNITY && !NETFX_CORE + using System.Net.Cache; +#endif + using System.IO; + using System.Collections.Generic; + using System.Threading; + using System.ComponentModel; + using Utils; +#if NETFX_CORE + using System.Net.Http; + using System.Linq; +#endif + + //using System.Windows.Threading; + + internal sealed class HTTPRequestThreaded : IAsyncRequest { + + + public bool IsCompleted { get; private set; } + + + private Action _callback; +#if !NETFX_CORE + private HttpWebRequest _request; +#else + private HttpClient _request; + private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); +#endif +#if !UNITY + private SynchronizationContext _sync = AsyncOperationManager.SynchronizationContext; +#endif + private int _timeOut; + private string _requestUrl; + private readonly string _userAgent = "mapbox-sdk-cs"; + + + /// + /// + /// + /// + /// + /// seconds + public HTTPRequestThreaded(string url, Action callback, int timeOut = 10) { + + IsCompleted = false; + _callback = callback; + _timeOut = timeOut; + _requestUrl = url; + + setupRequest(); + getResponseThreaded(_request, EvaluateResponse); + } + + + private void setupRequest() { + +#if !NETFX_CORE + _request = WebRequest.Create(_requestUrl) as HttpWebRequest; + _request.UserAgent = _userAgent; + //_hwr.UserAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36"; + //_hwr.CachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore); + _request.Credentials = CredentialCache.DefaultCredentials; + _request.KeepAlive = true; + _request.ProtocolVersion = HttpVersion.Version11; // improved performance + + // improved performance. + // ServicePointManager.DefaultConnectionLimit doesn't seem to change anything + // set ConnectionLimit per request + // https://msdn.microsoft.com/en-us/library/system.net.httpwebrequest(v=vs.90).aspx#Remarks + // use a value that is 12 times the number of CPUs on the local computer + _request.ServicePoint.ConnectionLimit = Environment.ProcessorCount * 6; + + _request.ServicePoint.UseNagleAlgorithm = true; + _request.ServicePoint.Expect100Continue = false; + _request.ServicePoint.MaxIdleTime = 2000; + _request.Method = "GET"; + _request.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip,deflate"); + _request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + //_hwr.Timeout = timeOut * 1000; doesn't work in async calls, see below + +#else + HttpClientHandler handler = new HttpClientHandler() { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, + AllowAutoRedirect = true, + UseDefaultCredentials = true + + }; + _request = new HttpClient(handler); + _request.DefaultRequestHeaders.Add("User-Agent", _userAgent); + _request.Timeout = TimeSpan.FromSeconds(_timeOut); + + // TODO: how to set ConnectionLimit? ServicePoint.ConnectionLimit doesn't seem to be available. +#endif + +#if !UNITY && !NETFX_CORE + // 'NoCacheNoStore' greatly reduced the number of faulty request + // seems that .Net caching and Mapbox API don't play well together + _request.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.NoCacheNoStore); +#endif + } + + + +#if NETFX_CORE + private async void getResponseThreaded(HttpClient request, Action gotResponse) { + + // TODO: implement a strategy similar to the full .Net one to avoid blocking of 'GetAsync()' + // see 'Remarks' https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.timeout?view=netcore-1.1#System_Net_Http_HttpClient_Timeout + // "A Domain Name System (DNS) query may take up to 15 seconds to return or time out." + + HttpResponseMessage response = null; + try { + response = await request.GetAsync(_requestUrl, _cancellationTokenSource.Token); + gotResponse(response, null); + } + catch (Exception ex) { + gotResponse(response, ex); + } + + } + + + private async void EvaluateResponse(HttpResponseMessage apiResponse, Exception apiEx) { + + var response = new Response(); + + if (null != apiEx) { + response.AddException(apiEx); + } + + // timeout: API response is null + if (null == apiResponse) { + response.AddException(new Exception("No Reponse.")); + } else { + // https://www.mapbox.com/api-documentation/#rate-limits + if (null != apiResponse.Headers) { + response.Headers = new Dictionary(); + foreach (var hdr in apiResponse.Headers) { + string key = hdr.Key; + string val = hdr.Value.FirstOrDefault(); + response.Headers.Add(key, val); + if (key.Equals("X-Rate-Limit-Interval", StringComparison.OrdinalIgnoreCase)) { + int limitInterval; + if (int.TryParse(val, out limitInterval)) { response.XRateLimitInterval = limitInterval; } + } else if (key.Equals("X-Rate-Limit-Limit", StringComparison.OrdinalIgnoreCase)) { + long limitLimit; + if (long.TryParse(val, out limitLimit)) { response.XRateLimitLimit = limitLimit; } + } else if (key.Equals("X-Rate-Limit-Reset", StringComparison.OrdinalIgnoreCase)) { + double unixTimestamp; + if (double.TryParse(val, out unixTimestamp)) { + DateTime beginningOfTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + response.XRateLimitReset = beginningOfTime.AddSeconds(unixTimestamp).ToLocalTime(); + } + } else if (key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase)) { + response.ContentType = val; + } + } + } + + if (apiResponse.StatusCode != HttpStatusCode.OK) { + response.AddException(new Exception(string.Format("{0}: {1}", apiResponse.StatusCode, apiResponse.ReasonPhrase))); + } + int statusCode = (int)apiResponse.StatusCode; + response.StatusCode = statusCode; + if (429 == statusCode) { + response.AddException(new Exception("Rate limit hit")); + } + + if (null != apiResponse) { + response.Data = await apiResponse.Content.ReadAsByteArrayAsync(); + } + } + + // post (async) callback back to the main/UI thread + // Unity: SynchronizationContext doesn't do anything + // use the Dispatcher +#if !UNITY + _sync.Post(delegate { + _callback(response); + IsCompleted = true; + _callback = null; +#if NETFX_CORE + if (null != _request) { + _request.Dispose(); + _request = null; + } +#endif + }, null); +#else + UnityToolbag.Dispatcher.InvokeAsync(() => { + _callback(response); + IsCompleted = true; + _callback = null; +#if NETFX_CORE + if (null != _request) { + _request.Dispose(); + _request = null; + } +#endif + }); +#endif + } + +#endif + + +#if !NETFX_CORE + + + private void getResponseThreaded(HttpWebRequest request, Action gotResponse) { + + // create an additional action wrapper, because of: + // https://msdn.microsoft.com/en-us/library/system.net.httpwebrequest.begingetresponse.aspx + // The BeginGetResponse method requires some synchronous setup tasks to complete (DNS resolution, + //proxy detection, and TCP socket connection, for example) before this method becomes asynchronous. + // As a result, this method should never be called on a user interface (UI) thread because it might + // take considerable time(up to several minutes depending on network settings) to complete the + // initial synchronous setup tasks before an exception for an error is thrown or the method succeeds. + + Action actionWrapper = () => { + try { + // BeginInvoke runs on a thread of the thread pool (!= main/UI thread) + // that's why we need SynchronizationContext when + // TODO: how to influence threadpool: nr of threads etc. + long startTicks = DateTime.Now.Ticks; + request.BeginGetResponse((asycnResult) => { + try { // there's a try/catch here because execution path is different from invokation one, exception here may cause a crash + long beforeEndGet = DateTime.Now.Ticks; + HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asycnResult); + //long finished = DateTime.Now.Ticks; + //long duration = finished - startTicks; + //TimeSpan ts = TimeSpan.FromTicks(duration); + //TimeSpan tsEndGet = TimeSpan.FromTicks(finished - beforeEndGet); + //TimeSpan tsBeginGet = TimeSpan.FromTicks(beforeEndGet - startTicks); + //UnityEngine.Debug.Log("received response - " + ts.Milliseconds + "ms" + " BeginGet: " + tsBeginGet.Milliseconds + " EndGet: " + tsEndGet.Milliseconds + " CompletedSynchronously: " + asycnResult.CompletedSynchronously); + gotResponse(response, null); + } + // EndGetResponse() throws on on some status codes, try to get response anyway (and status codes) + catch (WebException wex) { + //another place to watchout for HttpWebRequest.Abort to occur + if (wex.Status == WebExceptionStatus.RequestCanceled) { + gotResponse(null, wex); + } else { + HttpWebResponse hwr = wex.Response as HttpWebResponse; + if (null == hwr) { + throw; + } + gotResponse(hwr, wex); + } + } + catch (Exception ex) { + gotResponse(null, ex); + } + } + , null); + } + catch (Exception ex) { + //catch exception from HttpWebRequest.Abort + gotResponse(null, ex); + } + }; + + try { + actionWrapper.BeginInvoke(new AsyncCallback((iAsyncResult) => { + var action = (Action)iAsyncResult.AsyncState; + action.EndInvoke(iAsyncResult); + }) + , actionWrapper); + } + catch (Exception ex) { + gotResponse(null, ex); + } + } + + + + private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { + + var response = new Response(); + response.Request = this; + + if (null != apiEx) { + response.AddException(apiEx); + } + + // timeout: API response is null + if (null == apiResponse) { + response.AddException(new Exception("No Reponse.")); + } else { + // https://www.mapbox.com/api-documentation/#rate-limits + if (null != apiResponse.Headers) { + response.Headers = new Dictionary(); + for (int i = 0; i < apiResponse.Headers.Count; i++) { + // TODO: implement .Net Core / UWP implementation + string key = apiResponse.Headers.Keys[i]; + string val = apiResponse.Headers[i]; + response.Headers.Add(key, val); + if (key.Equals("X-Rate-Limit-Interval", StringComparison.InvariantCultureIgnoreCase)) { + int limitInterval; + if (int.TryParse(val, out limitInterval)) { response.XRateLimitInterval = limitInterval; } + } else if (key.Equals("X-Rate-Limit-Limit", StringComparison.InvariantCultureIgnoreCase)) { + long limitLimit; + if (long.TryParse(val, out limitLimit)) { response.XRateLimitLimit = limitLimit; } + } else if (key.Equals("X-Rate-Limit-Reset", StringComparison.InvariantCultureIgnoreCase)) { + double unixTimestamp; + if (double.TryParse(val, out unixTimestamp)) { + response.XRateLimitReset = UnixTimestampUtils.From(unixTimestamp); + } + } else if (key.Equals("Content-Type", StringComparison.InvariantCultureIgnoreCase)) { + response.ContentType = val; + } + } + } + + if (apiResponse.StatusCode != HttpStatusCode.OK) { + response.AddException(new Exception(string.Format("{0}: {1}", apiResponse.StatusCode, apiResponse.StatusDescription))); + } + int statusCode = (int)apiResponse.StatusCode; + response.StatusCode = statusCode; + if (429 == statusCode) { + response.AddException(new Exception("Rate limit hit")); + } + + if (null != apiResponse) { + using (Stream responseStream = apiResponse.GetResponseStream()) { + byte[] buffer = new byte[0x1000]; + int bytesRead; + using (MemoryStream ms = new MemoryStream()) { + while (0 != (bytesRead = responseStream.Read(buffer, 0, buffer.Length))) { + ms.Write(buffer, 0, bytesRead); + } + response.Data = ms.ToArray(); + } + } + apiResponse.Close(); + } + } + + // post (async) callback back to the main/UI thread + // Unity: SynchronizationContext doesn't do anything + // use the Dispatcher +#if !UNITY + _sync.Post(delegate { + _callback(response); + IsCompleted = true; + _callback = null; +#if NETFX_CORE + if (null != _request) { + _request.Dispose(); + _request = null; + } +#endif + }, null); +#else + UnityToolbag.Dispatcher.InvokeAsync(() => { + _callback(response); + IsCompleted = true; + _callback = null; +#if NETFX_CORE + if (null != _request) { + _request.Dispose(); + _request = null; + } +#endif + }); +#endif + } +#endif + + + + public void Cancel() { + +#if !NETFX_CORE + if (null != _request) { + _request.Abort(); + } +#else + _cancellationTokenSource.Cancel(); +#endif + } + + + } +} diff --git a/src/Platform/HTTPRequestThreaded.cs b/src/Platform/HTTPRequestThreaded.cs index d97ccaf..cde16e6 100644 --- a/src/Platform/HTTPRequestThreaded.cs +++ b/src/Platform/HTTPRequestThreaded.cs @@ -1,13 +1,14 @@ -#if UNITY_EDITOR || UNITY_STANDALONE || UNITY_ANDROID || UNITY_WP_8_1 || UNITY_WSA || UNITY_WEBGL || UNITY_IOS || UNITY_PS4 || UNITY_SAMSUNGTV || UNITY_XBOXONE || UNITY_TIZEN || UNITY_TVOS -#define UNITY -#endif -//----------------------------------------------------------------------- +//----------------------------------------------------------------------- // // Copyright (c) 2016 Mapbox. All rights reserved. // Based on http://stackoverflow.com/a/12606963 and http://wiki.unity3d.com/index.php/WebAsync // //----------------------------------------------------------------------- +#if UNITY_EDITOR || UNITY_STANDALONE || UNITY_ANDROID || UNITY_WP_8_1 || UNITY_WSA || UNITY_WEBGL || UNITY_IOS || UNITY_PS4 || UNITY_SAMSUNGTV || UNITY_XBOXONE || UNITY_TIZEN || UNITY_TVOS +#define UNITY +#endif + namespace Mapbox.Platform { @@ -63,7 +64,7 @@ public HTTPRequestThreaded(string url, Action callback, int timeOut = _requestUrl = url; setupRequest(); - getResponseThreaded(_request, EvaluateResponse); + getResponseAsync(_request, EvaluateResponse); } @@ -83,7 +84,7 @@ private void setupRequest() { // set ConnectionLimit per request // https://msdn.microsoft.com/en-us/library/system.net.httpwebrequest(v=vs.90).aspx#Remarks // use a value that is 12 times the number of CPUs on the local computer - _request.ServicePoint.ConnectionLimit = Environment.ProcessorCount * 6; + _request.ServicePoint.ConnectionLimit = Environment.ProcessorCount * 6; _request.ServicePoint.UseNagleAlgorithm = true; _request.ServicePoint.Expect100Continue = false; @@ -117,7 +118,8 @@ private void setupRequest() { #if NETFX_CORE - private async void getResponseThreaded(HttpClient request, Action gotResponse) { + + private async void getResponseAsync(HttpClient request, Action gotResponse) { // TODO: implement a strategy similar to the full .Net one to avoid blocking of 'GetAsync()' // see 'Remarks' https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.timeout?view=netcore-1.1#System_Net_Http_HttpClient_Timeout @@ -220,9 +222,7 @@ private async void EvaluateResponse(HttpResponseMessage apiResponse, Exception a #if !NETFX_CORE - - - private void getResponseThreaded(HttpWebRequest request, Action gotResponse) { + private void getResponseAsync(HttpWebRequest request, Action gotResponse) { // create an additional action wrapper, because of: // https://msdn.microsoft.com/en-us/library/system.net.httpwebrequest.begingetresponse.aspx @@ -367,17 +367,33 @@ private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { #endif }, null); #else - UnityToolbag.Dispatcher.InvokeAsync(() => { - _callback(response); - IsCompleted = true; - _callback = null; + // Unity is playing + if (UnityToolbag.Dispatcher._instanceExists) { + UnityToolbag.Dispatcher.InvokeAsync(() => { + _callback(response); + IsCompleted = true; + _callback = null; #if NETFX_CORE - if (null != _request) { - _request.Dispose(); - _request = null; - } + if (null != _request) { + _request.Dispose(); + _request = null; + } #endif - }); + }); + } else { // Unity is in Edit Mode + Mapbox.Unity.DispatcherEditor.InvokeAsync(() => { + _callback(response); + IsCompleted = true; + _callback = null; +#if NETFX_CORE + if (null != _request) { + _request.Dispose(); + _request = null; + } +#endif + }); + + } #endif } #endif diff --git a/src/Platform/IAsyncRequest.cs b/src/Platform/IAsyncRequest.cs index c6e6642..ae08c4a 100644 --- a/src/Platform/IAsyncRequest.cs +++ b/src/Platform/IAsyncRequest.cs @@ -6,7 +6,6 @@ namespace Mapbox.Platform { - using System; /// A handle to an asynchronous request. public interface IAsyncRequest { @@ -16,9 +15,5 @@ public interface IAsyncRequest { /// Cancel the ongoing request, preventing it from firing a callback. void Cancel(); - - - /// Issue a GET request. - void Get(string url, Action callback, int timeOut = 10); } } \ No newline at end of file diff --git a/src/Platform/IAsyncRequestFactory.cs b/src/Platform/IAsyncRequestFactory.cs new file mode 100644 index 0000000..edfeead --- /dev/null +++ b/src/Platform/IAsyncRequestFactory.cs @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) 2016 Mapbox. All rights reserved. +// +//----------------------------------------------------------------------- + +#if UNITY_EDITOR || UNITY_STANDALONE || UNITY_ANDROID || UNITY_WP_8_1 || UNITY_WSA || UNITY_WEBGL || UNITY_IOS || UNITY_PS4 || UNITY_SAMSUNGTV || UNITY_XBOXONE || UNITY_TIZEN || UNITY_TVOS +#define UNITY +#endif + +namespace Mapbox.Platform { + + using System; +#if UNITY + using UnityEditor; +#endif + + /// A handle to an asynchronous request. + public static class IAsyncRequestFactory { + + public static IAsyncRequest CreateRequest(string url, Action callback, int timeout=10) { +#if !UNITY + //return new HTTPRequestThreaded(url, callback, timeout); + return new HTTPRequestNonThreaded(url, callback, timeout); +#else +#endif + } + + } +} \ No newline at end of file diff --git a/src/Platform/IFileSource.cs b/src/Platform/IFileSource.cs index 73c9e06..e1e9699 100644 --- a/src/Platform/IFileSource.cs +++ b/src/Platform/IFileSource.cs @@ -22,6 +22,6 @@ public interface IFileSource { /// request. This handle can be completely ignored if there is no intention of ever /// canceling the request. /// - IAsyncRequest Request(string uri, Action callback, Action progressCallback = null, Action finishedCallback = null, int timeout = 10) where T : IAsyncRequest, new(); + IAsyncRequest Request(string uri, Action callback, int timeout = 10); } } \ No newline at end of file diff --git a/src/Platform/Platform.csproj b/src/Platform/Platform.csproj index a8201d1..32507d1 100644 --- a/src/Platform/Platform.csproj +++ b/src/Platform/Platform.csproj @@ -42,8 +42,9 @@ Properties\SharedAssemblyInfo.cs - + + diff --git a/src/Platform/PlatformUWP.csproj b/src/Platform/PlatformUWP.csproj index 5e08341..c0e0b73 100644 --- a/src/Platform/PlatformUWP.csproj +++ b/src/Platform/PlatformUWP.csproj @@ -72,6 +72,7 @@ + diff --git a/test/UnitTest/FileSourceMockApiTest.cs b/test/UnitTest/FileSourceMockApiTest.cs index ac77b2b..c43abaf 100644 --- a/test/UnitTest/FileSourceMockApiTest.cs +++ b/test/UnitTest/FileSourceMockApiTest.cs @@ -154,37 +154,6 @@ public void DoesNotExist404() { } - [Test] - public void ProgressAndFinishedCallback() { - - int responsesCount = 0; - int progressCount = 0; - bool finishReceived = false; - - - for (int i = 0; i < 3; i++) { - _fs.Request( - _mockBaseUrl + _testUrl.simpleJson - , (Response r) => { - responsesCount++; - } - , (int requestsLeft) => { - progressCount++; - } - , () => { - finishReceived = true; - } - ); - } - - _fs.WaitForAllRequests(); - - Assert.AreEqual(3, progressCount, "number of progress callbacks does not match"); - Assert.AreEqual(3, responsesCount, "number of responses does not match"); - Assert.IsTrue(finishReceived, "finished callback did not fire"); - } - - [Test] public void NonThreaded() { diff --git a/test/UnitTest/VectorTileTest.cs b/test/UnitTest/VectorTileTest.cs index 411f01b..3038d0c 100644 --- a/test/UnitTest/VectorTileTest.cs +++ b/test/UnitTest/VectorTileTest.cs @@ -60,33 +60,33 @@ public void ParseSuccess() { } - [Test] - public void ParseFailure() { - var resource = TileResource.MakeVector(new CanonicalTileId(13, 5465, 2371), null); + //[Test] + //public void ParseFailure() { + // var resource = TileResource.MakeVector(new CanonicalTileId(13, 5465, 2371), null); - var response = new Response(); - response.Data = Enumerable.Repeat((byte)0, 5000).ToArray(); + // var response = new Response(); + // response.Data = Enumerable.Repeat((byte)0, 5000).ToArray(); - var mockFs = new Utils.MockFileSource(); - mockFs.SetReponse(resource.GetUrl(), response); + // var mockFs = new Utils.MockFileSource(); + // mockFs.SetReponse(resource.GetUrl(), response); - var map = new Map(mockFs); + // var map = new Map(mockFs); - var mapObserver = new Utils.VectorMapObserver(); - map.Subscribe(mapObserver); + // var mapObserver = new Utils.VectorMapObserver(); + // map.Subscribe(mapObserver); - map.Center = new Vector2d(60.163200, 60.163200); - map.Zoom = 13; - map.Update(); + // map.Center = new Vector2d(60.163200, 60.163200); + // map.Zoom = 13; + // map.Update(); - mockFs.WaitForAllRequests(); + // mockFs.WaitForAllRequests(); - // TODO: Assert.AreEqual("Parse error.", mapObserver.Error); - Assert.AreEqual(1, mapObserver.Tiles.Count); - Assert.IsNull(mapObserver.Tiles[0].Data); + // // TODO: Assert.AreEqual("Parse error.", mapObserver.Error); + // Assert.AreEqual(1, mapObserver.Tiles.Count); + // Assert.IsNull(mapObserver.Tiles[0].Data); - map.Unsubscribe(mapObserver); - } + // map.Unsubscribe(mapObserver); + //} [Test] From d92d66a2a1ef5182c45f4890ab0319cd64cc8ab9 Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Mon, 8 May 2017 14:20:48 +0200 Subject: [PATCH 28/35] AppVeyor: another HTTPRequest test run --- src/Platform/HTTPRequestNonThreaded.cs | 5 ++++- src/Platform/IAsyncRequestFactory.cs | 4 ++-- test/UnitTest/FileSourceMockApiTest.cs | 22 ++++++++++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/Platform/HTTPRequestNonThreaded.cs b/src/Platform/HTTPRequestNonThreaded.cs index ed02560..2e24d88 100644 --- a/src/Platform/HTTPRequestNonThreaded.cs +++ b/src/Platform/HTTPRequestNonThreaded.cs @@ -63,7 +63,10 @@ public HTTPRequestNonThreaded(string url, Action callback, int timeOut _requestUrl = url; setupRequest(); - getResponseNonThreaded(_request, EvaluateResponse); + + Action a = () => { getResponseNonThreaded(_request, EvaluateResponse); }; + //Fire and forget ;-) + a.BeginInvoke(a.EndInvoke, null); } diff --git a/src/Platform/IAsyncRequestFactory.cs b/src/Platform/IAsyncRequestFactory.cs index edfeead..d3d7495 100644 --- a/src/Platform/IAsyncRequestFactory.cs +++ b/src/Platform/IAsyncRequestFactory.cs @@ -20,9 +20,9 @@ public static class IAsyncRequestFactory { public static IAsyncRequest CreateRequest(string url, Action callback, int timeout=10) { #if !UNITY - //return new HTTPRequestThreaded(url, callback, timeout); - return new HTTPRequestNonThreaded(url, callback, timeout); + return new HTTPRequestThreaded(url, callback, timeout); #else + return new HTTPRequestNonThreaded(url, callback, timeout); #endif } diff --git a/test/UnitTest/FileSourceMockApiTest.cs b/test/UnitTest/FileSourceMockApiTest.cs index c43abaf..fdd572a 100644 --- a/test/UnitTest/FileSourceMockApiTest.cs +++ b/test/UnitTest/FileSourceMockApiTest.cs @@ -29,6 +29,7 @@ private struct _testUrl { public static string customStatusCode = "/testmock2"; public static string rateLimitHit = "/ratelimithit"; public static string xrateheader = "/xrateheader"; + public static string cancel = "/cancel"; } [OneTimeTearDown] @@ -59,6 +60,11 @@ public void SetupMockHttp() { .Return(string.Empty) .WithStatus((HttpStatusCode)429); + Func wait = delegate () { System.Threading.Thread.Sleep(1000); return string.Empty; }; + _mockApi.Stub(r => r.Get(_testUrl.cancel)) + .Return(wait) + .OK(); + double unixTimestamp = UnixTimestampUtils.To(new DateTime(1981, 12, 2)); _mockApi.Stub(r => r.Get(_testUrl.xrateheader)) .AddHeader("X-Rate-Limit-Interval", "60") @@ -168,5 +174,21 @@ public void NonThreaded() { } + [Test] + public void Cancel() { + + IAsyncRequest request = _fs.Request( + _mockBaseUrl + _testUrl.cancel + , (Response r) => { + Assert.IsTrue(r.HasError); + } + ); + + request.Cancel(); + + _fs.WaitForAllRequests(); + } + + } } \ No newline at end of file From 474ee6598697c8106b8ae1806332cc0548f67bf7 Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Mon, 8 May 2017 14:38:08 +0200 Subject: [PATCH 29/35] clean up HTTPRequest calling callback ;-) --- src/Platform/HTTPRequestNonThreaded.cs | 60 ++++++++------------------ 1 file changed, 18 insertions(+), 42 deletions(-) diff --git a/src/Platform/HTTPRequestNonThreaded.cs b/src/Platform/HTTPRequestNonThreaded.cs index 2e24d88..9799e71 100644 --- a/src/Platform/HTTPRequestNonThreaded.cs +++ b/src/Platform/HTTPRequestNonThreaded.cs @@ -193,29 +193,9 @@ private async void EvaluateResponse(HttpResponseMessage apiResponse, Exception a // Unity: SynchronizationContext doesn't do anything // use the Dispatcher #if !UNITY - _sync.Post(delegate { - _callback(response); - IsCompleted = true; - _callback = null; -#if NETFX_CORE - if (null != _request) { - _request.Dispose(); - _request = null; - } -#endif - }, null); + _sync.Post(delegate { callCallbackAndcleanUp(response); }, null); #else - UnityToolbag.Dispatcher.InvokeAsync(() => { - _callback(response); - IsCompleted = true; - _callback = null; -#if NETFX_CORE - if (null != _request) { - _request.Dispose(); - _request = null; - } -#endif - }); + UnityToolbag.Dispatcher.InvokeAsync(() => { callCallbackAndcleanUp(response); }); #endif } @@ -315,34 +295,30 @@ private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { // post (async) callback back to the main/UI thread // Unity: SynchronizationContext doesn't do anything // use the Dispatcher - _sync.Post(delegate { - _callback(response); - IsCompleted = true; - _callback = null; + _sync.Post(delegate { callCallbackAndcleanUp(response); }, null); +#else + // Unity is playing + if (UnityToolbag.Dispatcher._instanceExists) { + UnityToolbag.Dispatcher.InvokeAsync(() => { callCallbackAndcleanUp(response); }); + } else { // Unity is in Edit Mode + Mapbox.Unity.DispatcherEditor.InvokeAsync(() => { callCallbackAndcleanUp(response); }); + } +#endif + } +#endif + + private void callCallbackAndcleanUp(Response response) { + _callback(response); + IsCompleted = true; + _callback = null; #if NETFX_CORE if (null != _request) { _request.Dispose(); _request = null; } -#endif - }, null); -#else - UnityToolbag.Dispatcher.InvokeAsync(() => { - _callback(response); - IsCompleted = true; - _callback = null; -#if NETFX_CORE - if (null != _request) { - _request.Dispose(); - _request = null; - } -#endif - }); #endif } -#endif - public void Cancel() { From 4824dab71812725cf34859f77757f5786cb3b6b4 Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Tue, 9 May 2017 12:42:40 +0200 Subject: [PATCH 30/35] use 'Mapbox.Unity.Utilities.HTTPRequest' in IAsyncFactory --- src/Platform/IAsyncRequestFactory.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Platform/IAsyncRequestFactory.cs b/src/Platform/IAsyncRequestFactory.cs index d3d7495..5429c80 100644 --- a/src/Platform/IAsyncRequestFactory.cs +++ b/src/Platform/IAsyncRequestFactory.cs @@ -18,13 +18,18 @@ namespace Mapbox.Platform { /// A handle to an asynchronous request. public static class IAsyncRequestFactory { - public static IAsyncRequest CreateRequest(string url, Action callback, int timeout=10) { + public static IAsyncRequest CreateRequest(string url, Action callback, int timeout = 10) { #if !UNITY - return new HTTPRequestThreaded(url, callback, timeout); + if (Environment.ProcessorCount > 2) { + return new HTTPRequestThreaded(url, callback, timeout); + } else { + return new HTTPRequestNonThreaded(url, callback, timeout); + } #else - return new HTTPRequestNonThreaded(url, callback, timeout); + return new Mapbox.Unity.Utilities.HTTPRequest(url, callback, timeout); #endif } + } } \ No newline at end of file From e7cc5a59109b76e2bb4d2753335bf13117e4375c Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Tue, 9 May 2017 12:54:27 +0200 Subject: [PATCH 31/35] remove left over HTTPREquestThreaded-2.cs --- src/Platform/HTTPRequestThreaded-2.cs | 400 -------------------------- 1 file changed, 400 deletions(-) delete mode 100644 src/Platform/HTTPRequestThreaded-2.cs diff --git a/src/Platform/HTTPRequestThreaded-2.cs b/src/Platform/HTTPRequestThreaded-2.cs deleted file mode 100644 index d97ccaf..0000000 --- a/src/Platform/HTTPRequestThreaded-2.cs +++ /dev/null @@ -1,400 +0,0 @@ -#if UNITY_EDITOR || UNITY_STANDALONE || UNITY_ANDROID || UNITY_WP_8_1 || UNITY_WSA || UNITY_WEBGL || UNITY_IOS || UNITY_PS4 || UNITY_SAMSUNGTV || UNITY_XBOXONE || UNITY_TIZEN || UNITY_TVOS -#define UNITY -#endif -//----------------------------------------------------------------------- -// -// Copyright (c) 2016 Mapbox. All rights reserved. -// Based on http://stackoverflow.com/a/12606963 and http://wiki.unity3d.com/index.php/WebAsync -// -//----------------------------------------------------------------------- - -namespace Mapbox.Platform { - - - using System; - using System.Net; -#if !UNITY && !NETFX_CORE - using System.Net.Cache; -#endif - using System.IO; - using System.Collections.Generic; - using System.Threading; - using System.ComponentModel; - using Utils; -#if NETFX_CORE - using System.Net.Http; - using System.Linq; -#endif - - //using System.Windows.Threading; - - internal sealed class HTTPRequestThreaded : IAsyncRequest { - - - public bool IsCompleted { get; private set; } - - - private Action _callback; -#if !NETFX_CORE - private HttpWebRequest _request; -#else - private HttpClient _request; - private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); -#endif -#if !UNITY - private SynchronizationContext _sync = AsyncOperationManager.SynchronizationContext; -#endif - private int _timeOut; - private string _requestUrl; - private readonly string _userAgent = "mapbox-sdk-cs"; - - - /// - /// - /// - /// - /// - /// seconds - public HTTPRequestThreaded(string url, Action callback, int timeOut = 10) { - - IsCompleted = false; - _callback = callback; - _timeOut = timeOut; - _requestUrl = url; - - setupRequest(); - getResponseThreaded(_request, EvaluateResponse); - } - - - private void setupRequest() { - -#if !NETFX_CORE - _request = WebRequest.Create(_requestUrl) as HttpWebRequest; - _request.UserAgent = _userAgent; - //_hwr.UserAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36"; - //_hwr.CachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore); - _request.Credentials = CredentialCache.DefaultCredentials; - _request.KeepAlive = true; - _request.ProtocolVersion = HttpVersion.Version11; // improved performance - - // improved performance. - // ServicePointManager.DefaultConnectionLimit doesn't seem to change anything - // set ConnectionLimit per request - // https://msdn.microsoft.com/en-us/library/system.net.httpwebrequest(v=vs.90).aspx#Remarks - // use a value that is 12 times the number of CPUs on the local computer - _request.ServicePoint.ConnectionLimit = Environment.ProcessorCount * 6; - - _request.ServicePoint.UseNagleAlgorithm = true; - _request.ServicePoint.Expect100Continue = false; - _request.ServicePoint.MaxIdleTime = 2000; - _request.Method = "GET"; - _request.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip,deflate"); - _request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; - //_hwr.Timeout = timeOut * 1000; doesn't work in async calls, see below - -#else - HttpClientHandler handler = new HttpClientHandler() { - AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, - AllowAutoRedirect = true, - UseDefaultCredentials = true - - }; - _request = new HttpClient(handler); - _request.DefaultRequestHeaders.Add("User-Agent", _userAgent); - _request.Timeout = TimeSpan.FromSeconds(_timeOut); - - // TODO: how to set ConnectionLimit? ServicePoint.ConnectionLimit doesn't seem to be available. -#endif - -#if !UNITY && !NETFX_CORE - // 'NoCacheNoStore' greatly reduced the number of faulty request - // seems that .Net caching and Mapbox API don't play well together - _request.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.NoCacheNoStore); -#endif - } - - - -#if NETFX_CORE - private async void getResponseThreaded(HttpClient request, Action gotResponse) { - - // TODO: implement a strategy similar to the full .Net one to avoid blocking of 'GetAsync()' - // see 'Remarks' https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.timeout?view=netcore-1.1#System_Net_Http_HttpClient_Timeout - // "A Domain Name System (DNS) query may take up to 15 seconds to return or time out." - - HttpResponseMessage response = null; - try { - response = await request.GetAsync(_requestUrl, _cancellationTokenSource.Token); - gotResponse(response, null); - } - catch (Exception ex) { - gotResponse(response, ex); - } - - } - - - private async void EvaluateResponse(HttpResponseMessage apiResponse, Exception apiEx) { - - var response = new Response(); - - if (null != apiEx) { - response.AddException(apiEx); - } - - // timeout: API response is null - if (null == apiResponse) { - response.AddException(new Exception("No Reponse.")); - } else { - // https://www.mapbox.com/api-documentation/#rate-limits - if (null != apiResponse.Headers) { - response.Headers = new Dictionary(); - foreach (var hdr in apiResponse.Headers) { - string key = hdr.Key; - string val = hdr.Value.FirstOrDefault(); - response.Headers.Add(key, val); - if (key.Equals("X-Rate-Limit-Interval", StringComparison.OrdinalIgnoreCase)) { - int limitInterval; - if (int.TryParse(val, out limitInterval)) { response.XRateLimitInterval = limitInterval; } - } else if (key.Equals("X-Rate-Limit-Limit", StringComparison.OrdinalIgnoreCase)) { - long limitLimit; - if (long.TryParse(val, out limitLimit)) { response.XRateLimitLimit = limitLimit; } - } else if (key.Equals("X-Rate-Limit-Reset", StringComparison.OrdinalIgnoreCase)) { - double unixTimestamp; - if (double.TryParse(val, out unixTimestamp)) { - DateTime beginningOfTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); - response.XRateLimitReset = beginningOfTime.AddSeconds(unixTimestamp).ToLocalTime(); - } - } else if (key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase)) { - response.ContentType = val; - } - } - } - - if (apiResponse.StatusCode != HttpStatusCode.OK) { - response.AddException(new Exception(string.Format("{0}: {1}", apiResponse.StatusCode, apiResponse.ReasonPhrase))); - } - int statusCode = (int)apiResponse.StatusCode; - response.StatusCode = statusCode; - if (429 == statusCode) { - response.AddException(new Exception("Rate limit hit")); - } - - if (null != apiResponse) { - response.Data = await apiResponse.Content.ReadAsByteArrayAsync(); - } - } - - // post (async) callback back to the main/UI thread - // Unity: SynchronizationContext doesn't do anything - // use the Dispatcher -#if !UNITY - _sync.Post(delegate { - _callback(response); - IsCompleted = true; - _callback = null; -#if NETFX_CORE - if (null != _request) { - _request.Dispose(); - _request = null; - } -#endif - }, null); -#else - UnityToolbag.Dispatcher.InvokeAsync(() => { - _callback(response); - IsCompleted = true; - _callback = null; -#if NETFX_CORE - if (null != _request) { - _request.Dispose(); - _request = null; - } -#endif - }); -#endif - } - -#endif - - -#if !NETFX_CORE - - - private void getResponseThreaded(HttpWebRequest request, Action gotResponse) { - - // create an additional action wrapper, because of: - // https://msdn.microsoft.com/en-us/library/system.net.httpwebrequest.begingetresponse.aspx - // The BeginGetResponse method requires some synchronous setup tasks to complete (DNS resolution, - //proxy detection, and TCP socket connection, for example) before this method becomes asynchronous. - // As a result, this method should never be called on a user interface (UI) thread because it might - // take considerable time(up to several minutes depending on network settings) to complete the - // initial synchronous setup tasks before an exception for an error is thrown or the method succeeds. - - Action actionWrapper = () => { - try { - // BeginInvoke runs on a thread of the thread pool (!= main/UI thread) - // that's why we need SynchronizationContext when - // TODO: how to influence threadpool: nr of threads etc. - long startTicks = DateTime.Now.Ticks; - request.BeginGetResponse((asycnResult) => { - try { // there's a try/catch here because execution path is different from invokation one, exception here may cause a crash - long beforeEndGet = DateTime.Now.Ticks; - HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asycnResult); - //long finished = DateTime.Now.Ticks; - //long duration = finished - startTicks; - //TimeSpan ts = TimeSpan.FromTicks(duration); - //TimeSpan tsEndGet = TimeSpan.FromTicks(finished - beforeEndGet); - //TimeSpan tsBeginGet = TimeSpan.FromTicks(beforeEndGet - startTicks); - //UnityEngine.Debug.Log("received response - " + ts.Milliseconds + "ms" + " BeginGet: " + tsBeginGet.Milliseconds + " EndGet: " + tsEndGet.Milliseconds + " CompletedSynchronously: " + asycnResult.CompletedSynchronously); - gotResponse(response, null); - } - // EndGetResponse() throws on on some status codes, try to get response anyway (and status codes) - catch (WebException wex) { - //another place to watchout for HttpWebRequest.Abort to occur - if (wex.Status == WebExceptionStatus.RequestCanceled) { - gotResponse(null, wex); - } else { - HttpWebResponse hwr = wex.Response as HttpWebResponse; - if (null == hwr) { - throw; - } - gotResponse(hwr, wex); - } - } - catch (Exception ex) { - gotResponse(null, ex); - } - } - , null); - } - catch (Exception ex) { - //catch exception from HttpWebRequest.Abort - gotResponse(null, ex); - } - }; - - try { - actionWrapper.BeginInvoke(new AsyncCallback((iAsyncResult) => { - var action = (Action)iAsyncResult.AsyncState; - action.EndInvoke(iAsyncResult); - }) - , actionWrapper); - } - catch (Exception ex) { - gotResponse(null, ex); - } - } - - - - private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { - - var response = new Response(); - response.Request = this; - - if (null != apiEx) { - response.AddException(apiEx); - } - - // timeout: API response is null - if (null == apiResponse) { - response.AddException(new Exception("No Reponse.")); - } else { - // https://www.mapbox.com/api-documentation/#rate-limits - if (null != apiResponse.Headers) { - response.Headers = new Dictionary(); - for (int i = 0; i < apiResponse.Headers.Count; i++) { - // TODO: implement .Net Core / UWP implementation - string key = apiResponse.Headers.Keys[i]; - string val = apiResponse.Headers[i]; - response.Headers.Add(key, val); - if (key.Equals("X-Rate-Limit-Interval", StringComparison.InvariantCultureIgnoreCase)) { - int limitInterval; - if (int.TryParse(val, out limitInterval)) { response.XRateLimitInterval = limitInterval; } - } else if (key.Equals("X-Rate-Limit-Limit", StringComparison.InvariantCultureIgnoreCase)) { - long limitLimit; - if (long.TryParse(val, out limitLimit)) { response.XRateLimitLimit = limitLimit; } - } else if (key.Equals("X-Rate-Limit-Reset", StringComparison.InvariantCultureIgnoreCase)) { - double unixTimestamp; - if (double.TryParse(val, out unixTimestamp)) { - response.XRateLimitReset = UnixTimestampUtils.From(unixTimestamp); - } - } else if (key.Equals("Content-Type", StringComparison.InvariantCultureIgnoreCase)) { - response.ContentType = val; - } - } - } - - if (apiResponse.StatusCode != HttpStatusCode.OK) { - response.AddException(new Exception(string.Format("{0}: {1}", apiResponse.StatusCode, apiResponse.StatusDescription))); - } - int statusCode = (int)apiResponse.StatusCode; - response.StatusCode = statusCode; - if (429 == statusCode) { - response.AddException(new Exception("Rate limit hit")); - } - - if (null != apiResponse) { - using (Stream responseStream = apiResponse.GetResponseStream()) { - byte[] buffer = new byte[0x1000]; - int bytesRead; - using (MemoryStream ms = new MemoryStream()) { - while (0 != (bytesRead = responseStream.Read(buffer, 0, buffer.Length))) { - ms.Write(buffer, 0, bytesRead); - } - response.Data = ms.ToArray(); - } - } - apiResponse.Close(); - } - } - - // post (async) callback back to the main/UI thread - // Unity: SynchronizationContext doesn't do anything - // use the Dispatcher -#if !UNITY - _sync.Post(delegate { - _callback(response); - IsCompleted = true; - _callback = null; -#if NETFX_CORE - if (null != _request) { - _request.Dispose(); - _request = null; - } -#endif - }, null); -#else - UnityToolbag.Dispatcher.InvokeAsync(() => { - _callback(response); - IsCompleted = true; - _callback = null; -#if NETFX_CORE - if (null != _request) { - _request.Dispose(); - _request = null; - } -#endif - }); -#endif - } -#endif - - - - public void Cancel() { - -#if !NETFX_CORE - if (null != _request) { - _request.Abort(); - } -#else - _cancellationTokenSource.Cancel(); -#endif - } - - - } -} From 5b440ead209b4a66f51f41995666119be7e57c64 Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Tue, 9 May 2017 13:44:57 +0200 Subject: [PATCH 32/35] convenience method: ExceptionsAsString --- src/Map/Tile.cs | 9 +++++++++ src/Platform/Response.cs | 26 +++++++++++++++++++------- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/Map/Tile.cs b/src/Map/Tile.cs index 788ec82..4ff8ca8 100644 --- a/src/Map/Tile.cs +++ b/src/Map/Tile.cs @@ -61,6 +61,15 @@ public ReadOnlyCollection Exceptions { } + /// Messages of exceptions otherwise empty string. + public string ExceptionsAsString { + get { + if (null == _exceptions || _exceptions.Count == 0) { return string.Empty; } + return string.Join(Environment.NewLine, _exceptions.Select(e => e.Message).ToArray()); + } + } + + /// /// Sets the error message. /// diff --git a/src/Platform/Response.cs b/src/Platform/Response.cs index 990b262..bc03fb6 100644 --- a/src/Platform/Response.cs +++ b/src/Platform/Response.cs @@ -4,12 +4,13 @@ // //----------------------------------------------------------------------- -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; - namespace Mapbox.Platform { + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + /// A response from a request. public struct Response { @@ -23,11 +24,10 @@ public bool RateLimitHit { /// Flag to indicate if the request was successful public bool HasError { - get { - return _exceptions == null ? false : _exceptions.Count > 0; - } + get { return _exceptions == null ? false : _exceptions.Count > 0; } } + public int? StatusCode; @@ -37,12 +37,15 @@ public bool HasError { /// Length of rate-limiting interval in seconds. https://www.mapbox.com/api-documentation/#rate-limits public int? XRateLimitInterval; + /// Maximum number of requests you may make in the current interval before reaching the limit. https://www.mapbox.com/api-documentation/#rate-limits public long? XRateLimitLimit; + /// Timestamp of when the current interval will end and the ratelimit counter is reset. https://www.mapbox.com/api-documentation/#rate-limits public DateTime? XRateLimitReset; + private List _exceptions; /// Exceptions that might have occured during the request. public ReadOnlyCollection Exceptions { @@ -50,6 +53,15 @@ public ReadOnlyCollection Exceptions { } + /// Messages of exceptions otherwise empty string. + public string ExceptionsAsString { + get { + if (null == _exceptions || _exceptions.Count == 0) { return string.Empty; } + return string.Join(Environment.NewLine, _exceptions.Select(e => e.Message).ToArray()); + } + } + + /// Headers of the response. public Dictionary Headers; From da62fc6b39855ca2b990d47ceb0df65bb2082828 Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Tue, 9 May 2017 14:43:29 +0200 Subject: [PATCH 33/35] some more Unity fixes --- src/Platform/HTTPRequestNonThreaded.cs | 2 ++ src/Platform/HTTPRequestThreaded.cs | 2 ++ src/Platform/IAsyncRequestFactory.cs | 3 --- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Platform/HTTPRequestNonThreaded.cs b/src/Platform/HTTPRequestNonThreaded.cs index 9799e71..04f533c 100644 --- a/src/Platform/HTTPRequestNonThreaded.cs +++ b/src/Platform/HTTPRequestNonThreaded.cs @@ -301,7 +301,9 @@ private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { if (UnityToolbag.Dispatcher._instanceExists) { UnityToolbag.Dispatcher.InvokeAsync(() => { callCallbackAndcleanUp(response); }); } else { // Unity is in Edit Mode +#if UNITY_EDITOR Mapbox.Unity.DispatcherEditor.InvokeAsync(() => { callCallbackAndcleanUp(response); }); +#endif } #endif } diff --git a/src/Platform/HTTPRequestThreaded.cs b/src/Platform/HTTPRequestThreaded.cs index cde16e6..5bf7c14 100644 --- a/src/Platform/HTTPRequestThreaded.cs +++ b/src/Platform/HTTPRequestThreaded.cs @@ -381,6 +381,7 @@ private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { #endif }); } else { // Unity is in Edit Mode +#if UNITY_EDITOR Mapbox.Unity.DispatcherEditor.InvokeAsync(() => { _callback(response); IsCompleted = true; @@ -392,6 +393,7 @@ private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { } #endif }); +#endif } #endif diff --git a/src/Platform/IAsyncRequestFactory.cs b/src/Platform/IAsyncRequestFactory.cs index 5429c80..7067a61 100644 --- a/src/Platform/IAsyncRequestFactory.cs +++ b/src/Platform/IAsyncRequestFactory.cs @@ -11,9 +11,6 @@ namespace Mapbox.Platform { using System; -#if UNITY - using UnityEditor; -#endif /// A handle to an asynchronous request. public static class IAsyncRequestFactory { From 3a981076520edcbfa34d3a9d1caf7e52e5e9bfaf Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Tue, 9 May 2017 16:40:49 +0200 Subject: [PATCH 34/35] * Force Response to be created from ApiResponses * Unity fixes for Response.FromWebResponse --- src/Platform/HTTPRequestNonThreaded.cs | 113 +------------- src/Platform/HTTPRequestThreaded.cs | 114 +------------- src/Platform/Response.cs | 207 ++++++++++++++++++++++++- 3 files changed, 218 insertions(+), 216 deletions(-) diff --git a/src/Platform/HTTPRequestNonThreaded.cs b/src/Platform/HTTPRequestNonThreaded.cs index 04f533c..d6a35e9 100644 --- a/src/Platform/HTTPRequestNonThreaded.cs +++ b/src/Platform/HTTPRequestNonThreaded.cs @@ -1,6 +1,7 @@ #if UNITY_EDITOR || UNITY_STANDALONE || UNITY_ANDROID || UNITY_WP_8_1 || UNITY_WSA || UNITY_WEBGL || UNITY_IOS || UNITY_PS4 || UNITY_SAMSUNGTV || UNITY_XBOXONE || UNITY_TIZEN || UNITY_TVOS #define UNITY #endif + //----------------------------------------------------------------------- // // Copyright (c) 2016 Mapbox. All rights reserved. @@ -8,6 +9,8 @@ // //----------------------------------------------------------------------- +#if !UNITY + namespace Mapbox.Platform { @@ -140,54 +143,7 @@ private async void getResponseNonThreaded(HttpClient request, Action(); - foreach (var hdr in apiResponse.Headers) { - string key = hdr.Key; - string val = hdr.Value.FirstOrDefault(); - response.Headers.Add(key, val); - if (key.Equals("X-Rate-Limit-Interval", StringComparison.OrdinalIgnoreCase)) { - int limitInterval; - if (int.TryParse(val, out limitInterval)) { response.XRateLimitInterval = limitInterval; } - } else if (key.Equals("X-Rate-Limit-Limit", StringComparison.OrdinalIgnoreCase)) { - long limitLimit; - if (long.TryParse(val, out limitLimit)) { response.XRateLimitLimit = limitLimit; } - } else if (key.Equals("X-Rate-Limit-Reset", StringComparison.OrdinalIgnoreCase)) { - double unixTimestamp; - if (double.TryParse(val, out unixTimestamp)) { - DateTime beginningOfTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); - response.XRateLimitReset = beginningOfTime.AddSeconds(unixTimestamp).ToLocalTime(); - } - } else if (key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase)) { - response.ContentType = val; - } - } - } - - if (apiResponse.StatusCode != HttpStatusCode.OK) { - response.AddException(new Exception(string.Format("{0}: {1}", apiResponse.StatusCode, apiResponse.ReasonPhrase))); - } - int statusCode = (int)apiResponse.StatusCode; - response.StatusCode = statusCode; - if (429 == statusCode) { - response.AddException(new Exception("Rate limit hit")); - } - - if (null != apiResponse) { - response.Data = await apiResponse.Content.ReadAsByteArrayAsync(); - } - } + var response = await Response.FromWebResponse(this, apiResponse, apiEx); // post (async) callback back to the main/UI thread // Unity: SynchronizationContext doesn't do anything @@ -231,65 +187,7 @@ private void getResponseNonThreaded(HttpWebRequest request, Action(); - for (int i = 0; i < apiResponse.Headers.Count; i++) { - // TODO: implement .Net Core / UWP implementation - string key = apiResponse.Headers.Keys[i]; - string val = apiResponse.Headers[i]; - response.Headers.Add(key, val); - if (key.Equals("X-Rate-Limit-Interval", StringComparison.InvariantCultureIgnoreCase)) { - int limitInterval; - if (int.TryParse(val, out limitInterval)) { response.XRateLimitInterval = limitInterval; } - } else if (key.Equals("X-Rate-Limit-Limit", StringComparison.InvariantCultureIgnoreCase)) { - long limitLimit; - if (long.TryParse(val, out limitLimit)) { response.XRateLimitLimit = limitLimit; } - } else if (key.Equals("X-Rate-Limit-Reset", StringComparison.InvariantCultureIgnoreCase)) { - double unixTimestamp; - if (double.TryParse(val, out unixTimestamp)) { - response.XRateLimitReset = UnixTimestampUtils.From(unixTimestamp); - } - } else if (key.Equals("Content-Type", StringComparison.InvariantCultureIgnoreCase)) { - response.ContentType = val; - } - } - } - - if (apiResponse.StatusCode != HttpStatusCode.OK) { - response.AddException(new Exception(string.Format("{0}: {1}", apiResponse.StatusCode, apiResponse.StatusDescription))); - } - int statusCode = (int)apiResponse.StatusCode; - response.StatusCode = statusCode; - if (429 == statusCode) { - response.AddException(new Exception("Rate limit hit")); - } - - if (null != apiResponse) { - using (Stream responseStream = apiResponse.GetResponseStream()) { - byte[] buffer = new byte[0x1000]; - int bytesRead; - using (MemoryStream ms = new MemoryStream()) { - while (0 != (bytesRead = responseStream.Read(buffer, 0, buffer.Length))) { - ms.Write(buffer, 0, bytesRead); - } - response.Data = ms.ToArray(); - } - } - apiResponse.Close(); - } - } + var response = Response.FromWebResponse(this, apiResponse, apiEx); #if !UNITY // post (async) callback back to the main/UI thread @@ -337,3 +235,4 @@ public void Cancel() { } } +#endif \ No newline at end of file diff --git a/src/Platform/HTTPRequestThreaded.cs b/src/Platform/HTTPRequestThreaded.cs index 5bf7c14..c8274b5 100644 --- a/src/Platform/HTTPRequestThreaded.cs +++ b/src/Platform/HTTPRequestThreaded.cs @@ -9,6 +9,8 @@ #define UNITY #endif +#if !UNITY + namespace Mapbox.Platform { @@ -139,54 +141,7 @@ private async void getResponseAsync(HttpClient request, Action(); - foreach (var hdr in apiResponse.Headers) { - string key = hdr.Key; - string val = hdr.Value.FirstOrDefault(); - response.Headers.Add(key, val); - if (key.Equals("X-Rate-Limit-Interval", StringComparison.OrdinalIgnoreCase)) { - int limitInterval; - if (int.TryParse(val, out limitInterval)) { response.XRateLimitInterval = limitInterval; } - } else if (key.Equals("X-Rate-Limit-Limit", StringComparison.OrdinalIgnoreCase)) { - long limitLimit; - if (long.TryParse(val, out limitLimit)) { response.XRateLimitLimit = limitLimit; } - } else if (key.Equals("X-Rate-Limit-Reset", StringComparison.OrdinalIgnoreCase)) { - double unixTimestamp; - if (double.TryParse(val, out unixTimestamp)) { - DateTime beginningOfTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); - response.XRateLimitReset = beginningOfTime.AddSeconds(unixTimestamp).ToLocalTime(); - } - } else if (key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase)) { - response.ContentType = val; - } - } - } - - if (apiResponse.StatusCode != HttpStatusCode.OK) { - response.AddException(new Exception(string.Format("{0}: {1}", apiResponse.StatusCode, apiResponse.ReasonPhrase))); - } - int statusCode = (int)apiResponse.StatusCode; - response.StatusCode = statusCode; - if (429 == statusCode) { - response.AddException(new Exception("Rate limit hit")); - } - - if (null != apiResponse) { - response.Data = await apiResponse.Content.ReadAsByteArrayAsync(); - } - } + var response = await Response.FromWebResponse(this, apiResponse, apiEx); // post (async) callback back to the main/UI thread // Unity: SynchronizationContext doesn't do anything @@ -291,65 +246,7 @@ private void getResponseAsync(HttpWebRequest request, Action(); - for (int i = 0; i < apiResponse.Headers.Count; i++) { - // TODO: implement .Net Core / UWP implementation - string key = apiResponse.Headers.Keys[i]; - string val = apiResponse.Headers[i]; - response.Headers.Add(key, val); - if (key.Equals("X-Rate-Limit-Interval", StringComparison.InvariantCultureIgnoreCase)) { - int limitInterval; - if (int.TryParse(val, out limitInterval)) { response.XRateLimitInterval = limitInterval; } - } else if (key.Equals("X-Rate-Limit-Limit", StringComparison.InvariantCultureIgnoreCase)) { - long limitLimit; - if (long.TryParse(val, out limitLimit)) { response.XRateLimitLimit = limitLimit; } - } else if (key.Equals("X-Rate-Limit-Reset", StringComparison.InvariantCultureIgnoreCase)) { - double unixTimestamp; - if (double.TryParse(val, out unixTimestamp)) { - response.XRateLimitReset = UnixTimestampUtils.From(unixTimestamp); - } - } else if (key.Equals("Content-Type", StringComparison.InvariantCultureIgnoreCase)) { - response.ContentType = val; - } - } - } - - if (apiResponse.StatusCode != HttpStatusCode.OK) { - response.AddException(new Exception(string.Format("{0}: {1}", apiResponse.StatusCode, apiResponse.StatusDescription))); - } - int statusCode = (int)apiResponse.StatusCode; - response.StatusCode = statusCode; - if (429 == statusCode) { - response.AddException(new Exception("Rate limit hit")); - } - - if (null != apiResponse) { - using (Stream responseStream = apiResponse.GetResponseStream()) { - byte[] buffer = new byte[0x1000]; - int bytesRead; - using (MemoryStream ms = new MemoryStream()) { - while (0 != (bytesRead = responseStream.Read(buffer, 0, buffer.Length))) { - ms.Write(buffer, 0, bytesRead); - } - response.Data = ms.ToArray(); - } - } - apiResponse.Close(); - } - } + var response = Response.FromWebResponse(this, apiResponse,apiEx); // post (async) callback back to the main/UI thread // Unity: SynchronizationContext doesn't do anything @@ -416,3 +313,6 @@ public void Cancel() { } } + + +#endif \ No newline at end of file diff --git a/src/Platform/Response.cs b/src/Platform/Response.cs index bc03fb6..c5d971b 100644 --- a/src/Platform/Response.cs +++ b/src/Platform/Response.cs @@ -4,18 +4,37 @@ // //----------------------------------------------------------------------- +#if UNITY_EDITOR || UNITY_STANDALONE || UNITY_ANDROID || UNITY_WP_8_1 || UNITY_WSA || UNITY_WEBGL || UNITY_IOS || UNITY_PS4 || UNITY_SAMSUNGTV || UNITY_XBOXONE || UNITY_TIZEN || UNITY_TVOS +#define UNITY +#endif + namespace Mapbox.Platform { using System; using System.Collections.Generic; using System.Collections.ObjectModel; + using System.IO; using System.Linq; + using System.Net; + using Utils; +#if NETFX_CORE + using System.Net.Http; + using System.Threading.Tasks; +#endif +#if UNITY + using UnityEngine.Networking; +#endif /// A response from a request. - public struct Response { + public class Response { + + + private Response() { } + + + public IAsyncRequest Request { get; private set; } - public IAsyncRequest Request; public bool RateLimitHit { get { return StatusCode.HasValue ? 429 == StatusCode.Value : false; } @@ -75,5 +94,189 @@ public void AddException(Exception ex) { } +#if !NETFX_CORE && !UNITY + public static Response FromWebResponse(IAsyncRequest request, HttpWebResponse apiResponse, Exception apiEx) { + + Response response = new Response(); + response.Request = request; + + if (null != apiEx) { + response.AddException(apiEx); + } + + // timeout: API response is null + if (null == apiResponse) { + response.AddException(new Exception("No Reponse.")); + } else { + // https://www.mapbox.com/api-documentation/#rate-limits + if (null != apiResponse.Headers) { + response.Headers = new Dictionary(); + for (int i = 0; i < apiResponse.Headers.Count; i++) { + // TODO: implement .Net Core / UWP implementation + string key = apiResponse.Headers.Keys[i]; + string val = apiResponse.Headers[i]; + response.Headers.Add(key, val); + if (key.Equals("X-Rate-Limit-Interval", StringComparison.InvariantCultureIgnoreCase)) { + int limitInterval; + if (int.TryParse(val, out limitInterval)) { response.XRateLimitInterval = limitInterval; } + } else if (key.Equals("X-Rate-Limit-Limit", StringComparison.InvariantCultureIgnoreCase)) { + long limitLimit; + if (long.TryParse(val, out limitLimit)) { response.XRateLimitLimit = limitLimit; } + } else if (key.Equals("X-Rate-Limit-Reset", StringComparison.InvariantCultureIgnoreCase)) { + double unixTimestamp; + if (double.TryParse(val, out unixTimestamp)) { + response.XRateLimitReset = UnixTimestampUtils.From(unixTimestamp); + } + } else if (key.Equals("Content-Type", StringComparison.InvariantCultureIgnoreCase)) { + response.ContentType = val; + } + } + } + + if (apiResponse.StatusCode != HttpStatusCode.OK) { + response.AddException(new Exception(string.Format("{0}: {1}", apiResponse.StatusCode, apiResponse.StatusDescription))); + } + int statusCode = (int)apiResponse.StatusCode; + response.StatusCode = statusCode; + if (429 == statusCode) { + response.AddException(new Exception("Rate limit hit")); + } + + if (null != apiResponse) { + using (Stream responseStream = apiResponse.GetResponseStream()) { + byte[] buffer = new byte[0x1000]; + int bytesRead; + using (MemoryStream ms = new MemoryStream()) { + while (0 != (bytesRead = responseStream.Read(buffer, 0, buffer.Length))) { + ms.Write(buffer, 0, bytesRead); + } + response.Data = ms.ToArray(); + } + } + apiResponse.Close(); + } + } + + return response; + } +#endif + +#if NETFX_CORE && !UNITY + public static async Task FromWebResponse(IAsyncRequest request, HttpResponseMessage apiResponse, Exception apiEx) { + + Response response = new Response(); + response.Request = request; + + if (null != apiEx) { + response.AddException(apiEx); + } + + // timeout: API response is null + if (null == apiResponse) { + response.AddException(new Exception("No Reponse.")); + } else { + // https://www.mapbox.com/api-documentation/#rate-limits + if (null != apiResponse.Headers) { + response.Headers = new Dictionary(); + foreach (var hdr in apiResponse.Headers) { + string key = hdr.Key; + string val = hdr.Value.FirstOrDefault(); + response.Headers.Add(key, val); + if (key.Equals("X-Rate-Limit-Interval", StringComparison.OrdinalIgnoreCase)) { + int limitInterval; + if (int.TryParse(val, out limitInterval)) { response.XRateLimitInterval = limitInterval; } + } else if (key.Equals("X-Rate-Limit-Limit", StringComparison.OrdinalIgnoreCase)) { + long limitLimit; + if (long.TryParse(val, out limitLimit)) { response.XRateLimitLimit = limitLimit; } + } else if (key.Equals("X-Rate-Limit-Reset", StringComparison.OrdinalIgnoreCase)) { + double unixTimestamp; + if (double.TryParse(val, out unixTimestamp)) { + DateTime beginningOfTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + response.XRateLimitReset = beginningOfTime.AddSeconds(unixTimestamp).ToLocalTime(); + } + } else if (key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase)) { + response.ContentType = val; + } + } + } + + if (apiResponse.StatusCode != HttpStatusCode.OK) { + response.AddException(new Exception(string.Format("{0}: {1}", apiResponse.StatusCode, apiResponse.ReasonPhrase))); + } + int statusCode = (int)apiResponse.StatusCode; + response.StatusCode = statusCode; + if (429 == statusCode) { + response.AddException(new Exception("Rate limit hit")); + } + + if (null != apiResponse) { + response.Data = await apiResponse.Content.ReadAsByteArrayAsync(); + } + } + + return response; + } +#endif + +#if UNITY + public static Response FromWebResponse(IAsyncRequest request, UnityWebRequest apiResponse, Exception apiEx) { + + Response response = new Response(); + response.Request = request; + + if (null != apiEx) { + response.AddException(apiEx); + } + + if (!string.IsNullOrEmpty(apiResponse.error)) { + response.AddException(new Exception(apiResponse.error)); + } + + if (null == apiResponse.downloadHandler.data) { + response.AddException(new Exception("Response has no data.")); + } + + Dictionary apiHeaders = apiResponse.GetResponseHeaders(); + if (null != apiHeaders) { + response.Headers = new Dictionary(); + foreach (var apiHdr in apiHeaders) { + string key = apiHdr.Key; + string val = apiHdr.Value; + response.Headers.Add(key, val); + if (key.Equals("X-Rate-Limit-Interval", StringComparison.InvariantCultureIgnoreCase)) { + int limitInterval; + if (int.TryParse(val, out limitInterval)) { response.XRateLimitInterval = limitInterval; } + } else if (key.Equals("X-Rate-Limit-Limit", StringComparison.InvariantCultureIgnoreCase)) { + long limitLimit; + if (long.TryParse(val, out limitLimit)) { response.XRateLimitLimit = limitLimit; } + } else if (key.Equals("X-Rate-Limit-Reset", StringComparison.InvariantCultureIgnoreCase)) { + double unixTimestamp; + if (double.TryParse(val, out unixTimestamp)) { + response.XRateLimitReset = UnixTimestampUtils.From(unixTimestamp); + } + } else if (key.Equals("Content-Type", StringComparison.InvariantCultureIgnoreCase)) { + response.ContentType = val; + } + } + } + + int statusCode = (int)apiResponse.responseCode; + response.StatusCode = statusCode; + + if (statusCode != 200) { + response.AddException(new Exception(string.Format("Status Code {0}", apiResponse.responseCode))); + } + if (429 == statusCode) { + response.AddException(new Exception("Rate limit hit")); + } + + response.Data = apiResponse.downloadHandler.data; + + return response; + } +#endif + + + } } \ No newline at end of file From 8c29eade5f20b77bf76f6abece242a2852466973 Mon Sep 17 00:00:00 2001 From: bergwerkgis Date: Tue, 9 May 2017 17:08:45 +0200 Subject: [PATCH 35/35] fixes for UWP apps built with Unity --- src/Platform/Response.cs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Platform/Response.cs b/src/Platform/Response.cs index c5d971b..99fedb1 100644 --- a/src/Platform/Response.cs +++ b/src/Platform/Response.cs @@ -94,7 +94,7 @@ public void AddException(Exception ex) { } -#if !NETFX_CORE && !UNITY +#if !NETFX_CORE && !UNITY // full .NET Framework public static Response FromWebResponse(IAsyncRequest request, HttpWebResponse apiResponse, Exception apiEx) { Response response = new Response(); @@ -161,7 +161,7 @@ public static Response FromWebResponse(IAsyncRequest request, HttpWebResponse ap } #endif -#if NETFX_CORE && !UNITY +#if NETFX_CORE && !UNITY //UWP but not Unity public static async Task FromWebResponse(IAsyncRequest request, HttpResponseMessage apiResponse, Exception apiEx) { Response response = new Response(); @@ -218,7 +218,7 @@ public static async Task FromWebResponse(IAsyncRequest request, HttpRe } #endif -#if UNITY +#if UNITY // within Unity or UWP build from Unity public static Response FromWebResponse(IAsyncRequest request, UnityWebRequest apiResponse, Exception apiEx) { Response response = new Response(); @@ -236,6 +236,12 @@ public static Response FromWebResponse(IAsyncRequest request, UnityWebRequest ap response.AddException(new Exception("Response has no data.")); } +#if NETFX_CORE + StringComparison stringComp = StringComparison.OrdinalIgnoreCase; +#else + StringComparison stringComp = StringComparison.InvariantCultureIgnoreCase; +#endif + Dictionary apiHeaders = apiResponse.GetResponseHeaders(); if (null != apiHeaders) { response.Headers = new Dictionary(); @@ -243,18 +249,18 @@ public static Response FromWebResponse(IAsyncRequest request, UnityWebRequest ap string key = apiHdr.Key; string val = apiHdr.Value; response.Headers.Add(key, val); - if (key.Equals("X-Rate-Limit-Interval", StringComparison.InvariantCultureIgnoreCase)) { + if (key.Equals("X-Rate-Limit-Interval", stringComp)) { int limitInterval; if (int.TryParse(val, out limitInterval)) { response.XRateLimitInterval = limitInterval; } - } else if (key.Equals("X-Rate-Limit-Limit", StringComparison.InvariantCultureIgnoreCase)) { + } else if (key.Equals("X-Rate-Limit-Limit", stringComp)) { long limitLimit; if (long.TryParse(val, out limitLimit)) { response.XRateLimitLimit = limitLimit; } - } else if (key.Equals("X-Rate-Limit-Reset", StringComparison.InvariantCultureIgnoreCase)) { + } else if (key.Equals("X-Rate-Limit-Reset", stringComp)) { double unixTimestamp; if (double.TryParse(val, out unixTimestamp)) { response.XRateLimitReset = UnixTimestampUtils.From(unixTimestamp); } - } else if (key.Equals("Content-Type", StringComparison.InvariantCultureIgnoreCase)) { + } else if (key.Equals("Content-Type", stringComp)) { response.ContentType = val; } }