/*
* Copyright (C) 2014 Alec Dhuse
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package co.foldingmap.map.vector;
import co.foldingmap.Logger;
import co.foldingmap.map.MapView;
import java.awt.Point;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.StringTokenizer;
/**
* A bunch of Static methods used for various calculations.
*
* @author Alec
*/
public class CoordinateMath {
/**
* Calculates the bearing in degrease from 0 to 360
*
* @param c1 The coordinate from which to find the Azimuth to c2
* @param c2 The coordinate the calculate the Azimuth to.
*
* @return float The Azimuth in degrees from (0-360)
*/
public static float calculateAzimuth(Coordinate c1, Coordinate c2) {
double azimuthDegrees, azimuthRadians;
float x, y;
double lat1 = c1.getLatitudeInRadians();
double lon1 = c1.getLongitudeInRadians();
double lat2 = c2.getLatitudeInRadians();
double lon2 = c2.getLongitudeInRadians();
if (lon1 == lon2) {
return lat1 > lat2 ? 180 : 0;
}
// Taken from "Map Projections - A Working Manual", page 30, equation 5-4b.
// The atan2() function is used in place of the traditional atan(y/x) to simplify the case when x==0.
y = (float) (Math.cos(lat2) * Math.sin(lon2 - lon1));
x = (float) (Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1));
azimuthRadians = Math.atan2(y, x);
azimuthDegrees = Double.isNaN(azimuthRadians) ? 0 : Math.toDegrees(azimuthRadians);
if (azimuthDegrees < 0) {
return (float) (360 + azimuthDegrees);
} else {
return (float) azimuthDegrees;
}
}
/**
* Calculates the slope between two Coordinates.
*
* @param c1
* @param c2
* @return
*/
public static float calculateSlope(Coordinate c1, Coordinate c2) {
double rise, run, slope;
rise = c2.getLatitude() - c1.getLatitude();
run = c2.getLongitude() - c1.getLongitude();
slope = rise / run;
return (float) slope;
}
/**
* Calculates the area a polygon represented by an ArrayList of Point2D.
*
* @param points
* @return
*/
public static double calculateSignedPolygonArea(ArrayList<Point2D> points) {
double signedArea, x1, x2, y1, y2;
int j, n;
Point2D p1, p2;
n = points.size();
signedArea = 0;
for (int i = 0; i < n; i++) {
j = (i + 1) % n;
p1 = points.get(i);
p2 = points.get(j);
x1 = p1.getX();
x2 = p2.getX();
y1 = p1.getY();
y2 = p2.getY();
signedArea += (x1 * y2) - (y1 * x2);
}
signedArea *= .5;
return signedArea;
}
/**
* Converts Degree, Minute, Second coordinates to decimal.
*
* @param degreeString
* In the format 44.0° 30.0' 13.720000000003552"
* @return
*/
public static float convertHourToDecimal(String degreeString) {
float degree, minute, seconds;
String currentToken;
StringTokenizer st;
st = new StringTokenizer(degreeString);
degree = 0;
minute = 0;
seconds = 0;
while (st.hasMoreTokens()) {
currentToken = st.nextToken();
if (currentToken.contains("°")) {
degree = Float.parseFloat(currentToken.substring(0, currentToken.length() -1));
} else if (currentToken.contains("'") && currentToken.contains("\"")) {
String[] split = currentToken.split("'");
minute = Float.parseFloat( split[0].substring(0, split[0].length()));
seconds = Float.parseFloat(split[1].substring(0, split[1].length() -1));
} else if (currentToken.contains("'")) {
minute = Float.parseFloat(currentToken.substring(0, currentToken.length() -1));
} else if (currentToken.contains("\"")) {
seconds = Float.parseFloat(currentToken.substring(0, currentToken.length() -1));
}
}
minute = minute / 60f;
seconds = seconds / 3600f;
return degree + minute + seconds;
}
/**
* Performs the Ramer–Douglas–Peucker algorithm on an Array of Points representing a line.
* This function is geared towards lines, to eliminate points that are less then the epsilon.
*
* @param points
* @param epsilon The min threshold for keeping a point.
* @return
*/
public static Point2D[] douglasPeuckerLine(Point2D[] points, double epsilon) {
double maxDistance = Double.MAX_VALUE;
double perpendicularDistance;
int maxIndex = 0;
Point2D[] filteredPoints;
try {
//Find the point with the max distance
for (int i = 2; i < points.length - 1; i++) {
perpendicularDistance = CoordinateMath.perpendicularDistance(points[i], points[1], points[points.length - 1]);
if (perpendicularDistance < maxDistance) {
maxIndex = i;
maxDistance = perpendicularDistance;
}
}
if (maxDistance <= epsilon) {
Point2D[] leftRecursiveResults = douglasPeuckerLine(Arrays.copyOfRange(points, 1, maxIndex), epsilon);
Point2D[] rightRecursiveResults = douglasPeuckerLine(Arrays.copyOfRange(points, maxIndex, points.length - 1), epsilon);
//Concatenate left and right
filteredPoints = new Point2D[leftRecursiveResults.length + rightRecursiveResults.length];
System.arraycopy(leftRecursiveResults, 0, filteredPoints, 0, leftRecursiveResults.length);
System.arraycopy(rightRecursiveResults, 0, filteredPoints, leftRecursiveResults.length, rightRecursiveResults.length);
} else {
filteredPoints = points;
}
return filteredPoints;
} catch (Exception e) {
Logger.log(Logger.ERR, "Error in CoordinateMath.douglasPeuckerLLine - " + e);
return points;
}
}
/**
* Returns the maximum distance between Coordinates in the list.
*
* @param clist
* @return
*/
public static float getMaxDistances(CoordinateList<Coordinate> clist) {
float currentDistance, maxDistance, totalDistance;
//init
totalDistance = 0;
maxDistance = 0;
for (int i = 1; i < clist.size(); i++) {
currentDistance = getDistance(clist.get(i-1), clist.get(i));
if (maxDistance < currentDistance)
maxDistance = currentDistance;
}
return maxDistance;
}
/**
* Calculates the geodetic distance between the two points according to the
* ellipsoid model of WGS84. Altitude is neglected from calculations.
*
* The implementation will calculate this as exactly as it can. However, it
* is required that the result is within 0.35% of the correct result.
*
* Haversine Formula (from R.W. Sinnott, "Virtues of the Haversine", Sky
* and Telescope, vol. 68, no. 2, 1984, p. 159):
*
* See the following URL for more info on calculating distances:
* http://www.census.gov/cgi-bin/geo/gisfaq?Q5.1
*
* @param c1
* the Coordinates of point 1
* @param c2
* the Coordinates of point 2
*
* @return the distance to the destination in meters
* @throws java.lang.NullPointerException
* if the parameter is null
*/
public static float getDistance(Coordinate c1, Coordinate c2) {
float earthRadius = 6371; //In KM
if (c1.equals(c2)) {
return 0.0f;
} else {
double lat1 = Math.toRadians(c1.getLatitude());
double lon1 = Math.toRadians(c1.getLongitude());
double lat2 = Math.toRadians(c2.getLatitude());
double lon2 = Math.toRadians(c2.getLongitude());
double dlon = (lon2 - lon1);
double dlat = (lat2 - lat1);
double a = (Math.sin(dlat / 2.0f)) * (Math.sin(dlat / 2.0f))
+ (Math.cos(lat1) * Math.cos(lat2) * (Math.sin(dlon / 2.0f)))
* (Math.cos(lat1) * Math.cos(lat2) * (Math.sin(dlon / 2.0f)));
double c = 2.0f * Math.asin(Math.min(1.0, Math.sqrt(a)));
double km = earthRadius * c;
return (float) (km * 1000);
}
}
/**
* Calculates the distance of a path of Coordinates in a CoordinateList.
* The values are returned as a float array, where each index corresponds
* with the index of a coordinate in the passed CoordinateList. The value
* is the distance, in meters, of that coordinate along the path from the start
* coordinate.
*
* @param clist
* @return
*/
public static float[] getDistancesM(CoordinateList<Coordinate> clist) {
float[] distances;
float currentDistance, totalDistance;
//init
distances = new float[clist.size()];
distances[0] = 0;
totalDistance = 0;
for (int i = 1; i < clist.size(); i++) {
currentDistance = getDistance(clist.get(i-1), clist.get(i));
totalDistance += currentDistance;
distances[i] = totalDistance;
}
return distances;
}
/**
* Calculates the distance of a path of Coordinates in a CoordinateList.
* The values are returned as a float array, where each index corresponds
* with the index of a coordinate in the passed CoordinateList. The value
* is the distance, in KM, of that coordinate along the path from the start
* coordinate.
*
* @param clist
* @return
*/
public static float[] getDistancesKM(CoordinateList<Coordinate> clist) {
float[] distances;
float currentDistance, totalDistance;
//init
distances = new float[clist.size()];
distances[0] = 0;
totalDistance = 0;
for (int i = 1; i < clist.size(); i++) {
currentDistance = getDistance(clist.get(i-1), clist.get(i)) / 1000f;
totalDistance += currentDistance;
distances[i] = totalDistance;
}
return distances;
}
/**
* Returns an ArrayList of Point2D in relative space used for calculating
* polygon areas.
*
* @param objects
* @return
*/
public static ArrayList<Point2D> getRelitivePoints(VectorObjectList objects) {
ArrayList<Point2D> points;
Coordinate baseCoordinateX, baseCoordinateY, compareCoordinate;
float baseLatitude, baseLongitude;
float x, y;
MapPoint currentPoint;
points = new ArrayList<Point2D>();
baseLatitude = objects.getSouthernMostLatitude();
baseLongitude = objects.getWesternMostLongitude();
for (int i = 0; i < objects.size(); i++) {
currentPoint = (MapPoint) objects.get(i);
compareCoordinate = currentPoint.getCoordinateList().get(0);
baseCoordinateX = new Coordinate(0f, compareCoordinate.getLatitude(), baseLongitude);
baseCoordinateY = new Coordinate(0f, baseLatitude, compareCoordinate.getLongitude());
x = getDistance(baseCoordinateX, compareCoordinate);
y = getDistance(baseCoordinateY, compareCoordinate);
points.add(new Point2D.Double(x, y));
}
return points;
}
/**
* Determines if a given Point is an end point of a Line2D vertex.
*
* @param p
* @param vertex
* @return
*/
public static boolean isPointVertexEndPoint(Point p, Line2D vertex) {
boolean isEndPoint = false;
int vertexX1 = (int) vertex.getX1();
int vertexY1 = (int) vertex.getY1();
int vertexX2 = (int) vertex.getX2();
int vertexY2 = (int) vertex.getY2();
if ((p.getX() == vertexX1) && (p.getY() == vertexY1))
isEndPoint = true;
if ((p.getX() == vertexX2) && (p.getY() == vertexY2))
isEndPoint = true;
return isEndPoint;
}
/**
* Gives the length of a given LineString.
*
* @param lsToMeasure
* @return The length in Meters.
*/
public static double measureLineStringLength(LineString lsToMeasure) {
Coordinate currentCoordinate;
CoordinateList<Coordinate> coordinates;
double length;
length = 0.0;
coordinates = lsToMeasure.getCoordinateList();
for (int i = 1; i < coordinates.size(); i++)
length += CoordinateMath.getDistance(coordinates.get(i-1), coordinates.get(i));
return length;
}
/**
* Returns the area of a Polygon.
*
* @param pToMeasure
* @param mapView
* @return The Polygon's Area.
*/
public static double measurePolygonArea(Polygon pToMeasure, MapView mapView)
{
double area;
VectorObjectList componetPoints = pToMeasure.getMapPoints();
ArrayList<Point2D> relitivePoints = getRelitivePoints(componetPoints);
area = calculateSignedPolygonArea(relitivePoints);
return Math.abs(area);
}
public static double measurePolygonPerimeter(Polygon pToMeasure) {
Coordinate currentCoordinate;
CoordinateList<Coordinate> coordinates;
double length;
length = 0.0;
coordinates = pToMeasure.getCoordinateList();
for (int i = 1; i < coordinates.size(); i++)
length += CoordinateMath.getDistance(coordinates.get(i-1), coordinates.get(i));
return length;
}
/**
* Finds the perpendicular distance of a point in respect to a line.
*
* @param point
* @param p1 Start point of the line
* @param p2 End point of the line
* @return
*/
public static double perpendicularDistance(Point2D point, Point2D p1, Point2D p2) {
double slope = (p2.getY() - p1.getY()) / (p2.getX() - p1.getX());
double intercept = p1.getY() - (slope * p1.getX());
double result = Math.abs(slope * point.getX() - point.getY() + intercept) / Math.sqrt(Math.pow(slope, 2) + 1);
return result;
}
/**
* Tests to see if vertices of a polygon cross
* @param vertices
* @return
*/
public static boolean testPolygonVertices(ArrayList<Line2D> vertices) {
boolean verticesCross = false;
Point intersectionPoint;
for (Line2D testVertex: vertices) {
for (Line2D compareVertex: vertices) {
if (testVertex != compareVertex) {
try {
intersectionPoint = Intersection.getIntersection(testVertex, compareVertex);
if (intersectionPoint == null) {
//
} else {
if ((CoordinateMath.isPointVertexEndPoint(intersectionPoint, testVertex))) {
//
} else {
verticesCross = true;
}
}
} catch (Exception e) {
//multiple intersections
verticesCross = true;
} // end try / catch block
}
} //end compareVertex loop
} //end testVertex loop
return verticesCross;
}
}