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 f040bc3..345dfc0 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..4ff8ca8 100644 --- a/src/Map/Tile.cs +++ b/src/Map/Tile.cs @@ -4,28 +4,31 @@ // //----------------------------------------------------------------------- -namespace Mapbox.Map -{ +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 /// bounding box. More info /// here . /// - public abstract class Tile - { + public abstract class Tile { + + + private CanonicalTileId _id; + private List _exceptions; + private State _state = State.New; + private IAsyncRequest _request; + private Action _callback; - private CanonicalTileId id; - private string error; - private State state = State.New; - private IAsyncRequest request; - private Action callback; /// Tile state. - public enum State - { + public enum State { /// New tile, not yet initialized. New, /// Loading data. @@ -38,67 +41,74 @@ public enum State /// Gets the identifier. /// The canonical tile identifier. - public CanonicalTileId Id - { - get - { - return this.id; - } - set - { - this.id = value; + public CanonicalTileId Id { + get { return _id; } + set { _id = value; } + } + + + /// Flag to indicate if the request was successful + public bool HasError { + get { + return _exceptions == null ? false : _exceptions.Count > 0; } } - /// Gets the error message if any. - /// The error string. - public string Error - { - get - { - return this.error; + + /// Exceptions that might have occured during creation of the tile. + public ReadOnlyCollection Exceptions { + get { return null == _exceptions ? null : _exceptions.AsReadOnly(); } + } + + + /// 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. /// /// - 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 /// is accusing any error. /// /// The tile state. - public State CurrentState - { - get - { - return this.state; + public State CurrentState { + get { + return _state; } } + /// /// Initializes the object. It will /// start a network request and fire the callback when completed. /// /// Initialization parameters. /// The completion callback. - public void Initialize(Parameters param, Action callback) - { - this.Cancel(); - - this.state = State.Loading; - this.id = param.Id; - this.request = param.Fs.Request(this.MakeTileResource(param.MapId).GetUrl(), this.HandleTileResponse); - this.callback = callback; + public void Initialize(Parameters param, Action callback) { + Cancel(); + + _state = State.Loading; + _id = param.Id; + _request = param.Fs.Request(MakeTileResource(param.MapId).GetUrl(), HandleTileResponse); + _callback = callback; } + /// /// Returns a that represents the current /// . @@ -107,11 +117,11 @@ public void Initialize(Parameters param, Action callback) /// A that represents the current /// . /// - public override string ToString() - { - return this.Id.ToString(); + public override string ToString() { + return Id.ToString(); } + /// /// Cancels the request for the object. /// It will stop a network request and set the tile's state to Canceled. @@ -121,7 +131,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)) /// { @@ -130,50 +140,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; + public void Cancel() { + 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 (!string.IsNullOrEmpty(response.Error)) - { - this.error = response.Error; - } - else if (this.ParseTileData(response.Data) == false) - { - this.error = "ParseError"; + private void HandleTileResponse(Response response) { + + if (response.HasError) { + 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. /// @@ -185,8 +197,7 @@ private void HandleTileResponse(Response response) /// parameters.MapId = "mapbox.mapbox-streets-v7"; /// /// - public struct Parameters - { + public struct Parameters { /// The tile id. public CanonicalTileId Id; @@ -200,5 +211,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/Mono/FileSource.cs b/src/Mono/FileSource.cs deleted file mode 100644 index fdd1aa1..0000000 --- a/src/Mono/FileSource.cs +++ /dev/null @@ -1,78 +0,0 @@ -//----------------------------------------------------------------------- -// -// Copyright (c) 2016 Mapbox. All rights reserved. -// -//----------------------------------------------------------------------- - -namespace Mapbox.Mono -{ - using System; - using System.Collections.Generic; - using System.Threading; - using Mapbox.Platform; - - /// - /// Mono implementation of the FileSource class. It will use Mono's - /// runtime to - /// asynchronously fetch data from the network via HTTP or HTTPS requests. - /// - /// - /// 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"); - - /// Performs a request asynchronously. - /// The HTTP/HTTPS url. - /// Callback to be called after the request is completed. - /// - /// Returns a that can be used for canceling a pending - /// 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; - } - - var request = new HTTPRequest(url, callback); - this.requests.Add(request); - - return request; - } - - /// - /// Block until all the requests are processed. - /// - 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); - } - } - - if (this.requests.Count == 0) - { - break; - } - -#if !WINDOWS_UWP - Thread.Sleep(10); -#else - System.Threading.Tasks.Task.Delay(5).Wait(); -#endif - } - } - } -} diff --git a/src/Mono/HTTPRequest.cs b/src/Mono/HTTPRequest.cs deleted file mode 100644 index 8c3d243..0000000 --- a/src/Mono/HTTPRequest.cs +++ /dev/null @@ -1,73 +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) - { - this.callback = callback; - this.task = this.DoRequestAsync(url); - } - - public void Cancel() - { - // FIXME: CancellationTokenSource not available on Mono? - // We should use it when it gets available. - this.callback = null; - } - - public bool Wait() - { - if (this.task.IsCompleted) - { - if (this.callback != null) - { - this.callback(this.task.Result); - this.callback = null; - } - } - - return this.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/Platform/FileSource.cs b/src/Platform/FileSource.cs new file mode 100644 index 0000000..5f94d9e --- /dev/null +++ b/src/Platform/FileSource.cs @@ -0,0 +1,149 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) 2016 Mapbox. All rights reserved. +// +//----------------------------------------------------------------------- + +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 + /// runtime to + /// asynchronously fetch data from the network via HTTP or HTTPS requests. + /// + /// + /// 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 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; + /// 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. + /// + /// Returns a that can be used for canceling a pending + /// request. This handle can be completely ignored if there is no intention of ever + /// canceling the request. + /// + public IAsyncRequest Request(string url, Action callback, int timeout = 10) { + if (_accessToken != null) { + url += "?access_token=" + _accessToken; + } + + // TODO: + // * add queue for requests + // * evaluate rate limits (headers and status code) + // * throttle requests accordingly + //var request = new HTTPRequest(url, callback); + //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) { + 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; } + callback(response); + lock (_lock) { + //another place to catch if request has been cancelled + try { + _requests.Remove(response.Request); + } + catch (Exception ex) { + System.Diagnostics.Debug.WriteLine(ex); + } + } + }); + lock (_lock) { + //sometimes we get here after the request has already finished + if (!request.IsCompleted) { + _requests.Add(request, 0); + } + } + //yield return request; + return request; + } + + + /// + /// 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 (((IAsyncRequest)req.Key).IsCompleted) { + // another place to watch out if request has been cancelled + try { + _requests.Remove(req.Key); + } + catch (Exception ex) { + System.Diagnostics.Debug.WriteLine(ex); + } + } + } + } + +#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(waitTimeMs); + resetEvent.Set(); + }), null); + resetEvent.WaitOne(); + resetEvent.Close(); + resetEvent = null; + +#else + System.Threading.Tasks.Task.Delay(waitTimeMs).Wait(); +#endif + } + } + + + + + + } +} diff --git a/src/Platform/HTTPRequestNonThreaded.cs b/src/Platform/HTTPRequestNonThreaded.cs new file mode 100644 index 0000000..d6a35e9 --- /dev/null +++ b/src/Platform/HTTPRequestNonThreaded.cs @@ -0,0 +1,238 @@ +#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 + +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 int _timeOut; + private string _requestUrl; + private readonly string _userAgent = "mapbox-sdk-cs"; + + + /// + /// + /// + /// + /// + /// seconds + public HTTPRequestNonThreaded(string url, Action callback, int timeOut = 10) { + + IsCompleted = false; + _callback = callback; + _timeOut = timeOut; + _requestUrl = url; + + setupRequest(); + + Action a = () => { getResponseNonThreaded(_request, EvaluateResponse); }; + //Fire and forget ;-) + a.BeginInvoke(a.EndInvoke, null); + } + + + 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 = await Response.FromWebResponse(this, apiResponse, apiEx); + + // post (async) callback back to the main/UI thread + // Unity: SynchronizationContext doesn't do anything + // use the Dispatcher +#if !UNITY + _sync.Post(delegate { callCallbackAndcleanUp(response); }, null); +#else + UnityToolbag.Dispatcher.InvokeAsync(() => { callCallbackAndcleanUp(response); }); +#endif + } + +#endif + + +#if !NETFX_CORE + private void getResponseNonThreaded(HttpWebRequest request, Action gotResponse) { + + HttpWebResponse response = null; + try { + response = (HttpWebResponse)request.GetResponse(); + gotResponse(response, null); + } + 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) { + gotResponse(null, wex); + } else { + gotResponse(hwr, wex); + } + } + } + catch (Exception ex) { + gotResponse(response, ex); + } + } + + + private void EvaluateResponse(HttpWebResponse apiResponse, Exception apiEx) { + + var response = Response.FromWebResponse(this, apiResponse, apiEx); + +#if !UNITY + // post (async) callback back to the main/UI thread + // Unity: SynchronizationContext doesn't do anything + // use the Dispatcher + _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 +#if UNITY_EDITOR + Mapbox.Unity.DispatcherEditor.InvokeAsync(() => { callCallbackAndcleanUp(response); }); +#endif + } +#endif + } +#endif + + + private void callCallbackAndcleanUp(Response response) { + _callback(response); + IsCompleted = true; + _callback = null; +#if NETFX_CORE + if (null != _request) { + _request.Dispose(); + _request = null; + } +#endif + } + + + public void Cancel() { + +#if !NETFX_CORE + if (null != _request) { + _request.Abort(); + } +#else + _cancellationTokenSource.Cancel(); +#endif + } + + + } +} +#endif \ No newline at end of file diff --git a/src/Platform/HTTPRequestThreaded.cs b/src/Platform/HTTPRequestThreaded.cs new file mode 100644 index 0000000..c8274b5 --- /dev/null +++ b/src/Platform/HTTPRequestThreaded.cs @@ -0,0 +1,318 @@ +//----------------------------------------------------------------------- +// +// 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 + +#if !UNITY + +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(); + getResponseAsync(_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 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); + } + + } + + + private async void EvaluateResponse(HttpResponseMessage apiResponse, Exception apiEx) { + + var response = await Response.FromWebResponse(this, apiResponse, apiEx); + + // 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 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 = () => { + 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 = Response.FromWebResponse(this, apiResponse,apiEx); + + // 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 + // 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; + } +#endif + }); + } else { // Unity is in Edit Mode +#if UNITY_EDITOR + Mapbox.Unity.DispatcherEditor.InvokeAsync(() => { + _callback(response); + IsCompleted = true; + _callback = null; +#if NETFX_CORE + if (null != _request) { + _request.Dispose(); + _request = null; + } +#endif + }); +#endif + + } +#endif + } +#endif + + + + public void Cancel() { + +#if !NETFX_CORE + if (null != _request) { + _request.Abort(); + } +#else + _cancellationTokenSource.Cancel(); +#endif + } + + + } +} + + +#endif \ No newline at end of file diff --git a/src/Platform/IAsyncRequest.cs b/src/Platform/IAsyncRequest.cs index 1e85342..ae08c4a 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/IAsyncRequestFactory.cs b/src/Platform/IAsyncRequestFactory.cs new file mode 100644 index 0000000..7067a61 --- /dev/null +++ b/src/Platform/IAsyncRequestFactory.cs @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------- +// +// 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; + + /// A handle to an asynchronous request. + public static class IAsyncRequestFactory { + + public static IAsyncRequest CreateRequest(string url, Action callback, int timeout = 10) { +#if !UNITY + if (Environment.ProcessorCount > 2) { + return new HTTPRequestThreaded(url, callback, timeout); + } else { + return new HTTPRequestNonThreaded(url, callback, timeout); + } +#else + return new Mapbox.Unity.Utilities.HTTPRequest(url, callback, timeout); +#endif + } + + + } +} \ No newline at end of file diff --git a/src/Platform/IFileSource.cs b/src/Platform/IFileSource.cs index 2698201..e1e9699 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, int timeout = 10); } } \ No newline at end of file diff --git a/src/Platform/Platform.csproj b/src/Platform/Platform.csproj index 792866b..32507d1 100644 --- a/src/Platform/Platform.csproj +++ b/src/Platform/Platform.csproj @@ -41,6 +41,10 @@ Properties\SharedAssemblyInfo.cs + + + + diff --git a/src/Platform/PlatformUWP.csproj b/src/Platform/PlatformUWP.csproj index 0f4452e..c0e0b73 100644 --- a/src/Platform/PlatformUWP.csproj +++ b/src/Platform/PlatformUWP.csproj @@ -68,7 +68,11 @@ Properties\SharedAssemblyInfo.cs + + + + diff --git a/src/Platform/Response.cs b/src/Platform/Response.cs index 613a620..99fedb1 100644 --- a/src/Platform/Response.cs +++ b/src/Platform/Response.cs @@ -4,20 +4,285 @@ // //----------------------------------------------------------------------- -using System.Collections.Generic; +#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 -namespace Mapbox.Platform -{ /// A response from a request. - public struct Response - { - /// Error description, set on error, empty otherwise. - public string Error; + public class Response { + + + private Response() { } + + + public IAsyncRequest Request { get; private set; } + + + 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; } + } + + + 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; + /// Exceptions that might have occured during the request. + public ReadOnlyCollection Exceptions { + get { return null == _exceptions ? null : _exceptions.AsReadOnly(); } + } + + + /// 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; + /// Raw data fetched from the request. public byte[] Data; + + public void AddException(Exception ex) { + if (null == _exceptions) { _exceptions = new List(); } + _exceptions.Add(ex); + } + + +#if !NETFX_CORE && !UNITY // full .NET Framework + 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 //UWP but not 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 // within Unity or UWP build from 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.")); + } + +#if NETFX_CORE + StringComparison stringComp = StringComparison.OrdinalIgnoreCase; +#else + StringComparison stringComp = StringComparison.InvariantCultureIgnoreCase; +#endif + + 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", stringComp)) { + int limitInterval; + if (int.TryParse(val, out limitInterval)) { response.XRateLimitInterval = limitInterval; } + } 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", stringComp)) { + double unixTimestamp; + if (double.TryParse(val, out unixTimestamp)) { + response.XRateLimitReset = UnixTimestampUtils.From(unixTimestamp); + } + } else if (key.Equals("Content-Type", stringComp)) { + 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 diff --git a/src/Utils/UnixTimestampUtils.cs b/src/Utils/UnixTimestampUtils.cs new file mode 100644 index 0000000..db6754b --- /dev/null +++ b/src/Utils/UnixTimestampUtils.cs @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------- +// +// 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 { + + // 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/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/CompressionTest.cs b/test/UnitTest/CompressionTest.cs index f455180..948d1b8 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,22 +48,25 @@ 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(); + //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/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/FileSourceMockApiTest.cs b/test/UnitTest/FileSourceMockApiTest.cs new file mode 100644 index 0000000..fdd572a --- /dev/null +++ b/test/UnitTest/FileSourceMockApiTest.cs @@ -0,0 +1,194 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) 2017 Mapbox. All rights reserved. +// +//----------------------------------------------------------------------- + + +namespace Mapbox.UnitTest { + using HttpMock; + using Mapbox.Json; + using Mapbox.Utils; + using Mapbox.Utils.JsonConverters; + using NUnit.Framework; + using Platform; + using System; + using System.Net; + + + [TestFixture] + internal class FileSourceMockApiTest { + + + private static readonly string _mockBaseUrl = "http://localhost:2345"; + private FileSource _fs = new FileSource(); + IHttpServer _mockApi; + + private struct _testUrl { + public static string simpleJson = "/testmock1"; + public static string customStatusCode = "/testmock2"; + public static string rateLimitHit = "/ratelimithit"; + public static string xrateheader = "/xrateheader"; + public static string cancel = "/cancel"; + } + + [OneTimeTearDown] + public void Finished() { + if (null == _mockApi) { return; } + _mockApi.Dispose(); + _mockApi = null; + } + + + [OneTimeSetUp] + public void SetupMockHttp() { + + var serverFactory = new HttpServerFactory(); + _mockApi = serverFactory.Get(new Uri(_mockBaseUrl)).WithNewContext(); + + _mockApi.Start(); + + _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.customStatusCode)).WithStatus((HttpStatusCode)451); + + _mockApi.Stub(r => r.Get(_testUrl.rateLimitHit)) + .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") + .AddHeader("X-Rate-Limit-Limit", "100000") + .AddHeader("X-Rate-Limit-Reset", unixTimestamp.ToString()) + .OK(); + + } + + + [Test] + public void SimpleJson() { + + _fs.Request( + _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"); + 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(); + + + _fs.Request( + _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"); + } + ); + + _fs.WaitForAllRequests(); + } + + + [Test] + public void RateLimitHit() { + + _fs.Request( + _mockBaseUrl + _testUrl.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.NotNull(r.Exceptions, "request did not set any exceptions"); + Assert.GreaterOrEqual(r.Exceptions.Count, 1, "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"); + } + ); + + _fs.WaitForAllRequests(); + } + + + [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(); + } + + + [Test] + public void NonThreaded() { + + _fs.Request( + _mockBaseUrl + _testUrl.simpleJson + , (Response r) => { + Assert.AreEqual(200, r.StatusCode); + } + ); + + _fs.WaitForAllRequests(); + } + + + [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 diff --git a/test/UnitTest/FileSourceTest.cs b/test/UnitTest/FileSourceTest.cs index f755f7a..f187d1f 100644 --- a/test/UnitTest/FileSourceTest.cs +++ b/test/UnitTest/FileSourceTest.cs @@ -4,109 +4,109 @@ // //----------------------------------------------------------------------- -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; + using System.Net; + + + [TestFixture] + internal class FileSourceTest { + private const string _url = "https://api.mapbox.com/geocoding/v5/mapbox.places/helsinki.json"; + private FileSource _fs; + + + [SetUp] + public void SetUp() { + _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() { + _fs.Request( + _url, + (Response res) => { + Assert.IsNotNull(res.Data, "No data received from the servers."); + }); + + _fs.WaitForAllRequests(); + } + + + [Test] + public void MultipleRequests() { + int count = 0; + + _fs.Request(_url, (Response res) => ++count); + _fs.Request(_url, (Response res) => ++count); + _fs.Request(_url, (Response res) => ++count); + + _fs.WaitForAllRequests(); + + Assert.AreEqual(count, 3, "Should have received 3 replies."); + } + + + [Test] + public void RequestCancel() { + var request = _fs.Request( + _url, + (Response res) => { + Assert.IsTrue(res.HasError); + WebException wex = res.Exceptions[0] as WebException; + Assert.IsNotNull(wex); + Assert.AreEqual(wex.Status, WebExceptionStatus.RequestCanceled); + }); + + request.Cancel(); + + _fs.WaitForAllRequests(); + } + + + [Test] + public void RequestDnsError() { + _fs.Request( + "https://dnserror.shouldnotwork", + (Response res) => { + Assert.IsTrue(res.HasError); + }); + + _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. + _fs.Request( + "https://mapbox.com/forbidden", + (Response res) => { + Assert.IsTrue(res.HasError); + }); + + _fs.WaitForAllRequests(); + } + + + [Test] + public void WaitWithNoRequests() { + // This should simply not block. + _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..bffaa4a 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 @@ -58,8 +70,10 @@ ..\..\packages\NUnit.3.5.0\lib\net45\nunit.framework.dll + + @@ -90,10 +104,6 @@ {89F8BAB2-2E7A-425E-B715-2CC4519D561F} Geocoding - - {346B1208-1587-490B-A7DE-A96B86E81CD6} - Mono - {FE49745C-01F6-4A3F-BF08-828113D3E19F} Platform @@ -108,6 +118,7 @@ + diff --git a/test/UnitTest/Utils.cs b/test/UnitTest/Utils.cs index 5cf0cac..4e4815c 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,62 +56,65 @@ 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); } } } - internal class MockFileSource : IFileSource { - private Dictionary responses = new Dictionary(); - private List requests = new List(); - - public IAsyncRequest Request(string uri, Action callback) { - 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; + // } + // } + //} } } diff --git a/test/UnitTest/VectorTileTest.cs b/test/UnitTest/VectorTileTest.cs index feda959..3038d0c 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 Mono.FileSource fs; + + + private FileSource _fs; + [SetUp] public void SetUp() { - this.fs = new Mono.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,37 +59,39 @@ public void ParseSuccess() { map.Unsubscribe(mapObserver); } - [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(); + //[Test] + //public void ParseFailure() { + // var resource = TileResource.MakeVector(new CanonicalTileId(13, 5465, 2371), null); - var mockFs = new Utils.MockFileSource(); - mockFs.SetReponse(resource.GetUrl(), response); + // var response = new Response(); + // response.Data = Enumerable.Repeat((byte)0, 5000).ToArray(); - var map = new Map(mockFs); + // var mockFs = new Utils.MockFileSource(); + // mockFs.SetReponse(resource.GetUrl(), response); - var mapObserver = new Utils.VectorMapObserver(); - map.Subscribe(mapObserver); + // var map = new Map(mockFs); - map.Center = new Vector2d(60.163200, 60.163200); - map.Zoom = 13; - map.Update(); + // var mapObserver = new Utils.VectorMapObserver(); + // map.Subscribe(mapObserver); - mockFs.WaitForAllRequests(); + // map.Center = new Vector2d(60.163200, 60.163200); + // map.Zoom = 13; + // map.Update(); - // TODO: Assert.AreEqual("Parse error.", mapObserver.Error); - Assert.AreEqual(1, mapObserver.Tiles.Count); - Assert.IsNull(mapObserver.Tiles[0].Data); + // mockFs.WaitForAllRequests(); + + // // 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] 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); } + + } } 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 3546b28..2850921 100644 --- a/test/UnitTest/packages.config +++ b/test/UnitTest/packages.config @@ -1,5 +1,8 @@  + + +