Open genaray opened 5 years ago
Thanks for share, i think devs should study this carefully and make tests to assure it works. Then add to mapbox as default or at least as an option. @jordy-isaac @brnkhy @atripathi-mb
@genaray sounds awesome! I'll definitely check it as soon as possible, thanks a lot for sharing!
That's an issue we're having as well. Could you share a video to see what the level of improvement is ?
This still works BTW, but it needs messing around because the AbstractMap.cs and AbstractMapVisualizer.cs have changed since the OP's base version. Here is a gross version of the modified files that I painstakingly copy pasted now knowing exactly how to properly diff them. I have done a basic test to navigate the map in the unity editor, and it is works incredibly smooth! Well done @genaray and double thanks for introducing me to the ThreadNinja asset!!!
For future reference these files are based on the commit 0f851d16090db5ff5c356cb057f43198085f94ef from master branch, 16 oct 2019, version number 2.1.1
AbstractMap.cs
using Mapbox.Platform.Cache;
using Mapbox.Unity.Map.Interfaces;
using Mapbox.Unity.Map.Strategies;
using Mapbox.Unity.Map.TileProviders;
namespace Mapbox.Unity.Map
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Mapbox.Unity.Utilities;
using Utils;
using UnityEngine;
using Mapbox.Map;
using Mapbox.Unity.MeshGeneration.Factories;
using Mapbox.Unity.MeshGeneration.Data;
using System.Globalization;
using CielaSpike;
/// <summary>
/// Abstract map.
/// This is the main monobehavior which controls the map. It controls the visualization of map data.
/// Abstract map encapsulates the image, terrain and vector sources and provides a centralized interface to control the visualization of the map.
/// </summary>
[ExecuteInEditMode]
public class AbstractMap : MonoBehaviour, IMap
{
#region Private Fields
[SerializeField] private MapOptions _options = new MapOptions();
[SerializeField] private bool _initializeOnStart = true;
[SerializeField] protected ImageryLayer _imagery = new ImageryLayer();
[SerializeField] protected TerrainLayer _terrain = new TerrainLayer();
[SerializeField] protected VectorLayer _vectorData = new VectorLayer();
[SerializeField] protected AbstractTileProvider _tileProvider;
[SerializeField] protected HashSet<UnwrappedTileId> _currentExtent;
[SerializeField] protected EditorPreviewOptions _previewOptions = new EditorPreviewOptions();
private List<UnwrappedTileId> tilesToProcess;
public bool isInitialized = false;
protected AbstractMapVisualizer _mapVisualizer;
protected float _unityTileSize = 1;
protected bool _worldHeightFixed = false;
protected MapboxAccess _fileSource;
protected int _initialZoom;
protected Vector2d _centerLatitudeLongitude;
protected Vector2d _centerMercator;
protected float _worldRelativeScale;
protected Vector3 _mapScaleFactor;
protected Vector3 _cachedPosition;
protected Quaternion _cachedRotation;
protected Vector3 _cachedScale = Vector3.one;
#endregion
#region Properties
public bool IsEditorPreviewEnabled
{
get
{
return _previewOptions.isPreviewEnabled;
}
set
{
_previewOptions.isPreviewEnabled = value;
}
}
public AbstractMapVisualizer MapVisualizer
{
get
{
if (_mapVisualizer == null)
{
_mapVisualizer = ScriptableObject.CreateInstance<MapVisualizer>();
}
return _mapVisualizer;
}
set
{
_mapVisualizer = value;
}
}
public AbstractTileProvider TileProvider
{
get
{
return _tileProvider;
}
set
{
if (_tileProvider != null)
{
_tileProvider.ExtentChanged -= OnMapExtentChanged;
}
_tileProvider = value;
if (_tileProvider != null)
{
_tileProvider.ExtentChanged += OnMapExtentChanged;
}
}
}
/// <summary>
/// The map options.
/// Options to control the behaviour of the map like location,extent, scale and placement.
/// </summary>
public MapOptions Options
{
get
{
return _options;
}
set
{
_options = value;
}
}
/// <summary>
/// Options to control the imagery component of the map.
/// </summary>
[NodeEditorElement("Layers")]
public IImageryLayer ImageLayer
{
get
{
return _imagery;
}
}
/// <summary>
/// Options to control the terrain/ elevation component of the map.
/// </summary>
[NodeEditorElement("Layers")]
public ITerrainLayer Terrain
{
get
{
return _terrain;
}
}
/// <summary>
/// The vector data.
/// Options to control the vector data component of the map.
/// Adds a vector source and visualizers to define the rendering behaviour of vector data layers.
/// </summary>
[NodeEditorElement("Layers")]
public IVectorDataLayer VectorData
{
get
{
return _vectorData;
}
}
public Vector2d CenterLatitudeLongitude
{
get
{
return _centerLatitudeLongitude;
}
}
public Vector2d CenterMercator
{
get
{
return _centerMercator;
}
}
public float WorldRelativeScale
{
get
{
return _worldRelativeScale;
}
}
public float UnityTileSize
{
get
{
return _unityTileSize;
}
}
/// <summary>
/// Gets the absolute zoom of the tiles being currently rendered.
/// <seealso cref="Zoom"/>
/// </summary>
/// <value>The absolute zoom.</value>
public int AbsoluteZoom
{
get
{
return (int)Math.Floor(Options.locationOptions.zoom);
}
}
/// <summary>
/// Gets the current zoom value of the map.
/// Use <c>AbsoluteZoom</c> to get the zoom level of the tileset.
/// <seealso cref="AbsoluteZoom"/>
/// </summary>
/// <value>The zoom.</value>
public float Zoom
{
get
{
return Options.locationOptions.zoom;
}
}
public void SetZoom(float zoom)
{
Options.locationOptions.zoom = zoom;
}
/// <summary>
/// Gets the initial zoom at which the map was initialized.
/// This parameter is useful in calculating the scale of the tiles and the map.
/// </summary>
/// <value>The initial zoom.</value>
public int InitialZoom
{
get
{
return _initialZoom;
}
}
public Transform Root
{
get
{
return transform;
}
}
/// <summary>
/// Setting to trigger map initialization in Unity's Start method.
/// if set to false, Initialize method should be called explicitly to initialize the map.
/// </summary>
public bool InitializeOnStart
{
get
{
return _initializeOnStart;
}
set
{
_initializeOnStart = value;
}
}
public HashSet<UnwrappedTileId> CurrentExtent
{
get
{
return _currentExtent;
}
}
/// <summary>
/// Gets the loading texture used as a placeholder while the image tile is loading.
/// </summary>
/// <value>The loading texture.</value>
public Texture2D LoadingTexture
{
get
{
return _options.loadingTexture;
}
}
/// <summary>
/// Gets the tile material used for map tiles.
/// </summary>
/// <value>The tile material.</value>
public Material TileMaterial
{
get
{
return _options.tileMaterial;
}
}
public Type ExtentCalculatorType
{
get
{
return TileProvider.GetType();
}
}
#endregion
#region Public Methods
/// <summary>
/// Initialize the map using the specified latLon and zoom.
/// Map will automatically get initialized in the <c>Start</c> method.
/// Use this method to explicitly initialize the map and disable intialize on <c>Start</c>
/// </summary>
/// <returns>The initialize.</returns>
/// <param name="latLon">Lat lon.</param>
/// <param name="zoom">Zoom.</param>
public virtual void Initialize(Vector2d latLon, int zoom)
{
_initializeOnStart = false;
if (_options == null)
{
_options = new MapOptions();
}
_options.locationOptions.latitudeLongitude = String.Format(CultureInfo.InvariantCulture, "{0},{1}", latLon.x, latLon.y);
_options.locationOptions.zoom = zoom;
SetUpMap();
}
protected virtual void Update()
{
if (Application.isEditor && !Application.isPlaying && IsEditorPreviewEnabled == false)
{
return;
}
if (TileProvider != null)
{
TileProvider.UpdateTileProvider();
}
}
public virtual void UpdateMap()
{
UpdateMap(Conversions.StringToLatLon(_options.locationOptions.latitudeLongitude), Zoom);
}
public virtual void UpdateMap(Vector2d latLon)
{
UpdateMap(latLon, Zoom);
}
public virtual void UpdateMap(float zoom)
{
UpdateMap(Conversions.StringToLatLon(_options.locationOptions.latitudeLongitude), zoom);
}
/// <summary>
/// Updates the map.
/// Use this method to update the location of the map.
/// Update method should be used when panning, zooming or changing location of the map.
/// This method avoid startup delays that might occur on re-initializing the map.
/// </summary>
/// <param name="latLon">LatitudeLongitude.</param>
/// <param name="zoom">Zoom level.</param>
public virtual void UpdateMap(Vector2d latLon, float zoom)
{
if (Application.isEditor && !Application.isPlaying && !IsEditorPreviewEnabled)
{
return;
}
//so map will be snapped to zero using next new tile loaded
_worldHeightFixed = false;
float differenceInZoom = 0.0f;
bool isAtInitialZoom = false;
// Update map zoom, if it has changed.
if (Math.Abs(Zoom - zoom) > Constants.EpsilonFloatingPoint)
{
SetZoom(zoom);
}
// Compute difference in zoom. Will be used to calculate correct scale of the map.
differenceInZoom = Zoom - InitialZoom;
isAtInitialZoom = (differenceInZoom - 0.0 < Constants.EpsilonFloatingPoint);
//Update center latitude longitude
var centerLatitudeLongitude = latLon;
double xDelta = centerLatitudeLongitude.x;
double zDelta = centerLatitudeLongitude.y;
xDelta = xDelta > 0 ? Mathd.Min(xDelta, Mapbox.Utils.Constants.LatitudeMax) : Mathd.Max(xDelta, -Mapbox.Utils.Constants.LatitudeMax);
zDelta = zDelta > 0 ? Mathd.Min(zDelta, Mapbox.Utils.Constants.LongitudeMax) : Mathd.Max(zDelta, -Mapbox.Utils.Constants.LongitudeMax);
//Set Center in Latitude Longitude and Mercator.
SetCenterLatitudeLongitude(new Vector2d(xDelta, zDelta));
Options.scalingOptions.scalingStrategy.SetUpScaling(this);
Options.placementOptions.placementStrategy.SetUpPlacement(this);
//Scale the map accordingly.
if (Math.Abs(differenceInZoom) > Constants.EpsilonFloatingPoint || isAtInitialZoom)
{
_mapScaleFactor = Vector3.one * Mathf.Pow(2, differenceInZoom);
Root.localScale = _mapScaleFactor;
}
//Update Tile extent.
if (TileProvider != null)
{
TileProvider.UpdateTileExtent();
}
if (OnUpdated != null)
{
OnUpdated();
}
}
private void Reset()
{
DisableEditorPreview();
}
/// <summary>
/// Resets the map.
/// Use this method to reset the map.
/// </summary>
[ContextMenu("ResetMap")]
public void ResetMap()
{
if (_previewOptions.isPreviewEnabled)
{
DisableEditorPreview();
EnableEditorPreview();
}
else
{
MapOnAwakeRoutine();
MapOnStartRoutine(false);
}
}
public bool IsAccessTokenValid
{
get
{
bool isAccessTokenValid = false;
try
{
var accessTokenCheck = Unity.MapboxAccess.Instance;
if (Unity.MapboxAccess.Instance.Configuration == null || string.IsNullOrEmpty(Unity.MapboxAccess.Instance.Configuration.AccessToken))
{
return false;
}
isAccessTokenValid = true;
}
catch (System.Exception)
{
isAccessTokenValid = false;
}
return isAccessTokenValid;
}
}
#endregion
#region Private/Protected Methods
private void OnEnable()
{
tilesToProcess = new List<UnwrappedTileId>();
if (_options.tileMaterial == null)
{
_options.tileMaterial = new Material(Shader.Find("Standard"));
}
if (_options.loadingTexture == null)
{
_options.loadingTexture = new Texture2D(1, 1);
}
}
// TODO: implement IDisposable, instead?
protected virtual void OnDestroy()
{
if (TileProvider != null)
{
TileProvider.ExtentChanged -= OnMapExtentChanged;
}
_mapVisualizer.ClearMap();
_mapVisualizer.Destroy();
}
protected virtual void Awake()
{
if (_previewOptions.isPreviewEnabled == true)
{
DisableEditorPreview();
_previewOptions.isPreviewEnabled = false;
}
MapOnAwakeRoutine();
}
protected virtual void Start()
{
MapOnStartRoutine();
}
private void MapOnAwakeRoutine()
{
// Destroy any ghost game objects.
DestroyChildObjects();
// Setup a visualizer to get a "Starter" map.
if (_mapVisualizer == null)
{
_mapVisualizer = ScriptableObject.CreateInstance<MapVisualizer>();
}
_mapVisualizer.OnTileFinished += (s) =>
{
OnTileFinished(s);
};
}
public void DestroyChildObjects()
{
int destroyChildStartIndex = transform.childCount - 1;
for (int i = destroyChildStartIndex; i >= 0; i--)
{
transform.GetChild(i).gameObject.Destroy();
}
}
private void MapOnStartRoutine(bool coroutine = true)
{
if (Application.isPlaying)
{
if (coroutine)
{
StartCoroutine("SetupAccess");
}
if (_initializeOnStart)
{
SetUpMap();
}
}
}
private void EnableDisablePreview(object sender, EventArgs e)
{
if (!Application.isPlaying)
{
if (_previewOptions.isPreviewEnabled)
{
EnableEditorPreview();
}
else
{
DisableEditorPreview();
}
}
}
public void EnableEditorPreview()
{
_cachedPosition = transform.position;
_cachedRotation = transform.rotation;
_cachedScale = transform.localScale;
SetUpMap();
if (OnEditorPreviewEnabled != null)
{
OnEditorPreviewEnabled();
}
}
public void DisableEditorPreview()
{
_imagery.UpdateLayer -= OnImageOrTerrainUpdateLayer;
_terrain.UpdateLayer -= OnImageOrTerrainUpdateLayer;
_vectorData.SubLayerRemoved -= OnVectorDataSubLayerRemoved;
_vectorData.SubLayerAdded -= OnVectorDataSubLayerAdded;
_vectorData.UpdateLayer -= OnVectorDataUpdateLayer;
_vectorData.UnbindAllEvents();
if (_mapVisualizer != null)
{
_mapVisualizer.ClearMap();
}
DestroyTileProvider();
if (OnEditorPreviewDisabled != null)
{
OnEditorPreviewDisabled();
}
transform.position = _cachedPosition;
transform.rotation = _cachedRotation;
transform.localScale = _cachedScale;
}
public void DestroyTileProvider()
{
var tileProvider = TileProvider ?? gameObject.GetComponent<AbstractTileProvider>();
if (_options.extentOptions.extentType != MapExtentType.Custom && tileProvider != null)
{
tileProvider.gameObject.Destroy();
_tileProvider = null;
}
}
protected IEnumerator SetupAccess()
{
_fileSource = MapboxAccess.Instance;
yield return new WaitUntil(() => MapboxAccess.Configured);
}
/// <summary>
/// Sets up map.
/// This method uses the mapOptions and layer properties to setup the map to be rendered.
/// Override <c>SetUpMap</c> to write custom behavior to map setup.
/// </summary>
protected virtual void SetUpMap()
{
SetPlacementStrategy();
SetScalingStrategy();
SetTileProvider();
if (_imagery == null)
{
_imagery = new ImageryLayer();
}
_imagery.Initialize();
if (_terrain == null)
{
_terrain = new TerrainLayer();
}
_terrain.Initialize();
if (_vectorData == null)
{
_vectorData = new VectorLayer();
}
_vectorData.Initialize();
_mapVisualizer.Factories = new List<AbstractTileFactory>
{
_terrain.Factory,
_imagery.Factory,
_vectorData.Factory
};
InitializeMap(_options);
}
protected virtual IEnumerator TileProvider_OnTileAdded(UnwrappedTileId tileId)
{
yield return Ninja.JumpToUnity;
var tile = _mapVisualizer.LoadTile(tileId);
yield return Ninja.JumpBack;
if (Options.placementOptions.snapMapToZero && !_worldHeightFixed)
{
_worldHeightFixed = true;
if (tile.HeightDataState == MeshGeneration.Enums.TilePropertyState.Loaded)
{
ApplySnapWorldToZero(tile);
}
else
{
tile.OnHeightDataChanged += (s) => { ApplySnapWorldToZero(tile); };
}
}
// not needed? cause we are already back? just an extra wait for no reason
yield return Ninja.JumpBack;
}
protected virtual void TileProvider_OnTileRemoved(UnwrappedTileId tileId)
{
_mapVisualizer.DisposeTile(tileId);
}
protected virtual void TileProvider_OnTileRepositioned(UnwrappedTileId tileId)
{
_mapVisualizer.RepositionTile(tileId);
}
protected void SendInitialized()
{
OnInitialized();
}
/// <summary>
/// Apply Snap World to Zero setting by moving map in Y Axis such that
/// center of the given tile will be at y=0.
/// </summary>
/// <param name="referenceTile">Tile to use for Y axis correction.</param>
private void ApplySnapWorldToZero(UnityTile referenceTile)
{
if (_options.placementOptions.snapMapToZero)
{
var h = referenceTile.QueryHeightData(.5f, .5f);
Root.transform.localPosition = new Vector3(
Root.transform.position.x,
-h,
Root.transform.position.z);
}
else
{
Root.transform.localPosition = new Vector3(
Root.transform.position.x,
0,
Root.transform.position.z);
}
}
/// <summary>
/// Initializes the map using the mapOptions.
/// </summary>
/// <param name="options">Options.</param>
protected virtual void InitializeMap(MapOptions options)
{
Options = options;
_worldHeightFixed = false;
_fileSource = MapboxAccess.Instance;
_centerLatitudeLongitude = Conversions.StringToLatLon(options.locationOptions.latitudeLongitude);
_initialZoom = (int)options.locationOptions.zoom;
options.scalingOptions.scalingStrategy.SetUpScaling(this);
options.placementOptions.placementStrategy.SetUpPlacement(this);
//Set up events for changes.
_imagery.UpdateLayer += OnImageOrTerrainUpdateLayer;
_terrain.UpdateLayer += OnImageOrTerrainUpdateLayer;
_vectorData.SubLayerRemoved += OnVectorDataSubLayerRemoved;
_vectorData.SubLayerAdded += OnVectorDataSubLayerAdded;
_vectorData.UpdateLayer += OnVectorDataUpdateLayer;
_options.locationOptions.PropertyHasChanged += (object sender, System.EventArgs eventArgs) =>
{
UpdateMap();
};
_options.extentOptions.PropertyHasChanged += (object sender, System.EventArgs eventArgs) =>
{
OnTileProviderChanged();
};
_options.extentOptions.defaultExtents.PropertyHasChanged += (object sender, System.EventArgs eventArgs) =>
{
if (Application.isEditor && !Application.isPlaying && IsEditorPreviewEnabled == false)
{
Debug.Log("defaultExtents");
return;
}
if (TileProvider != null)
{
TileProvider.UpdateTileExtent();
}
};
_options.placementOptions.PropertyHasChanged += (object sender, System.EventArgs eventArgs) =>
{
SetPlacementStrategy();
UpdateMap();
};
_options.scalingOptions.PropertyHasChanged += (object sender, System.EventArgs eventArgs) =>
{
SetScalingStrategy();
UpdateMap();
};
_mapVisualizer.Initialize(this, _fileSource);
TileProvider.Initialize(this);
SendInitialized();
TileProvider.UpdateTileExtent();
}
private void SetScalingStrategy()
{
switch (_options.scalingOptions.scalingType)
{
case MapScalingType.WorldScale:
_options.scalingOptions.scalingStrategy = new MapScalingAtWorldScaleStrategy();
break;
case MapScalingType.Custom:
_options.scalingOptions.scalingStrategy = new MapScalingAtUnityScaleStrategy();
break;
}
}
private void SetPlacementStrategy()
{
switch (_options.placementOptions.placementType)
{
case MapPlacementType.AtTileCenter:
_options.placementOptions.placementStrategy = new MapPlacementAtTileCenterStrategy();
break;
case MapPlacementType.AtLocationCenter:
_options.placementOptions.placementStrategy = new MapPlacementAtLocationCenterStrategy();
break;
default:
_options.placementOptions.placementStrategy = new MapPlacementAtTileCenterStrategy();
break;
}
}
private void SetTileProvider()
{
if (_options.extentOptions.extentType != MapExtentType.Custom)
{
ITileProviderOptions tileProviderOptions = _options.extentOptions.GetTileProviderOptions();
string tileProviderName = "TileProvider";
// Setup tileprovider based on type.
switch (_options.extentOptions.extentType)
{
case MapExtentType.CameraBounds:
{
if (TileProvider != null)
{
if (!(TileProvider is QuadTreeTileProvider))
{
TileProvider.gameObject.Destroy();
var go = new GameObject(tileProviderName);
go.transform.parent = transform;
TileProvider = go.AddComponent<QuadTreeTileProvider>();
}
}
else
{
var go = new GameObject(tileProviderName);
go.transform.parent = transform;
TileProvider = go.AddComponent<QuadTreeTileProvider>();
}
break;
}
case MapExtentType.RangeAroundCenter:
{
if (TileProvider != null)
{
TileProvider.gameObject.Destroy();
var go = new GameObject(tileProviderName);
go.transform.parent = transform;
TileProvider = go.AddComponent<RangeTileProvider>();
}
else
{
var go = new GameObject(tileProviderName);
go.transform.parent = transform;
TileProvider = go.AddComponent<RangeTileProvider>();
}
break;
}
case MapExtentType.RangeAroundTransform:
{
if (TileProvider != null)
{
if (!(TileProvider is RangeAroundTransformTileProvider))
{
TileProvider.gameObject.Destroy();
var go = new GameObject(tileProviderName);
go.transform.parent = transform;
TileProvider = go.AddComponent<RangeAroundTransformTileProvider>();
}
}
else
{
var go = new GameObject(tileProviderName);
go.transform.parent = transform;
TileProvider = go.AddComponent<RangeAroundTransformTileProvider>();
}
break;
}
default:
break;
}
TileProvider.SetOptions(tileProviderOptions);
}
else
{
TileProvider = _tileProvider;
}
}
private IEnumerator TriggerTileRedrawForExtent(ExtentArgs currentExtent)
{
yield return Ninja.JumpBack;
var _activeTiles = _mapVisualizer.ActiveTiles;
_currentExtent = new HashSet<UnwrappedTileId>(currentExtent.activeTiles);
if (tilesToProcess == null)
{
tilesToProcess = new List<UnwrappedTileId>();
}
else
{
tilesToProcess.Clear();
}
foreach (var item in _activeTiles)
{
if (TileProvider.Cleanup(item.Key))
{
tilesToProcess.Add(item.Key);
}
}
yield return Ninja.JumpToUnity;
if (tilesToProcess.Count > 0)
{
OnTilesDisposing(tilesToProcess);
foreach (var t2r in tilesToProcess)
{
TileProvider_OnTileRemoved(t2r);
}
}
foreach (var tile in _activeTiles)
{
// Reposition tiles in case we panned.
TileProvider_OnTileRepositioned(tile.Key);
}
tilesToProcess.Clear();
foreach (var tile in _currentExtent)
{
if (!_activeTiles.ContainsKey(tile))
{
tilesToProcess.Add(tile);
}
}
if (tilesToProcess.Count > 0)
{
OnTilesStarting(tilesToProcess);
foreach (var tileId in tilesToProcess)
{
_mapVisualizer.State = ModuleState.Working;
yield return TileProvider_OnTileAdded(tileId);
}
}
}
private void OnMapExtentChanged(object sender, ExtentArgs currentExtent)
{
this.StartCoroutineAsync(TriggerTileRedrawForExtent(currentExtent));
}
private void OnImageOrTerrainUpdateLayer(object sender, System.EventArgs eventArgs)
{
LayerUpdateArgs layerUpdateArgs = eventArgs as LayerUpdateArgs;
if (layerUpdateArgs != null)
{
_mapVisualizer.UpdateTileForProperty(layerUpdateArgs.factory, layerUpdateArgs);
if (layerUpdateArgs.effectsVectorLayer)
{
RedrawVectorDataLayer();
}
OnMapRedrawn();
}
}
private void RedrawVectorDataLayer()
{
_mapVisualizer.UnregisterTilesFrom(_vectorData.Factory);
_vectorData.UnbindAllEvents();
_vectorData.UpdateFactorySettings();
_mapVisualizer.ReregisterTilesTo(_vectorData.Factory);
}
private void OnVectorDataSubLayerRemoved(object sender, EventArgs eventArgs)
{
VectorLayerUpdateArgs layerUpdateArgs = eventArgs as VectorLayerUpdateArgs;
if (layerUpdateArgs.visualizer != null)
{
_mapVisualizer.RemoveTilesFromLayer((VectorTileFactory)layerUpdateArgs.factory, layerUpdateArgs.visualizer);
}
OnMapRedrawn();
}
private void OnVectorDataSubLayerAdded(object sender, EventArgs eventArgs)
{
RedrawVectorDataLayer();
OnMapRedrawn();
}
private void OnVectorDataUpdateLayer(object sender, System.EventArgs eventArgs)
{
VectorLayerUpdateArgs layerUpdateArgs = eventArgs as VectorLayerUpdateArgs;
if (layerUpdateArgs.visualizer != null)
{
//We have a visualizer. Update only the visualizer.
//No need to unload the entire factory to apply changes.
_mapVisualizer.UnregisterAndRedrawTilesFromLayer((VectorTileFactory)layerUpdateArgs.factory, layerUpdateArgs.visualizer);
}
else
{
//We are updating a core property of vector section.
//All vector features need to get unloaded and re-created.
RedrawVectorDataLayer();
}
OnMapRedrawn();
}
private void OnTileProviderChanged()
{
if (Application.isEditor && !Application.isPlaying && IsEditorPreviewEnabled == false)
{
Debug.Log("extentOptions");
return;
}
SetTileProvider();
TileProvider.Initialize(this);
if (IsEditorPreviewEnabled)
{
TileProvider.UpdateTileExtent();
}
}
#endregion
#region Conversion and Height Query Methods
private Vector3 GeoToWorldPositionXZ(Vector2d latitudeLongitude)
{
// For quadtree implementation of the map, the map scale needs to be compensated for.
var scaleFactor = Mathf.Pow(2, (InitialZoom - AbsoluteZoom));
var worldPos = Conversions.GeoToWorldPosition(latitudeLongitude, CenterMercator, WorldRelativeScale * scaleFactor).ToVector3xz();
return Root.TransformPoint(worldPos);
}
protected virtual float QueryElevationAtInternal(Vector2d latlong, out float tileScale)
{
var _meters = Conversions.LatLonToMeters(latlong.x, latlong.y);
UnityTile tile;
bool foundTile = MapVisualizer.ActiveTiles.TryGetValue(Conversions.LatitudeLongitudeToTileId(latlong.x, latlong.y, (int)Zoom), out tile);
if (foundTile)
{
tileScale = tile.TileScale;
var _rect = tile.Rect;
return tile.QueryHeightData((float)((_meters - _rect.Min).x / _rect.Size.x), (float)((_meters.y - _rect.Max.y) / _rect.Size.y));
}
else
{
tileScale = 1f;
return 0f;
}
}
/// <summary>
/// Converts a latitude longitude into map space position.
/// </summary>
/// <returns>Position in map space.</returns>
/// <param name="latitudeLongitude">Latitude longitude.</param>
/// <param name="queryHeight">If set to <c>true</c> will return the terrain height(in Unity units) at that point.</param>
public virtual Vector3 GeoToWorldPosition(Vector2d latitudeLongitude, bool queryHeight = true)
{
Vector3 worldPos = GeoToWorldPositionXZ(latitudeLongitude);
if (queryHeight)
{
//Query Height.
float tileScale = 1f;
float height = QueryElevationAtInternal(latitudeLongitude, out tileScale);
// Apply height inside the unity tile space
UnityTile tile;
if (MapVisualizer.ActiveTiles.TryGetValue(Conversions.LatitudeLongitudeToTileId(latitudeLongitude.x, latitudeLongitude.y, (int)Zoom), out tile))
{
if (tile != null)
{
// Calculate height in the local space of the tile gameObject.
// Height is aligned with the y axis in local space.
// This also helps us avoid scale values when setting the height.
var localPos = tile.gameObject.transform.InverseTransformPoint(worldPos);
localPos.y = height;
worldPos = tile.gameObject.transform.TransformPoint(localPos);
}
}
}
return worldPos;
}
/// <summary>
/// Converts a position in map space into a laitude longitude.
/// </summary>
/// <returns>Position in Latitude longitude.</returns>
/// <param name="realworldPoint">Realworld point.</param>
public virtual Vector2d WorldToGeoPosition(Vector3 realworldPoint)
{
// For quadtree implementation of the map, the map scale needs to be compensated for.
var scaleFactor = Mathf.Pow(2, (InitialZoom - AbsoluteZoom));
return (Root.InverseTransformPoint(realworldPoint)).GetGeoPosition(CenterMercator, WorldRelativeScale * scaleFactor);
}
/// <summary>
/// Queries the real world elevation data in Unity units at a given latitude longitude.
/// </summary>
/// <returns>The height data.</returns>
/// <param name="latlong">Latlong.</param>
public virtual float QueryElevationInUnityUnitsAt(Vector2d latlong)
{
float tileScale = 1f;
return QueryElevationAtInternal(latlong, out tileScale);
}
/// <summary>
/// Queries the real world elevation data in Meters at a given latitude longitude.
/// </summary>
/// <returns>The height data.</returns>
/// <param name="latlong">Latlong.</param>
public virtual float QueryElevationInMetersAt(Vector2d latlong)
{
float tileScale = 1f;
float height = QueryElevationAtInternal(latlong, out tileScale);
return (height / tileScale);
}
#endregion
#region Map Property Related Changes Methods
public virtual void SetCenterMercator(Vector2d centerMercator)
{
_centerMercator = centerMercator;
}
public virtual void SetCenterLatitudeLongitude(Vector2d centerLatitudeLongitude)
{
_options.locationOptions.latitudeLongitude = string.Format("{0}, {1}", centerLatitudeLongitude.x, centerLatitudeLongitude.y);
_centerLatitudeLongitude = centerLatitudeLongitude;
}
public virtual void SetWorldRelativeScale(float scale)
{
_worldRelativeScale = scale;
}
public virtual void SetLoadingTexture(Texture2D loadingTexture)
{
Options.loadingTexture = loadingTexture;
}
public virtual void SetTileMaterial(Material tileMaterial)
{
Options.tileMaterial = tileMaterial;
}
/// <summary>
/// Sets the extent type and parameters to control the maps extent.
/// </summary>
/// <param name="extentType">Extent type.</param>
/// <param name="extentOptions">Extent options.</param>
public virtual void SetExtent(MapExtentType extentType, ExtentOptions extentOptions = null)
{
_options.extentOptions.extentType = extentType;
if (extentOptions != null)
{
var currentOptions = _options.extentOptions.GetTileProviderOptions();
if (currentOptions.GetType() == extentOptions.GetType())
{
currentOptions = extentOptions;
}
}
OnTileProviderChanged();
}
/// <summary>
/// Set parameters for current extent calculator strategy.
/// </summary>
/// <param name="extentOptions">Parameters to control the map extent.</param>
public virtual void SetExtentOptions(ExtentOptions extentOptions)
{
_options.extentOptions.GetTileProviderOptions().SetOptions(extentOptions);
_options.extentOptions.defaultExtents.HasChanged = true;
}
/// <summary>
/// Sets the positions of the map's root transform.
/// Use <paramref name="placementType"/> = <c> MapPlacementType.AtTileCenter</c> to place map root at the center of tile containing the latitude,longitude.
/// Use <paramref name="placementType"/> = <c> MapPlacementType.AtLocationCenter</c> to place map root at the latitude,longitude.
/// </summary>
/// <param name="placementType">Placement type.</param>
public virtual void SetPlacementType(MapPlacementType placementType)
{
_options.placementOptions.placementType = placementType;
_options.placementOptions.HasChanged = true;
}
/// <summary>
/// Translates map root by the terrain elevation at the center geo location.
/// Use this method with <c>TerrainWithElevation</c>
/// </summary>
/// <param name="active">If set to <c>true</c> active.</param>
public virtual void SnapMapToZero(bool active)
{
_options.placementOptions.snapMapToZero = active;
_options.placementOptions.HasChanged = true;
}
/// <summary>
/// Sets the map to use real world scale for map tile.
/// Use world scale for AR use cases or applications that need true world scale.
/// </summary>
public virtual void UseWorldScale()
{
_options.scalingOptions.scalingType = MapScalingType.WorldScale;
_options.scalingOptions.HasChanged = true;
}
/// <summary>
/// Sets the map to use custom scale for map tiles.
/// </summary>
/// <param name="tileSizeInUnityUnits">Tile size in unity units to scale each Web Mercator tile.</param>
public virtual void UseCustomScale(float tileSizeInUnityUnits)
{
_options.scalingOptions.scalingType = MapScalingType.Custom;
_options.scalingOptions.unityTileSize = tileSizeInUnityUnits;
_options.scalingOptions.HasChanged = true;
}
#endregion
#region Events
/// <summary>
/// Event delegate, gets called after map is initialized
/// <seealso cref="OnUpdated"/>
/// </summary>
public event Action OnInitialized = delegate { };
/// <summary>
/// Event delegate, gets called after map is updated.
/// <c>UpdateMap</c> will trigger this event.
/// <seealso cref="OnInitialized"/>
/// </summary>
public event Action OnUpdated = delegate { };
public event Action OnMapRedrawn = delegate { };
/// <summary>
/// Event delegate, gets called when map preview is enabled
/// </summary>
public event Action OnEditorPreviewEnabled = delegate { };
/// <summary>
/// Event delegate, gets called when map preview is disabled
/// </summary>
public event Action OnEditorPreviewDisabled = delegate { };
/// <summary>
/// Event delegate, gets called when a tile is completed.
/// </summary>
public event Action<UnityTile> OnTileFinished = delegate { };
/// <summary>
/// Event delegate, gets called when new tiles coordinates are registered.
/// </summary>
public event Action<List<UnwrappedTileId>> OnTilesStarting = delegate { };
/// <summary>
/// Event delegate, gets called before a tile is getting recycled.
/// </summary>
public event Action<List<UnwrappedTileId>> OnTilesDisposing = delegate { };
#endregion
}
}
AbstractMapVisualizer.cs
using Mapbox.Unity.Map.Interfaces;
namespace Mapbox.Unity.Map
{
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using Mapbox.Map;
using Mapbox.Unity.MeshGeneration.Factories;
using Mapbox.Unity.MeshGeneration.Data;
using System;
using Mapbox.Platform;
using UnityEngine.Serialization;
using Mapbox.Unity.Utilities;
using Mapbox.Unity.MeshGeneration.Enums;
using Mapbox.Unity.MeshGeneration.Interfaces;
using CielaSpike;
using Realtime.Messaging.Internal;
/// <summary>
/// Map Visualizer
/// Represents a map.Doesn’t contain much logic and at the moment, it creates requested tiles and relays them to the factories
/// under itself.It has a caching mechanism to reuse tiles and does the tile positioning in unity world.
/// Later we’ll most likely keep track of map features here as well to allow devs to query for features easier
/// (i.e.query all buildings x meters around any restaurant etc).
/// </summary>
public abstract class AbstractMapVisualizer : ScriptableObject
{
[SerializeField]
[NodeEditorElementAttribute("Factories")]
[FormerlySerializedAs("_factories")]
public List<AbstractTileFactory> Factories;
protected IMapReadable _map;
protected ConcurrentDictionary<UnwrappedTileId, UnityTile> _activeTiles = new ConcurrentDictionary<UnwrappedTileId, UnityTile>();
protected Queue<UnityTile> _inactiveTiles = new Queue<UnityTile>();
private int _counter;
private ModuleState _state;
public ModuleState State
{
get
{
return _state;
}
internal set
{
if (_state != value)
{
_state = value;
OnMapVisualizerStateChanged(_state);
}
}
}
public IMapReadable Map { get { return _map; } }
public ConcurrentDictionary<UnwrappedTileId, UnityTile> ActiveTiles { get { return _activeTiles; } }
public Dictionary<UnwrappedTileId, int> _tileProgress;
public event Action<ModuleState> OnMapVisualizerStateChanged = delegate { };
public event Action<UnityTile> OnTileFinished = delegate { };
/// <summary>
/// Gets the unity tile from unwrapped tile identifier.
/// </summary>
/// <returns>The unity tile from unwrapped tile identifier.</returns>
/// <param name="tileId">Tile identifier.</param>
public UnityTile GetUnityTileFromUnwrappedTileId(UnwrappedTileId tileId)
{
return _activeTiles[tileId];
}
/// <summary>
/// Initializes the factories by passing the file source down, which is necessary for data (web/file) calls
/// </summary>
/// <param name="fileSource"></param>
public virtual void Initialize(IMapReadable map, IFileSource fileSource)
{
_map = map;
_tileProgress = new Dictionary<UnwrappedTileId, int>();
// Allow for map re-use by recycling any active tiles.
var activeTiles = _activeTiles.Keys.ToList();
foreach (var tile in activeTiles)
{
DisposeTile(tile);
}
State = ModuleState.Initialized;
foreach (var factory in Factories)
{
if (null == factory)
{
Debug.LogError("AbstractMapVisualizer: Factory is NULL");
}
else
{
factory.Initialize(fileSource);
UnregisterEvents(factory);
RegisterEvents(factory);
}
}
}
private void RegisterEvents(AbstractTileFactory factory)
{
factory.OnTileError += Factory_OnTileError;
}
private void UnregisterEvents(AbstractTileFactory factory)
{
factory.OnTileError -= Factory_OnTileError;
}
public virtual void Destroy()
{
if (Factories != null)
{
_counter = Factories.Count;
for (int i = 0; i < _counter; i++)
{
if (Factories[i] != null)
{
UnregisterEvents(Factories[i]);
}
}
}
// Inform all downstream nodes that we no longer need to process these tiles.
// This scriptable object may be re-used, but it's gameobjects are likely
// to be destroyed by a scene change, for example.
foreach (var tileId in _activeTiles.Keys.ToList())
{
DisposeTile(tileId);
}
_activeTiles.Clear();
_inactiveTiles.Clear();
}
#region Factory event callbacks
//factory event callback, not relaying this up for now
private void TileHeightStateChanged(UnityTile tile)
{
if (tile.HeightDataState == TilePropertyState.Loaded)
{
OnTileHeightProcessingFinished(tile);
}
TileStateChanged(tile);
}
private void TileRasterStateChanged(UnityTile tile)
{
if (tile.RasterDataState == TilePropertyState.Loaded)
{
OnTileImageProcessingFinished(tile);
}
TileStateChanged(tile);
}
private void TileVectorStateChanged(UnityTile tile)
{
if (tile.VectorDataState == TilePropertyState.Loaded)
{
OnTileVectorProcessingFinished(tile);
}
TileStateChanged(tile);
}
public virtual void TileStateChanged(UnityTile tile)
{
bool rasterDone = (tile.RasterDataState == TilePropertyState.None ||
tile.RasterDataState == TilePropertyState.Loaded ||
tile.RasterDataState == TilePropertyState.Error ||
tile.RasterDataState == TilePropertyState.Cancelled);
bool terrainDone = (tile.HeightDataState == TilePropertyState.None ||
tile.HeightDataState == TilePropertyState.Loaded ||
tile.HeightDataState == TilePropertyState.Error ||
tile.HeightDataState == TilePropertyState.Cancelled);
bool vectorDone = (tile.VectorDataState == TilePropertyState.None ||
tile.VectorDataState == TilePropertyState.Loaded ||
tile.VectorDataState == TilePropertyState.Error ||
tile.VectorDataState == TilePropertyState.Cancelled);
if (rasterDone && terrainDone && vectorDone)
{
tile.TileState = MeshGeneration.Enums.TilePropertyState.Loaded;
OnTileFinished(tile);
// Check if all tiles in extent are active tiles
if (_map.CurrentExtent.Count == _activeTiles.Count)
{
bool allDone = true;
// Check if all tiles are loaded.
foreach (var currentTile in _map.CurrentExtent)
{
allDone = allDone && (_activeTiles.ContainsKey(currentTile) && _activeTiles[currentTile].TileState == TilePropertyState.Loaded);
}
if (allDone)
{
State = ModuleState.Finished;
}
else
{
State = ModuleState.Working;
}
}
else
{
State = ModuleState.Working;
}
}
}
#endregion
/// <summary>
/// Registers requested tiles to the factories
/// </summary>
/// <param name="tileId"></param>
public virtual UnityTile LoadTile(UnwrappedTileId tileId)
{
UnityTile unityTile = null;
if (_inactiveTiles.Count > 0)
{
unityTile = _inactiveTiles.Dequeue();
}
if (unityTile == null)
{
unityTile = new GameObject().AddComponent<UnityTile>();
try
{
unityTile.MeshRenderer.sharedMaterial = Instantiate(_map.TileMaterial);
}
catch
{
Debug.Log("Tile Material not set. Using default material");
unityTile.MeshRenderer.sharedMaterial = Instantiate(new Material(Shader.Find("Diffuse")));
}
unityTile.transform.SetParent(_map.Root, false);
}
unityTile.Initialize(_map, tileId, _map.WorldRelativeScale, _map.AbsoluteZoom, _map.LoadingTexture);
PlaceTile(tileId, unityTile, _map);
// Don't spend resources naming objects, as you shouldn't find objects by name anyway!
#if UNITY_EDITOR
unityTile.gameObject.name = unityTile.CanonicalTileId.ToString();
#endif
unityTile.OnHeightDataChanged += TileHeightStateChanged;
unityTile.OnRasterDataChanged += TileRasterStateChanged;
unityTile.OnVectorDataChanged += TileVectorStateChanged;
unityTile.TileState = MeshGeneration.Enums.TilePropertyState.Loading;
ActiveTiles.TryAdd(tileId, unityTile);
foreach (var factory in Factories)
{
factory.Register(unityTile);
}
return unityTile;
}
public virtual void DisposeTile(UnwrappedTileId tileId)
{
if (!ActiveTiles.ContainsKey(tileId)) return;
var unityTile = ActiveTiles[tileId];
foreach (var factory in Factories)
{
factory.Unregister(unityTile);
}
unityTile.Recycle();
UnityTile holder;
ActiveTiles.TryRemove(tileId, out holder);
_inactiveTiles.Enqueue(unityTile);
}
/// <summary>
/// Repositions active tiles instead of recreating them. Useful for panning the map
/// </summary>
/// <param name="tileId"></param>
public virtual void RepositionTile(UnwrappedTileId tileId)
{
UnityTile currentTile;
if (ActiveTiles.TryGetValue(tileId, out currentTile))
{
PlaceTile(tileId, currentTile, _map);
}
}
protected abstract void PlaceTile(UnwrappedTileId tileId, UnityTile tile, IMapReadable map);
public void ClearMap()
{
UnregisterAllTiles();
if (Factories != null)
{
foreach (var tileFactory in Factories)
{
if (tileFactory != null)
{
tileFactory.Clear();
DestroyImmediate(tileFactory);
}
}
}
foreach (var tileId in _activeTiles.Keys.ToList())
{
_activeTiles[tileId].ClearAssets();
DisposeTile(tileId);
}
foreach (var tile in _inactiveTiles)
{
tile.ClearAssets();
DestroyImmediate(tile.gameObject);
}
_inactiveTiles.Clear();
State = ModuleState.Initialized;
}
public void ReregisterAllTiles()
{
foreach (var activeTile in _activeTiles)
{
foreach (var abstractTileFactory in Factories)
{
abstractTileFactory.Register(activeTile.Value);
}
}
}
public void UnregisterAllTiles()
{
foreach (var activeTile in _activeTiles)
{
foreach (var abstractTileFactory in Factories)
{
abstractTileFactory.Unregister(activeTile.Value);
}
}
}
public void UnregisterTilesFrom(AbstractTileFactory factory)
{
foreach (KeyValuePair<UnwrappedTileId, UnityTile> tileBundle in _activeTiles)
{
factory.Unregister(tileBundle.Value);
}
}
public void UnregisterAndRedrawTilesFromLayer(VectorTileFactory factory, LayerVisualizerBase layerVisualizer)
{
foreach (KeyValuePair<UnwrappedTileId, UnityTile> tileBundle in _activeTiles)
{
factory.UnregisterLayer(tileBundle.Value, layerVisualizer);
}
layerVisualizer.Clear();
layerVisualizer.UnbindSubLayerEvents();
layerVisualizer.SetProperties(layerVisualizer.SubLayerProperties);
layerVisualizer.InitializeStack();
foreach (KeyValuePair<UnwrappedTileId, UnityTile> tileBundle in _activeTiles)
{
factory.RedrawSubLayer(tileBundle.Value, layerVisualizer);
}
}
public void RemoveTilesFromLayer(VectorTileFactory factory, LayerVisualizerBase layerVisualizer)
{
foreach (KeyValuePair<UnwrappedTileId, UnityTile> tileBundle in _activeTiles)
{
factory.UnregisterLayer(tileBundle.Value, layerVisualizer);
}
factory.RemoveVectorLayerVisualizer(layerVisualizer);
}
public void ReregisterTilesTo(VectorTileFactory factory)
{
foreach (KeyValuePair<UnwrappedTileId, UnityTile> tileBundle in _activeTiles)
{
factory.Register(tileBundle.Value);
}
}
public void UpdateTileForProperty(AbstractTileFactory factory, LayerUpdateArgs updateArgs)
{
foreach (KeyValuePair<UnwrappedTileId, UnityTile> tileBundle in _activeTiles)
{
factory.UpdateTileProperty(tileBundle.Value, updateArgs);
}
}
#region Events
/// <summary>
/// The <c>OnTileError</c> event triggers when there's a <c>Tile</c> error.
/// Returns a <see cref="T:Mapbox.Map.TileErrorEventArgs"/> instance as a parameter, for the tile on which error occurred.
/// </summary>
public event EventHandler<TileErrorEventArgs> OnTileError;
private void Factory_OnTileError(object sender, TileErrorEventArgs e)
{
EventHandler<TileErrorEventArgs> handler = OnTileError;
if (handler != null)
{
handler(this, e);
}
}
/// <summary>
/// Event delegate, gets called when terrain factory finishes processing a tile.
/// </summary>
public event Action<UnityTile> OnTileHeightProcessingFinished = delegate {};
/// <summary>
/// Event delegate, gets called when image factory finishes processing a tile.
/// </summary>
public event Action<UnityTile> OnTileImageProcessingFinished = delegate {};
/// <summary>
/// Event delegate, gets called when vector factory finishes processing a tile.
/// </summary>
public event Action<UnityTile> OnTileVectorProcessingFinished = delegate {};
#endregion
}
}
This still works BTW, but it needs messing around because the AbstractMap.cs and AbstractMapVisualizer.cs have changed since the OP's base version. Here is a gross version of the modified files that I painstakingly copy pasted now knowing exactly how to properly diff them. I have done a basic test to navigate the map in the unity editor, and it is works incredibly smooth! Well done @genaray and double thanks for introducing me to the ThreadNinja asset!!!
For future reference these files are based on the commit 0f851d1 from master branch, 16 oct 2019, version number 2.1.1
AbstractMap.cs
using Mapbox.Platform.Cache; using Mapbox.Unity.Map.Interfaces; using Mapbox.Unity.Map.Strategies; using Mapbox.Unity.Map.TileProviders; namespace Mapbox.Unity.Map { using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Mapbox.Unity.Utilities; using Utils; using UnityEngine; using Mapbox.Map; using Mapbox.Unity.MeshGeneration.Factories; using Mapbox.Unity.MeshGeneration.Data; using System.Globalization; using CielaSpike; /// <summary> /// Abstract map. /// This is the main monobehavior which controls the map. It controls the visualization of map data. /// Abstract map encapsulates the image, terrain and vector sources and provides a centralized interface to control the visualization of the map. /// </summary> [ExecuteInEditMode] public class AbstractMap : MonoBehaviour, IMap { #region Private Fields [SerializeField] private MapOptions _options = new MapOptions(); [SerializeField] private bool _initializeOnStart = true; [SerializeField] protected ImageryLayer _imagery = new ImageryLayer(); [SerializeField] protected TerrainLayer _terrain = new TerrainLayer(); [SerializeField] protected VectorLayer _vectorData = new VectorLayer(); [SerializeField] protected AbstractTileProvider _tileProvider; [SerializeField] protected HashSet<UnwrappedTileId> _currentExtent; [SerializeField] protected EditorPreviewOptions _previewOptions = new EditorPreviewOptions(); private List<UnwrappedTileId> tilesToProcess; public bool isInitialized = false; protected AbstractMapVisualizer _mapVisualizer; protected float _unityTileSize = 1; protected bool _worldHeightFixed = false; protected MapboxAccess _fileSource; protected int _initialZoom; protected Vector2d _centerLatitudeLongitude; protected Vector2d _centerMercator; protected float _worldRelativeScale; protected Vector3 _mapScaleFactor; protected Vector3 _cachedPosition; protected Quaternion _cachedRotation; protected Vector3 _cachedScale = Vector3.one; #endregion #region Properties public bool IsEditorPreviewEnabled { get { return _previewOptions.isPreviewEnabled; } set { _previewOptions.isPreviewEnabled = value; } } public AbstractMapVisualizer MapVisualizer { get { if (_mapVisualizer == null) { _mapVisualizer = ScriptableObject.CreateInstance<MapVisualizer>(); } return _mapVisualizer; } set { _mapVisualizer = value; } } public AbstractTileProvider TileProvider { get { return _tileProvider; } set { if (_tileProvider != null) { _tileProvider.ExtentChanged -= OnMapExtentChanged; } _tileProvider = value; if (_tileProvider != null) { _tileProvider.ExtentChanged += OnMapExtentChanged; } } } /// <summary> /// The map options. /// Options to control the behaviour of the map like location,extent, scale and placement. /// </summary> public MapOptions Options { get { return _options; } set { _options = value; } } /// <summary> /// Options to control the imagery component of the map. /// </summary> [NodeEditorElement("Layers")] public IImageryLayer ImageLayer { get { return _imagery; } } /// <summary> /// Options to control the terrain/ elevation component of the map. /// </summary> [NodeEditorElement("Layers")] public ITerrainLayer Terrain { get { return _terrain; } } /// <summary> /// The vector data. /// Options to control the vector data component of the map. /// Adds a vector source and visualizers to define the rendering behaviour of vector data layers. /// </summary> [NodeEditorElement("Layers")] public IVectorDataLayer VectorData { get { return _vectorData; } } public Vector2d CenterLatitudeLongitude { get { return _centerLatitudeLongitude; } } public Vector2d CenterMercator { get { return _centerMercator; } } public float WorldRelativeScale { get { return _worldRelativeScale; } } public float UnityTileSize { get { return _unityTileSize; } } /// <summary> /// Gets the absolute zoom of the tiles being currently rendered. /// <seealso cref="Zoom"/> /// </summary> /// <value>The absolute zoom.</value> public int AbsoluteZoom { get { return (int)Math.Floor(Options.locationOptions.zoom); } } /// <summary> /// Gets the current zoom value of the map. /// Use <c>AbsoluteZoom</c> to get the zoom level of the tileset. /// <seealso cref="AbsoluteZoom"/> /// </summary> /// <value>The zoom.</value> public float Zoom { get { return Options.locationOptions.zoom; } } public void SetZoom(float zoom) { Options.locationOptions.zoom = zoom; } /// <summary> /// Gets the initial zoom at which the map was initialized. /// This parameter is useful in calculating the scale of the tiles and the map. /// </summary> /// <value>The initial zoom.</value> public int InitialZoom { get { return _initialZoom; } } public Transform Root { get { return transform; } } /// <summary> /// Setting to trigger map initialization in Unity's Start method. /// if set to false, Initialize method should be called explicitly to initialize the map. /// </summary> public bool InitializeOnStart { get { return _initializeOnStart; } set { _initializeOnStart = value; } } public HashSet<UnwrappedTileId> CurrentExtent { get { return _currentExtent; } } /// <summary> /// Gets the loading texture used as a placeholder while the image tile is loading. /// </summary> /// <value>The loading texture.</value> public Texture2D LoadingTexture { get { return _options.loadingTexture; } } /// <summary> /// Gets the tile material used for map tiles. /// </summary> /// <value>The tile material.</value> public Material TileMaterial { get { return _options.tileMaterial; } } public Type ExtentCalculatorType { get { return TileProvider.GetType(); } } #endregion #region Public Methods /// <summary> /// Initialize the map using the specified latLon and zoom. /// Map will automatically get initialized in the <c>Start</c> method. /// Use this method to explicitly initialize the map and disable intialize on <c>Start</c> /// </summary> /// <returns>The initialize.</returns> /// <param name="latLon">Lat lon.</param> /// <param name="zoom">Zoom.</param> public virtual void Initialize(Vector2d latLon, int zoom) { _initializeOnStart = false; if (_options == null) { _options = new MapOptions(); } _options.locationOptions.latitudeLongitude = String.Format(CultureInfo.InvariantCulture, "{0},{1}", latLon.x, latLon.y); _options.locationOptions.zoom = zoom; SetUpMap(); } protected virtual void Update() { if (Application.isEditor && !Application.isPlaying && IsEditorPreviewEnabled == false) { return; } if (TileProvider != null) { TileProvider.UpdateTileProvider(); } } public virtual void UpdateMap() { UpdateMap(Conversions.StringToLatLon(_options.locationOptions.latitudeLongitude), Zoom); } public virtual void UpdateMap(Vector2d latLon) { UpdateMap(latLon, Zoom); } public virtual void UpdateMap(float zoom) { UpdateMap(Conversions.StringToLatLon(_options.locationOptions.latitudeLongitude), zoom); } /// <summary> /// Updates the map. /// Use this method to update the location of the map. /// Update method should be used when panning, zooming or changing location of the map. /// This method avoid startup delays that might occur on re-initializing the map. /// </summary> /// <param name="latLon">LatitudeLongitude.</param> /// <param name="zoom">Zoom level.</param> public virtual void UpdateMap(Vector2d latLon, float zoom) { if (Application.isEditor && !Application.isPlaying && !IsEditorPreviewEnabled) { return; } //so map will be snapped to zero using next new tile loaded _worldHeightFixed = false; float differenceInZoom = 0.0f; bool isAtInitialZoom = false; // Update map zoom, if it has changed. if (Math.Abs(Zoom - zoom) > Constants.EpsilonFloatingPoint) { SetZoom(zoom); } // Compute difference in zoom. Will be used to calculate correct scale of the map. differenceInZoom = Zoom - InitialZoom; isAtInitialZoom = (differenceInZoom - 0.0 < Constants.EpsilonFloatingPoint); //Update center latitude longitude var centerLatitudeLongitude = latLon; double xDelta = centerLatitudeLongitude.x; double zDelta = centerLatitudeLongitude.y; xDelta = xDelta > 0 ? Mathd.Min(xDelta, Mapbox.Utils.Constants.LatitudeMax) : Mathd.Max(xDelta, -Mapbox.Utils.Constants.LatitudeMax); zDelta = zDelta > 0 ? Mathd.Min(zDelta, Mapbox.Utils.Constants.LongitudeMax) : Mathd.Max(zDelta, -Mapbox.Utils.Constants.LongitudeMax); //Set Center in Latitude Longitude and Mercator. SetCenterLatitudeLongitude(new Vector2d(xDelta, zDelta)); Options.scalingOptions.scalingStrategy.SetUpScaling(this); Options.placementOptions.placementStrategy.SetUpPlacement(this); //Scale the map accordingly. if (Math.Abs(differenceInZoom) > Constants.EpsilonFloatingPoint || isAtInitialZoom) { _mapScaleFactor = Vector3.one * Mathf.Pow(2, differenceInZoom); Root.localScale = _mapScaleFactor; } //Update Tile extent. if (TileProvider != null) { TileProvider.UpdateTileExtent(); } if (OnUpdated != null) { OnUpdated(); } } private void Reset() { DisableEditorPreview(); } /// <summary> /// Resets the map. /// Use this method to reset the map. /// </summary> [ContextMenu("ResetMap")] public void ResetMap() { if (_previewOptions.isPreviewEnabled) { DisableEditorPreview(); EnableEditorPreview(); } else { MapOnAwakeRoutine(); MapOnStartRoutine(false); } } public bool IsAccessTokenValid { get { bool isAccessTokenValid = false; try { var accessTokenCheck = Unity.MapboxAccess.Instance; if (Unity.MapboxAccess.Instance.Configuration == null || string.IsNullOrEmpty(Unity.MapboxAccess.Instance.Configuration.AccessToken)) { return false; } isAccessTokenValid = true; } catch (System.Exception) { isAccessTokenValid = false; } return isAccessTokenValid; } } #endregion #region Private/Protected Methods private void OnEnable() { tilesToProcess = new List<UnwrappedTileId>(); if (_options.tileMaterial == null) { _options.tileMaterial = new Material(Shader.Find("Standard")); } if (_options.loadingTexture == null) { _options.loadingTexture = new Texture2D(1, 1); } } // TODO: implement IDisposable, instead? protected virtual void OnDestroy() { if (TileProvider != null) { TileProvider.ExtentChanged -= OnMapExtentChanged; } _mapVisualizer.ClearMap(); _mapVisualizer.Destroy(); } protected virtual void Awake() { if (_previewOptions.isPreviewEnabled == true) { DisableEditorPreview(); _previewOptions.isPreviewEnabled = false; } MapOnAwakeRoutine(); } protected virtual void Start() { MapOnStartRoutine(); } private void MapOnAwakeRoutine() { // Destroy any ghost game objects. DestroyChildObjects(); // Setup a visualizer to get a "Starter" map. if (_mapVisualizer == null) { _mapVisualizer = ScriptableObject.CreateInstance<MapVisualizer>(); } _mapVisualizer.OnTileFinished += (s) => { OnTileFinished(s); }; } public void DestroyChildObjects() { int destroyChildStartIndex = transform.childCount - 1; for (int i = destroyChildStartIndex; i >= 0; i--) { transform.GetChild(i).gameObject.Destroy(); } } private void MapOnStartRoutine(bool coroutine = true) { if (Application.isPlaying) { if (coroutine) { StartCoroutine("SetupAccess"); } if (_initializeOnStart) { SetUpMap(); } } } private void EnableDisablePreview(object sender, EventArgs e) { if (!Application.isPlaying) { if (_previewOptions.isPreviewEnabled) { EnableEditorPreview(); } else { DisableEditorPreview(); } } } public void EnableEditorPreview() { _cachedPosition = transform.position; _cachedRotation = transform.rotation; _cachedScale = transform.localScale; SetUpMap(); if (OnEditorPreviewEnabled != null) { OnEditorPreviewEnabled(); } } public void DisableEditorPreview() { _imagery.UpdateLayer -= OnImageOrTerrainUpdateLayer; _terrain.UpdateLayer -= OnImageOrTerrainUpdateLayer; _vectorData.SubLayerRemoved -= OnVectorDataSubLayerRemoved; _vectorData.SubLayerAdded -= OnVectorDataSubLayerAdded; _vectorData.UpdateLayer -= OnVectorDataUpdateLayer; _vectorData.UnbindAllEvents(); if (_mapVisualizer != null) { _mapVisualizer.ClearMap(); } DestroyTileProvider(); if (OnEditorPreviewDisabled != null) { OnEditorPreviewDisabled(); } transform.position = _cachedPosition; transform.rotation = _cachedRotation; transform.localScale = _cachedScale; } public void DestroyTileProvider() { var tileProvider = TileProvider ?? gameObject.GetComponent<AbstractTileProvider>(); if (_options.extentOptions.extentType != MapExtentType.Custom && tileProvider != null) { tileProvider.gameObject.Destroy(); _tileProvider = null; } } protected IEnumerator SetupAccess() { _fileSource = MapboxAccess.Instance; yield return new WaitUntil(() => MapboxAccess.Configured); } /// <summary> /// Sets up map. /// This method uses the mapOptions and layer properties to setup the map to be rendered. /// Override <c>SetUpMap</c> to write custom behavior to map setup. /// </summary> protected virtual void SetUpMap() { SetPlacementStrategy(); SetScalingStrategy(); SetTileProvider(); if (_imagery == null) { _imagery = new ImageryLayer(); } _imagery.Initialize(); if (_terrain == null) { _terrain = new TerrainLayer(); } _terrain.Initialize(); if (_vectorData == null) { _vectorData = new VectorLayer(); } _vectorData.Initialize(); _mapVisualizer.Factories = new List<AbstractTileFactory> { _terrain.Factory, _imagery.Factory, _vectorData.Factory }; InitializeMap(_options); } protected virtual IEnumerator TileProvider_OnTileAdded(UnwrappedTileId tileId) { yield return Ninja.JumpToUnity; var tile = _mapVisualizer.LoadTile(tileId); yield return Ninja.JumpBack; if (Options.placementOptions.snapMapToZero && !_worldHeightFixed) { _worldHeightFixed = true; if (tile.HeightDataState == MeshGeneration.Enums.TilePropertyState.Loaded) { ApplySnapWorldToZero(tile); } else { tile.OnHeightDataChanged += (s) => { ApplySnapWorldToZero(tile); }; } } // not needed? cause we are already back? just an extra wait for no reason yield return Ninja.JumpBack; } protected virtual void TileProvider_OnTileRemoved(UnwrappedTileId tileId) { _mapVisualizer.DisposeTile(tileId); } protected virtual void TileProvider_OnTileRepositioned(UnwrappedTileId tileId) { _mapVisualizer.RepositionTile(tileId); } protected void SendInitialized() { OnInitialized(); } /// <summary> /// Apply Snap World to Zero setting by moving map in Y Axis such that /// center of the given tile will be at y=0. /// </summary> /// <param name="referenceTile">Tile to use for Y axis correction.</param> private void ApplySnapWorldToZero(UnityTile referenceTile) { if (_options.placementOptions.snapMapToZero) { var h = referenceTile.QueryHeightData(.5f, .5f); Root.transform.localPosition = new Vector3( Root.transform.position.x, -h, Root.transform.position.z); } else { Root.transform.localPosition = new Vector3( Root.transform.position.x, 0, Root.transform.position.z); } } /// <summary> /// Initializes the map using the mapOptions. /// </summary> /// <param name="options">Options.</param> protected virtual void InitializeMap(MapOptions options) { Options = options; _worldHeightFixed = false; _fileSource = MapboxAccess.Instance; _centerLatitudeLongitude = Conversions.StringToLatLon(options.locationOptions.latitudeLongitude); _initialZoom = (int)options.locationOptions.zoom; options.scalingOptions.scalingStrategy.SetUpScaling(this); options.placementOptions.placementStrategy.SetUpPlacement(this); //Set up events for changes. _imagery.UpdateLayer += OnImageOrTerrainUpdateLayer; _terrain.UpdateLayer += OnImageOrTerrainUpdateLayer; _vectorData.SubLayerRemoved += OnVectorDataSubLayerRemoved; _vectorData.SubLayerAdded += OnVectorDataSubLayerAdded; _vectorData.UpdateLayer += OnVectorDataUpdateLayer; _options.locationOptions.PropertyHasChanged += (object sender, System.EventArgs eventArgs) => { UpdateMap(); }; _options.extentOptions.PropertyHasChanged += (object sender, System.EventArgs eventArgs) => { OnTileProviderChanged(); }; _options.extentOptions.defaultExtents.PropertyHasChanged += (object sender, System.EventArgs eventArgs) => { if (Application.isEditor && !Application.isPlaying && IsEditorPreviewEnabled == false) { Debug.Log("defaultExtents"); return; } if (TileProvider != null) { TileProvider.UpdateTileExtent(); } }; _options.placementOptions.PropertyHasChanged += (object sender, System.EventArgs eventArgs) => { SetPlacementStrategy(); UpdateMap(); }; _options.scalingOptions.PropertyHasChanged += (object sender, System.EventArgs eventArgs) => { SetScalingStrategy(); UpdateMap(); }; _mapVisualizer.Initialize(this, _fileSource); TileProvider.Initialize(this); SendInitialized(); TileProvider.UpdateTileExtent(); } private void SetScalingStrategy() { switch (_options.scalingOptions.scalingType) { case MapScalingType.WorldScale: _options.scalingOptions.scalingStrategy = new MapScalingAtWorldScaleStrategy(); break; case MapScalingType.Custom: _options.scalingOptions.scalingStrategy = new MapScalingAtUnityScaleStrategy(); break; } } private void SetPlacementStrategy() { switch (_options.placementOptions.placementType) { case MapPlacementType.AtTileCenter: _options.placementOptions.placementStrategy = new MapPlacementAtTileCenterStrategy(); break; case MapPlacementType.AtLocationCenter: _options.placementOptions.placementStrategy = new MapPlacementAtLocationCenterStrategy(); break; default: _options.placementOptions.placementStrategy = new MapPlacementAtTileCenterStrategy(); break; } } private void SetTileProvider() { if (_options.extentOptions.extentType != MapExtentType.Custom) { ITileProviderOptions tileProviderOptions = _options.extentOptions.GetTileProviderOptions(); string tileProviderName = "TileProvider"; // Setup tileprovider based on type. switch (_options.extentOptions.extentType) { case MapExtentType.CameraBounds: { if (TileProvider != null) { if (!(TileProvider is QuadTreeTileProvider)) { TileProvider.gameObject.Destroy(); var go = new GameObject(tileProviderName); go.transform.parent = transform; TileProvider = go.AddComponent<QuadTreeTileProvider>(); } } else { var go = new GameObject(tileProviderName); go.transform.parent = transform; TileProvider = go.AddComponent<QuadTreeTileProvider>(); } break; } case MapExtentType.RangeAroundCenter: { if (TileProvider != null) { TileProvider.gameObject.Destroy(); var go = new GameObject(tileProviderName); go.transform.parent = transform; TileProvider = go.AddComponent<RangeTileProvider>(); } else { var go = new GameObject(tileProviderName); go.transform.parent = transform; TileProvider = go.AddComponent<RangeTileProvider>(); } break; } case MapExtentType.RangeAroundTransform: { if (TileProvider != null) { if (!(TileProvider is RangeAroundTransformTileProvider)) { TileProvider.gameObject.Destroy(); var go = new GameObject(tileProviderName); go.transform.parent = transform; TileProvider = go.AddComponent<RangeAroundTransformTileProvider>(); } } else { var go = new GameObject(tileProviderName); go.transform.parent = transform; TileProvider = go.AddComponent<RangeAroundTransformTileProvider>(); } break; } default: break; } TileProvider.SetOptions(tileProviderOptions); } else { TileProvider = _tileProvider; } } private IEnumerator TriggerTileRedrawForExtent(ExtentArgs currentExtent) { yield return Ninja.JumpBack; var _activeTiles = _mapVisualizer.ActiveTiles; _currentExtent = new HashSet<UnwrappedTileId>(currentExtent.activeTiles); if (tilesToProcess == null) { tilesToProcess = new List<UnwrappedTileId>(); } else { tilesToProcess.Clear(); } foreach (var item in _activeTiles) { if (TileProvider.Cleanup(item.Key)) { tilesToProcess.Add(item.Key); } } yield return Ninja.JumpToUnity; if (tilesToProcess.Count > 0) { OnTilesDisposing(tilesToProcess); foreach (var t2r in tilesToProcess) { TileProvider_OnTileRemoved(t2r); } } foreach (var tile in _activeTiles) { // Reposition tiles in case we panned. TileProvider_OnTileRepositioned(tile.Key); } tilesToProcess.Clear(); foreach (var tile in _currentExtent) { if (!_activeTiles.ContainsKey(tile)) { tilesToProcess.Add(tile); } } if (tilesToProcess.Count > 0) { OnTilesStarting(tilesToProcess); foreach (var tileId in tilesToProcess) { _mapVisualizer.State = ModuleState.Working; yield return TileProvider_OnTileAdded(tileId); } } } private void OnMapExtentChanged(object sender, ExtentArgs currentExtent) { this.StartCoroutineAsync(TriggerTileRedrawForExtent(currentExtent)); } private void OnImageOrTerrainUpdateLayer(object sender, System.EventArgs eventArgs) { LayerUpdateArgs layerUpdateArgs = eventArgs as LayerUpdateArgs; if (layerUpdateArgs != null) { _mapVisualizer.UpdateTileForProperty(layerUpdateArgs.factory, layerUpdateArgs); if (layerUpdateArgs.effectsVectorLayer) { RedrawVectorDataLayer(); } OnMapRedrawn(); } } private void RedrawVectorDataLayer() { _mapVisualizer.UnregisterTilesFrom(_vectorData.Factory); _vectorData.UnbindAllEvents(); _vectorData.UpdateFactorySettings(); _mapVisualizer.ReregisterTilesTo(_vectorData.Factory); } private void OnVectorDataSubLayerRemoved(object sender, EventArgs eventArgs) { VectorLayerUpdateArgs layerUpdateArgs = eventArgs as VectorLayerUpdateArgs; if (layerUpdateArgs.visualizer != null) { _mapVisualizer.RemoveTilesFromLayer((VectorTileFactory)layerUpdateArgs.factory, layerUpdateArgs.visualizer); } OnMapRedrawn(); } private void OnVectorDataSubLayerAdded(object sender, EventArgs eventArgs) { RedrawVectorDataLayer(); OnMapRedrawn(); } private void OnVectorDataUpdateLayer(object sender, System.EventArgs eventArgs) { VectorLayerUpdateArgs layerUpdateArgs = eventArgs as VectorLayerUpdateArgs; if (layerUpdateArgs.visualizer != null) { //We have a visualizer. Update only the visualizer. //No need to unload the entire factory to apply changes. _mapVisualizer.UnregisterAndRedrawTilesFromLayer((VectorTileFactory)layerUpdateArgs.factory, layerUpdateArgs.visualizer); } else { //We are updating a core property of vector section. //All vector features need to get unloaded and re-created. RedrawVectorDataLayer(); } OnMapRedrawn(); } private void OnTileProviderChanged() { if (Application.isEditor && !Application.isPlaying && IsEditorPreviewEnabled == false) { Debug.Log("extentOptions"); return; } SetTileProvider(); TileProvider.Initialize(this); if (IsEditorPreviewEnabled) { TileProvider.UpdateTileExtent(); } } #endregion #region Conversion and Height Query Methods private Vector3 GeoToWorldPositionXZ(Vector2d latitudeLongitude) { // For quadtree implementation of the map, the map scale needs to be compensated for. var scaleFactor = Mathf.Pow(2, (InitialZoom - AbsoluteZoom)); var worldPos = Conversions.GeoToWorldPosition(latitudeLongitude, CenterMercator, WorldRelativeScale * scaleFactor).ToVector3xz(); return Root.TransformPoint(worldPos); } protected virtual float QueryElevationAtInternal(Vector2d latlong, out float tileScale) { var _meters = Conversions.LatLonToMeters(latlong.x, latlong.y); UnityTile tile; bool foundTile = MapVisualizer.ActiveTiles.TryGetValue(Conversions.LatitudeLongitudeToTileId(latlong.x, latlong.y, (int)Zoom), out tile); if (foundTile) { tileScale = tile.TileScale; var _rect = tile.Rect; return tile.QueryHeightData((float)((_meters - _rect.Min).x / _rect.Size.x), (float)((_meters.y - _rect.Max.y) / _rect.Size.y)); } else { tileScale = 1f; return 0f; } } /// <summary> /// Converts a latitude longitude into map space position. /// </summary> /// <returns>Position in map space.</returns> /// <param name="latitudeLongitude">Latitude longitude.</param> /// <param name="queryHeight">If set to <c>true</c> will return the terrain height(in Unity units) at that point.</param> public virtual Vector3 GeoToWorldPosition(Vector2d latitudeLongitude, bool queryHeight = true) { Vector3 worldPos = GeoToWorldPositionXZ(latitudeLongitude); if (queryHeight) { //Query Height. float tileScale = 1f; float height = QueryElevationAtInternal(latitudeLongitude, out tileScale); // Apply height inside the unity tile space UnityTile tile; if (MapVisualizer.ActiveTiles.TryGetValue(Conversions.LatitudeLongitudeToTileId(latitudeLongitude.x, latitudeLongitude.y, (int)Zoom), out tile)) { if (tile != null) { // Calculate height in the local space of the tile gameObject. // Height is aligned with the y axis in local space. // This also helps us avoid scale values when setting the height. var localPos = tile.gameObject.transform.InverseTransformPoint(worldPos); localPos.y = height; worldPos = tile.gameObject.transform.TransformPoint(localPos); } } } return worldPos; } /// <summary> /// Converts a position in map space into a laitude longitude. /// </summary> /// <returns>Position in Latitude longitude.</returns> /// <param name="realworldPoint">Realworld point.</param> public virtual Vector2d WorldToGeoPosition(Vector3 realworldPoint) { // For quadtree implementation of the map, the map scale needs to be compensated for. var scaleFactor = Mathf.Pow(2, (InitialZoom - AbsoluteZoom)); return (Root.InverseTransformPoint(realworldPoint)).GetGeoPosition(CenterMercator, WorldRelativeScale * scaleFactor); } /// <summary> /// Queries the real world elevation data in Unity units at a given latitude longitude. /// </summary> /// <returns>The height data.</returns> /// <param name="latlong">Latlong.</param> public virtual float QueryElevationInUnityUnitsAt(Vector2d latlong) { float tileScale = 1f; return QueryElevationAtInternal(latlong, out tileScale); } /// <summary> /// Queries the real world elevation data in Meters at a given latitude longitude. /// </summary> /// <returns>The height data.</returns> /// <param name="latlong">Latlong.</param> public virtual float QueryElevationInMetersAt(Vector2d latlong) { float tileScale = 1f; float height = QueryElevationAtInternal(latlong, out tileScale); return (height / tileScale); } #endregion #region Map Property Related Changes Methods public virtual void SetCenterMercator(Vector2d centerMercator) { _centerMercator = centerMercator; } public virtual void SetCenterLatitudeLongitude(Vector2d centerLatitudeLongitude) { _options.locationOptions.latitudeLongitude = string.Format("{0}, {1}", centerLatitudeLongitude.x, centerLatitudeLongitude.y); _centerLatitudeLongitude = centerLatitudeLongitude; } public virtual void SetWorldRelativeScale(float scale) { _worldRelativeScale = scale; } public virtual void SetLoadingTexture(Texture2D loadingTexture) { Options.loadingTexture = loadingTexture; } public virtual void SetTileMaterial(Material tileMaterial) { Options.tileMaterial = tileMaterial; } /// <summary> /// Sets the extent type and parameters to control the maps extent. /// </summary> /// <param name="extentType">Extent type.</param> /// <param name="extentOptions">Extent options.</param> public virtual void SetExtent(MapExtentType extentType, ExtentOptions extentOptions = null) { _options.extentOptions.extentType = extentType; if (extentOptions != null) { var currentOptions = _options.extentOptions.GetTileProviderOptions(); if (currentOptions.GetType() == extentOptions.GetType()) { currentOptions = extentOptions; } } OnTileProviderChanged(); } /// <summary> /// Set parameters for current extent calculator strategy. /// </summary> /// <param name="extentOptions">Parameters to control the map extent.</param> public virtual void SetExtentOptions(ExtentOptions extentOptions) { _options.extentOptions.GetTileProviderOptions().SetOptions(extentOptions); _options.extentOptions.defaultExtents.HasChanged = true; } /// <summary> /// Sets the positions of the map's root transform. /// Use <paramref name="placementType"/> = <c> MapPlacementType.AtTileCenter</c> to place map root at the center of tile containing the latitude,longitude. /// Use <paramref name="placementType"/> = <c> MapPlacementType.AtLocationCenter</c> to place map root at the latitude,longitude. /// </summary> /// <param name="placementType">Placement type.</param> public virtual void SetPlacementType(MapPlacementType placementType) { _options.placementOptions.placementType = placementType; _options.placementOptions.HasChanged = true; } /// <summary> /// Translates map root by the terrain elevation at the center geo location. /// Use this method with <c>TerrainWithElevation</c> /// </summary> /// <param name="active">If set to <c>true</c> active.</param> public virtual void SnapMapToZero(bool active) { _options.placementOptions.snapMapToZero = active; _options.placementOptions.HasChanged = true; } /// <summary> /// Sets the map to use real world scale for map tile. /// Use world scale for AR use cases or applications that need true world scale. /// </summary> public virtual void UseWorldScale() { _options.scalingOptions.scalingType = MapScalingType.WorldScale; _options.scalingOptions.HasChanged = true; } /// <summary> /// Sets the map to use custom scale for map tiles. /// </summary> /// <param name="tileSizeInUnityUnits">Tile size in unity units to scale each Web Mercator tile.</param> public virtual void UseCustomScale(float tileSizeInUnityUnits) { _options.scalingOptions.scalingType = MapScalingType.Custom; _options.scalingOptions.unityTileSize = tileSizeInUnityUnits; _options.scalingOptions.HasChanged = true; } #endregion #region Events /// <summary> /// Event delegate, gets called after map is initialized /// <seealso cref="OnUpdated"/> /// </summary> public event Action OnInitialized = delegate { }; /// <summary> /// Event delegate, gets called after map is updated. /// <c>UpdateMap</c> will trigger this event. /// <seealso cref="OnInitialized"/> /// </summary> public event Action OnUpdated = delegate { }; public event Action OnMapRedrawn = delegate { }; /// <summary> /// Event delegate, gets called when map preview is enabled /// </summary> public event Action OnEditorPreviewEnabled = delegate { }; /// <summary> /// Event delegate, gets called when map preview is disabled /// </summary> public event Action OnEditorPreviewDisabled = delegate { }; /// <summary> /// Event delegate, gets called when a tile is completed. /// </summary> public event Action<UnityTile> OnTileFinished = delegate { }; /// <summary> /// Event delegate, gets called when new tiles coordinates are registered. /// </summary> public event Action<List<UnwrappedTileId>> OnTilesStarting = delegate { }; /// <summary> /// Event delegate, gets called before a tile is getting recycled. /// </summary> public event Action<List<UnwrappedTileId>> OnTilesDisposing = delegate { }; #endregion } }
AbstractMapVisualizer.cs
using Mapbox.Unity.Map.Interfaces; namespace Mapbox.Unity.Map { using System.Linq; using System.Collections.Generic; using UnityEngine; using Mapbox.Map; using Mapbox.Unity.MeshGeneration.Factories; using Mapbox.Unity.MeshGeneration.Data; using System; using Mapbox.Platform; using UnityEngine.Serialization; using Mapbox.Unity.Utilities; using Mapbox.Unity.MeshGeneration.Enums; using Mapbox.Unity.MeshGeneration.Interfaces; using CielaSpike; using Realtime.Messaging.Internal; /// <summary> /// Map Visualizer /// Represents a map.Doesn’t contain much logic and at the moment, it creates requested tiles and relays them to the factories /// under itself.It has a caching mechanism to reuse tiles and does the tile positioning in unity world. /// Later we’ll most likely keep track of map features here as well to allow devs to query for features easier /// (i.e.query all buildings x meters around any restaurant etc). /// </summary> public abstract class AbstractMapVisualizer : ScriptableObject { [SerializeField] [NodeEditorElementAttribute("Factories")] [FormerlySerializedAs("_factories")] public List<AbstractTileFactory> Factories; protected IMapReadable _map; protected ConcurrentDictionary<UnwrappedTileId, UnityTile> _activeTiles = new ConcurrentDictionary<UnwrappedTileId, UnityTile>(); protected Queue<UnityTile> _inactiveTiles = new Queue<UnityTile>(); private int _counter; private ModuleState _state; public ModuleState State { get { return _state; } internal set { if (_state != value) { _state = value; OnMapVisualizerStateChanged(_state); } } } public IMapReadable Map { get { return _map; } } public ConcurrentDictionary<UnwrappedTileId, UnityTile> ActiveTiles { get { return _activeTiles; } } public Dictionary<UnwrappedTileId, int> _tileProgress; public event Action<ModuleState> OnMapVisualizerStateChanged = delegate { }; public event Action<UnityTile> OnTileFinished = delegate { }; /// <summary> /// Gets the unity tile from unwrapped tile identifier. /// </summary> /// <returns>The unity tile from unwrapped tile identifier.</returns> /// <param name="tileId">Tile identifier.</param> public UnityTile GetUnityTileFromUnwrappedTileId(UnwrappedTileId tileId) { return _activeTiles[tileId]; } /// <summary> /// Initializes the factories by passing the file source down, which is necessary for data (web/file) calls /// </summary> /// <param name="fileSource"></param> public virtual void Initialize(IMapReadable map, IFileSource fileSource) { _map = map; _tileProgress = new Dictionary<UnwrappedTileId, int>(); // Allow for map re-use by recycling any active tiles. var activeTiles = _activeTiles.Keys.ToList(); foreach (var tile in activeTiles) { DisposeTile(tile); } State = ModuleState.Initialized; foreach (var factory in Factories) { if (null == factory) { Debug.LogError("AbstractMapVisualizer: Factory is NULL"); } else { factory.Initialize(fileSource); UnregisterEvents(factory); RegisterEvents(factory); } } } private void RegisterEvents(AbstractTileFactory factory) { factory.OnTileError += Factory_OnTileError; } private void UnregisterEvents(AbstractTileFactory factory) { factory.OnTileError -= Factory_OnTileError; } public virtual void Destroy() { if (Factories != null) { _counter = Factories.Count; for (int i = 0; i < _counter; i++) { if (Factories[i] != null) { UnregisterEvents(Factories[i]); } } } // Inform all downstream nodes that we no longer need to process these tiles. // This scriptable object may be re-used, but it's gameobjects are likely // to be destroyed by a scene change, for example. foreach (var tileId in _activeTiles.Keys.ToList()) { DisposeTile(tileId); } _activeTiles.Clear(); _inactiveTiles.Clear(); } #region Factory event callbacks //factory event callback, not relaying this up for now private void TileHeightStateChanged(UnityTile tile) { if (tile.HeightDataState == TilePropertyState.Loaded) { OnTileHeightProcessingFinished(tile); } TileStateChanged(tile); } private void TileRasterStateChanged(UnityTile tile) { if (tile.RasterDataState == TilePropertyState.Loaded) { OnTileImageProcessingFinished(tile); } TileStateChanged(tile); } private void TileVectorStateChanged(UnityTile tile) { if (tile.VectorDataState == TilePropertyState.Loaded) { OnTileVectorProcessingFinished(tile); } TileStateChanged(tile); } public virtual void TileStateChanged(UnityTile tile) { bool rasterDone = (tile.RasterDataState == TilePropertyState.None || tile.RasterDataState == TilePropertyState.Loaded || tile.RasterDataState == TilePropertyState.Error || tile.RasterDataState == TilePropertyState.Cancelled); bool terrainDone = (tile.HeightDataState == TilePropertyState.None || tile.HeightDataState == TilePropertyState.Loaded || tile.HeightDataState == TilePropertyState.Error || tile.HeightDataState == TilePropertyState.Cancelled); bool vectorDone = (tile.VectorDataState == TilePropertyState.None || tile.VectorDataState == TilePropertyState.Loaded || tile.VectorDataState == TilePropertyState.Error || tile.VectorDataState == TilePropertyState.Cancelled); if (rasterDone && terrainDone && vectorDone) { tile.TileState = MeshGeneration.Enums.TilePropertyState.Loaded; OnTileFinished(tile); // Check if all tiles in extent are active tiles if (_map.CurrentExtent.Count == _activeTiles.Count) { bool allDone = true; // Check if all tiles are loaded. foreach (var currentTile in _map.CurrentExtent) { allDone = allDone && (_activeTiles.ContainsKey(currentTile) && _activeTiles[currentTile].TileState == TilePropertyState.Loaded); } if (allDone) { State = ModuleState.Finished; } else { State = ModuleState.Working; } } else { State = ModuleState.Working; } } } #endregion /// <summary> /// Registers requested tiles to the factories /// </summary> /// <param name="tileId"></param> public virtual UnityTile LoadTile(UnwrappedTileId tileId) { UnityTile unityTile = null; if (_inactiveTiles.Count > 0) { unityTile = _inactiveTiles.Dequeue(); } if (unityTile == null) { unityTile = new GameObject().AddComponent<UnityTile>(); try { unityTile.MeshRenderer.sharedMaterial = Instantiate(_map.TileMaterial); } catch { Debug.Log("Tile Material not set. Using default material"); unityTile.MeshRenderer.sharedMaterial = Instantiate(new Material(Shader.Find("Diffuse"))); } unityTile.transform.SetParent(_map.Root, false); } unityTile.Initialize(_map, tileId, _map.WorldRelativeScale, _map.AbsoluteZoom, _map.LoadingTexture); PlaceTile(tileId, unityTile, _map); // Don't spend resources naming objects, as you shouldn't find objects by name anyway! #if UNITY_EDITOR unityTile.gameObject.name = unityTile.CanonicalTileId.ToString(); #endif unityTile.OnHeightDataChanged += TileHeightStateChanged; unityTile.OnRasterDataChanged += TileRasterStateChanged; unityTile.OnVectorDataChanged += TileVectorStateChanged; unityTile.TileState = MeshGeneration.Enums.TilePropertyState.Loading; ActiveTiles.TryAdd(tileId, unityTile); foreach (var factory in Factories) { factory.Register(unityTile); } return unityTile; } public virtual void DisposeTile(UnwrappedTileId tileId) { if (!ActiveTiles.ContainsKey(tileId)) return; var unityTile = ActiveTiles[tileId]; foreach (var factory in Factories) { factory.Unregister(unityTile); } unityTile.Recycle(); UnityTile holder; ActiveTiles.TryRemove(tileId, out holder); _inactiveTiles.Enqueue(unityTile); } /// <summary> /// Repositions active tiles instead of recreating them. Useful for panning the map /// </summary> /// <param name="tileId"></param> public virtual void RepositionTile(UnwrappedTileId tileId) { UnityTile currentTile; if (ActiveTiles.TryGetValue(tileId, out currentTile)) { PlaceTile(tileId, currentTile, _map); } } protected abstract void PlaceTile(UnwrappedTileId tileId, UnityTile tile, IMapReadable map); public void ClearMap() { UnregisterAllTiles(); if (Factories != null) { foreach (var tileFactory in Factories) { if (tileFactory != null) { tileFactory.Clear(); DestroyImmediate(tileFactory); } } } foreach (var tileId in _activeTiles.Keys.ToList()) { _activeTiles[tileId].ClearAssets(); DisposeTile(tileId); } foreach (var tile in _inactiveTiles) { tile.ClearAssets(); DestroyImmediate(tile.gameObject); } _inactiveTiles.Clear(); State = ModuleState.Initialized; } public void ReregisterAllTiles() { foreach (var activeTile in _activeTiles) { foreach (var abstractTileFactory in Factories) { abstractTileFactory.Register(activeTile.Value); } } } public void UnregisterAllTiles() { foreach (var activeTile in _activeTiles) { foreach (var abstractTileFactory in Factories) { abstractTileFactory.Unregister(activeTile.Value); } } } public void UnregisterTilesFrom(AbstractTileFactory factory) { foreach (KeyValuePair<UnwrappedTileId, UnityTile> tileBundle in _activeTiles) { factory.Unregister(tileBundle.Value); } } public void UnregisterAndRedrawTilesFromLayer(VectorTileFactory factory, LayerVisualizerBase layerVisualizer) { foreach (KeyValuePair<UnwrappedTileId, UnityTile> tileBundle in _activeTiles) { factory.UnregisterLayer(tileBundle.Value, layerVisualizer); } layerVisualizer.Clear(); layerVisualizer.UnbindSubLayerEvents(); layerVisualizer.SetProperties(layerVisualizer.SubLayerProperties); layerVisualizer.InitializeStack(); foreach (KeyValuePair<UnwrappedTileId, UnityTile> tileBundle in _activeTiles) { factory.RedrawSubLayer(tileBundle.Value, layerVisualizer); } } public void RemoveTilesFromLayer(VectorTileFactory factory, LayerVisualizerBase layerVisualizer) { foreach (KeyValuePair<UnwrappedTileId, UnityTile> tileBundle in _activeTiles) { factory.UnregisterLayer(tileBundle.Value, layerVisualizer); } factory.RemoveVectorLayerVisualizer(layerVisualizer); } public void ReregisterTilesTo(VectorTileFactory factory) { foreach (KeyValuePair<UnwrappedTileId, UnityTile> tileBundle in _activeTiles) { factory.Register(tileBundle.Value); } } public void UpdateTileForProperty(AbstractTileFactory factory, LayerUpdateArgs updateArgs) { foreach (KeyValuePair<UnwrappedTileId, UnityTile> tileBundle in _activeTiles) { factory.UpdateTileProperty(tileBundle.Value, updateArgs); } } #region Events /// <summary> /// The <c>OnTileError</c> event triggers when there's a <c>Tile</c> error. /// Returns a <see cref="T:Mapbox.Map.TileErrorEventArgs"/> instance as a parameter, for the tile on which error occurred. /// </summary> public event EventHandler<TileErrorEventArgs> OnTileError; private void Factory_OnTileError(object sender, TileErrorEventArgs e) { EventHandler<TileErrorEventArgs> handler = OnTileError; if (handler != null) { handler(this, e); } } /// <summary> /// Event delegate, gets called when terrain factory finishes processing a tile. /// </summary> public event Action<UnityTile> OnTileHeightProcessingFinished = delegate {}; /// <summary> /// Event delegate, gets called when image factory finishes processing a tile. /// </summary> public event Action<UnityTile> OnTileImageProcessingFinished = delegate {}; /// <summary> /// Event delegate, gets called when vector factory finishes processing a tile. /// </summary> public event Action<UnityTile> OnTileVectorProcessingFinished = delegate {}; #endregion } }
Im still using my old approach for my unity game, but we will switch to Unity 2019.3 soon, glad you updated this little feature ! :D Gonna try that out once we migrate... Still hope that one of the devs put this into production soon.
You're welcome. A small note, after further testing: due to the non-thread-safety, sometimes tiles don't delete properly, and it has to do with the tilesToProcess list changing during the execution of a foreach loop in AbstractMap.cs. So there is some work to be done, plus I can't imagine how many other things might break.
@genaray @horatiu665 Awesome work both! Have either of you succeeded in using this with the up to date SDK? I've done some testing and as horatiu665 mentions, thread safety issues during runtime cause a lot of missing tiles.
Any updates on this working on the new SDK?
Nothing from me.
@Budtom Im currently reworking my game to fit a ecs architecture... might switch to a newer version once that rework is finished, gonna let this thread know if i come up with a new multithreaded version :)
@genaray that's great news! On a related issue have you come across any issues with buildings appearing in incorrect places (when generating a new set of tiles)? Good luck with your refactor 😀
Any updates on this? Have been looking for solutions for weeks now. It would be a dealbreaker for me if I could'nt get this implemented with mapbox I might have to find another solution than mapbox. Those hangers are just too annoying 😢.
Any updates on this? Have been looking for solutions for weeks now. It would be a dealbreaker for me if I could'nt get this implemented with mapbox I might have to find another solution than mapbox. Those hangers are just too annoying 😢.
@TheStickCH It's been a while since I've looked into this (focused on other things) but I did have some limited success with thread ninja. I'll report back if I crack it.
Thats great news! Good luck and I hope you crack it. 😉
Hey everyone !
Im using mapbox 2.0.0 for one of my mobile games and for that case its really important that the map doesnt "lag" while scrolling... in my case it lagged pretty bad (Even more with tile layers ) and i needed a way to solve this...
Mapbox alone doesnt include "multi threading" itself right now... and so i came up with my own way... i ended up with modifying the tile visualizer and map a bit... My current solution isnt that polished right now, but it works pretty well for my cases, and hopefully you find a use case too...
Things you need :
ConcurrentDictionary ( normal Dictionarys arent thread safe... ) https://github.com/realtime-framework/unity3d-plugin/blob/master/Messaging/Internal/ConcurrentDictionary.cs
Thread-Ninja asset for Unity ( all versions should work ) https://forum.unity.com/threads/released-free-thread-ninja-run-co-routines-on-background-thread.232755/
Modified Mapbox files ( 2 classes ) https://drive.google.com/file/d/1SMh6hQaGy02VKND37Gs63VfeSf19UCkp/view?usp=sharing
The rest is pretty easy... simply copy the ConcurrentDictionary as a c# class somewhere into your project, install the Thread-Ninja Asset... and replace "AbstractMapVisualizer" & "AbstractMap" with the files from the zip... there you go, the map now adds tiles fluently without any blocking and waiting...
Please leave some feedback or probably even adjustments !
Disclaimer : Do a project backup... and it isnt the best solution this far, working on a better approach... code not polished yet, i bet theres a way to improve the performance even more.