/* Copyright (c) 2011 Danish Maritime Authority.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.maritimecloud.util.geometry;
import static java.lang.StrictMath.cos;
import static java.lang.StrictMath.sin;
import static java.lang.StrictMath.sqrt;
import static java.lang.StrictMath.toRadians;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import net.maritimecloud.message.MessageReader;
import net.maritimecloud.message.MessageSerializer;
import net.maritimecloud.message.MessageWriter;
/**
* This class holds the defining parameters for en ellipse.
*
* The location of the ellipse can optionally be offset from the geodetic reference point by dx, dy meters.
*/
public final class Ellipse extends Area {
/** serialVersionUID */
private static final long serialVersionUID = 1L;
/** Length of half axis in direction theta (in meters) */
private final double alpha;
/** Length of half axis in direction orthogonal to theta (in meters) */
private final double beta;
private final CoordinateConverter coordinateConverter;
/** Location offset of X coordinate from geodetic reference (in meters). */
private final double dx;
/** Location offset of Y coordinate from geodetic reference (in meters). */
private final double dy;
/** The geodetic point on Earth corresponding to (dx,dy) = (0,0) */
private final Position geodeticReference;
/**
* Direction of half axis alpha measured in Cartesian degrees; 0 degrees is parallel with the increasing direction
* of the X axis.
*/
private final double thetaDeg;
public static final MessageSerializer<Ellipse> SERIALIZER = new MessageSerializer<Ellipse>() {
/** {@inheritDoc} */
@Override
public Ellipse read(MessageReader reader) throws IOException {
double alpha = reader.readDouble(1, "alpha");
double beta = reader.readDouble(2, "beta");
double theta = reader.readDouble(3, "theta");
double dx = reader.readDouble(4, "dx");
double dy = reader.readDouble(5, "dy");
Position geodeticReference = reader.readPosition(6, "geodeticReference");
return new Ellipse(geodeticReference, dx, dy, alpha, beta, theta);
}
public void write(Ellipse message, MessageWriter w) throws IOException {
w.writeDouble(1, "alpha", message.getAlpha());
w.writeDouble(2, "beta", message.getBeta());
w.writeDouble(3, "theta", message.getY());
w.writeDouble(4, "dx", message.getX());
w.writeDouble(5, "dy", message.getY());
w.writePosition(6, "geodeticReference", message.getGeodeticReference());
}
};
/**
* Create an ellipse with center in the geodetic reference point.
*
* @param geodeticReference
* The center point of the ellipse
* @param alpha
* Length of half axis in direction theta (in meters)
* @param beta
* Length of half axis in direction orthogonal to theta (in meters)
* @param thetaDeg
* Direction of half axis alpha measured in degrees; 0 degrees is parallel with the increasing direction
* of the cartesian X axis.
*/
public Ellipse(Position geodeticReference, double alpha, double beta, double thetaDeg) {
this.geodeticReference = geodeticReference;
this.coordinateConverter = geodeticReference == null ? null : new CoordinateConverter(
geodeticReference.longitude, geodeticReference.latitude);
this.dx = 0.0;
this.dy = 0.0;
this.alpha = alpha;
this.beta = beta;
this.thetaDeg = thetaDeg;
}
/**
* Create an ellipse offset dx, dy meters from the geodetic reference point.
*
* @param geodeticReference
* The position, from which the center of the ellipse is offset by (dx, dy) meters.
* @param dx
* dx
* @param dy
* dy
* @param alpha
* Length of half axis in direction theta (in meters)
* @param beta
* Length of half axis in direction orthogonal to theta (in meters)
* @param thetaDeg
* Direction of half axis alpha measured in degrees; 0 degrees is parallel with the increasing direction
* of the cartesian X axis.
*/
public Ellipse(Position geodeticReference, double dx, double dy, double alpha, double beta, double thetaDeg) {
this.geodeticReference = geodeticReference;
this.coordinateConverter = geodeticReference == null ? null : new CoordinateConverter(
geodeticReference.longitude, geodeticReference.latitude);
this.dx = dx;
this.dy = dy;
this.alpha = alpha;
this.beta = beta;
this.thetaDeg = thetaDeg;
}
/** {@inheritDoc} */
@Override
public boolean contains(Position position) {
throw new UnsupportedOperationException();
}
public double getAlpha() {
return alpha;
}
public double getBeta() {
return beta;
}
/** {@inheritDoc} */
@Override
public Rectangle getBoundingBox() {
throw new UnsupportedOperationException();
}
public Position getGeodeticReference() {
return geodeticReference;
}
public double getMajorAxisGeodeticHeading() {
return CoordinateConverter.cartesian2compass(thetaDeg);
}
/** {@inheritDoc} */
@Override
public Position getRandomPosition(Random random) {
throw new UnsupportedOperationException();
}
public double getThetaDeg() {
return thetaDeg;
}
public double getX() {
return dx;
}
public double getY() {
return dy;
}
/** {@inheritDoc} */
public Ellipse immutable() {
return this;
}
/** {@inheritDoc} */
@Override
public boolean intersects(Area other) {
throw new UnsupportedOperationException();
}
/**
* Returns true if two safety zones intersect.
*
* @param otherEllipse
* the other safety zone.
* @return whether or not the two elipses intersect
*/
public boolean intersects(Ellipse otherEllipse) {
// TODO must have equal geodeticReference to compare
final double h1x = cos(toRadians(thetaDeg));
final double h1y = sin(toRadians(thetaDeg));
final double h2x = cos(toRadians(otherEllipse.thetaDeg));
final double h2y = sin(toRadians(otherEllipse.thetaDeg));
final double vx = otherEllipse.dx - dx;
final double vy = otherEllipse.dy - dy;
final double d = sqrt(vx * vx + vy * vy);
boolean intersects = true;
final double SMALL_NUM = 0.1;
if (d > SMALL_NUM) {
final double cosb1 = (h1x * vx + h1y * vy) / (sqrt(h1x * h1x + h1y * h1y) * d);
final double sinb1 = (h1x * vy - h1y * vx) / (sqrt(h1x * h1x + h1y * h1y) * d);
final double d1 = sqrt(alpha * alpha * beta * beta
/ (alpha * alpha * sinb1 * sinb1 + beta * beta * cosb1 * cosb1));
final double cosb2 = (h2x * vx + h2y * vy) / (sqrt(h2x * h2x + h2y * h2y) * d);
final double sinb2 = (h2x * vy - h2y * vx) / (sqrt(h2x * h2x + h2y * h2y) * d);
final double d2 = sqrt(otherEllipse.alpha
* otherEllipse.alpha
* otherEllipse.beta
* otherEllipse.beta
/ (otherEllipse.alpha * otherEllipse.alpha * sinb2 * sinb2 + otherEllipse.beta * otherEllipse.beta
* cosb2 * cosb2));
if (d - d1 - d2 < 0.0) {
intersects = true;
} else {
intersects = false;
}
}
return intersects;
}
/**
* Sample the perimeter along the ellipse in 'n' points, and return a list of positions all located and evenly
* distributed on the perimeter. This is useful e.g. to draw the perimeter on a chart using geodetic coordinates.
*
* @param n
* the number of perimeter samples to return.
* @return a list of positions on the perimeter.
*/
public List<Position> samplePerimeter(int n) {
// Sample ellipse scaled to meters
List<Point> unitPerimeter = new ArrayList<>(n);
double pi2 = 2 * Math.PI;
double dtheta = pi2 / n;
double theta = 0.0;
do {
unitPerimeter.add(new Point(alpha * cos(theta), beta * sin(theta)));
theta += dtheta;
} while (theta < pi2);
// Rotate ellipse to thetaDeg
List<Point> rotatedPerimeter = new ArrayList<>(n);
for (Point point : unitPerimeter) {
Point pr = point.rotate(Point.ORIGIN, thetaDeg).translate(dx, dy);
rotatedPerimeter.add(pr);
}
// Convert to geodetic
List<Position> perimeter = new ArrayList<>(n);
for (Point point : rotatedPerimeter) {
double lon = coordinateConverter.x2Lon(point.getX(), point.getY());
double lat = coordinateConverter.y2Lat(point.getX(), point.getY());
perimeter.add(Position.create(lat, lon));
}
return perimeter;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("Ellipse{");
sb.append("geodeticReference=").append(geodeticReference);
sb.append(", dx=").append(dx);
sb.append(", dy=").append(dy);
sb.append(", alpha=").append(alpha);
sb.append(", beta=").append(beta);
sb.append(", thetaDeg=").append(thetaDeg);
sb.append('}');
return sb.toString();
}
}