using System; using System.Collections.Generic; using System.IO; using System.Reflection; using UnityEngine; public class Horizon2_ArcGIS : MonoBehaviour { private const int maxDownloads = 1; private const string server = "https://sampleserver4.arcgisonline.com/ArcGIS/rest/services/Elevation/ESRI_Elevation_World/MapServer/exts/ElevationsSOE/ElevationLayers/1/GetElevationData?f=json&Extent="; public bool interceptDownloadElevations = true; /// /// Number of tiles horizontally (X axis). /// [Tooltip("Number of tiles horizontally (X axis).")] public int countX = 3; /// /// Number of tiles vertically (Z axis). /// [Tooltip("Number of tiles vertically (Z axis).")] public int countY = 3; /// /// Offset of the horizon mesh along the y-axis relative to the map. Keep it negative. /// [Tooltip("Offset of the horizon mesh along the y-axis relative to the map. Keep it negative.")] public float positionYOffset = -5; public float inMapViewYOffset = -35; /// /// Offset zoom of tiles relative to map.zoom. Keep positive. /// [Tooltip("Offset zoom of tiles relative to map.zoom. Keep positive.")] public int zoomOffset = 3; /// /// Shader of the tiles mesh. /// [Tooltip("Shader of the tiles mesh.")] public Shader shader; /// /// Offset of the render queue relative to render queue of the shader. /// [Tooltip("Offset of the render queue relative to render queue of the shader.")] public int renderQueueOffset = 100; /// /// Tile resolution. /// [Tooltip("Tile resolution.")] public int resolution = 32; public int requestResolution = 100; public int mapResolution = 32; private OnlineMapsTile[] tiles; private Mesh mesh; private Vector3[] vertices; private Vector3[] normals; private Vector2[] uv; private int[] triangles; private OnlineMaps map; private OnlineMapsTileSetControl control; private MeshRenderer meshRenderer; private Vector2 ctl; private Vector2 cbr; private Queue requestQueue; private List elevationRequests; internal static OverviewElevation overviewElevation; private static OverviewElevation nextOverviewElevation; private static Horizon2_ArcGIS instance; private void DownloadElevation(OnlineMapsTile tile) { CData data = tile["cdata"] as CData; if (data != null) return; CData cdata = new CData(); tile["cdata"] = cdata; if (!cdata.TryLoadHeights(tile)) { requestQueue.Enqueue(tile); StartNextDownload(); } } private void DownloadOverview() { if (nextOverviewElevation != null) return; int zoom = map.zoom - zoomOffset; if (zoom < 3) zoom = 3; double tx, ty; map.GetTilePosition(out tx, out ty, zoom); int tlx = Mathf.RoundToInt((float)(tx - countX)); int tly = Mathf.RoundToInt((float)(ty - countY)); int max = 1 << zoom; int brx = tlx + countX * 2; int bry = tly + countY * 2; if (tlx >= max) tlx -= max; if (brx >= max) brx -= max; if (overviewElevation != null) { if (overviewElevation.tlx == tlx && overviewElevation.tly == tly && overviewElevation.brx == brx && overviewElevation.bry == bry) return; } nextOverviewElevation = new OverviewElevation(tlx, tly, brx, bry, zoom); nextOverviewElevation.OnDownloaded += OnOverviewDownloaded; } private void GetBordersUsingReflection() { Type type = typeof(OnlineMapsArcGISElevationManager); OnlineMapsArcGISElevationManager manager = OnlineMapsArcGISElevationManager.instance; if (manager != null) { IEnumerable fields = OnlineMapsReflectionHelper.GetFields(type, BindingFlags.Instance | BindingFlags.NonPublic); foreach (FieldInfo field in fields) { string n = field.Name; if (n == "elevationX1") ctl.x = (float)field.GetValue(manager); else if (n == "elevationY1") ctl.y = (float)field.GetValue(manager); else if (n == "elevationW") cbr.x = ctl.x + (float)field.GetValue(manager); else if (n == "elevationH") cbr.y = ctl.y + (float)field.GetValue(manager); } } } private void InitMesh() { GameObject go = new GameObject("Horizon"); go.transform.parent = map.gameObject.transform; go.transform.localPosition = Vector3.zero; go.transform.localRotation = Quaternion.Euler(Vector3.zero); go.transform.localScale = Vector3.one; MeshFilter meshFilter = go.AddComponent(); meshRenderer = go.AddComponent(); int countTiles = countX * countY; mesh = new Mesh(); mesh.name = "Horizon"; mesh.MarkDynamic(); meshFilter.sharedMesh = mesh; tiles = new OnlineMapsTile[countTiles]; int countVertices = (countX + 1) * (countY + 1) * resolution * resolution; vertices = new Vector3[countVertices]; normals = new Vector3[countVertices]; uv = new Vector2[countVertices]; triangles = new int[6 * resolution * resolution]; mesh.vertices = vertices; mesh.subMeshCount = countTiles; float r1 = resolution - 1; int index = 0; for (int i = 0; i < (countX + 1) * (countY + 1); i++) { for (int x = 0; x < resolution; x++) { for (int y = 0; y < resolution; y++) { normals[index] = Vector3.up; uv[index++] = new Vector2(x / r1, 1 - y / r1); } } } mesh.uv = uv; mesh.normals = normals; Material[] materials = new Material[countTiles]; for (int i = 0; i < countTiles; i++) { int ti = 0; for (int x = 0; x < resolution - 1; x++) { for (int y = 0; y < resolution - 1; y++) { int vi = i * resolution * resolution + x * resolution + y; triangles[ti] = vi; triangles[ti + 1] = vi + resolution + 1; triangles[ti + 2] = vi + 1; triangles[ti + 3] = vi; triangles[ti + 4] = vi + resolution; triangles[ti + 5] = vi + resolution + 1; ti += 6; } } mesh.SetTriangles(triangles, i); Material material = new Material(shader); material.renderQueue = shader.renderQueue + renderQueueOffset; materials[i] = material; } meshRenderer.sharedMaterials = materials; UpdateMesh(); mesh.RecalculateBounds(); } private void OnDisable() { meshRenderer.enabled = false; } private void OnEnable() { instance = this; if (meshRenderer == null) return; meshRenderer.enabled = true; OnlineMapsTile.OnTileDownloaded += OnTileDownloaded; if (OnlineMapsCache.instance != null) { OnlineMapsCache.instance.OnLoadedFromCache -= OnTileDownloaded; OnlineMapsCache.instance.OnLoadedFromCache += OnTileDownloaded; } map.OnMapUpdated += UpdateMesh; if (interceptDownloadElevations && OnlineMapsElevationManagerBase.isActive) { OnlineMapsElevationManagerBase.instance.OnGetElevation -= OnGetElevation; OnlineMapsElevationManagerBase.instance.OnGetElevation += OnGetElevation; } } private void OnGetElevation(double leftLng, double topLat, double rightLng, double bottomLat) { ctl = new Vector2((float)leftLng, (float)topLat); cbr = new Vector2((float)rightLng, (float)bottomLat); double tlx, tly, brx, bry; map.projection.CoordinatesToTile(leftLng, topLat, map.zoom, out tlx, out tly); map.projection.CoordinatesToTile(rightLng, bottomLat, map.zoom, out brx, out bry); int scale = 1 << zoomOffset; int zoom = map.zoom - zoomOffset; int res = instance.mapResolution; int res2 = res - 1; short[,] heights = new short[res, res]; double rx = (brx - tlx) / res2; double ry = (bry - tly) / res2; OnlineMapsTile tile = null; int max = 1 << zoom; for (int x = 0; x < res; x++) { double tx = (rx * x + tlx) / scale; for (int y = 0; y < res; y++) { double ty = (ry * y + tly) / scale; if (tile == null || tile.x != (int)tx || tile.y != (int)ty) { tile = map.tileManager.GetTile(zoom, (int)tx, (int)ty); } if (tile == null) { if (overviewElevation != null) heights[x, res2 - y] = (short)Mathf.Round(overviewElevation.GetElevation(tx / max, ty / max)); else heights[x, res2 - y] = 0; continue; } CData data = tile["cdata"] as CData; if (data == null || !data.hasData) { if (overviewElevation != null) heights[x, res2 - y] = (short)Mathf.Round(overviewElevation.GetElevation(tx / max, ty / max)); else heights[x, res2 - y] = 0; continue; } heights[x, res2 - y] = (short)Mathf.Round(data.GetElevation(tx, ty)); } } if (interceptDownloadElevations && OnlineMapsElevationManagerBase.isActive) OnlineMapsArcGISElevationManager.instance.SetElevationData(heights); } private void OnOverviewDownloaded() { if (ctl == Vector2.zero && cbr == Vector2.zero) GetBordersUsingReflection(); OnGetElevation(ctl.x, ctl.y, cbr.x, cbr.y); UpdateMesh(); } private void OnTileDownloaded(OnlineMapsTile tile) { for (int i = 0; i < countX * countY; i++) { if (tiles[i] == tile) { meshRenderer.sharedMaterials[i].mainTexture = tile.texture; break; } } } private void Start() { elevationRequests = new List(); requestQueue = new Queue(); if (zoomOffset <= 0) throw new Exception("Zoom offset should be positive."); if (shader == null) shader = Shader.Find("Diffuse"); map = OnlineMaps.instance; control = OnlineMapsTileSetControl.instance; control.elevationResolution = mapResolution; OnlineMapsTile.OnTileDownloaded += OnTileDownloaded; if (OnlineMapsCache.instance != null) OnlineMapsCache.instance.OnLoadedFromCache += OnTileDownloaded; map.OnMapUpdated += UpdateMesh; if (interceptDownloadElevations && OnlineMapsElevationManagerBase.isActive) OnlineMapsElevationManagerBase.instance.OnGetElevation += OnGetElevation; InitMesh(); } private void StartNextDownload() { if (requestQueue.Count == 0) return; if (elevationRequests.Count == maxDownloads) return; OnlineMapsTile tile = requestQueue.Dequeue(); CData data = tile["cdata"] as CData; string url = "{\"spatialReference\":{\"wkid\":4326},\"ymin\":" + tile.bottomRight.y.ToString(OnlineMapsUtils.numberFormat) + ",\"ymax\":" + tile.topLeft.y.ToString(OnlineMapsUtils.numberFormat) + ",\"xmin\":" + tile.topLeft.x.ToString(OnlineMapsUtils.numberFormat) + ",\"xmax\":" + tile.bottomRight.x.ToString(OnlineMapsUtils.numberFormat) + "}"; OnlineMapsWWW request = new OnlineMapsWWW(server + OnlineMapsWWW.EscapeURL(url) + "&Rows=" + instance.requestResolution + "&Columns=" + instance.requestResolution); request.OnComplete += www => { elevationRequests.Remove(request); if (data != null) { short[,] elevations = ParseResponse(www); if (elevations != null) { data.SetHeight(elevations); data.SaveHeights(tile); if (ctl == Vector2.zero && cbr == Vector2.zero) GetBordersUsingReflection(); OnGetElevation(ctl.x, ctl.y, cbr.x, cbr.y); UpdateMesh(); } } StartNextDownload(); }; elevationRequests.Add(request); } private static short[,] ParseResponse(OnlineMapsWWW www) { if (www.hasError) { Debug.Log(www.error); return null; } string response = www.text; short[,] data = null; try { int dataIndex = response.IndexOf("\"data\":["); if (dataIndex == -1) return null; int res = instance.requestResolution; int res2 = res - 1; data = new short[res, res]; dataIndex += 8; int index = 0; int v = 0; bool isNegative = false; for (int i = dataIndex; i < response.Length; i++) { char c = response[i]; if (c == ',') { int x = index % res; int y = res2 - index / res; if (isNegative) v = -v; data[x, y] = (short)v; v = 0; isNegative = false; index++; } else if (c == '-') isNegative = true; else if (c > 47 && c < 58) v = v * 10 + (c - 48); else break; } if (isNegative) v = -v; data[res2, 0] = (short)v; } catch (Exception exception) { Debug.Log(exception.Message); } return data; } private void UpdateMesh() { int zoom = map.zoom - zoomOffset; if (zoom < 3) zoom = 3; for (int i = 0; i < countX * countY; i++) if (tiles[i] != null) tiles[i].Unblock(this); double tx, ty; map.GetTilePosition(out tx, out ty, zoom); int itx = Mathf.RoundToInt((float)(tx - countX / 2f)); int ity = Mathf.RoundToInt((float)(ty - countY / 2f)); Vector3 offset = new Vector3(0, positionYOffset, 0) - transform.position; int max = 1 << zoom; Material[] materials = meshRenderer.sharedMaterials; float r1 = resolution - 1; int vi = 0; double tlx, tly, brx, bry; map.GetCorners(out tlx, out tly, out brx, out bry); float elevationScale = OnlineMapsElevationManagerBase.GetBestElevationYScale(tlx, tly, brx, bry); int ox = countY * resolution * resolution - (resolution - 1) * resolution; int oz = resolution * (resolution - 1) + 1; Vector2 sizeInScene = OnlineMapsTileSetControl.instance.sizeInScene; float elevationOffset = 0; if (OnlineMapsElevationManagerBase.isActive) { if (OnlineMapsElevationManagerBase.instance.bottomMode == OnlineMapsElevationBottomMode.minValue) { elevationOffset = OnlineMapsElevationManagerBase.instance.GetMinElevation(1 / OnlineMapsElevationManagerBase.instance.scale); } elevationScale *= OnlineMapsElevationManagerBase.instance.scale; } DownloadOverview(); Matrix4x4 matrix = transform.worldToLocalMatrix; for (int x = 0; x < countX; x++) { int tileX = itx + x; int nextTileX = tileX + 1; if (tileX >= max) tileX -= max; if (nextTileX >= max) nextTileX -= max; for (int y = 0; y < countY; y++) { int tileY = ity + y; int nextTileY = tileY + 1; if (tileY >= max) tileY -= max; if (nextTileY >= max) nextTileY -= max; OnlineMapsTile tile = map.tileManager.GetTile(zoom, tileX, tileY); if (tile == null) { OnlineMapsTile parentTile = map.tileManager.GetTile(zoom - 1, tileX / 2, tileY / 2); tile = new OnlineMapsRasterTile(tileX, tileY, zoom, map); tile.parent = parentTile; } int tileIndex = x * countY + y; tiles[tileIndex] = tile; tile.Block(this); CData data = tile["cdata"] as CData; if (data == null) { DownloadElevation(tile); data = tile["cdata"] as CData; } double px, py; map.projection.TileToCoordinates(tileX, tileY, zoom, out px, out py); Vector3 v1 = matrix.MultiplyPoint(control.GetWorldPosition(px, py) + offset); map.projection.TileToCoordinates(nextTileX, nextTileY, zoom, out px, out py); Vector3 v2 = matrix.MultiplyPoint(control.GetWorldPosition(px, py) + offset); Vector3 ov = (v2 - v1) / r1; for (int vx = 0; vx < resolution; vx++) { for (int vz = 0; vz < resolution; vz++) { Vector3 v = new Vector3(ov.x * vx + v1.x, 0, ov.z * vz + v1.z); if (vz == 0 && y > 0) v.y = vertices[vi - oz].y; else if (vx == 0 && x > 0) v.y = vertices[vi - ox].y; else { double evx = vx / r1; double evz = vz / r1; if (evx >= 1) evx = 0.999; if (evz >= 1) evz = 0.999; if (OnlineMapsElevationManagerBase.isActive) { float elevation = 0; if (data.hasData) elevation = data.GetElevation(evx, evz); else if (overviewElevation != null) elevation = overviewElevation.GetElevation((tileX + vx / r1) / max, (tileY + vz / r1) / max); v.y = (elevation - elevationOffset) * elevationScale + offset.y; } else v.y = positionYOffset; if (v.x <= 0 && v.x >= -sizeInScene.x && v.z >= 0 && v.z <= sizeInScene.y) { float rx = Mathf.Abs(sizeInScene.x / 2 + v.x) / sizeInScene.x * 2; float ry = Mathf.Abs(sizeInScene.y / 2 - v.z) / sizeInScene.y * 2; v.y += (1 - Mathf.Max(rx, ry)) * inMapViewYOffset; } } vertices[vi++] = v; } } materials[tileIndex].mainTexture = tile.texture; materials[tileIndex].color = new Color(1, 1, 1, tile.texture != null ? 1 : 0); } } mesh.vertices = vertices; mesh.RecalculateBounds(); } internal class OverviewElevation { public Action OnDownloaded; public int tlx; public int tly; public int brx; public int bry; public double mx1; public double my1; public double mx2; public double my2; public double mrx; public double mry; public short[,] heights; public OverviewElevation(int tlx, int tly, int brx, int bry, int zoom) { this.tlx = tlx; this.tly = tly; this.brx = brx; this.bry = bry; double z = 1 << zoom; mx1 = tlx / z; my1 = tly / z; mx2 = brx / z; my2 = bry / z; mrx = mx2 - mx1; mry = my2 - my1; double left, right, bottom, top; OnlineMaps.instance.projection.TileToCoordinates(tlx, tly, zoom, out left, out top); OnlineMaps.instance.projection.TileToCoordinates(brx, bry, zoom, out right, out bottom); string url = "{\"spatialReference\":{\"wkid\":4326},\"ymin\":" + bottom.ToString(OnlineMapsUtils.numberFormat) + ",\"ymax\":" + top.ToString(OnlineMapsUtils.numberFormat) + ",\"xmin\":" + left.ToString(OnlineMapsUtils.numberFormat) + ",\"xmax\":" + right.ToString(OnlineMapsUtils.numberFormat) + "}"; OnlineMapsWWW request = new OnlineMapsWWW(server + OnlineMapsWWW.EscapeURL(url) + "&Rows=" + instance.requestResolution + "&Columns=" + instance.requestResolution); request.OnComplete += OnRequestComplete; } public void Dispose() { heights = null; } public float GetElevation(double mx, double my) { if (heights == null) return 0; int res = instance.requestResolution; int res2 = res - 1; double x1 = (mx - mx1) / mrx * res2; double y1 = (my - my1) / mry * res2; int ix1 = (int)x1; int iy1 = (int)y1; int ix2 = ix1 + 1; int iy2 = iy1 + 1; if (ix1 < 0) ix1 = 0; else if (ix1 > res2) ix1 = res2; if (iy1 < 0) iy1 = 0; else if (iy1 > res2) iy1 = res2; if (ix2 < 0) { x1 = 0; ix2 = 0; } else if (ix2 > res2) { x1 = res2; ix2 = res2; } if (iy2 < 0) { y1 = 0; iy2 = 0; } else if (iy2 > res2) { y1 = res2; iy2 = res2; } double rx = x1 - ix1; double ry = y1 - iy1; short h11 = heights[ix1, res2 - iy1]; short h12 = heights[ix1, res2 - iy2]; short h21 = heights[ix2, res2 - iy1]; short h22 = heights[ix2, res2 - iy2]; double h1 = (h21 - h11) * rx + h11; double h2 = (h22 - h12) * rx + h12; double h = (h2 - h1) * ry + h1; return (float)h; } private void OnRequestComplete(OnlineMapsWWW www) { heights = ParseResponse(www); if (overviewElevation != null) overviewElevation.Dispose(); overviewElevation = this; nextOverviewElevation = null; if (OnDownloaded == null) OnDownloaded(); } } internal class CData { private short[,] heights; public bool hasData { get { return heights != null; } } public CData() { } public float GetElevation(double tx, double ty) { if (heights == null) return 0; int res = instance.requestResolution; int res2 = res - 1; double x1 = (tx - Math.Floor(tx)) * res2; double y1 = (ty - Math.Floor(ty)) * res2; int ix1 = (int)x1; int iy1 = (int)y1; int ix2 = ix1 + 1; int iy2 = iy1 + 1; if (ix2 > res2) x1 = res2; if (iy2 > res2) y1 = res2; double rx = x1 - ix1; double ry = y1 - iy1; short h11 = heights[ix1, res2 - iy1]; short h12 = heights[ix1, res2 - iy2]; short h21 = heights[ix2, res2 - iy1]; short h22 = heights[ix2, res2 - iy2]; double h1 = (h21 - h11) * rx + h11; double h2 = (h22 - h12) * rx + h12; double h = (h2 - h1) * ry + h1; return (float)h; } private string GetTilePath(OnlineMapsTile tile) { return Application.persistentDataPath + "/ArcGIS Elevations/" + tile.zoom + "/" + tile.x + "/" + tile.y + ".elv"; } public void SaveHeights(OnlineMapsTile tile) { #if !UNITY_WEBGL string path = GetTilePath(tile); FileInfo info = new FileInfo(path); if (!info.Directory.Exists) info.Directory.Create(); if (File.Exists(path)) File.Delete(path); FileStream stream = File.OpenWrite(path); BinaryWriter writer = new BinaryWriter(stream); for (int i = 0; i < heights.GetLength(0); i++) { for (int j = 0; j < heights.GetLength(1); j++) { writer.Write(heights[i, j]); } } writer.Close(); Debug.Log("Saved to Cache " + tile); #endif } public void SetHeight(short[,] elevations) { heights = elevations; } public bool TryLoadHeights(OnlineMapsTile tile) { #if !UNITY_WEBGL string path = GetTilePath(tile); if (!File.Exists(path)) return false; FileInfo info = new FileInfo(path); if (info.Length != instance.requestResolution * instance.requestResolution * 2) return false; try { short[,] hs = new short[instance.requestResolution, instance.requestResolution]; FileStream stream = File.OpenRead(path); BinaryReader reader = new BinaryReader(stream); for (int i = 0; i < instance.requestResolution; i++) { for (int j = 0; j < instance.requestResolution; j++) { hs[i, j] = reader.ReadInt16(); } } reader.Close(); heights = hs; Debug.Log("Loaded From Cache " + tile); return true; } catch { return false; } #else return false; #endif } } }