//----------------------------------------------------------------------- // // Copyright (c) 2016 Mapbox. All rights reserved. // //----------------------------------------------------------------------- namespace Mapbox.Unity.Utilities { using Mapbox.Map; using System; using Mapbox.Utils; using UnityEngine; using System.Globalization; using Mapbox.Unity.MeshGeneration.Data; /// /// A set of Geo and Terrain Conversion utils. /// public static class Conversions { private const int TileSize = 256; /// according to https://wiki.openstreetmap.org/wiki/Zoom_levels private const int EarthRadius = 6378137; //no seams with globe example private const double InitialResolution = 2 * Math.PI * EarthRadius / TileSize; private const double OriginShift = 2 * Math.PI * EarthRadius / 2; /// /// Converts struct, WGS84 /// lat/lon to Spherical Mercator EPSG:900913 xy meters. /// /// The . /// A of coordinates in meters. public static Vector2d LatLonToMeters(Vector2d v) { return LatLonToMeters(v.x, v.y); } /// /// Convert a simple string to a latitude longitude. /// Expects format: latitude, longitude /// /// The lat/lon as Vector2d. /// string. public static Vector2d StringToLatLon(string s) { var latLonSplit = s.Split(','); if (latLonSplit.Length != 2) { throw new ArgumentException("Wrong number of arguments"); } double latitude = 0; double longitude = 0; if (!double.TryParse(latLonSplit[0], NumberStyles.Any, NumberFormatInfo.InvariantInfo, out latitude)) { throw new Exception(string.Format("Could not convert latitude to double: {0}", latLonSplit[0])); } if (!double.TryParse(latLonSplit[1], NumberStyles.Any, NumberFormatInfo.InvariantInfo, out longitude)) { throw new Exception(string.Format("Could not convert longitude to double: {0}", latLonSplit[0])); } return new Vector2d(latitude, longitude); } /// /// Converts WGS84 lat/lon to Spherical Mercator EPSG:900913 xy meters. /// SOURCE: http://stackoverflow.com/questions/12896139/geographic-coordinates-converter. /// /// The latitude. /// The longitude. /// A of xy meters. public static Vector2d LatLonToMeters(double lat, double lon) { var posx = lon * OriginShift / 180; var posy = Math.Log(Math.Tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180); posy = posy * OriginShift / 180; return new Vector2d(posx, posy); } /// /// Converts WGS84 lat/lon to x/y meters in reference to a center point /// /// The latitude. /// The longitude. /// A center point to offset resultant xy, this is usually map's center mercator /// Scale in meters. (default scale = 1) /// A xy tile ID. /// /// Converts a Lat/Lon of (37.7749, 122.4194) into Unity coordinates for a map centered at (10,10) and a scale of 2.5 meters for every 1 Unity unit /// /// var worldPosition = Conversions.GeoToWorldPosition(37.7749, 122.4194, new Vector2d(10, 10), (float)2.5); /// // worldPosition = ( 11369163.38585, 34069138.17805 ) /// /// public static Vector2d GeoToWorldPosition(double lat, double lon, Vector2d refPoint, float scale = 1) { var posx = lon * OriginShift / 180; var posy = Math.Log(Math.Tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180); posy = posy * OriginShift / 180; return new Vector2d((posx - refPoint.x) * scale, (posy - refPoint.y) * scale); } public static Vector2d GeoToWorldPosition(Vector2d latLong, Vector2d refPoint, float scale = 1) { return GeoToWorldPosition(latLong.x, latLong.y, refPoint, scale); } public static Vector3 GeoToWorldGlobePosition(double lat, double lon, float radius) { double xPos = (radius) * Math.Cos(Mathf.Deg2Rad * lat) * Math.Cos(Mathf.Deg2Rad * lon); double zPos = (radius) * Math.Cos(Mathf.Deg2Rad * lat) * Math.Sin(Mathf.Deg2Rad * lon); double yPos = (radius) * Math.Sin(Mathf.Deg2Rad * lat); return new Vector3((float)xPos, (float)yPos, (float)zPos); } public static Vector3 GeoToWorldGlobePosition(Vector2d latLong, float radius) { return GeoToWorldGlobePosition(latLong.x, latLong.y, radius); } public static Vector2d GeoFromGlobePosition(Vector3 point, float radius) { float latitude = Mathf.Asin(point.y / radius); float longitude = Mathf.Atan2(point.z, point.x); return new Vector2d(latitude * Mathf.Rad2Deg, longitude * Mathf.Rad2Deg); } /// /// Converts Spherical Mercator EPSG:900913 in xy meters to WGS84 lat/lon. /// Inverse of LatLonToMeters. /// /// A of coordinates in meters. /// The in lat/lon. /// /// Converts EPSG:900913 xy meter coordinates to lat lon /// /// var worldPosition = new Vector2d (4547675.35434,13627665.27122); /// var latlon = Conversions.MetersToLatLon(worldPosition); /// // latlon = ( 37.77490, 122.41940 ) /// /// public static Vector2d MetersToLatLon(Vector2d m) { var vx = (m.x / OriginShift) * 180; var vy = (m.y / OriginShift) * 180; vy = 180 / Math.PI * (2 * Math.Atan(Math.Exp(vy * Math.PI / 180)) - Math.PI / 2); return new Vector2d(vy, vx); } /// /// Gets the xy tile ID from Spherical Mercator EPSG:900913 xy coords. /// /// XY coords in meters. /// Zoom level. /// A xy tile ID. /// /// /// Converts EPSG:900913 xy meter coordinates to web mercator tile XY coordinates at zoom 12. /// /// var meterXYPosition = new Vector2d (4547675.35434,13627665.27122); /// var tileXY = Conversions.MetersToTile (meterXYPosition, 12); /// // tileXY = ( 655, 2512 ) /// /// public static Vector2 MetersToTile(Vector2d m, int zoom) { var p = MetersToPixels(m, zoom); return PixelsToTile(p); } /// /// Gets the tile bounds in Spherical Mercator EPSG:900913 meters from an xy tile ID. /// /// XY tile ID. /// Zoom level. /// A in meters. public static RectD TileBounds(Vector2 tileCoordinate, int zoom) { var min = PixelsToMeters(new Vector2d(tileCoordinate.x * TileSize, tileCoordinate.y * TileSize), zoom); var max = PixelsToMeters(new Vector2d((tileCoordinate.x + 1) * TileSize, (tileCoordinate.y + 1) * TileSize), zoom); return new RectD(min, max - min); } public static RectD TileBounds(UnwrappedTileId unwrappedTileId) { var min = PixelsToMeters(new Vector2d(unwrappedTileId.X * TileSize, unwrappedTileId.Y * TileSize), unwrappedTileId.Z); var max = PixelsToMeters(new Vector2d((unwrappedTileId.X + 1) * TileSize, (unwrappedTileId.Y + 1) * TileSize), unwrappedTileId.Z); return new RectD(min, max - min); } /// /// Gets the xy tile ID at the requested zoom that contains the WGS84 lat/lon point. /// See: http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames. /// /// The latitude. /// The longitude. /// Zoom level. /// A xy tile ID. public static UnwrappedTileId LatitudeLongitudeToTileId(double latitude, double longitude, int zoom) { var x = (int)Math.Floor((longitude + 180.0) / 360.0 * Math.Pow(2.0, zoom)); var y = (int)Math.Floor((1.0 - Math.Log(Math.Tan(latitude * Math.PI / 180.0) + 1.0 / Math.Cos(latitude * Math.PI / 180.0)) / Math.PI) / 2.0 * Math.Pow(2.0, zoom)); return new UnwrappedTileId(zoom, x, y); } /// /// Get coordinates for a given latitude/longitude in tile-space. Useful when comparing feature geometry to lat/lon coordinates. /// /// The longitude to tile position. /// Coordinate. /// The zoom level of the tile. /// Layer extent. Optional, but recommended. Defaults to 4096, the standard for Mapbox Tiles public static Vector2 LatitudeLongitudeToVectorTilePosition(Vector2d coordinate, int tileZoom, ulong layerExtent = 4096) { var coordinateTileId = Conversions.LatitudeLongitudeToTileId( coordinate.x, coordinate.y, tileZoom); var _meters = LatLonToMeters(coordinate); var _rect = Conversions.TileBounds(coordinateTileId); //vectortile space point (0 - layerExtent) var vectorTilePoint = new Vector2((float)((_meters - _rect.Min).x / _rect.Size.x) * layerExtent, (float)(layerExtent - ((_meters - _rect.Max).y / _rect.Size.y) * layerExtent)); return vectorTilePoint; } public static Vector2 LatitudeLongitudeToUnityTilePosition(Vector2d coordinate, UnityTile tile, ulong layerExtent = 4096) { return LatitudeLongitudeToUnityTilePosition(coordinate, tile.CurrentZoom, tile.TileScale, layerExtent); } /// /// Get coordinates for a given latitude/longitude in tile-space. Useful when comparing feature geometry to lat/lon coordinates. /// /// The longitude to tile position. /// Coordinate. /// The zoom level of the tile. /// Tile scale. Optional, but recommended. Defaults to a scale of 1. /// Layer extent. Optional, but recommended. Defaults to 4096, the standard for Mapbox Tiles public static Vector2 LatitudeLongitudeToUnityTilePosition(Vector2d coordinate, int tileZoom, float tileScale, ulong layerExtent = 4096) { var coordinateTileId = Conversions.LatitudeLongitudeToTileId( coordinate.x, coordinate.y, tileZoom); var _rect = Conversions.TileBounds(coordinateTileId); //vectortile space point (0 - layerExtent) var vectorTilePoint = LatitudeLongitudeToVectorTilePosition(coordinate, tileZoom, layerExtent); //UnityTile space var unityTilePoint = new Vector2((float)(vectorTilePoint.x / layerExtent * _rect.Size.x - (_rect.Size.x / 2)) * tileScale, (float)((layerExtent - vectorTilePoint.y) / layerExtent * _rect.Size.y - (_rect.Size.y / 2)) * tileScale); return unityTilePoint; } /// /// Gets the WGS84 longitude of the northwest corner from a tile's X position and zoom level. /// See: http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames. /// /// Tile X position. /// Zoom level. /// NW Longitude. public static double TileXToNWLongitude(int x, int zoom) { var n = Math.Pow(2.0, zoom); var lon_deg = x / n * 360.0 - 180.0; return lon_deg; } /// /// Gets the WGS84 latitude of the northwest corner from a tile's Y position and zoom level. /// See: http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames. /// /// Tile Y position. /// Zoom level. /// NW Latitude. public static double TileYToNWLatitude(int y, int zoom) { var n = Math.Pow(2.0, zoom); var lat_rad = Math.Atan(Math.Sinh(Math.PI * (1 - 2 * y / n))); var lat_deg = lat_rad * 180.0 / Math.PI; return lat_deg; } /// /// Gets the of a tile. /// /// Tile X position. /// Tile Y position. /// Zoom level. /// The of the tile. public static Vector2dBounds TileIdToBounds(int x, int y, int zoom) { var sw = new Vector2d(TileYToNWLatitude(y, zoom), TileXToNWLongitude(x + 1, zoom)); var ne = new Vector2d(TileYToNWLatitude(y + 1, zoom), TileXToNWLongitude(x, zoom)); return new Vector2dBounds(sw, ne); } /// /// Gets the WGS84 lat/lon of the center of a tile. /// /// Tile X position. /// Tile Y position. /// Zoom level. /// A of lat/lon coordinates. public static Vector2d TileIdToCenterLatitudeLongitude(int x, int y, int zoom) { var bb = TileIdToBounds(x, y, zoom); var center = bb.Center; return new Vector2d(center.x, center.y); } /// /// Gets the Web Mercator x/y of the center of a tile. /// /// Tile X position. /// Tile Y position. /// Zoom level. /// A of lat/lon coordinates. public static Vector2d TileIdToCenterWebMercator(int x, int y, int zoom) { double tileCnt = Math.Pow(2, zoom); double centerX = x + 0.5; double centerY = y + 0.5; centerX = ((centerX / tileCnt * 2) - 1) * Constants.WebMercMax; centerY = (1 - (centerY / tileCnt * 2)) * Constants.WebMercMax; return new Vector2d(centerX, centerY); } /// /// Gets the meters per pixels at given latitude and zoom level for a 256x256 tile. /// See: http://wiki.openstreetmap.org/wiki/Zoom_levels. /// /// The latitude. /// Zoom level. /// Meters per pixel. public static float GetTileScaleInMeters(float latitude, int zoom) { return (float)(40075016.685578d * Math.Cos(Mathf.Deg2Rad * latitude) / Math.Pow(2f, zoom + 8)); } /// /// Gets the degrees per tile at given zoom level for Web Mercator tile. /// See: http://wiki.openstreetmap.org/wiki/Zoom_levels. /// /// The latitude. /// Zoom level. /// Degrees per tile. public static float GetTileScaleInDegrees(float latitude, int zoom) { return (float)(360.0f / Math.Pow(2f, zoom + 8)); } /// /// Gets height from terrain-rgb adjusted for a given scale. /// /// The . /// Relative scale. /// Adjusted height in meters. public static float GetRelativeHeightFromColor(Color color, float relativeScale) { return GetAbsoluteHeightFromColor(color) * relativeScale; } /// /// Specific formula for mapbox.terrain-rgb to decode height values from pixel values. /// See: https://www.mapbox.com/blog/terrain-rgb/. /// /// The . /// Height in meters. public static float GetAbsoluteHeightFromColor(Color color) { return (float)(-10000 + ((color.r * 255 * 256 * 256 + color.g * 255 * 256 + color.b * 255) * 0.1)); } public static float GetAbsoluteHeightFromColor32(Color32 color) { return (float)(-10000 + ((color.r * 256 * 256 + color.g * 256 + color.b) * 0.1)); } public static float GetAbsoluteHeightFromColor(float r, float g, float b) { return (-10000f + ((r * 65536f + g * 256f + b) * 0.1f)); } private static double Resolution(int zoom) { return InitialResolution / Math.Pow(2, zoom); } private static Vector2d PixelsToMeters(Vector2d p, int zoom) { var res = Resolution(zoom); var met = new Vector2d(); met.x = (p.x * res - OriginShift); met.y = -(p.y * res - OriginShift); return met; } private static Vector2d MetersToPixels(Vector2d m, int zoom) { var res = Resolution(zoom); var pix = new Vector2d(((m.x + OriginShift) / res), ((-m.y + OriginShift) / res)); return pix; } private static Vector2 PixelsToTile(Vector2d p) { var t = new Vector2((int)Math.Ceiling(p.x / (double)TileSize) - 1, (int)Math.Ceiling(p.y / (double)TileSize) - 1); return t; } public static double GeoDistance( double lon1, double lat1, double lon2, double lat2) { var toRad = 3.141592653589793 / 180; double dlon = (lon2 - lon1) * toRad; double dlat = (lat2 - lat1) * toRad; double a = (Math.Sin(dlat / 2) * Math.Sin(dlat / 2)) + Math.Cos(lat1 * toRad) * Math.Cos(lat2 * toRad) * (Math.Sin(dlon / 2) * Math.Sin(dlon / 2)); double angle = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a)); return angle * 6378.16; } } }