/*
* Copyright (c) 2016 Fraunhofer IGD
*
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution. If not, see <http://www.gnu.org/licenses/>.
*
* Contributors:
* Fraunhofer IGD <http://www.igd.fraunhofer.de/>
*/
package de.fhg.igd.mapviewer.view.arecalculation;
import java.awt.geom.Point2D;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.measure.quantity.Length;
import javax.measure.unit.Unit;
import org.geotools.referencing.CRS;
import org.jdesktop.swingx.mapviewer.GeoPosition;
import org.jdesktop.swingx.mapviewer.GeotoolsConverter;
import org.jdesktop.swingx.mapviewer.IllegalGeoPositionException;
import org.jdesktop.swingx.mapviewer.JXMapViewer;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import de.fhg.igd.geom.Point3D;
import de.fhg.igd.geom.algorithm.FaceTriangulation;
/**
* This class manages the calculation of any 2D surface.
*
* @author <a href="mailto:andreas.burchert@igd.fhg.de">Andreas Burchert</a>
*/
public class AreaCalc extends Thread {
/**
* Instance of this class.
*/
private static AreaCalc instance = null;
/**
* Calculation is active (default: false)
*/
private static boolean active = false;
/**
* True if a calculation is in progress.
*/
private boolean calculation = false;
// /**
// * Contains true if a update for GeoPositions is available.
// */
// private boolean updated = false;
/**
* This is needed for not calculating permanently.
*/
private boolean bufferIsFinished = false;
/**
* Last update run for GeoCoordinates.
*/
private long lastUpdate = 0;
/**
* Delay in ms.
*/
private final long delay = 100;
/**
* Contains the view.
*/
private JXMapViewer map = null;
/**
* Contains all vertexes
*/
private List<GeoPosition> geoPos = new ArrayList<GeoPosition>(300);
/**
* Contains the current GeoPosition
*/
private GeoPosition currentGeoPos = new GeoPosition(0, 0, 0);
private final Set<AreaListener> listeners = new HashSet<AreaListener>();
/**
* Contains the type of selection: rectangle, polygon, buffer, line FIXME
* use an enumeration for this
*/
private String selectionType = "";
/**
* Contains a formatted String with the surface area.
*/
private String area = "";
/**
* Constructor.
*/
public AreaCalc() {
/* nothing */
}
/**
* @see Thread#run()
*/
@Override
public void run() {
while (isActive()) {
this.calculate();
try {
sleep(this.delay);
} catch (InterruptedException e) {
e.printStackTrace();
// try to get this thread back
instance.start();
}
}
}
/**
* Add an area listener
*
* @param listener the listener to add
*/
public void addListener(AreaListener listener) {
listeners.add(listener);
}
/**
* Remove an area listener
*
* @param listener the listener to remove
*/
public void removeListener(AreaListener listener) {
listeners.remove(listener);
}
/**
* @return current instance of AreaCalc
*/
public static AreaCalc getInstance() {
if (instance == null) {
AreaCalc tmp = new AreaCalc();
tmp.setName("AreaCalculation");
tmp.start();
instance = tmp;
}
return instance;
}
/**
* @param state activestate
*/
public void setActive(boolean state) {
if (!AreaCalc.active) {
new Thread(instance).start();
}
active = state;
fireActiveChanged();
}
/**
* @return action state
*/
public boolean isActive() {
return active;
}
/**
* Adds the ToolTip to {@link JXMapViewer}.
*
* @param map mapviewer
*/
public void setMap(JXMapViewer map) {
if (this.map == null) {
// set "big" map
this.map = map;
}
}
/**
* Setter for current {@link GeoPosition}.
*
* @param pos current GeoPosition
*/
public void setCurrentGeoPos(GeoPosition pos) {
if (!pos.equals(this.currentGeoPos)) {
this.currentGeoPos = pos;
// this.updated = true;
}
}
/**
* Setter for all selected {@link GeoPosition}s.
*
* @param pos from AbstractMapTool
*/
public void setGeoPositions(List<GeoPosition> pos) {
if (!pos.equals(this.geoPos)) {
this.geoPos = pos;
// this.updated = true;
}
}
/**
* Converts a Polygon to GeoPositions.
*
* @param buffer contains the polygon
*/
public void setBufferPolygon(java.awt.Polygon buffer) {
if (!calculation && this.lastUpdate < System.currentTimeMillis()) {
List<java.awt.geom.Point2D> pts = new ArrayList<java.awt.geom.Point2D>(buffer.npoints);
// create Point2D and add them
for (int i = 0; i < buffer.xpoints.length; i++) {
pts.add(new Point2D.Double(buffer.xpoints[i], buffer.ypoints[i]));
}
this.setPoint2DAsGeoPositions(pts);
}
}
/**
* This function sets {@link java.awt.geom.Point2D} coordinates as
* {@link GeoPosition} to calculate the surface area.
*
* @param points List of {@link java.awt.geom.Point2D}
*/
public void setPoint2DAsGeoPositions(List<java.awt.geom.Point2D> points) {
if (isActive()) {
this.geoPos = this.map.convertAllPointsToGeoPositions(points);
}
this.lastUpdate = System.currentTimeMillis() + this.delay;
}
/**
* Setter for selection type.
*
* @param type new selection type
*/
public void setSelectionType(String type) {
if (!type.equals(this.selectionType)) {
if (!type.equals("buffer")) {
this.bufferIsFinished = false;
}
this.selectionType = type;
this.area = "";
fireAreaChanged();
}
}
/**
*
*/
public void bufferReset() {
this.bufferIsFinished = false;
this.geoPos.clear();
}
/**
* Setter for {@link AreaCalc#area}. Used for external access.
*
* @param text new text
*/
public void setArea(String text) {
this.area = text;
fireAreaChanged();
}
private void fireAreaChanged() {
for (AreaListener listener : listeners) {
listener.areaChanged(area);
}
}
private void fireActiveChanged() {
for (AreaListener listener : listeners) {
listener.activationStateChanged(active);
}
}
/**
* Returns lastUpdate.
*
* @return lastUpdate in milliseconds
*/
public long getLastUpdate() {
return this.lastUpdate;
}
/**
* Returns the formatted area.
*
* @return the area
*/
public String getArea() {
return this.area;
}
/**
* Checks if GeoPosition are in a metric system and if not convert them if
* necessary.
*
* @param pos List of {@link GeoPosition}
*
* @return List of {@link GeoPosition}
*/
public List<GeoPosition> checkEPSG(List<GeoPosition> pos) {
// list is empty
if (pos.size() == 0) {
return pos;
}
//
int epsg = pos.get(0).getEpsgCode();
int FALLBACK_EPSG = 3395; // Worldmercator
FALLBACK_EPSG = 4326; // WGS84
try {
CoordinateSystem cs = CRS.decode("EPSG:" + epsg).getCoordinateSystem();
for (int i = 0; epsg != FALLBACK_EPSG && i < cs.getDimension(); i++) {
CoordinateSystemAxis axis = cs.getAxis(i);
try {
Unit<Length> unit = axis.getUnit().asType(Length.class);
if (!unit.toString().equals("m")) { //$NON-NLS-1$
// not metric
epsg = FALLBACK_EPSG;
}
} catch (ClassCastException e) {
// no length unit
epsg = FALLBACK_EPSG;
}
}
} catch (Exception e) {
e.printStackTrace();
}
// convert all coordinates
try {
GeotoolsConverter g = (GeotoolsConverter) GeotoolsConverter.getInstance();
pos = g.convertAll(pos, epsg);
} catch (IllegalGeoPositionException e1) {
e1.printStackTrace();
}
return pos;
}
/**
* This function calculates the area of a polygon.
*/
public void calculate() {
if (isActive() && this.geoPos.size() > 0 && !this.calculation
/* && this.updated */ && !this.bufferIsFinished) {
// set calculation to true to prevent double calculating
this.calculation = true;
// but allow new position data
// this.updated = false;
List<GeoPosition> pos = new ArrayList<GeoPosition>();
pos.addAll(this.geoPos);
pos.add(this.currentGeoPos);
// contains information for the tooltip
String tip = "";
// check epsg code and maybe convert to a metric system
pos = this.checkEPSG(pos);
// initialize formater
NumberFormat df = NumberFormat.getNumberInstance();
// calculate size
double area = 0.0;
// rectangle
if (pos.size() == 2 && this.selectionType.equals("rectangle")) {
area = this.calculateRectangle(pos.get(0), pos.get(1));
if (area > 1000000) {
area /= 1000000;
tip = df.format(area) + " km\u00B2";
}
else {
tip = df.format(area) + " m\u00B2";
}
}
// distance (polygon tool)
else if (pos.size() == 2 && this.selectionType.equals("polygon")) {
area = AreaCalc.calculateDistance(pos.get(0), pos.get(1));
tip = df.format(area) + " m";
}
// distance (buffer tool)
else if (pos.size() > 1 && this.selectionType.equals("line")) {
double temp = 0.0;
for (int i = 0; i < pos.size() - 1; i++) {
temp += AreaCalc.calculateDistance(pos.get(i), pos.get(i + 1));
}
tip = df.format(temp) + " m";
}
// polygon
else if (pos.size() > 2 && this.selectionType.equals("polygon")) {
area = this.calculatePolygon(pos);
if (area > 1000000) {
area /= 1000000;
tip = df.format(area) + " km\u00B2";
}
else {
tip = df.format(area) + " m\u00B2";
}
}
// buffer tool
else if (this.selectionType.equals("buffer")) {
if (pos.size() == 2) {
area = AreaCalc.calculateDistance(pos.get(0), pos.get(1));
tip = df.format(area) + " m";
}
else {
// calculate
area = this.calculatePolygon(pos);
if (area > 1000000) {
area /= 1000000;
tip = df.format(area) + " km\u00B2";
}
else {
tip = df.format(area) + " m\u00B2";
}
}
}
// update tooltip
this.area = tip;
fireAreaChanged();
// calculation has finished
this.calculation = false;
}
}
/**
* Calculates the distance between two geo coordinates using the haversine
* formula.
*
* @param a first {@link GeoPosition}
* @param b second {@link GeoPosition}
*
* @return area
*/
public static double calculateDistance(GeoPosition a, GeoPosition b) {
double value = 0.0;
// use haversine for wgs84 related systems
if (a.getEpsgCode() >= 4326 && a.getEpsgCode() <= 4329) {
value = AreaCalc.haversine(a, b);
}
else {
value = Math
.sqrt(Math.pow((a.getX() - b.getX()), 2) + Math.pow((a.getY() - b.getY()), 2));
}
return value;
}
/**
* Calculates the distance between to points.
*
* @param StartP source
* @param EndP destination
*
* @return distance
*/
public static double haversine(GeoPosition StartP, GeoPosition EndP) {
// Earth radius
double Radius = 6.371;
// latitude
double lat1 = StartP.getY();
double lat2 = EndP.getY();
// longitude
double lon1 = StartP.getX();
double lon2 = EndP.getX();
// convert to radians
double dLat = Math.toRadians(lat2 - lat1);
double dLon = Math.toRadians(lon2 - lon1);
// calculation
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(Math.toRadians(lat1))
* Math.cos(Math.toRadians(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
double c = 2 * Math.asin(Math.sqrt(a));
// multiplication with 1mio is needed for correct values
return Radius * c * Math.pow(1000, 2);
}
/**
* Calculate the area of a rectangle.
*
* @param a first {@link GeoPosition}
* @param b second {@link GeoPosition}
*
* @return area
*/
public double calculateRectangle(GeoPosition a, GeoPosition b) {
double value = 0.0;
value = AreaCalc.calculateDistance(new GeoPosition(a.getX(), a.getY(), a.getEpsgCode()),
new GeoPosition(b.getX(), a.getY(), a.getEpsgCode()))
* AreaCalc.calculateDistance(new GeoPosition(a.getX(), a.getY(), a.getEpsgCode()),
new GeoPosition(a.getX(), b.getY(), a.getEpsgCode()));
return Math.abs(value);
}
/**
* Function to calculate the surface of a polygon.
*
* @param pos List of {@link GeoPosition}
*
* @return area
*/
public double calculatePolygon(List<GeoPosition> pos) {
// contains the result
double result = 0.0;
// epsg code
int epsg = pos.get(0).getEpsgCode();
// check if it can be done with the shoelace formula
if (epsg >= 31461 && epsg <= 31469) {
result = this.shoelaceFormula(pos);
}
if (result == 0.0) {
result = this.heronFormula(pos);
}
return result;
}
/**
* Heron formula.
*
* @param pos List of {@link GeoPosition}
*
* @return area
*/
private double heronFormula(List<GeoPosition> pos) {
double result = 0.0;
List<Triangle> triangles = this.triangulate(pos);
for (Triangle t : triangles) {
result += t.getArea();
}
return result;
}
/**
* Triangulates the polygon.
*
* @param pos List of {@link GeoPosition}
*
* @return List of {@link Triangle}
*/
private List<Triangle> triangulate(List<GeoPosition> pos) {
// contains all triangles
List<Triangle> triangles = new ArrayList<Triangle>(pos.size() - 1);
// standard epsg code
int epsg = pos.get(0).getEpsgCode();
// check if it's already a triangle
if (pos.size() == 3) {
triangles.add(new Triangle(pos.get(0), pos.get(1), pos.get(2)));
return triangles;
}
// contains all points from the surface
List<Point3D> face = new ArrayList<>();
// convert Point2D to Vertex
for (int i = 0; i < pos.size(); i++) {
GeoPosition p = pos.get(i);
face.add(new Point3D(p.getX(), p.getY(), 0.0));
}
// create FaceSet and triangulate
FaceTriangulation fst = new FaceTriangulation();
List<List<Point3D>> faces = fst.triangulateFace(face);
// convert
for (List<Point3D> f : faces) {
// create GeoPositions
GeoPosition p1, p2, p3;
p1 = new GeoPosition(f.get(0).getX(), f.get(0).getY(), epsg);
p2 = new GeoPosition(f.get(1).getX(), f.get(1).getY(), epsg);
p3 = new GeoPosition(f.get(2).getX(), f.get(2).getY(), epsg);
// add triangle
triangles.add(new Triangle(p1, p2, p3));
}
return triangles;
}
/**
* Gauss' Area Formula
*
* @param positions polygon
*
* @return the base of the polygon
*/
private double shoelaceFormula(List<GeoPosition> positions) {
double result = 0;
// int epsg = positions.get(0).getEpsgCode();
ArrayList<Double> listY = new ArrayList<Double>();
ArrayList<Double> listX = new ArrayList<Double>();
for (GeoPosition v : positions) {
listY.add(v.getY());
listX.add(v.getX());
}
for (int i = 0; i < listX.size(); i++) {
result = result + listX.get(i)
* (listY.get(getFirst(i, listY)) - listY.get(getSecond(i, listY)));
}
result = result / 2;
//
double reduction = 0.0;
// earth's radius
// double Radius = 6.371;
// default: no reduction
// double y;
//
// switch(epsg) {
// // 3-degree Gauss zone 1
// case 31461:
// y = 1;
// break;
//
// // 3-degree Gauss zone 2
// case 31462:
// case 31466:
// y = 2;
// break;
//
// // 3-degree Gauss zone 3
// case 31463:
// case 31467:
// y = 3;
// break;
//
// // 3-degree Gauss zone 4
// case 31464:
// case 31468:
// y = 4;
// break;
//
// // 3-degree Gauss zone 5
// case 31465:
// case 31469:
// y = 5;
// break;
// default:
// y = 0;
// }
// calculate reduction for gauss-kruger-systems
// reduction = (result * Math.pow(y, 2))/Math.pow(Radius, 2);
return Math.abs(result - reduction)/* *10000 */;
}
/**
* Help method for gauss
*
* @param i nn
* @param listY nn
*
* @return the first value
*/
private int getFirst(int i, ArrayList<Double> listY) {
if (i == listY.size() - 1) {
return 0;
}
return (i + 1);
}
/**
* Help method for gauss
*
* @param i nn
* @param listY nn
*
* @return the second value
*/
private int getSecond(int i, ArrayList<Double> listY) {
if (i == 0) {
return listY.size() - 1;
}
return (i - 1);
}
}