using System.Collections.Generic;
using System;
using UnityEngine;
class CheapRuler
{
private float lat;
private string units;
private float kx;
private float ky;
/**
* Multipliers for converting between units.
*
* @example
* // convert 50 meters to yards
* 50 * cheapRuler.units.yards / cheapRuler.units.meters;
*/
//units
public Dictionary<string,float> factors = new Dictionary<string,float>(){
["kilometers"] = 1.0f,
["miles"] = 1000.0f / 1609.344f,
["nauticalmiles"] = 1000f / 1852f,
["meters"] = 1000f,
["metres"] = 1000f,
["yards"] = 1000f / 0.9144f,
["feet"] = 1000f / 0.3048f,
["inches"] = 1000f / 0.0254f
};
/**
* Creates a ruler object from tile coordinates (y and z). Convenient in tile-reduce scripts.
*
* @param {number} y
* @param {number} z
* @param {string} [units="kilometers"]
* @returns {CheapRuler}
* @example
* var ruler = cheapRuler.fromTile(1567, 12);
* //=ruler
*/
static CheapRuler FromTile(float y,float z,string units)
{
var n = Mathf.PI * (1 - 2 * (y + 0.5f) / Mathf.Pow(2, z));
var lat = Mathf.Atan(0.5f * (Mathf.Exp(n) - Mathf.Exp(-n))) * 180f / Mathf.PI;
return new CheapRuler(lat, units);
}
string GetFactorsContents()
{
string r = "";
foreach (KeyValuePair<string, float> kvp in factors)
{
r += String.Format("Key = {0} Value = {1}", kvp.Key, kvp.Value);
}
return r;
}
/**
* A collection of very fast approximations to common geodesic measurements. Useful for performance-sensitive code that measures things on a city scale.
*
* @param latitude
* @param unitsName [units="kilometers"]
* @returns {CheapRuler}
* @example
* var ruler = new cheapRuler(35.05f, "miles");
* //=ruler
*/
public CheapRuler(float latitude, string unitsName)
{
lat = latitude;
units = unitsName;
if (!factors.ContainsKey(units))
{
throw new Exception("Unknown unit " + units + ". Use one of: " + GetFactorsContents());
}
float m = factors.ContainsKey(units) ? factors[units] : 1f;
float cos = Mathf.Cos(lat * Mathf.PI / 180f);
float cos2 = 2f * cos * cos - 1f;
float cos3 = 2f * cos * cos2 - cos;
float cos4 = 2f * cos * cos3 - cos2;
float cos5 = 2f * cos * cos4 - cos3;
// multipliers for converting longitude and latitude degrees into distance (http://1.usa.gov/1Wb1bv7)
this.kx = m * (111.41513f * cos - 0.09455f * cos3 + 0.00012f * cos5);
this.ky = m * (111.13209f - 0.56605f * cos2 + 0.0012f * cos4);
}
/**
* Given two points of the form [longitude, latitude], returns the distance.
*
* @param {Array<number>} a point [longitude, latitude]
* @param {Array<number>} b point [longitude, latitude]
* @returns {number} distance
* @example
* var distance = ruler.distance([30.5, 50.5], [30.51, 50.49]);
* //=distance
*/
float distance(Vector2 a,Vector2 b)
{
var dx = (a[0] - b[0]) * this.kx;
var dy = (a[1] - b[1]) * this.ky;
return Mathf.Sqrt(dx * dx + dy * dy);
}
/**
* Returns the bearing between two points in angles.
*
* @param {Array<number>} a point [longitude, latitude]
* @param {Array<number>} b point [longitude, latitude]
* @returns {number} bearing
* @example
* var bearing = ruler.bearing([30.5, 50.5], [30.51, 50.49]);
* //=bearing
*/
public float bearing(Vector2 a, Vector2 b)
{
var dx = (b[0] - a[0]) * kx;
var dy = (b[1] - a[1]) * ky;
float bearing = Mathf.Atan2(dx, dy) * 180f / Mathf.PI;
if (bearing > 180) bearing -= 360f;
return bearing;
}
/**
* Returns a new point given distance and bearing from the starting point.
*
* @param {Array<number>} p point [longitude, latitude]
* @param {number} dist distance
* @param {number} bearing
* @returns {Array<number>} point [longitude, latitude]
* @example
* var point = ruler.destination([30.5, 50.5], 0.1, 90);
* //=point
*/
public Vector2 destination(Vector2 p,float dist,float bearing)
{
float a = bearing * Mathf.PI / 180f;
return offset(p,
Mathf.Sin(a) * dist,
Mathf.Cos(a) * dist);
}
/**
* Returns a new point given easting and northing offsets (in ruler units) from the starting point.
*
* @param {Array<number>} p point [longitude, latitude]
* @param {number} dx easting
* @param {number} dy northing
* @returns {Array<number>} point [longitude, latitude]
* @example
* var point = ruler.offset([30.5, 50.5], 10, 10);
* //=point
*/
public Vector2 offset(Vector2 p, float dx, float dy) {
return new Vector2(
p[0] + dx / kx,
p[1] + dy / ky
);
}
/**
* Given a line (an array of points), returns the total line distance.
*
* @param {Array<Array<number>>} points [longitude, latitude]
* @returns {number} total line distance
* @example
* var length = ruler.lineDistance([
* [-67.031, 50.458], [-67.031, 50.534],
* [-66.929, 50.534], [-66.929, 50.458]
* ]);
* //=length
*/
public float lineDistance(List<Vector2> points) {
float total = 0f;
for (var i = 0; i < points.Count - 1; i++) {
total += distance(points[i], points[i + 1]);
}
return total;
}
/**
* Given a polygon (an array of rings, where each ring is an array of points), returns the area.
*
* @param {Array<Array<Array<number>>>} polygon
* @returns {number} area value in the specified units (square kilometers by default)
* @example
* var area = ruler.area([[
* [-67.031, 50.458], [-67.031, 50.534], [-66.929, 50.534],
* [-66.929, 50.458], [-67.031, 50.458]
* ]]);
* //=area
*/
public float area(List<List<Vector2>> polygon)
{
var sum = 0f;
for (int i = 0; i < polygon.Count; i++) {
var ring = polygon[i];
for (int j = 0, len = ring.Count, k = len - 1; j < len; k = j++)
{
sum += (ring[j][0] - ring[k][0]) * (ring[j][1] + ring[k][1]) * (i == 1 ? -1 : 1);
}
}
return (Mathf.Abs(sum) / 2f) * kx * ky;
}
/**
* Returns the point at a specified distance along the line.
*
* @param {Array<Array<number>>} line
* @param {number} dist distance
* @returns {Array<number>} point [longitude, latitude]
* @example
* var point = ruler.along(line, 2.5);
* //=point
*/
public Vector2 along(List<Vector2> line, float dist)
{
float sum = 0f;
if (dist <= 0f) return line[0];
for (var i = 0; i < line.Count - 1; i++) {
var p0 = line[i];
var p1 = line[i + 1];
var d = this.distance(p0, p1);
sum += d;
if (sum > dist) return interpolate(p0, p1, (dist - (sum - d)) / d);
}
return line[line.Count - 1];
}
public struct PointOnLine
{
public Vector2 point;
public int index;
public float t;
}
/**
* Returns an object of the form {point, index, t} where point is closest point on the line
* from the given point, index is the start index of the segment with the closest point,
* and t is a parameter from 0 to 1 that indicates where the closest point is on that segment.
*
* @pointOnLine
* @param {Array<Array<number>>} line
* @param {Array<number>} p point [longitude, latitude]
* @returns {Object} {point, index, t}
* @example
* var point = ruler.pointOnLine(line, [-67.04, 50.5]).point;
* //=point
*/
public PointOnLine pointOnLine(List<Vector2> line,Vector2 p) {
float minDist = float.PositiveInfinity;
float minX, minY, minT;
int minI = 0;
minX = minY = minT = 0.0f;
for (int i = 0; i < line.Count - 1; i++) {
float x = line[i][0];
float y = line[i][1];
float dx = (line[i + 1][0] - x) * this.kx;
float dy = (line[i + 1][1] - y) * this.ky;
float t = 0.0f;
if (dx != 0f || dy != 0f)
{
t = ((p[0] - x) * this.kx * dx + (p[1] - y) * this.ky * dy) / (dx * dx + dy * dy);
if (t > 1) {
x = line[i + 1][0];
y = line[i + 1][1];
} else if (t > 0) {
x += (dx / this.kx) * t;
y += (dy / this.ky) * t;
}
}
dx = (p[0] - x) * this.kx;
dy = (p[1] - y) * this.ky;
float sqDist = dx * dx + dy * dy;
if (sqDist < minDist) {
minDist = sqDist;
minX = x;
minY = y;
minI = i;
minT = t;
}
}
return new PointOnLine()
{
point = new Vector2(minX, minY),
index = (int)minI,
t = Math.Max(0, Math.Min(1f, minT))
};
}
/**
* Returns a part of the given line between the start and the stop points (or their closest points on the line).
*
* @param {Array<number>} start point [longitude, latitude]
* @param {Array<number>} stop point [longitude, latitude]
* @param {Array<Array<number>>} line
* @returns {Array<Array<number>>} line part of a line
* @example
* var line2 = ruler.lineSlice([-67.04, 50.5], [-67.05, 50.56], line1);
* //=line2
*/
List<Vector2> lineSlice (Vector2 start,Vector2 stop,List<Vector2> line)
{
var p1 = pointOnLine(line, start);
var p2 = pointOnLine(line, stop);
if (p1.index > p2.index || (p1.index == p2.index && p1.t > p2.t))
{
var tmp = p1;
p1 = p2;
p2 = tmp;
}
var slice = new List<Vector2>(){p1.point};
int l = p1.index + 1;
var r = p2.index;
if (!equals(line[l], slice[0]) && l <= r)
slice.Add(line[l]);
for (var i = l + 1; i <= r; i++) {
slice.Add(line[i]);
}
if (!equals(line[r], p2.point))
slice.Add(p2.point);
return slice;
}
/**
* Returns a part of the given line between the start and the stop points indicated by distance along the line.
*
* @param {number} start distance
* @param {number} stop distance
* @param {Array<Array<number>>} line
* @returns {Array<Array<number>>} line part of a line
* @example
* var line2 = ruler.lineSliceAlong(10, 20, line1);
* //=line2
*/
List<Vector2> lineSliceAlong (float start,float stop, List<Vector2> line)
{
float sum = 0.0f;
List<Vector2> slice = new List<Vector2>();
for (int i = 0; i < line.Count - 1; i++) {
Vector2 p0 = line[i];
Vector2 p1 = line[i + 1];
float d = distance(p0, p1);
sum += d;
if (sum > start && slice.Count == 0) {
slice.Add(interpolate(p0, p1, (start - (sum - d)) / d));
}
if (sum >= stop) {
slice.Add(interpolate(p0, p1, (stop - (sum - d)) / d));
return slice;
}
if (sum > start) slice.Add(p1);
}
return slice;
}
/**
* Given a point, returns a bounding box object ([w, s, e, n]) created from the given point buffered by a given distance.
*
* @param {Array<number>} p point [longitude, latitude]
* @param {number} buffer
* @returns {Array<number>} box object ([w, s, e, n])
* @example
* var bbox = ruler.bufferPoint([30.5, 50.5], 0.01);
* //=bbox
*/
/*public float bufferPoint(Vector2 p,Quaternion buffer) {
var v = buffer / this.ky;
var h = buffer / this.kx;
return [
p[0] - h,
p[1] - v,
p[0] + h,
p[1] + v
];
}*/
/**
* Given a bounding box, returns the box buffered by a given distance.
*
* @param {Array<number>} box object ([w, s, e, n])
* @param {number} buffer
* @returns {Array<number>} box object ([w, s, e, n])
* @example
* var bbox = ruler.bufferBBox([30.5, 50.5, 31, 51], 0.2);
* //=bbox
*/
/*public float bufferBBox (Quaternion bbox, float buffer) {
var v = buffer / this.ky;
var h = buffer / this.kx;
return [
bbox[0] - h,
bbox[1] - v,
bbox[2] + h,
bbox[3] + v
];
}*/
/**
* Returns true if the given point is inside in the given bounding box, otherwise false.
*
* @param {Array<number>} p point [longitude, latitude]
* @param {Array<number>} box object ([w, s, e, n])
* @returns {boolean}
* @example
* var inside = ruler.insideBBox([30.5, 50.5], [30, 50, 31, 51]);
* //=inside
*/
public bool insideBBox(Vector2 p, Quaternion bbox)
{
return p[0] >= bbox[0] &&
p[0] <= bbox[2] &&
p[1] >= bbox[1] &&
p[1] <= bbox[3];
}
public bool equals(Vector2 a,Vector2 b) {
return a[0] == b[0] && a[1] == b[1];
}
public Vector2 interpolate(Vector2 a,Vector2 b,float t) {
var dx = b[0] - a[0];
var dy = b[1] - a[1];
return new Vector2(
a[0] + dx * t,
a[1] + dy * t
);
}
}