/* 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 net.maritimecloud.internal.message.BinaryUtil;
import net.maritimecloud.message.MessageReader;
import net.maritimecloud.message.MessageSerializer;
import net.maritimecloud.message.MessageWriter;
import net.maritimecloud.util.Binary;
import net.maritimecloud.util.Timestamp;
import java.io.IOException;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.ParseException;
import static java.lang.Math.cos;
import static java.lang.Math.sin;
import static java.util.Objects.requireNonNull;
import static net.maritimecloud.util.geometry.CoordinateConverter.compass2cartesian;
/**
* A position couple with a timestamp.
*
* @author Kasper Nielsen
*/
public class PositionTime extends Position {
/** A parser of PositionTime messages. */
public static final MessageSerializer<PositionTime> SERIALIZER = new MessageSerializer<PositionTime>() {
/** {@inheritDoc} */
@Override
public PositionTime read(MessageReader reader) throws IOException {
return readFrom(reader);
}
public void write(PositionTime message, MessageWriter writer) throws IOException {
writer.writeDouble(1, "latitude", message.latitude);
writer.writeDouble(2, "longitude", message.longitude);
writer.writeInt64(3, "time", message.time);
}
};
/** serialVersionUID */
private static final long serialVersionUID = 1L;
final long time;
PositionTime(double latitude, double longitude, long time) {
super(latitude, longitude);
this.time = time;
}
public static PositionTime fromBinary(Binary b) {
byte[] bytes = b.toByteArray();
PositionTime pt = PositionTime.create(
BinaryUtil.readInt(bytes, 0) / POS_INT_SCALE,
BinaryUtil.readInt(bytes, 4) / POS_INT_SCALE,
BinaryUtil.readLong(bytes, 8)
);
return pt;
}
public Binary toBinary() {
byte[] b = new byte[4 + 4 + 8];
BinaryUtil.writeInt(getLatitudeAsInt(), b, 0);
BinaryUtil.writeInt(getLongitudeAsInt(), b, 4);
BinaryUtil.writeLong(getTime(), b, 8);
return Binary.copyFrom(b);
}
/** {@inheritDoc} */
@Override
public boolean equals(Object other) {
return other instanceof PositionTime && equals((PositionTime) other);
}
/** {@inheritDoc} */
@Override
public boolean equals(Position other) {
return other instanceof PositionTime && equals((PositionTime) other);
}
// We probably want another function that also takes a precision.
public boolean equals(PositionTime other) {
return super.equals(other) && time == other.time;
}
/**
* Calculate - using linear extrapolation (or dead reckoning) - a position based on known speed and course from this
* position.
*
* @see <a href="http://en.wikipedia.org/wiki/Dead_reckoning">Dead reckoning</a>
*
* @param cog
* is the course over ground.
* @param sog
* is the speed over ground in knots.
* @param time
* the absolute time (in millis since Epoch) to extrapolate to.
* @return a new PositionTime instance containing the dead reckoned position at time t.
*/
public PositionTime extrapolatePosition(float cog, float sog, long time) {
if (time < getTime()) {
throw new IllegalArgumentException("'time' arguments cannot be earlier than 'pt1'. " + time + " "
+ getTime());
}
final CoordinateConverter coordinateConverter = new CoordinateConverter(getLongitude(), getLatitude());
final double x0 = coordinateConverter.lon2x(getLongitude(), getLatitude());
final double y0 = coordinateConverter.lat2y(getLongitude(), getLatitude());
final int dt = (int) ((time - getTime()) / 1000); // dt the time sailed in seconds
final double dist = dt * sog * 0.5144; // The distance sailed in dt seconds
final double angle = compass2cartesian(cog); // COG converted to cartesian angle
final double dx = cos(angle / 180 * Math.PI) * dist; // Distance sailed horisontal
final double dy = sin(angle / 180 * Math.PI) * dist; // Distance sailed vertical
final double x1 = x0 + dx;
final double y1 = y0 + dy;
final double lon1 = coordinateConverter.x2Lon(x1, y1); // The new longitude
final double lat1 = coordinateConverter.y2Lat(x1, y1); // The new latitude
return create(lat1, lon1, time);
}
public long getTime() {
return time;
}
/**
* Hash code for the location
*/
@Override
public int hashCode() {
// If we need to use this as a key somewhere we can use the same hash
// code technique as java.lang.String
long latLong = Double.doubleToLongBits(latitude);
long lonLong = Double.doubleToLongBits(longitude);
return (int) (time ^ latLong ^ latLong >>> 32) ^ (int) (time ^ lonLong ^ lonLong >>> 32);
}
/**
* Calculate - using linear interpolation - a position between this position and a specified. Assuming constant
* speed between the two positions.
*
* @param laterPosition
* the later position to use in the interpolation.
* @param time
* the time at which the interpolated position should be calculated.
* @return a new PositionTime instance containing the interpolated position at absolute time t.
*/
public PositionTime interpolatedPosition(PositionTime laterPosition, long time) {
requireNonNull(laterPosition);
if (laterPosition.getTime() < getTime()) {
throw new IllegalArgumentException("Provided position 1 must be earlier than position 2." + getTime() + " "
+ laterPosition.getTime());
}
if (time < getTime()) {
throw new IllegalArgumentException("time parameter must be later than position 1's. " + time + " "
+ getTime());
}
if (time > laterPosition.getTime()) {
throw new IllegalArgumentException("time parameter must be earlier than position 2's. " + time + " "
+ laterPosition.getTime());
}
double interpolatedLatitude = linearInterpolation(getLatitude(), getTime(), laterPosition.getLatitude(),
laterPosition.getTime(), time);
double interpolatedLongitude = linearInterpolation(getLongitude(), getTime(), laterPosition.getLongitude(),
laterPosition.getTime(), time);
return create(interpolatedLatitude, interpolatedLongitude, time);
}
public boolean positionEquals(Position other) {
return super.equals(other);
}
@Override
public String toString() {
return "(" + getLatitude() + ", " + getLongitude() + ", time= " + time + ")";
}
public Timestamp timestamp() {
return Timestamp.create(getTime());
}
public static PositionTime create(double latitude, double longitude) {
return new PositionTime(latitude, longitude, System.currentTimeMillis());
}
/**
* Creates a new position from the specified latitude and longitude.
*
* @param latitude
* the latitude
* @param longitude
* the longitude
* @param time
* the epoch time
* @return the new position
* @throws IllegalArgumentException
* if the
*/
public static PositionTime create(double latitude, double longitude, long time) {
return new PositionTime(latitude, longitude, time);
}
public static PositionTime create(Position position, long time) {
return create(position.latitude, position.longitude, time);
}
public static PositionTime create(String pos) {
String[] spli = pos.split(",");
if (spli.length == 2 || spli.length == 3) {
DecimalFormat df = new DecimalFormat();
DecimalFormatSymbols symbols = new DecimalFormatSymbols();
symbols.setDecimalSeparator('.');
symbols.setGroupingSeparator(' ');
df.setDecimalFormatSymbols(symbols);
double lat, lon;
try {
lat = df.parse(spli[0]).doubleValue();
} catch (ParseException e) {
throw new IllegalArgumentException("'" + spli[0] + "' is not a valid degree latitude");
}
try {
lon = df.parse(spli[1]).doubleValue();
} catch (ParseException e) {
throw new IllegalArgumentException("'" + spli[1] + "' is not a valid degree longitude");
}
return PositionTime.create(lat, lon, System.currentTimeMillis());
}
throw new IllegalArgumentException("Position was not valid '" + pos
+ "' must be lat, lon in decimal degrees, example '23.23, -23.12'");
}
static double linearInterpolation(double y1, long x1, double y2, long x2, long x) {
return y1 + (y2 - y1) / (x2 - x1) * (x - x1);
}
public static PositionTime readFrom(MessageReader r) throws IOException {
// if (r.isCompact()) {
// int lat = r.readInt32(1, "latitude");
// int lon = r.readInt32(2, "longitude");
// return Position.create(lat / 10_000_000d, lon / 10_000_000d);
// } else {
double lat = r.readDouble(1, "latitude");
double lon = r.readDouble(2, "longitude");
long time = r.readInt64(3, "time", 0L);
return PositionTime.create(lat, lon, time);
// }
}
}