com.tomtom.speedtools.geometry.GeoLine Maven / Gradle / Ivy
/*
* Copyright (C) 2012-2019, TomTom (http://tomtom.com).
*
* 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 com.tomtom.speedtools.geometry;
import com.tomtom.speedtools.utils.MathUtils;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
/**
* Geometric line between two points, from west to east (or, for vertical vectors east=west). If west > east, the
* vector is wrapped along the long side of Earth.
*
* Constructor: {@link #GeoLine}
*/
@Immutable
public final class GeoLine extends GeoObject {
@Nonnull
private final GeoPoint southWest;
@Nonnull
private final GeoPoint northEast;
/**
* Create a line. A line is always defined from west to east. Note that if the west.longitude > east.longitude
* that the line is wrapped along the long side of the Earth.
*
* Either both end points may have an elevation, or neither has. If, at construction, the elevation is only specified
* for one end point, then the elevation will be used for the other end point as well.
*
* @param southWest Start point, west side.
* @param northEast End point, east side, although east and west may be same longitude.
*/
public GeoLine(
@Nonnull final GeoPoint southWest,
@Nonnull final GeoPoint northEast) {
super();
assert southWest != null;
assert northEast != null;
GeoPoint lowerLeft = (southWest.getLat() <= northEast.getLat()) ?
southWest : southWest.withLat(northEast.getLat());
GeoPoint upperRight = (southWest.getLat() <= northEast.getLat()) ?
northEast : northEast.withLat(southWest.getLat());
// Make sure either both or neither of the end points have an elevation.
if ((lowerLeft.getElevationMeters() == null) || (upperRight.getElevationMeters() == null)) {
final double elevationMeters = (lowerLeft.getElevationMeters() != null) ? lowerLeft.getElevationMetersOrNaN() : upperRight.getElevationMetersOrNaN();
lowerLeft = lowerLeft.withElevationMeters(elevationMeters);
upperRight = upperRight.withElevationMeters(elevationMeters);
}
this.southWest = lowerLeft;
this.northEast = upperRight;
assert this.southWest.getLat() <= this.northEast.getLat() : "SW above NE: " + this;
}
/**
* Default constructor, used for deserialization only.
*/
@SuppressWarnings({"ConstantConditions", "UnusedDeclaration"})
@Deprecated
private GeoLine() {
super();
southWest = null;
northEast = null;
}
/**
* Return origin == southWest.
*
* @return Origin.
*/
@Nonnull
@Override
public GeoPoint getOrigin() {
return southWest;
}
/**
* Return center == Mid point of (northEast + southWest).
*
* @return Center.
*/
@Nonnull
@Override
public GeoPoint getCenter() {
final double lat = (southWest.getLat() + northEast.getLat()) / 2.0;
final double lon = (((northEast.getLon() >= southWest.getLon()) ?
(northEast.getLon()) : ((northEast.getLon() + 360.0))) +
southWest.getLon()) / 2.0;
final double elevationMeters = (southWest.getElevationMetersOrNaN() + northEast.getElevationMetersOrNaN()) / 2.0;
// The elevationMeters may be NaN, which is fine.
return new GeoPoint(lat, lon, elevationMeters);
}
/**
* Get start point.
*
* @return Start point.
*/
@Nonnull
public GeoPoint getSouthWest() {
return southWest;
}
/**
* Get end point.
*
* @return End point.
*/
@Nonnull
public GeoPoint getNorthEast() {
return northEast;
}
/**
* Setter for {@link #getSouthWest()}.
*
* @param southWest Location.
* @return New line.
*/
@Nonnull
public GeoLine withSouthWest(@Nonnull final GeoPoint southWest) {
return new GeoLine(southWest, northEast);
}
/**
* Setter for {@link #getNorthEast()}.
*
* @param northEast Location.
* @return New line.
*/
@Nonnull
public GeoLine withNorthEast(@Nonnull final GeoPoint northEast) {
return new GeoLine(southWest, northEast);
}
@Nonnull
@Override
public GeoLine translate(@Nonnull final GeoVector vector) {
return new GeoLine(southWest.translate(vector), northEast.translate(vector));
}
@Nonnull
@Override
public GeoLine moveTo(@Nonnull final GeoPoint origin) {
final GeoPoint p1 = origin;
final GeoPoint p2 = origin.translate(new GeoVector(getNorthing(), getEasting()));
return new GeoLine(p1, p2);
}
/**
* Return degrees northing.
*
* @return Degrees northing, [0, 180].
*/
public double getNorthing() {
final double northing = northEast.getLat() - southWest.getLat();
assert MathUtils.isBetween(northing, 0, 180) : "Northing not in [0, 180]: " + northing + ", " + this;
return northing;
}
/**
* Return degrees easting.
*
* @return Degrees easting, [0, 360).
*/
public double getEasting() {
final double easting;
if (northEast.getLon() >= southWest.getLon()) {
easting = northEast.getLon() - southWest.getLon();
} else {
easting = 360.0 + (northEast.getLon() - southWest.getLon());
}
assert (0.0 <= easting) && (easting < 360.0) : "Easting not in [0, 360): " + easting + ", " + this;
return easting;
}
/**
* Return length of line in meters (approximation).
*
* @return Length in meters.
*/
public double getLengthMeters() {
return Geo.distanceInMeters(southWest, northEast);
}
/**
* Return whether the vector is wrapped around the long or short side of Earth. The 'normal case' is that vectors
* and rectangles are wrapped along the short side indeed.
*
* @return True if wrapped around long side, false if wrapped along short side.
*/
public boolean isWrappedOnLongSide() {
return getEasting() >= 180.0;
}
/**
* Return the shortest GeoLine for two points, which is never wrapped around the long side of the Earth.
*
* @param from From.
* @param to To.
* @return Shortest connection.
*/
@Nonnull
public static GeoLine getShortestLine(
@Nonnull final GeoPoint from,
@Nonnull final GeoPoint to) {
assert from != null;
assert to != null;
GeoLine shortest = new GeoLine(from, to);
if (shortest.isWrappedOnLongSide()) {
shortest = new GeoLine(to, from);
}
return shortest;
}
/**
* Returns the heading angle with respect to North, counted clockwise.
*
* @return Clock-wise angle to North, range [-180, 180). This method is only accurate for small
* distances (sub-degree). It does not take the curvature of the surface into account.
*/
public double getHeading() {
double lat1 = Math.toRadians(southWest.getLat());
double lon1 = Math.toRadians(southWest.getLon());
double lat2 = Math.toRadians(northEast.getLat());
double lon2 = Math.toRadians(northEast.getLon());
double dLon = lon2 - lon1;
double y = Math.sin(dLon) * Math.cos(lat2);
double x = (Math.cos(lat1) * Math.sin(lat2)) - (Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLon));
double bearing = Math.atan2(y, x);
bearing = Geo.mapToLon(Math.toDegrees(bearing));
return bearing;
}
@Override
public boolean canEqual(@Nonnull final Object obj) {
return obj instanceof GeoLine;
}
@Override
public boolean equals(@Nullable final Object obj) {
boolean eq;
if (this == obj) {
eq = true;
} else if ((obj != null) && (obj instanceof GeoLine)) {
final GeoLine that = (GeoLine) obj;
eq = that.canEqual(this);
// Top-level entity, so don't: super.equals(that)
eq = eq && southWest.equals(that.southWest);
eq = eq && northEast.equals(that.northEast);
} else {
eq = false;
}
return eq;
}
@Override
public int hashCode() {
return hashCodeSuper(southWest, northEast);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy