You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
321 lines
7.4 KiB
321 lines
7.4 KiB
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.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|