20240422
This commit is contained in:
commit
e0afef912e
1285 files changed
+83219
No files matched your search
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 15f35c4d53ec8ae459a497d0bee43f77
|
||||
folderAsset: yes
|
||||
timeCreated: 1495030845
|
||||
licenseType: Pro
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
namespace Mapbox.Platform.Cache
|
||||
{
|
||||
|
||||
using System;
|
||||
|
||||
|
||||
public class CacheItem
|
||||
{
|
||||
/// <summary> Raw response data- </summary>
|
||||
public byte[] Data;
|
||||
/// <summary> UTC ticks when item was added to the cache. </summary>
|
||||
public long AddedToCacheTicksUtc;
|
||||
/// <summary> ETag value of API response. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag </summary>
|
||||
public string ETag;
|
||||
/// <summary> Can be 'null' as not all APIs populated this value. Last-Modified value of API response in GMT: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified </summary>
|
||||
public DateTime? LastModified;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3bad456c58de85d47b4ff050dbd99915
|
||||
timeCreated: 1510665832
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,321 @@
|
||||
namespace Mapbox.Platform.Cache
|
||||
{
|
||||
using System;
|
||||
using Mapbox.Platform;
|
||||
using System.Collections.Generic;
|
||||
using Mapbox.Unity.Utilities;
|
||||
using Mapbox.Map;
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
|
||||
|
||||
public class CachingWebFileSource : IFileSource, IDisposable
|
||||
{
|
||||
|
||||
|
||||
#if MAPBOX_DEBUG_CACHE
|
||||
private string _className;
|
||||
#endif
|
||||
private bool _disposed;
|
||||
private List<ICache> _caches = new List<ICache>();
|
||||
private string _accessToken;
|
||||
private Func<string> _getMapsSkuToken;
|
||||
private bool _autoRefreshCache;
|
||||
|
||||
|
||||
public CachingWebFileSource(string accessToken, Func<string> getMapsSkuToken, bool autoRefreshCache)
|
||||
{
|
||||
#if MAPBOX_DEBUG_CACHE
|
||||
_className = this.GetType().Name;
|
||||
#endif
|
||||
_accessToken = accessToken;
|
||||
_getMapsSkuToken = getMapsSkuToken;
|
||||
_autoRefreshCache = autoRefreshCache;
|
||||
}
|
||||
|
||||
|
||||
#region idisposable
|
||||
|
||||
|
||||
~CachingWebFileSource()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposeManagedResources)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposeManagedResources)
|
||||
{
|
||||
for (int i = 0; i < _caches.Count; i++)
|
||||
{
|
||||
IDisposable cache = _caches[i] as IDisposable;
|
||||
if (null != cache)
|
||||
{
|
||||
cache.Dispose();
|
||||
cache = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Add an ICache instance
|
||||
/// </summary>
|
||||
/// <param name="cache">Implementation of ICache</param>
|
||||
/// <returns></returns>
|
||||
public CachingWebFileSource AddCache(ICache cache)
|
||||
{
|
||||
// don't add cache when cache size is 0
|
||||
if (0 == cache.MaxCacheSize)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
_caches.Add(cache);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Clear all caches
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var cache in _caches)
|
||||
{
|
||||
cache.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void ReInit() {
|
||||
foreach (var cache in _caches)
|
||||
{
|
||||
cache.ReInit();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public IAsyncRequest Request(
|
||||
string uri
|
||||
, Action<Response> callback
|
||||
, int timeout = 10
|
||||
, CanonicalTileId tileId = new CanonicalTileId()
|
||||
, string tilesetId = null
|
||||
)
|
||||
{
|
||||
|
||||
if (string.IsNullOrEmpty(tilesetId))
|
||||
{
|
||||
throw new Exception("Cannot cache without a tileset id");
|
||||
}
|
||||
|
||||
CacheItem cachedItem = null;
|
||||
|
||||
// go through existing caches and check if we already have the requested tile available
|
||||
foreach (var cache in _caches)
|
||||
{
|
||||
cachedItem = cache.Get(tilesetId, tileId);
|
||||
if (null != cachedItem)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var uriBuilder = new UriBuilder(uri);
|
||||
if (!string.IsNullOrEmpty(_accessToken))
|
||||
{
|
||||
string accessTokenQuery = "access_token=" + _accessToken;
|
||||
string mapsSkuToken = "sku=" + _getMapsSkuToken();
|
||||
if (uriBuilder.Query != null && uriBuilder.Query.Length > 1)
|
||||
{
|
||||
uriBuilder.Query = uriBuilder.Query.Substring(1) + "&" + accessTokenQuery + "&" + mapsSkuToken;
|
||||
}
|
||||
else
|
||||
{
|
||||
uriBuilder.Query = accessTokenQuery + "&" + mapsSkuToken;
|
||||
}
|
||||
}
|
||||
string finalUrl = uriBuilder.ToString();
|
||||
|
||||
#if MAPBOX_DEBUG_CACHE
|
||||
string methodName = _className + "." + new System.Diagnostics.StackFrame().GetMethod().Name;
|
||||
#endif
|
||||
|
||||
// if tile was available call callback with it, propagate to all other caches and check if a newer one is available
|
||||
if (null != cachedItem)
|
||||
{
|
||||
#if MAPBOX_DEBUG_CACHE
|
||||
UnityEngine.Debug.LogFormat("{0} {1} {2} {3}", methodName, tilesetId, tileId, null != cachedItem.Data ? cachedItem.Data.Length.ToString() : "cachedItem.Data is NULL");
|
||||
#endif
|
||||
// immediately return cached tile
|
||||
callback(Response.FromCache(cachedItem.Data));
|
||||
|
||||
// check for updated tiles online if this is enabled in the settings
|
||||
if (_autoRefreshCache)
|
||||
{
|
||||
// check if tile on the web is newer than the one we already have locally
|
||||
IAsyncRequestFactory.CreateRequest(
|
||||
finalUrl,
|
||||
(Response headerOnly) =>
|
||||
{
|
||||
// on error getting information from API just return. tile we have locally has already been returned above
|
||||
if (headerOnly.HasError)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: remove Debug.Log before PR
|
||||
//UnityEngine.Debug.LogFormat(
|
||||
// "{1}{0}cached:{2}{0}header:{3}"
|
||||
// , Environment.NewLine
|
||||
// , finalUrl
|
||||
// , cachedItem.ETag
|
||||
// , headerOnly.Headers["ETag"]
|
||||
//);
|
||||
|
||||
// data from cache is the same as on the web:
|
||||
// * tile has already been returned above
|
||||
// * make sure all all other caches have it too, but don't force insert via cache.add(false)
|
||||
// additional ETag empty check: for backwards compability with old caches
|
||||
if (!string.IsNullOrEmpty(cachedItem.ETag) && cachedItem.ETag.Equals(headerOnly.Headers["ETag"]))
|
||||
{
|
||||
foreach (var cache in _caches)
|
||||
{
|
||||
cache.Add(tilesetId, tileId, cachedItem, false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: remove Debug.Log before PR
|
||||
UnityEngine.Debug.LogWarningFormat(
|
||||
"updating cached tile {1} tilesetId:{2}{0}cached etag:{3}{0}remote etag:{4}{0}{5}"
|
||||
, Environment.NewLine
|
||||
, tileId
|
||||
, tilesetId
|
||||
, cachedItem.ETag
|
||||
, headerOnly.Headers["ETag"]
|
||||
, finalUrl
|
||||
);
|
||||
|
||||
// request updated tile and pass callback to return new data to subscribers
|
||||
requestTileAndCache(finalUrl, tilesetId, tileId, timeout, callback);
|
||||
}
|
||||
}
|
||||
, timeout
|
||||
, HttpRequestType.Head
|
||||
);
|
||||
}
|
||||
|
||||
return new MemoryCacheAsyncRequest(uri);
|
||||
}
|
||||
else
|
||||
{
|
||||
// requested tile is not in any of the caches yet, get it
|
||||
#if MAPBOX_DEBUG_CACHE
|
||||
UnityEngine.Debug.LogFormat("{0} {1} {2} not cached", methodName, tilesetId, tileId);
|
||||
#endif
|
||||
return requestTileAndCache(finalUrl, tilesetId, tileId, timeout, callback);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private IAsyncRequest requestTileAndCache(string url, string tilesetId, CanonicalTileId tileId, int timeout, Action<Response> callback)
|
||||
{
|
||||
return IAsyncRequestFactory.CreateRequest(
|
||||
url,
|
||||
(Response r) =>
|
||||
{
|
||||
// if the request was successful add tile to all caches
|
||||
if (!r.HasError && null != r.Data)
|
||||
{
|
||||
string eTag = string.Empty;
|
||||
DateTime? lastModified = null;
|
||||
|
||||
if (!r.Headers.ContainsKey("ETag"))
|
||||
{
|
||||
UnityEngine.Debug.LogWarningFormat("no 'ETag' header present in response for {0}", url);
|
||||
}
|
||||
else
|
||||
{
|
||||
eTag = r.Headers["ETag"];
|
||||
}
|
||||
|
||||
// not all APIs populate 'Last-Modified' header
|
||||
// don't log error if it's missing
|
||||
if (r.Headers.ContainsKey("Last-Modified"))
|
||||
{
|
||||
lastModified = DateTime.ParseExact(r.Headers["Last-Modified"], "r", null);
|
||||
}
|
||||
|
||||
// propagate to all caches forcing update
|
||||
foreach (var cache in _caches)
|
||||
{
|
||||
cache.Add(
|
||||
tilesetId
|
||||
, tileId
|
||||
, new CacheItem()
|
||||
{
|
||||
Data = r.Data,
|
||||
ETag = eTag,
|
||||
LastModified = lastModified
|
||||
}
|
||||
, true // force insert/update
|
||||
);
|
||||
}
|
||||
}
|
||||
if (null != callback)
|
||||
{
|
||||
r.IsUpdate = true;
|
||||
callback(r);
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
|
||||
class MemoryCacheAsyncRequest : IAsyncRequest
|
||||
{
|
||||
|
||||
|
||||
public string RequestUrl { get; private set; }
|
||||
|
||||
|
||||
public MemoryCacheAsyncRequest(string requestUrl)
|
||||
{
|
||||
RequestUrl = requestUrl;
|
||||
}
|
||||
|
||||
|
||||
public bool IsCompleted
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public HttpRequestType RequestType { get { return HttpRequestType.Get; } }
|
||||
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
// Empty. We can't cancel an instantaneous response.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 149bb7da83149324dab3982be7bc8cca
|
||||
timeCreated: 1494581732
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,53 @@
|
||||
|
||||
namespace Mapbox.Platform.Cache
|
||||
{
|
||||
|
||||
using Mapbox.Map;
|
||||
using System;
|
||||
|
||||
|
||||
public interface ICache
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of tiles to store
|
||||
/// </summary>
|
||||
uint MaxCacheSize { get; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Add tile data to the cache
|
||||
/// </summary>
|
||||
/// <param name="tilesetId">Tile set name</param>
|
||||
/// <param name="tileId</param>
|
||||
/// <param name="item">Item to cache</param>
|
||||
/// <param name="replaceIfExists">Force insert even if item already exists.</param>
|
||||
void Add(string tilesetId, CanonicalTileId tileId, CacheItem item, bool replaceIfExists);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get tile
|
||||
/// </summary>
|
||||
/// <param name="tilesetId"></param>
|
||||
/// <param name="tileId"></param>
|
||||
/// <returns>byte[] with tile data. Null if requested tile is not in cache</returns>
|
||||
CacheItem Get(string tilesetId, CanonicalTileId tileId);
|
||||
|
||||
|
||||
/// <summary>Clear cache for all tile sets</summary>
|
||||
void Clear();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Clear cache for one tile set
|
||||
/// </summary>
|
||||
/// <param name="tilesetId"></param>
|
||||
void Clear(string tilesetId);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Reinitialize cache. Might be needed after 'Clear', eg for SQLiteCache
|
||||
/// </summary>
|
||||
void ReInit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ebb2d197f248a7469620904e2adb43e
|
||||
timeCreated: 1494580896
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,112 @@
|
||||
using Mapbox.Map;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
|
||||
namespace Mapbox.Platform.Cache
|
||||
{
|
||||
|
||||
|
||||
public class MemoryCache : ICache
|
||||
{
|
||||
|
||||
|
||||
// TODO: add support for disposal strategy (timestamp, distance, etc.)
|
||||
public MemoryCache(uint maxCacheSize)
|
||||
{
|
||||
#if MAPBOX_DEBUG_CACHE
|
||||
_className = this.GetType().Name;
|
||||
#endif
|
||||
_maxCacheSize = maxCacheSize;
|
||||
_cachedResponses = new Dictionary<string, CacheItem>();
|
||||
}
|
||||
|
||||
|
||||
#if MAPBOX_DEBUG_CACHE
|
||||
private string _className;
|
||||
#endif
|
||||
private uint _maxCacheSize;
|
||||
private object _lock = new object();
|
||||
private Dictionary<string, CacheItem> _cachedResponses;
|
||||
|
||||
|
||||
public uint MaxCacheSize
|
||||
{
|
||||
get { return _maxCacheSize; }
|
||||
}
|
||||
|
||||
|
||||
public void ReInit()
|
||||
{
|
||||
_cachedResponses = new Dictionary<string, CacheItem>();
|
||||
}
|
||||
|
||||
|
||||
public void Add(string mapdId, CanonicalTileId tilesetId, CacheItem item, bool forceInsert)
|
||||
{
|
||||
string key = mapdId + "||" + tilesetId;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_cachedResponses.Count >= _maxCacheSize)
|
||||
{
|
||||
_cachedResponses.Remove(_cachedResponses.OrderBy(c => c.Value.AddedToCacheTicksUtc).First().Key);
|
||||
}
|
||||
|
||||
// TODO: forceInsert
|
||||
if (!_cachedResponses.ContainsKey(key))
|
||||
{
|
||||
item.AddedToCacheTicksUtc = DateTime.UtcNow.Ticks;
|
||||
_cachedResponses.Add(key, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public CacheItem Get(string tilesetId, CanonicalTileId tileId)
|
||||
{
|
||||
string key = tilesetId + "||" + tileId;
|
||||
|
||||
#if MAPBOX_DEBUG_CACHE
|
||||
string methodName = _className + "." + new System.Diagnostics.StackFrame().GetMethod().Name;
|
||||
UnityEngine.Debug.LogFormat("{0} {1}", methodName, key);
|
||||
#endif
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_cachedResponses.ContainsKey(key))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _cachedResponses[key];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_cachedResponses.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void Clear(string tilesetId)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
tilesetId += "||";
|
||||
List<string> toDelete = _cachedResponses.Keys.Where(k => k.Contains(tilesetId)).ToList();
|
||||
foreach (string key in toDelete)
|
||||
{
|
||||
_cachedResponses.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2bd70b309ec2fd645af3e189d1d7ea5f
|
||||
timeCreated: 1494580911
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c6f1c408c0acc43538b678d17f6ea53d
|
||||
folderAsset: yes
|
||||
timeCreated: 1497883478
|
||||
licenseType: Pro
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,485 @@
|
||||
namespace Mapbox.Platform.Cache
|
||||
{
|
||||
|
||||
using Mapbox.Map;
|
||||
using Mapbox.Utils;
|
||||
using SQLite4Unity3d;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
public class SQLiteCache : ICache, IDisposable
|
||||
{
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// maximum number tiles that get cached
|
||||
/// </summary>
|
||||
public uint MaxCacheSize { get { return _maxTileCount; } }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Check cache size every n inserts
|
||||
/// </summary>
|
||||
public uint PruneCacheDelta { get { return _pruneCacheDelta; } }
|
||||
|
||||
|
||||
#if MAPBOX_DEBUG_CACHE
|
||||
private string _className;
|
||||
#endif
|
||||
private bool _disposed;
|
||||
private string _dbName;
|
||||
private string _dbPath;
|
||||
private SQLiteConnection _sqlite;
|
||||
private readonly uint _maxTileCount;
|
||||
/// <summary>check cache size only every '_pruneCacheDelta' calls to 'Add()' to avoid being too chatty with the database</summary>
|
||||
private const int _pruneCacheDelta = 20;
|
||||
/// <summary>counter to keep track of calls to `Add()`</summary>
|
||||
private int _pruneCacheCounter = 0;
|
||||
private object _lock = new object();
|
||||
|
||||
|
||||
public SQLiteCache(uint? maxTileCount = null, string dbName = "cache.db")
|
||||
{
|
||||
_maxTileCount = maxTileCount ?? 3000;
|
||||
_dbName = dbName;
|
||||
init();
|
||||
}
|
||||
|
||||
|
||||
#region idisposable
|
||||
|
||||
|
||||
~SQLiteCache()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposeManagedResources)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposeManagedResources)
|
||||
{
|
||||
if (null != _sqlite)
|
||||
{
|
||||
_sqlite.Execute("VACUUM;"); // compact db to keep file size small
|
||||
_sqlite.Close();
|
||||
_sqlite.Dispose();
|
||||
_sqlite = null;
|
||||
}
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
private void init()
|
||||
{
|
||||
|
||||
#if MAPBOX_DEBUG_CACHE
|
||||
_className = this.GetType().Name;
|
||||
#endif
|
||||
openOrCreateDb(_dbName);
|
||||
|
||||
//hrmpf: multiple PKs not supported by sqlite.net
|
||||
//https://github.com/praeclarum/sqlite-net/issues/282
|
||||
//do it via plain SQL
|
||||
|
||||
List<SQLiteConnection.ColumnInfo> colInfoTileset = _sqlite.GetTableInfo(typeof(tilesets).Name);
|
||||
if (0 == colInfoTileset.Count)
|
||||
{
|
||||
string cmdCreateTableTilesets = @"CREATE TABLE tilesets(
|
||||
id INTEGER PRIMARY KEY ASC AUTOINCREMENT NOT NULL UNIQUE,
|
||||
name STRING NOT NULL
|
||||
);";
|
||||
_sqlite.Execute(cmdCreateTableTilesets);
|
||||
string cmdCreateIdxNames = @"CREATE UNIQUE INDEX idx_names ON tilesets (name ASC);";
|
||||
_sqlite.Execute(cmdCreateIdxNames);
|
||||
}
|
||||
|
||||
List<SQLiteConnection.ColumnInfo> colInfoTiles = _sqlite.GetTableInfo(typeof(tiles).Name);
|
||||
if (0 == colInfoTiles.Count)
|
||||
{
|
||||
|
||||
string cmdCreateTableTiles = @"CREATE TABLE tiles(
|
||||
tile_set INTEGER REFERENCES tilesets (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
zoom_level INTEGER NOT NULL,
|
||||
tile_column BIGINT NOT NULL,
|
||||
tile_row BIGINT NOT NULL,
|
||||
tile_data BLOB NOT NULL,
|
||||
timestamp INTEGER NOT NULL,
|
||||
etag TEXT,
|
||||
lastmodified INTEGER,
|
||||
PRIMARY KEY(
|
||||
tile_set ASC,
|
||||
zoom_level ASC,
|
||||
tile_column ASC,
|
||||
tile_row ASC
|
||||
)
|
||||
);";
|
||||
_sqlite.Execute(cmdCreateTableTiles);
|
||||
|
||||
string cmdIdxTileset = "CREATE INDEX idx_tileset ON tiles (tile_set ASC);";
|
||||
_sqlite.Execute(cmdIdxTileset);
|
||||
string cmdIdxTimestamp = "CREATE INDEX idx_timestamp ON tiles (timestamp ASC);";
|
||||
_sqlite.Execute(cmdIdxTimestamp);
|
||||
}
|
||||
|
||||
|
||||
// some pragmas to speed things up a bit :-)
|
||||
// inserting 1,000 tiles takes 1-2 sec as opposed to ~20 sec
|
||||
string[] cmds = new string[]
|
||||
{
|
||||
"PRAGMA synchronous=OFF",
|
||||
"PRAGMA count_changes=OFF",
|
||||
"PRAGMA journal_mode=MEMORY",
|
||||
"PRAGMA temp_store=MEMORY"
|
||||
};
|
||||
foreach (var cmd in cmds)
|
||||
{
|
||||
try
|
||||
{
|
||||
_sqlite.Execute(cmd);
|
||||
}
|
||||
catch (SQLiteException ex)
|
||||
{
|
||||
// workaround for sqlite.net's exeception:
|
||||
// https://stackoverflow.com/a/23839503
|
||||
if (ex.Result != SQLite3.Result.Row)
|
||||
{
|
||||
UnityEngine.Debug.LogErrorFormat("{0}: {1}", cmd, ex);
|
||||
// TODO: when mapbox-sdk-cs gets backported to its own repo -> throw
|
||||
//throw; // to throw or not to throw???
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void openOrCreateDb(string dbName)
|
||||
{
|
||||
_dbPath = GetFullDbPath(dbName);
|
||||
_sqlite = new SQLiteConnection(_dbPath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// <para>Reinitialize cache.</para>
|
||||
/// <para>This is needed after 'Clear()' to recreate the cache database.</para>
|
||||
/// <para>And has been implemented on purpose to not hold on to any references to the cache directory after 'Clear()'</para>
|
||||
/// </summary>
|
||||
public void ReInit()
|
||||
{
|
||||
if (null != _sqlite)
|
||||
{
|
||||
_sqlite.Dispose();
|
||||
_sqlite = null;
|
||||
}
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
|
||||
public static string GetFullDbPath(string dbName)
|
||||
{
|
||||
string dbPath = Path.Combine(Application.persistentDataPath, "cache");
|
||||
#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_WSA
|
||||
dbPath = Path.GetFullPath(dbPath);
|
||||
#endif
|
||||
if (!Directory.Exists(dbPath)) { Directory.CreateDirectory(dbPath); }
|
||||
dbPath = Path.Combine(dbPath, dbName);
|
||||
|
||||
return dbPath;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void Add(string tilesetName, CanonicalTileId tileId, CacheItem item, bool forceInsert = false)
|
||||
{
|
||||
|
||||
#if MAPBOX_DEBUG_CACHE
|
||||
string methodName = _className + "." + new System.Diagnostics.StackFrame().GetMethod().Name;
|
||||
UnityEngine.Debug.LogFormat("{0} {1} {2} forceInsert:{3}", methodName, tileset, tileId, forceInsert);
|
||||
#endif
|
||||
try
|
||||
{
|
||||
// tile exists and we don't want to overwrite -> exit early
|
||||
if (
|
||||
TileExists(tilesetName, tileId)
|
||||
&& !forceInsert
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int? tilesetId = null;
|
||||
lock (_lock)
|
||||
{
|
||||
tilesetId = getTilesetId(tilesetName);
|
||||
if (!tilesetId.HasValue)
|
||||
{
|
||||
tilesetId = insertTileset(tilesetName);
|
||||
}
|
||||
}
|
||||
|
||||
if (tilesetId < 0)
|
||||
{
|
||||
Debug.LogErrorFormat("could not get tilesetID for [{0}] tile: {1}", tilesetName, tileId);
|
||||
return;
|
||||
}
|
||||
|
||||
int rowsAffected = _sqlite.InsertOrReplace(new tiles
|
||||
{
|
||||
tile_set = tilesetId.Value,
|
||||
zoom_level = tileId.Z,
|
||||
tile_column = tileId.X,
|
||||
tile_row = tileId.Y,
|
||||
tile_data = item.Data,
|
||||
timestamp = (int)UnixTimestampUtils.To(DateTime.Now),
|
||||
etag = item.ETag
|
||||
});
|
||||
if (1 != rowsAffected)
|
||||
{
|
||||
throw new Exception(string.Format("tile [{0} / {1}] was not inserted, rows affected:{2}", tilesetName, tileId, rowsAffected));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogErrorFormat("Error inserting {0} {1} {2} ", tilesetName, tileId, ex);
|
||||
}
|
||||
|
||||
// update counter only when new tile gets inserted
|
||||
if (!forceInsert)
|
||||
{
|
||||
_pruneCacheCounter++;
|
||||
}
|
||||
if (0 == _pruneCacheCounter % _pruneCacheDelta)
|
||||
{
|
||||
_pruneCacheCounter = 0;
|
||||
prune();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void prune()
|
||||
{
|
||||
|
||||
long tileCnt = _sqlite.ExecuteScalar<long>("SELECT COUNT(zoom_level) FROM tiles");
|
||||
|
||||
if (tileCnt < _maxTileCount) { return; }
|
||||
|
||||
long toDelete = tileCnt - _maxTileCount;
|
||||
|
||||
#if MAPBOX_DEBUG_CACHE
|
||||
string methodName = _className + "." + new System.Diagnostics.StackFrame().GetMethod().Name;
|
||||
Debug.LogFormat("{0} {1} about to prune()", methodName, _tileset);
|
||||
#endif
|
||||
|
||||
try
|
||||
{
|
||||
// no 'ORDER BY' or 'LIMIT' possible if sqlite hasn't been compiled with 'SQLITE_ENABLE_UPDATE_DELETE_LIMIT'
|
||||
// https://sqlite.org/compile.html#enable_update_delete_limit
|
||||
_sqlite.Execute("DELETE FROM tiles WHERE rowid IN ( SELECT rowid FROM tiles ORDER BY timestamp ASC LIMIT ? );", toDelete);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogErrorFormat("error pruning: {0}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the tile data, otherwise null
|
||||
/// </summary>
|
||||
/// <param name="tileId">Canonical tile id to identify the tile</param>
|
||||
/// <returns>tile data as byte[], if tile is not cached returns null</returns>
|
||||
public CacheItem Get(string tilesetName, CanonicalTileId tileId)
|
||||
{
|
||||
#if MAPBOX_DEBUG_CACHE
|
||||
string methodName = _className + "." + new System.Diagnostics.StackFrame().GetMethod().Name;
|
||||
Debug.LogFormat("{0} {1} {2}", methodName, _tileset, tileId);
|
||||
#endif
|
||||
tiles tile = null;
|
||||
|
||||
try
|
||||
{
|
||||
int? tilesetId = getTilesetId(tilesetName);
|
||||
if (!tilesetId.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
tile = _sqlite
|
||||
.Table<tiles>()
|
||||
.Where(t =>
|
||||
t.tile_set == tilesetId.Value
|
||||
&& t.zoom_level == tileId.Z
|
||||
&& t.tile_column == tileId.X
|
||||
&& t.tile_row == tileId.Y
|
||||
)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogErrorFormat("error getting tile {1} {2} from cache{0}{3}", Environment.NewLine, tilesetName, tileId, ex);
|
||||
return null;
|
||||
}
|
||||
if (null == tile)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
DateTime? lastModified = null;
|
||||
if (tile.lastmodified.HasValue) { lastModified = UnixTimestampUtils.From((double)tile.lastmodified.Value); }
|
||||
|
||||
return new CacheItem()
|
||||
{
|
||||
Data = tile.tile_data,
|
||||
AddedToCacheTicksUtc = tile.timestamp,
|
||||
ETag = tile.etag,
|
||||
LastModified = lastModified
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Check if tile exists
|
||||
/// </summary>
|
||||
/// <param name="tileId">Canonical tile id</param>
|
||||
/// <returns>True if tile exists</returns>
|
||||
public bool TileExists(string tilesetName, CanonicalTileId tileId)
|
||||
{
|
||||
int? tilesetId = getTilesetId(tilesetName);
|
||||
if (!tilesetId.HasValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return null != _sqlite
|
||||
.Table<tiles>()
|
||||
.Where(t =>
|
||||
t.tile_set == tilesetId.Value
|
||||
&& t.zoom_level == tileId.Z
|
||||
&& t.tile_column == tileId.X
|
||||
&& t.tile_row == tileId.Y
|
||||
)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
|
||||
private int insertTileset(string tilesetName)
|
||||
{
|
||||
try
|
||||
{
|
||||
_sqlite.BeginTransaction(true);
|
||||
tilesets newTileset = new tilesets { name = tilesetName };
|
||||
int rowsAffected = _sqlite.Insert(newTileset);
|
||||
if (1 != rowsAffected)
|
||||
{
|
||||
throw new Exception(string.Format("tileset [{0}] was not inserted, rows affected:{1}", tilesetName, rowsAffected));
|
||||
}
|
||||
return newTileset.id;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogErrorFormat("could not insert tileset [{0}]: {1}", tilesetName, ex);
|
||||
return -1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sqlite.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private int? getTilesetId(string tilesetName)
|
||||
{
|
||||
tilesets tileset = _sqlite
|
||||
.Table<tilesets>()
|
||||
.Where(ts => ts.name.Equals(tilesetName))
|
||||
.FirstOrDefault();
|
||||
return null == tileset ? (int?)null : tileset.id;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// FOR INTERNAL DEBUGGING ONLY - DON'T RELY ON IN PRODUCTION
|
||||
/// </summary>
|
||||
/// <param name="tilesetName"></param>
|
||||
/// <returns></returns>
|
||||
public long TileCount(string tilesetName)
|
||||
{
|
||||
int? tilesetId = getTilesetId(tilesetName);
|
||||
if (!tilesetId.HasValue) { return 0; }
|
||||
|
||||
return _sqlite
|
||||
.Table<tiles>()
|
||||
.Where(t => t.tile_set == tilesetId.Value)
|
||||
.LongCount();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Clear cache for one tile set
|
||||
/// </summary>
|
||||
/// <param name="tilesetName"></param>
|
||||
public void Clear(string tilesetName)
|
||||
{
|
||||
int? tilesetId = getTilesetId(tilesetName);
|
||||
if (!tilesetId.HasValue) { return; }
|
||||
//just delete on table 'tilesets', we've setup cascading which should take care of tabls 'tiles'
|
||||
_sqlite.Delete<tilesets>(tilesetId.Value);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// <para>Delete the database file.</para>
|
||||
/// <para>Call 'ReInit()' if you intend to continue using the cache after 'Clear()!</para>
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
//already disposed
|
||||
if (null == _sqlite) { return; }
|
||||
|
||||
_sqlite.Close();
|
||||
_sqlite.Dispose();
|
||||
_sqlite = null;
|
||||
|
||||
Debug.LogFormat("deleting {0}", _dbPath);
|
||||
|
||||
// try several times in case SQLite needs a bit more time to dispose
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(_dbPath);
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
#if !WINDOWS_UWP
|
||||
System.Threading.Thread.Sleep(100);
|
||||
#else
|
||||
System.Threading.Tasks.Task.Delay(100).Wait();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// if we got till here, throw on last try
|
||||
File.Delete(_dbPath);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8aeee08edfa6c46648cf1dc623b72a1e
|
||||
timeCreated: 1497883479
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using SQLite4Unity3d;
|
||||
|
||||
namespace Mapbox.Platform.Cache
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Don't change the class name: sqlite-net uses it for table creation
|
||||
/// </summary>
|
||||
public class tiles
|
||||
{
|
||||
|
||||
public int tile_set { get; set; }
|
||||
|
||||
//hrmpf: multiple PKs not supported by sqlite.net
|
||||
//https://github.com/praeclarum/sqlite-net/issues/282
|
||||
//TODO: do it via plain SQL
|
||||
//[PrimaryKey]
|
||||
public int zoom_level { get; set; }
|
||||
|
||||
//[PrimaryKey]
|
||||
public long tile_column { get; set; }
|
||||
|
||||
//[PrimaryKey]
|
||||
public long tile_row { get; set; }
|
||||
|
||||
public byte[] tile_data { get; set; }
|
||||
|
||||
/// <summary>Unix epoch for simple FIFO pruning </summary>
|
||||
public int timestamp { get; set; }
|
||||
|
||||
/// <summary> ETag Header value of the reponse for auto updating cache</summary>
|
||||
public string etag { get; set; }
|
||||
|
||||
/// <summary>Last-Modified header value of API response. Not all APIs populate it, will be -1 in that case. </summary>
|
||||
public int? lastmodified { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7d8f9ca4f2240481ba4023eff7781487
|
||||
timeCreated: 1497883479
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using SQLite4Unity3d;
|
||||
|
||||
namespace Mapbox.Platform.Cache
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Don't change the class name: sqlite-net uses it for table creation
|
||||
/// </summary>
|
||||
public class tilesets
|
||||
{
|
||||
|
||||
//hrmpf: multiple PKs not supported by sqlite.net
|
||||
//https://github.com/praeclarum/sqlite-net/issues/282
|
||||
//TODO: do it via plain SQL
|
||||
[PrimaryKey, AutoIncrement]
|
||||
public int id { get; set; }
|
||||
|
||||
public string name { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bb0cc61c516c1984f8ab3ac0ce3c3e64
|
||||
timeCreated: 1527515199
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,255 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="FileSource.cs" company="Mapbox">
|
||||
// Copyright (c) 2016 Mapbox. All rights reserved.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
using Mapbox.Unity;
|
||||
|
||||
namespace Mapbox.Platform
|
||||
{
|
||||
using Mapbox.Map;
|
||||
using Mapbox.Unity.Utilities;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
#if !NETFX_CORE
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
#endif
|
||||
#if !UNITY_5_3_OR_NEWER
|
||||
using System.Threading;
|
||||
#endif
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
#if UNITY_5_3_OR_NEWER
|
||||
using UnityEngine;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Mono implementation of the FileSource class. It will use Mono's
|
||||
/// <see href="http://www.mono-project.com/docs/advanced/runtime/">runtime</see> to
|
||||
/// asynchronously fetch data from the network via HTTP or HTTPS requests.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This implementation requires .NET 4.5 and later. The access token is expected to
|
||||
/// be exported to the environment as MAPBOX_ACCESS_TOKEN.
|
||||
/// </remarks>
|
||||
public sealed class FileSource : IFileSource
|
||||
{
|
||||
|
||||
private Func<string> _getMapsSkuToken;
|
||||
private readonly Dictionary<IAsyncRequest, int> _requests = new Dictionary<IAsyncRequest, int>();
|
||||
private readonly string _accessToken;
|
||||
private readonly object _lock = new object();
|
||||
|
||||
/// <summary>Length of rate-limiting interval in seconds. https://www.mapbox.com/api-documentation/#rate-limit-headers </summary>
|
||||
#pragma warning disable 0414
|
||||
private int? XRateLimitInterval;
|
||||
/// <summary>Maximum number of requests you may make in the current interval before reaching the limit. https://www.mapbox.com/api-documentation/#rate-limit-headers </summary>
|
||||
private long? XRateLimitLimit;
|
||||
/// <summary>Timestamp of when the current interval will end and the ratelimit counter is reset. https://www.mapbox.com/api-documentation/#rate-limit-headers </summary>
|
||||
private DateTime? XRateLimitReset;
|
||||
#pragma warning restore 0414
|
||||
|
||||
|
||||
public FileSource(Func<string> getMapsSkuToken, string acessToken = null)
|
||||
{
|
||||
_getMapsSkuToken = getMapsSkuToken;
|
||||
if (string.IsNullOrEmpty(acessToken))
|
||||
{
|
||||
_accessToken = Environment.GetEnvironmentVariable("MAPBOX_ACCESS_TOKEN");
|
||||
}
|
||||
else
|
||||
{
|
||||
_accessToken = acessToken;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Performs a request asynchronously. </summary>
|
||||
/// <param name="url"> The HTTP/HTTPS url. </param>
|
||||
/// <param name="callback"> Callback to be called after the request is completed. </param>
|
||||
/// <returns>
|
||||
/// Returns a <see cref="IAsyncRequest" /> 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.
|
||||
/// </returns>
|
||||
public IAsyncRequest Request(
|
||||
string url
|
||||
, Action<Response> callback
|
||||
, int timeout = 10
|
||||
, CanonicalTileId tileId = new CanonicalTileId()
|
||||
, string tilesetId = null
|
||||
)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_accessToken))
|
||||
{
|
||||
var uriBuilder = new UriBuilder(url);
|
||||
string accessTokenQuery = "access_token=" + _accessToken;
|
||||
string skuToken = "sku=" + _getMapsSkuToken();
|
||||
if (uriBuilder.Query != null && uriBuilder.Query.Length > 1)
|
||||
{
|
||||
uriBuilder.Query = uriBuilder.Query.Substring(1) + "&" + accessTokenQuery + "&" + skuToken;;
|
||||
}
|
||||
else
|
||||
{
|
||||
uriBuilder.Query = accessTokenQuery + "&" + skuToken;
|
||||
}
|
||||
|
||||
url = uriBuilder.ToString();
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// * add queue for requests
|
||||
// * evaluate rate limits (headers and status code)
|
||||
// * throttle requests accordingly
|
||||
//var request = new HTTPRequest(url, callback);
|
||||
//IEnumerator<IAsyncRequest> proxy = proxyResponse(url, callback);
|
||||
//proxy.MoveNext();
|
||||
//IAsyncRequest request = proxy.Current;
|
||||
|
||||
//return request;
|
||||
|
||||
return proxyResponse(url, callback, timeout, tileId, tilesetId);
|
||||
}
|
||||
|
||||
|
||||
// TODO: look at requests and implement throttling if needed
|
||||
//private IEnumerator<IAsyncRequest> proxyResponse(string url, Action<Response> callback) {
|
||||
private IAsyncRequest proxyResponse(
|
||||
string url
|
||||
, Action<Response> callback
|
||||
, int timeout
|
||||
, CanonicalTileId tileId
|
||||
, string tilesetId
|
||||
)
|
||||
{
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
, timeout
|
||||
);
|
||||
lock (_lock)
|
||||
{
|
||||
//sometimes we get here after the request has already finished
|
||||
if (!request.IsCompleted)
|
||||
{
|
||||
_requests.Add(request, 0);
|
||||
}
|
||||
}
|
||||
//yield return request;
|
||||
return request;
|
||||
}
|
||||
|
||||
|
||||
#if UNITY_5_3_OR_NEWER
|
||||
/// <summary>
|
||||
/// Block until all the requests are processed.
|
||||
/// </summary>
|
||||
public IEnumerator WaitForAllRequests()
|
||||
{
|
||||
while (_requests.Count > 0)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
List<IAsyncRequest> reqs = _requests.Keys.ToList();
|
||||
for (int i = reqs.Count - 1; i > -1; i--)
|
||||
{
|
||||
if (reqs[i].IsCompleted)
|
||||
{
|
||||
// another place to watch out if request has been cancelled
|
||||
try
|
||||
{
|
||||
_requests.Remove(reqs[i]);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
yield return new WaitForSeconds(0.2f);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
#if !UNITY_5_3_OR_NEWER
|
||||
/// <summary>
|
||||
/// Block until all the requests are processed.
|
||||
/// </summary>
|
||||
public void WaitForAllRequests()
|
||||
{
|
||||
int waitTimeMs = 200;
|
||||
while (_requests.Count > 0)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
List<IAsyncRequest> reqs = _requests.Keys.ToList();
|
||||
for (int i = reqs.Count - 1; i > -1; i--)
|
||||
{
|
||||
if (reqs[i].IsCompleted)
|
||||
{
|
||||
// another place to watch out if request has been cancelled
|
||||
try
|
||||
{
|
||||
_requests.Remove(reqs[i]);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if WINDOWS_UWP
|
||||
System.Threading.Tasks.Task.Delay(waitTimeMs).Wait();
|
||||
#else
|
||||
//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);
|
||||
UnityEngine.Debug.Log("before waitOne " + DateTime.Now.Ticks);
|
||||
resetEvent.WaitOne();
|
||||
UnityEngine.Debug.Log("after waitOne " + DateTime.Now.Ticks);
|
||||
resetEvent.Close();
|
||||
resetEvent = null;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a17c999e5ca80474584c39dfa7d5a62a
|
||||
timeCreated: 1494952070
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,236 @@
|
||||
#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 file="HTTPRequest.cs" company="Mapbox">
|
||||
// Copyright (c) 2016 Mapbox. All rights reserved.
|
||||
// Based on http://stackoverflow.com/a/12606963 and http://wiki.unity3d.com/index.php/WebAsync
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
#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
|
||||
|
||||
internal sealed class HTTPRequestNonThreaded : IAsyncRequest {
|
||||
|
||||
|
||||
public bool IsCompleted { get; private set; }
|
||||
|
||||
|
||||
private Action<Response> _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";
|
||||
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="callback"></param>
|
||||
/// <param name="timeOut">seconds</param>
|
||||
public HTTPRequestNonThreaded(string url, Action<Response> 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<HttpResponseMessage, Exception> 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<HttpWebResponse, Exception> 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
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca6d4261d474b4de0b14637c81b1795c
|
||||
timeCreated: 1494952070
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,318 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="HTTPRequest.cs" company="Mapbox">
|
||||
// Copyright (c) 2016 Mapbox. All rights reserved.
|
||||
// Based on http://stackoverflow.com/a/12606963 and http://wiki.unity3d.com/index.php/WebAsync
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
#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<Response> _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";
|
||||
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="callback"></param>
|
||||
/// <param name="timeOut">seconds</param>
|
||||
public HTTPRequestThreaded(string url, Action<Response> 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<HttpResponseMessage, Exception> 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<HttpWebResponse, Exception> 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
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7952dd05dd1ff40d0bff547dacc6e714
|
||||
timeCreated: 1494952070
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,25 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="IAsyncRequest.cs" company="Mapbox">
|
||||
// Copyright (c) 2016 Mapbox. All rights reserved.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
|
||||
namespace Mapbox.Platform {
|
||||
|
||||
using Mapbox.Unity.Utilities;
|
||||
|
||||
|
||||
/// <summary> A handle to an asynchronous request. </summary>
|
||||
public interface IAsyncRequest {
|
||||
|
||||
/// <summary> True after the request has finished. </summary>
|
||||
bool IsCompleted { get; }
|
||||
|
||||
/// <summary> Cancel the ongoing request, preventing it from firing a callback. </summary>
|
||||
void Cancel();
|
||||
|
||||
/// <summary>Type of request: GET, HEAD, ...</summary>
|
||||
HttpRequestType RequestType { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3687819725d334e82b70887393d8c63f
|
||||
timeCreated: 1494951007
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,39 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="IAsyncRequest.cs" company="Mapbox">
|
||||
// Copyright (c) 2016 Mapbox. All rights reserved.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
#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 Mapbox.Map;
|
||||
using Mapbox.Unity.Utilities;
|
||||
using System;
|
||||
|
||||
/// <summary> A handle to an asynchronous request. </summary>
|
||||
public static class IAsyncRequestFactory {
|
||||
|
||||
public static IAsyncRequest CreateRequest(
|
||||
string url
|
||||
, Action<Response> callback
|
||||
, int timeout
|
||||
, HttpRequestType requestType= HttpRequestType.Get
|
||||
) {
|
||||
#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, requestType);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2ee60049c3e114aed8e10d5b28d83efe
|
||||
timeCreated: 1494952069
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,30 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="IFileSource.cs" company="Mapbox">
|
||||
// Copyright (c) 2016 Mapbox. All rights reserved.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace Mapbox.Platform
|
||||
{
|
||||
using Mapbox.Map;
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// A data source abstraction. Used by classes that need to fetch data but really
|
||||
/// don't care about from where the data is coming from. An implementation of
|
||||
/// IFileSource could fetch the data from the network, disk cache or even generate
|
||||
/// the data at runtime.
|
||||
/// </summary>
|
||||
public interface IFileSource
|
||||
{
|
||||
/// <summary> Performs a request asynchronously. </summary>
|
||||
/// <param name="uri"> The resource description in the URI format. </param>
|
||||
/// <param name="callback"> Callback to be called after the request is completed. </param>
|
||||
/// <returns>
|
||||
/// Returns a <see cref="IAsyncRequest" /> 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.
|
||||
/// </returns>
|
||||
IAsyncRequest Request(string uri, Action<Response> callback, int timeout = 10, CanonicalTileId tileId = new CanonicalTileId(), string tilesetId = null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 891a55d979ad44487b2ad8e1384cfc65
|
||||
timeCreated: 1494951007
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,21 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="IResource.cs" company="Mapbox">
|
||||
// Copyright (c) 2016 Mapbox. All rights reserved.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace Mapbox.Platform
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Interface representing a Mapbox resource URL. Used to build request strings
|
||||
/// and return full URLs to a Mapbox Web Service API resource.
|
||||
/// </summary>
|
||||
public interface IResource
|
||||
{
|
||||
/// <summary>Builds a complete, valid URL string.</summary>
|
||||
/// <returns>URL string.</returns>
|
||||
string GetUrl();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6c11ed4c71914479ebeb8b69bf0bb677
|
||||
timeCreated: 1491243034
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,76 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="Resource.cs" company="Mapbox">
|
||||
// Copyright (c) 2016 Mapbox. All rights reserved.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace Mapbox.Platform
|
||||
{
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
#if UNITY_IOS
|
||||
using UnityEngine;
|
||||
#endif
|
||||
|
||||
/// <summary> Abstract class representing a Mapbox resource URL. </summary>
|
||||
public abstract class Resource
|
||||
{
|
||||
/// <summary> Gets the API endpoint, which is a partial URL path. </summary>
|
||||
public abstract string ApiEndpoint { get; }
|
||||
|
||||
/// <summary>Builds a complete, valid URL string.</summary>
|
||||
/// <returns>Returns URL string.</returns>
|
||||
public abstract string GetUrl();
|
||||
|
||||
/// <summary> Encodes a URI with a querystring. </summary>
|
||||
/// <param name="values"> Querystring values. </param>
|
||||
/// <returns> Encoded URL. </returns>
|
||||
protected static String EncodeQueryString(IEnumerable<KeyValuePair<string, string>> values)
|
||||
{
|
||||
if (values != null)
|
||||
{
|
||||
// we are seeing super weird crashes on some iOS devices:
|
||||
// see 'ForwardGeocodeResource' for more details
|
||||
var encodedValues = from p in values
|
||||
#if UNITY_IOS
|
||||
#if UNITY_2017_1_OR_NEWER
|
||||
let k = UnityEngine.Networking.UnityWebRequest.EscapeURL(p.Key.Trim())
|
||||
let v = UnityEngine.Networking.UnityWebRequest.EscapeURL(p.Value)
|
||||
#else
|
||||
let k = WWW.EscapeURL(p.Key.Trim())
|
||||
let v = WWW.EscapeURL(p.Value)
|
||||
#endif
|
||||
#else
|
||||
let k = Uri.EscapeDataString(p.Key.Trim())
|
||||
let v = Uri.EscapeDataString(p.Value)
|
||||
#endif
|
||||
orderby k
|
||||
select string.IsNullOrEmpty(v) ? k : string.Format("{0}={1}", k, v);
|
||||
if (encodedValues.Count() == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
return "?" + string.Join(
|
||||
"&", encodedValues.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Builds a string from an array of options for use in URLs.</summary>
|
||||
/// <param name="items"> Array of option strings. </param>
|
||||
/// <param name="separator"> Character to use for separating items in arry. Defaults to ",". </param>
|
||||
/// <returns>Comma-separated string of options.</returns>
|
||||
/// <typeparam name="U">Type in the array.</typeparam>
|
||||
protected static string GetUrlQueryFromArray<U>(U[] items, string separator = ",")
|
||||
{
|
||||
return string.Join(separator, items.Select(item => item.ToString()).ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c337c92960274b8098678fcfebbc8d0
|
||||
timeCreated: 1491243034
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,339 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="Response.cs" company="Mapbox">
|
||||
// Copyright (c) 2016 Mapbox. All rights reserved.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
#if UNITY_2017_1_OR_NEWER
|
||||
#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;
|
||||
using Mapbox.Unity.Utilities;
|
||||
#endif
|
||||
|
||||
/// <summary> A response from a <see cref="IFileSource" /> request. </summary>
|
||||
public class Response
|
||||
{
|
||||
|
||||
|
||||
private Response() { }
|
||||
|
||||
|
||||
public IAsyncRequest Request { get; private set; }
|
||||
|
||||
|
||||
public bool RateLimitHit
|
||||
{
|
||||
get { return StatusCode.HasValue ? 429 == StatusCode.Value : false; }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Flag to indicate if the request was successful</summary>
|
||||
public bool HasError
|
||||
{
|
||||
get { return _exceptions == null ? false : _exceptions.Count > 0; }
|
||||
}
|
||||
|
||||
/// <summary>Flag to indicate if the request was fullfilled from a local cache</summary>
|
||||
public bool LoadedFromCache;
|
||||
|
||||
/// <summary>Flag to indicate if the request was issued before but was issued again and updated</summary>
|
||||
public bool IsUpdate = false;
|
||||
|
||||
public string RequestUrl;
|
||||
|
||||
|
||||
public int? StatusCode;
|
||||
|
||||
|
||||
public string ContentType;
|
||||
|
||||
|
||||
/// <summary>Length of rate-limiting interval in seconds. https://www.mapbox.com/api-documentation/#rate-limit-headers </summary>
|
||||
public int? XRateLimitInterval;
|
||||
|
||||
|
||||
/// <summary>Maximum number of requests you may make in the current interval before reaching the limit. https://www.mapbox.com/api-documentation/#rate-limit-headers </summary>
|
||||
public long? XRateLimitLimit;
|
||||
|
||||
|
||||
/// <summary>Timestamp of when the current interval will end and the ratelimit counter is reset. https://www.mapbox.com/api-documentation/#rate-limit-headers </summary>
|
||||
public DateTime? XRateLimitReset;
|
||||
|
||||
|
||||
private List<Exception> _exceptions;
|
||||
/// <summary> Exceptions that might have occured during the request. </summary>
|
||||
public ReadOnlyCollection<Exception> Exceptions
|
||||
{
|
||||
get { return null == _exceptions ? null : _exceptions.AsReadOnly(); }
|
||||
}
|
||||
|
||||
|
||||
/// <summary> Messages of exceptions otherwise empty string. </summary>
|
||||
public string ExceptionsAsString
|
||||
{
|
||||
get
|
||||
{
|
||||
if (null == _exceptions || _exceptions.Count == 0) { return string.Empty; }
|
||||
return string.Join(Environment.NewLine, _exceptions.Select(e => e.Message).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary> Headers of the response. </summary>
|
||||
public Dictionary<string, string> Headers;
|
||||
|
||||
|
||||
/// <summary> Raw data fetched from the request. </summary>
|
||||
public byte[] Data;
|
||||
|
||||
public void AddException(Exception ex)
|
||||
{
|
||||
if (null == _exceptions) { _exceptions = new List<Exception>(); }
|
||||
_exceptions.Add(ex);
|
||||
}
|
||||
|
||||
// TODO: we should store timestamp of the cache!
|
||||
public static Response FromCache(byte[] data)
|
||||
{
|
||||
Response response = new Response();
|
||||
response.Data = data;
|
||||
response.LoadedFromCache = true;
|
||||
return response;
|
||||
}
|
||||
|
||||
#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-limit-headers
|
||||
if (null != apiResponse.Headers) {
|
||||
response.Headers = new Dictionary<string, string>();
|
||||
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<Response> 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-limit-headers
|
||||
if (null != apiResponse.Headers) {
|
||||
response.Headers = new Dictionary<string, string>();
|
||||
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);
|
||||
}
|
||||
|
||||
// additional string.empty check for apiResponse.error:
|
||||
// on UWP isNetworkError is sometimes set to true despite all being well
|
||||
if (apiResponse.isNetworkError && !string.IsNullOrEmpty(apiResponse.error))
|
||||
{
|
||||
response.AddException(new Exception(apiResponse.error));
|
||||
}
|
||||
|
||||
if (request.RequestType != HttpRequestType.Head)
|
||||
{
|
||||
if (null == apiResponse.downloadHandler.data)
|
||||
{
|
||||
response.AddException(new Exception("Response has no data."));
|
||||
}
|
||||
}
|
||||
|
||||
#if NETFX_CORE
|
||||
StringComparison stringComp = StringComparison.OrdinalIgnoreCase;
|
||||
#elif WINDOWS_UWP
|
||||
StringComparison stringComp = StringComparison.OrdinalIgnoreCase;
|
||||
#else
|
||||
StringComparison stringComp = StringComparison.InvariantCultureIgnoreCase;
|
||||
#endif
|
||||
|
||||
Dictionary<string, string> apiHeaders = apiResponse.GetResponseHeaders();
|
||||
if (null != apiHeaders)
|
||||
{
|
||||
response.Headers = new Dictionary<string, string>();
|
||||
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"));
|
||||
}
|
||||
|
||||
if (request.RequestType != HttpRequestType.Head)
|
||||
{
|
||||
response.Data = apiResponse.downloadHandler.data;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f904217270b634d929d1a92bee809d3e
|
||||
timeCreated: 1494951007
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e42bed265428942f4817d4918fbb7c41
|
||||
folderAsset: yes
|
||||
timeCreated: 1497883478
|
||||
licenseType: Pro
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aee17b4969829456097c55d1bc42f57b
|
||||
timeCreated: 1497883479
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b68f3c7ef59bf3246a91043a1d382926
|
||||
folderAsset: yes
|
||||
timeCreated: 1515589539
|
||||
licenseType: Pro
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,54 @@
|
||||
using Mapbox.Json;
|
||||
using Mapbox.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Mapbox.Platform.TilesetTileJSON
|
||||
{
|
||||
public class TileJSON
|
||||
{
|
||||
|
||||
private IFileSource _fileSource;
|
||||
private int _timeout;
|
||||
|
||||
|
||||
public IFileSource FileSource { get { return _fileSource; } }
|
||||
|
||||
|
||||
public TileJSON(IFileSource fileSource, int timeout)
|
||||
{
|
||||
_fileSource = fileSource;
|
||||
_timeout = timeout;
|
||||
}
|
||||
|
||||
|
||||
public IAsyncRequest Get(string tilesetName, Action<TileJSONResponse> callback)
|
||||
{
|
||||
string url = string.Format(
|
||||
"{0}v4/{1}.json?secure"
|
||||
, Constants.BaseAPI
|
||||
, tilesetName
|
||||
);
|
||||
|
||||
return _fileSource.Request(
|
||||
url
|
||||
, (Response response) =>
|
||||
{
|
||||
string json = Encoding.UTF8.GetString(response.Data);
|
||||
TileJSONResponse tileJSONResponse = JsonConvert.DeserializeObject<TileJSONResponse>(json);
|
||||
if (tileJSONResponse != null)
|
||||
{
|
||||
tileJSONResponse.Source = tilesetName;
|
||||
}
|
||||
callback(tileJSONResponse);
|
||||
}
|
||||
, _timeout
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ab78da4f6b501ed4b8ea92121d3392b8
|
||||
timeCreated: 1515589539
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace Mapbox.Platform.TilesetTileJSON
|
||||
{
|
||||
using Mapbox.Json;
|
||||
using System.Collections.Generic;
|
||||
|
||||
|
||||
|
||||
public class TileJSONObjectVectorLayer
|
||||
{
|
||||
|
||||
[JsonProperty("description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
|
||||
[JsonProperty("fields")]
|
||||
public Dictionary<string, string> Fields { get; set; }
|
||||
|
||||
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
|
||||
[JsonProperty("source")]
|
||||
public string Source { get; set; }
|
||||
|
||||
|
||||
[JsonProperty("source_name")]
|
||||
public string SourceName { get; set; }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4d926842f7750b041be895c39e959799
|
||||
timeCreated: 1515590225
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,165 @@
|
||||
namespace Mapbox.Platform.TilesetTileJSON
|
||||
{
|
||||
|
||||
using Mapbox.Json;
|
||||
using Mapbox.Utils;
|
||||
using System;
|
||||
|
||||
public class TileJSONResponse
|
||||
{
|
||||
|
||||
|
||||
[JsonProperty("attribution")]
|
||||
public string Attribution { get; set; }
|
||||
|
||||
|
||||
[JsonProperty("autoscale")]
|
||||
public bool AutoScale { get; set; }
|
||||
|
||||
|
||||
private double[] _bounds;
|
||||
[JsonProperty("bounds")]
|
||||
public double[] Bounds
|
||||
{
|
||||
get { return _bounds; }
|
||||
set
|
||||
{
|
||||
_bounds = value;
|
||||
BoundsParsed = new Vector2dBounds(
|
||||
new Vector2d(Bounds[1], Bounds[0])
|
||||
, new Vector2d(Bounds[3], Bounds[2])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[JsonIgnore]
|
||||
public Vector2dBounds BoundsParsed { get; private set; }
|
||||
|
||||
|
||||
private double[] _center;
|
||||
[JsonProperty("center")]
|
||||
public double[] Center
|
||||
{
|
||||
get { return _center; }
|
||||
set
|
||||
{
|
||||
_center = value;
|
||||
CenterParsed = new Vector2d(_center[1], _center[0]);
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public Vector2d CenterParsed { get; private set; }
|
||||
|
||||
|
||||
private long? _created;
|
||||
/// <summary>Concatenated tilesets don't have a created property </summary>
|
||||
[JsonProperty("created")]
|
||||
public long? Created
|
||||
{
|
||||
get { return _created; }
|
||||
set
|
||||
{
|
||||
_created = value;
|
||||
if (_created.HasValue)
|
||||
{
|
||||
CreatedUtc = UnixTimestampUtils.From(_created.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
CreatedUtc = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Concatenated tilesets don't have a created property </summary>
|
||||
[JsonIgnore]
|
||||
public DateTime? CreatedUtc { get; private set; }
|
||||
|
||||
|
||||
[JsonProperty("description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
|
||||
/// <summary>Can be empty</summary>
|
||||
[JsonProperty("format")]
|
||||
public string Format { get; set; }
|
||||
|
||||
|
||||
/// <summary>Can be empty (for concatenated tilesets)</summary>
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
|
||||
[JsonProperty("maxzoom")]
|
||||
public int MaxZoom { get; set; }
|
||||
|
||||
|
||||
[JsonProperty("minzoom")]
|
||||
public int MinZoom { get; set; }
|
||||
|
||||
|
||||
private long? _modified;
|
||||
/// <summary>Unmodified tilesets don't have a modfied property </summary>
|
||||
[JsonProperty("modified")]
|
||||
public long? Modified
|
||||
{
|
||||
get { return _modified; }
|
||||
set
|
||||
{
|
||||
_modified = value;
|
||||
if (_modified.HasValue)
|
||||
{
|
||||
ModifiedUtc = UnixTimestampUtils.From(_modified.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
ModifiedUtc = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Unmodified tilesets don't have a modfied property </summary>
|
||||
[JsonIgnore]
|
||||
public DateTime? ModifiedUtc { get; private set; }
|
||||
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
|
||||
[JsonProperty("private")]
|
||||
public bool Private { get; set; }
|
||||
|
||||
|
||||
[JsonProperty("scheme")]
|
||||
public string Scheme { get; set; }
|
||||
|
||||
|
||||
/// <summary>Can be empty</summary>
|
||||
[JsonProperty("source")]
|
||||
public string Source { get; set; }
|
||||
|
||||
|
||||
[JsonProperty("tilejson")]
|
||||
public string TileJSONVersion { get; set; }
|
||||
|
||||
|
||||
[JsonProperty("tiles")]
|
||||
public string[] Tiles { get; set; }
|
||||
|
||||
|
||||
[JsonProperty("vector_layers")]
|
||||
public TileJSONObjectVectorLayer[] VectorLayers { get; set; }
|
||||
|
||||
|
||||
/// <summary>Can be empty (for concatenated tilesets)</summary>
|
||||
[JsonProperty("webpage")]
|
||||
public string WebPage { get; set; }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f58425aa7b211946a73691da0414edb
|
||||
timeCreated: 1515589539
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in new issue
Block a user