diff --git a/src/main/java/de/serosystems/lib1090/cpr/CPREncodedPosition.java b/src/main/java/de/serosystems/lib1090/cpr/CPREncodedPosition.java
index 16aa635..757e587 100644
--- a/src/main/java/de/serosystems/lib1090/cpr/CPREncodedPosition.java
+++ b/src/main/java/de/serosystems/lib1090/cpr/CPREncodedPosition.java
@@ -2,62 +2,312 @@
import de.serosystems.lib1090.Position;
-@SuppressWarnings("unused")
-public class CPREncodedPosition {
-
- private final boolean is_odd;
- private final int encoded_lat;
- private final int encoded_lon;
- private final int nbits;
- private final boolean surface;
- private final Long timestamp;
-
- /**
- * @param is_odd true if it is a odd format, false if it is even (format field in most position messags)
- * @param encoded_lat CPR encoded latitude
- * @param encoded_lon CPR encoded longitude
- * @param nbits number of bits used to encode latitude and longitude; 17 for airborne position, 14 for intent,
- * and 12 for TIS-B
- * @param surface true if encoded position is surface position
- * @param timestamp timestamp when this position was received in milliseconds (null disables all tests based on time)
- */
- public CPREncodedPosition(boolean is_odd, int encoded_lat, int encoded_lon, int nbits, boolean surface, Long timestamp) {
- this.is_odd = is_odd;
- this.encoded_lat = encoded_lat;
- this.encoded_lon = encoded_lon;
- this.nbits = nbits;
- this.surface = surface;
+import java.util.Objects;
+
+/**
+ * CPR encoded position with decoding functions.
+ */
+public final class CPREncodedPosition {
+ /**
+ * Number of bits in {@link #yz} and {@link #xz}.
+ */
+ private final int nBits;
+
+ /**
+ * Whether this encoded position originates from an odd message.
+ */
+ private final boolean isOdd;
+
+ /**
+ * Whether this encoded position originates from a surface position message
+ */
+ private final boolean isSurface;
+
+ /**
+ * Whether the corresponding surface position message indicated a high or unknown speed.
+ * False (and not applicable) if {@link #isSurface} is false.
+ */
+ private final boolean isHighSurfaceSpeed;
+
+ /**
+ * Y coordinate within CPR Zone.
+ */
+ private final int yz;
+
+ /**
+ * X coordinate within CPR Zone.
+ */
+ private final int xz;
+
+ /**
+ * Timestamp of position message.
+ */
+ private final long timestamp;
+
+ /**
+ * Scaling factor for encoded values
+ */
+ private final double scale;
+
+ /**
+ * New CPR Encoded Position.
+ *
+ * @param nBits number of bits for encoded latitude and longitude. Must be 12, 14, or 17
+ * @param isOdd whether this encoded position originates from an odd position message
+ * @param isSurface whether this encoded position originates from a surface position message
+ * @param isHighSurfaceSpeed whether the corresponding surface position message indicated a high or unknown speed. Can be arbitrary if isSurface is false.
+ * @param yz Y coordinate within CPR zone, i.e. encoded latitude as in position message
+ * @param xz X coordinate within CPR zone, i.e. encoded longitude as in position message
+ * @param timestamp timestamp of position message
+ */
+ private CPREncodedPosition(int nBits,
+ boolean isOdd,
+ boolean isSurface,
+ boolean isHighSurfaceSpeed,
+ int yz,
+ int xz,
+ long timestamp) {
+ if (nBits != 12 && nBits != 14 && nBits != 17)
+ throw new IllegalArgumentException("Unexpected number of bits");
+ this.nBits = nBits;
+ this.isOdd = isOdd;
+ this.isSurface = isSurface;
+ // note: setting to this to false if !isSurface makes equals/hashCode easier
+ this.isHighSurfaceSpeed = isSurface && isHighSurfaceSpeed;
+ this.yz = yz;
+ this.xz = xz;
this.timestamp = timestamp;
+
+ scale = 1L << nBits;
}
/**
- * @return timestamp of this position message in milliseconds
+ * New CPR Encoded Position for an airborne position message.
+ *
+ * @param nBits number of bits for encoded latitude and longitude. Must be 12, 14, or 17
+ * @param isOdd whether this encoded position originates from an odd position message
+ * @param yz Y coordinate within CPR zone, i.e. encoded latitude as in position message
+ * @param xz X coordinate within CPR zone, i.e. encoded longitude as in position message
+ * @param timestamp timestamp of position message
+ * @return CPR encoded position
*/
- public Long getTimestamp() {
- return timestamp;
+ public static CPREncodedPosition ofAirborne(int nBits,
+ boolean isOdd,
+ int yz,
+ int xz,
+ long timestamp) {
+ return new CPREncodedPosition(nBits, isOdd, false, false, yz, xz, timestamp);
}
/**
- * @return true if message was odd format
+ * New CPR Encoded Position for a surface position message.
+ *
+ * @param nBits number of bits for encoded latitude and longitude. Must be 12, 14, or 17
+ * @param isOdd whether this encoded position originates from an odd position message
+ * @param isHighSurfaceSpeed whether the corresponding surface position message indicated a high or unknown speed. Can be arbitrary if isSurface is false.
+ * @param yz Y coordinate within CPR zone, i.e. encoded latitude as in position message
+ * @param xz X coordinate within CPR zone, i.e. encoded longitude as in position message
+ * @param timestamp timestamp of position message
+ * @return CPR encoded position
*/
- public boolean isOddFormat() {
- return is_odd;
+ public static CPREncodedPosition ofSurface(int nBits,
+ boolean isOdd,
+ boolean isHighSurfaceSpeed,
+ int yz,
+ int xz,
+ long timestamp) {
+ return new CPREncodedPosition(nBits, isOdd, true, isHighSurfaceSpeed, yz, xz, timestamp);
}
- public int getEncodedLat() {
- return encoded_lat;
+ public int getNBits() {
+ return nBits;
}
- public int getEncodedLon() {
- return encoded_lon;
+ public boolean isOddFormat() {
+ return isOdd;
}
public boolean isSurface() {
- return surface;
+ return isSurface;
+ }
+
+ public int yz() {
+ return yz;
}
- public int getNumBits() {
- return nbits;
+ public int xz() {
+ return xz;
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ /**
+ * Get maximum time gap between messages, based on their type.
+ * This is only applicable if messages are of different CPR format (even/odd).
+ *
+ * @param other other message, see constraints above
+ * @return maximum duration [ms] between messages
+ */
+ public long maxGap(CPREncodedPosition other) {
+ if (isSurface && other.isSurface) {
+ if (isHighSurfaceSpeed || other.isHighSurfaceSpeed)
+ return 25_000L;
+ else
+ return 50_000L;
+ } else {
+ return 10_000L;
+ }
+ }
+
+ /**
+ * Reconstruct zone index.
+ *
+ * @param zones number of even zones
+ * @param even CPR coordinate (xz or yz) of even message
+ * @param odd CPR coordinate (xz or yz) of odd message
+ * @return reconstructed zone index
+ */
+ private int zoneIndex(int zones, int even, int odd) {
+ int halfScale = 1 << (nBits - 1);
+ return (zones * (even - odd) - even + halfScale) >> nBits;
+ }
+
+ /**
+ * Compact Position Reporting: Global decoding.
+ * Can only be used if another position report with a different format (even/odd) is available.
+ *
+ * @param other position message of the other format (even/odd). Note that the time between those message must not exceed {@link #maxGap(CPREncodedPosition)}
+ * @param reference reference (e.g. receiver's) position to determine the correct surface position; use arbitrary (or null) for airborne (will be ignored)
+ * @return globally unambiguously decoded position or empty if the two encoded positions cannot be combined or if the position is otherwise unavailable or invalid
+ */
+ public Position decodeGlobal(CPREncodedPosition other, Position reference) {
+ /* early sanity checks */
+ if (other.nBits != nBits) return null;
+ if (isOdd == other.isOdd) return null;
+ if (isSurface != other.isSurface) return null;
+ if (isSurface && reference == null) return null;
+ long gap = Math.abs(timestamp - other.timestamp);
+ if (gap > maxGap(other)) return null;
+
+ final CPREncodedPosition even = isOdd ? other : this;
+ final CPREncodedPosition odd = isOdd ? this : other;
+
+ final double angle = isSurface ? 90. : 360.;
+
+ // latitude index
+ int j = zoneIndex(60, even.yz, odd.yz);
+
+ // global latitudes
+ final double refLat = reference == null ? 0. : reference.getLatitude();
+ final L0Latitude Rlat0L = L0Latitude.ofGlobal(even, j, refLat);
+ final L0Latitude Rlat1L = L0Latitude.ofGlobal(odd, j, refLat);
+
+ // additional check against invalid latitudes
+ if (!Rlat0L.isValid() || !Rlat1L.isValid())
+ return null;
+
+ // require that the number of longitude zones are equal
+ final int nLon = Rlat0L.NL();
+ if (nLon != Rlat1L.NL()) return null; // straddling position
+
+ // reconstruct latitude
+ final double Rlat = isOdd ? Rlat1L.toDegrees() : Rlat0L.toDegrees();
+
+ // reconstruct longitude
+ double Rlon;
+ if (nLon != 1) {
+ // longitude index
+ int m = zoneIndex(nLon, even.xz, odd.xz);
+ // global longitude
+ int n_helper = nLon - (isOdd ? 1 : 0);
+ Rlon = reconstructGlobal(angle, n_helper, m, xz);
+ } else {
+ Rlon = angle * (xz / scale);
+ }
+
+ if (isSurface) {
+ double delta = normalize(reference.getLongitude() - Rlon);
+ int k = (int) Math.round(delta / 90.);
+ Rlon = normalize(Rlon + k * 90);
+ } else {
+ Rlon = normalize(Rlon);
+ }
+
+ return new Position(Rlon, Rlat, 0.);
+ }
+
+ /**
+ * Normalize angle to [-180, 180).
+ *
+ * @param phi angle in degrees
+ * @return normalized angle
+ */
+ private static double normalize(double phi) {
+ return phi - 360.0 * Math.floor((phi + 180.0) / 360.0);
+ }
+
+ /**
+ * Compact Position Reporting: Local decoding.
+ *
+ * This function uses a locally unambiguous decoding for airborne position messages.
+ * It uses a reference position known to be within 180NM (airborne) resp. within 45NM (surface) the target's true position.
+ * This reference position may be a previously decoded position that has been confirmed by global decoding, see
+ * {@link #decodeGlobal(CPREncodedPosition, Position)}.
+ *
+ * Note that the returned position can still be invalid, e.g. it is possible to construct latitudes that are not within [-90,90]°.
+ *
+ * @param reference reference position
+ * @return decoded position
+ */
+ Position decodeLocal(Position reference) {
+ if (reference == null)
+ return null;
+
+ // latitude/longitude zone size
+ final double angle = isSurface ? 90. : 360.;
+
+ // decode position latitude
+ final L0Latitude RlatL = L0Latitude.ofLocal(this, reference.getLatitude());
+ final double Rlat = RlatL.toDegrees();
+
+ // number of longitude zones
+ int nLon = Math.max(1, RlatL.NL() - (isOdd ? 1 : 0));
+
+ // decode position longitude
+ double Rlon = reconstructLocal(angle, nLon, reference.getLongitude(), xz);
+
+ return new Position(Rlon, Rlat, 0.);
+ }
+
+ /**
+ * Reconstruct latitude resp. longitude from an CPR encoded number and a reference position.
+ *
+ * @param angle full range angle
+ * @param zones number of zones
+ * @param ref reference latitude resp. longitude
+ * @param coordinate CPR coordinate (xz or yz)
+ * @return reconstructed latitude resp. longitude
+ */
+ private double reconstructLocal(double angle, int zones, double ref, int coordinate) {
+ final double D = angle / zones;
+ final double scaled = coordinate / scale;
+ final double zone = Math.floor(0.5 + ref / D - scaled);
+ return D * (zone + scaled);
+ }
+
+ /**
+ * Reconstruct latitude resp. longitude from an CPR encoded number its zone index.
+ *
+ * @param angle full range angle
+ * @param zones number of zones
+ * @param zone zone index
+ * @param coordinate CPR coordinate (xz or yz)
+ * @return reconstructed latitude resp. longitude
+ */
+ private double reconstructGlobal(double angle, int zones, int zone, int coordinate) {
+ return angle / zones * (Util.mod(zone, zones) + coordinate / scale);
}
/**
@@ -71,27 +321,11 @@ public int getNumBits() {
* @return the decoded position or null if could not be decoded
*/
public Position decodePosition(CPREncodedPosition other, Position reference) {
- // can we apply global decoding?
- boolean global = other != null && // need other pos for global decoding
- this.is_odd != other.is_odd && // other pos must be complementary format
- this.surface == other.surface && // cannot combine surface and airborne
- (!this.surface || reference != null); // we need reference position for surface positions
-
- // time-based tests
- global = global && this.timestamp != null && other.timestamp != null &&
- (this.surface || Math.abs(this.timestamp - other.timestamp) < 10_000L) && // airborne should not be more than 10 seconds apart
- (!this.surface || Math.abs(this.timestamp - other.timestamp) < 25_000L); // surface should not be more than 25 seconds apart
-
- // can we apply local decoding?
- boolean local = reference != null; // need reference position for local decoding
-
- Position globalPos = null;
// apply global decoding
- if (global) globalPos = CompactPositionReporting.decodeGlobalPosition(this, other, reference);
+ Position globalPos = other == null ? null : decodeGlobal(other, reference);
- Position localPos = null;
// apply local decoding
- if (local) localPos = CompactPositionReporting.decodeLocalPosition(this, reference);
+ Position localPos = reference != null ? decodeLocal(reference) : null;
//////// Reasonableness Test //////////
// see A.1.7.10.2 of DO-260B
@@ -105,26 +339,26 @@ public Position decodePosition(CPREncodedPosition other, Position reference) {
// use local CPR to verify even and odd position
if (globalPos != null) {
- Position localThis = CompactPositionReporting.decodeLocalPosition(this, globalPos);
+ Position localThis = decodeLocal(globalPos);
// check local/global dist of new message
if (globalPos.haversine(localThis) > mu)
reasonable = false;
// check if distance to other is within limits
- Position globalOther = CompactPositionReporting.decodeGlobalPosition(other, this, reference);
- Position localOther = CompactPositionReporting.decodeLocalPosition(other, globalPos);
+ Position globalOther = other.decodeGlobal(this, reference);
+ Position localOther = other.decodeLocal(globalPos);
// should be within 3 NM (= 555.6 m/s * 10 seconds)
- if (globalOther != null && !surface && globalOther.haversine(globalPos) > 5556)
+ if (globalOther != null && !isSurface && globalOther.haversine(globalPos) > 5556)
reasonable = false;
- if (localOther != null && !surface && localOther.haversine(globalPos) > 5556)
+ if (localOther != null && !isSurface && localOther.haversine(globalPos) > 5556)
reasonable = false;
}
// prefer global over local position
- Position ret = global ? globalPos : localPos;
+ Position ret = globalPos != null ? globalPos : localPos;
if (ret != null) {
// is it a valid coordinate?
@@ -137,15 +371,34 @@ public Position decodePosition(CPREncodedPosition other, Position reference) {
return ret;
}
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) return true;
+ if (obj == null || obj.getClass() != this.getClass()) return false;
+ final CPREncodedPosition that = (CPREncodedPosition) obj;
+ return this.nBits == that.nBits &&
+ this.isOdd == that.isOdd &&
+ this.isSurface == that.isSurface &&
+ this.isHighSurfaceSpeed == that.isHighSurfaceSpeed &&
+ this.yz == that.yz &&
+ this.xz == that.xz &&
+ this.timestamp == that.timestamp;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(nBits, isOdd, isSurface, isHighSurfaceSpeed, yz, xz, timestamp);
+ }
+
@Override
public String toString() {
- return "CPREncodedPosition{" +
- "is_odd=" + is_odd +
- ", encoded_lat=" + encoded_lat +
- ", encoded_lon=" + encoded_lon +
- ", nbits=" + nbits +
- ", surface=" + surface +
- ", timestamp=" + timestamp +
- '}';
+ return "CPREncodedPosition[" +
+ "nBits=" + nBits + ", " +
+ "isOdd=" + isOdd + ", " +
+ "isSurface=" + isSurface + ", " +
+ "isHighSurfaceSpeed=" + isHighSurfaceSpeed + ", " +
+ "yz=" + yz + ", " +
+ "xz=" + xz + ", " +
+ "timestamp=" + timestamp + ']';
}
}
diff --git a/src/main/java/de/serosystems/lib1090/cpr/CompactPositionReporting.java b/src/main/java/de/serosystems/lib1090/cpr/CompactPositionReporting.java
deleted file mode 100644
index 0b483a1..0000000
--- a/src/main/java/de/serosystems/lib1090/cpr/CompactPositionReporting.java
+++ /dev/null
@@ -1,166 +0,0 @@
-package de.serosystems.lib1090.cpr;
-
-/*
- * This file is part of de.serosystems.lib1090.
- *
- * de.serosystems.lib1090 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.
- *
- * de.serosystems.lib1090 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 de.serosystems.lib1090. If not, see .
- */
-
-import de.serosystems.lib1090.Position;
-
-/**
- * Decoder for CPR encoded positions
- * @author Matthias Schaefer (schaefer@sero-systems.de)
- */
-public class CompactPositionReporting {
-
- /**
- * This method can only be used if another position report with a different format (even/odd) is available
- * and set with msg.setOtherFormatMsg(other).
- * @param pos CPR encoded position
- * @param old airborne position message of the other format (even/odd). Note that the time between
- * both messages should be not longer than 10 seconds!
- * @param reference position to determine right surface position; use null for airborne (will be ignored)
- * @return globally unambiguously decoded position for cpr1 or null if two encoded positions cannot be combined or
- * if position is otherwise unavailable. Altitude of resulting position is null.
- */
- public static Position decodeGlobalPosition(CPREncodedPosition pos, CPREncodedPosition old, Position reference) {
-
- if (pos.isOddFormat() == old.isOddFormat()) return null;
- if (pos.isSurface() != old.isSurface()) return null;
- if (pos.isSurface() && reference == null) return null;
-
- CPREncodedPosition even = pos.isOddFormat() ? old : pos;
- CPREncodedPosition odd = pos.isOddFormat() ? pos : old;
-
- double angle = even.isSurface() ? 90 : 360.0;
-
- // Helper for latitude (Number of zones NZ is set to 15)
- double Dlat0 = angle / 60.0;
- double Dlat1 = angle / 59.0;
-
- // latitude index
- double j = Math.floor((
- (59.0 * ((double) even.getEncodedLat())) / ((double) (1 << even.getNumBits())) -
- (60.0 * ((double) odd.getEncodedLat())) / ((double) (1 << odd.getNumBits()))) + 0.5);
-
- // global latitudes
- double Rlat0 = Dlat0 * (mod(j, 60.0) + ((double) even.getEncodedLat()) / ((double) (1 << even.getNumBits())));
- double Rlat1 = Dlat1 * (mod(j, 59.0) + ((double) odd.getEncodedLat()) / ((double) (1 << odd.getNumBits())));
-
- // Southern hemisphere?
- if (!pos.isSurface()) {
- if (Rlat0 >= 270.0 && Rlat0 <= 360.0) Rlat0 -= 360.0;
- if (Rlat1 >= 270.0 && Rlat1 <= 360.0) Rlat1 -= 360.0;
- } else {
- if (Rlat0 == 0 && reference.getLatitude() > 45) Rlat0 = 90.0;
- else if (Rlat0 - reference.getLatitude() > 45.0) Rlat0 -= 90.0;
- if (Rlat1 == 0 && reference.getLatitude() > 45) Rlat1 = 90.0;
- else if (Rlat1 - reference.getLatitude() > 45.0) Rlat1 -= 90.0;
- }
-
- // ensure that the number of even longitude zones are equal
- if (NL(Rlat0) != NL(Rlat1)) return null; // position straddle
-
- double Rlat = pos.isOddFormat() ? Rlat1 : Rlat0;
-
- // Helper for longitude
- double NL_helper = NL(Rlat0);// NL(Rlat0) and NL(Rlat1) are equal
-
- // longitude index
- double m = Math.floor(
- ((double) even.getEncodedLon() * (NL_helper - 1.0)) / ((double) (1 << even.getNumBits())) -
- ((double) odd.getEncodedLon() * NL_helper) / ((double) (1 << odd.getNumBits())) + 0.5);
-
- // global longitude
- double n_helper = Math.max(1.0, NL_helper - (pos.isOddFormat() ? 1.0 : 0.0));
- double Rlon = (angle / n_helper) * (mod(m, n_helper) + ((double) pos.getEncodedLon()) / ((double) (1 << pos.getNumBits())));
-
- if (pos.isSurface()) {
- // check the 4 possible solutions of the surface decoding
- Position candidate = new Position(Rlon, Rlat, 0.0);
- for (int o : new int[] {90, 180, 270}) {
- Position alternative = new Position(Rlon + o, Rlat, 0.0);
- if (reference.haversine(alternative) < reference.haversine(candidate))
- candidate = alternative;
- }
- Rlon = candidate.getLongitude();
- }
-
- // correct longitude
- if (Rlon < -180.0 && Rlon > -360.0) Rlon += 360.0;
- if (Rlon > 180.0 && Rlon < 360.0) Rlon -= 360.0;
-
- return new Position(Rlon, Rlat, null);
- }
-
- /**
- * This method uses a locally unambiguous decoding for airborne position messages. It
- * uses a reference position known to be within 180NM (= 333.36km) of the true target
- * airborne position and within 45NM for surface positions. The reference point may be
- * a previously decoded position that has been confirmed by global decoding (see
- * {@link #decodeGlobalPosition(CPREncodedPosition, CPREncodedPosition, Position)}) or
- * the receiver position.
- * @param pos CPR encoded position
- * @param ref reference position
- * @return decoded position (without altitude)
- */
- public static Position decodeLocalPosition(CPREncodedPosition pos, Position ref) {
- if (ref == null) return null;
-
- // latitude zone size
- double angle = pos.isSurface() ? 90.0 : 360.0;
- double Dlat = pos.isOddFormat() ? angle / 59.0 : angle / 60.0;
-
- // latitude zone index
- double j = Math.floor(ref.getLatitude() / Dlat) + Math.floor(
- 0.5 + mod(ref.getLatitude(), Dlat) / Dlat - ((double) pos.getEncodedLat()) / ((double) (1 << pos.getNumBits())));
-
- // decoded position latitude
- double Rlat = Dlat * (j + ((double) pos.getEncodedLat()) / ((double) (1 << pos.getNumBits())));
-
- // longitude zone size
- double Dlon = angle / Math.max(1.0, NL(Rlat) - (pos.isOddFormat() ? 1.0 : 0.0));
-
- // longitude zone coordinate
- double m = Math.floor(ref.getLongitude() / Dlon) + Math.floor(0.5 + mod(ref.getLongitude(), Dlon) / Dlon
- - ((double) pos.getEncodedLon()) / ((double) (1 << pos.getNumBits())));
-
- // and finally the longitude
- double Rlon = Dlon * (m + ((double) pos.getEncodedLon()) / ((double) (1 << pos.getNumBits())));
-
- return new Position(Rlon, Rlat, null);
- }
-
- /**
- * @param Rlat Even or odd Rlat value (CPR internal)
- * @return the number of even longitude zones at a latitude
- */
- private static double NL(double Rlat) {
- if (Rlat == 0) return 59;
- else if (Math.abs(Rlat) == 87) return 2;
- else if (Math.abs(Rlat) > 87) return 1;
-
- double tmp = 1-(1-Math.cos(Math.PI/(2.0*15.0)))/Math.pow(Math.cos(Math.PI/180.0*Math.abs(Rlat)), 2);
- return Math.floor(2*Math.PI/Math.acos(tmp));
- }
-
- /**
- * Modulo operator in java has stupid behavior
- */
- private static double mod(double a, double b) {
- return ((a%b)+b)%b;
- }
-
-}
diff --git a/src/main/java/de/serosystems/lib1090/cpr/L0Latitude.java b/src/main/java/de/serosystems/lib1090/cpr/L0Latitude.java
new file mode 100644
index 0000000..cd0ba8b
--- /dev/null
+++ b/src/main/java/de/serosystems/lib1090/cpr/L0Latitude.java
@@ -0,0 +1,199 @@
+package de.serosystems.lib1090.cpr;
+
+import java.util.Arrays;
+
+/**
+ * Latitude represented on Lattice L0.
+ */
+class L0Latitude {
+ /**
+ * Transition latitudes, scaled into L0.
+ */
+ private static final int[] T_LAT = {
+ 0x0337ad54,
+ 0x048e7ba9,
+ 0x0596a719,
+ 0x06764ff9,
+ 0x073c35cf,
+ 0x07efe698,
+ 0x0895ddf2,
+ 0x093106d8,
+ 0x09c367ac,
+ 0x0a4e798e,
+ 0x0ad35901,
+ 0x0b52e2ec,
+ 0x0bcdc6e6,
+ 0x0c449344,
+ 0x0cb7bd4c,
+ 0x0d27a700,
+ 0x0d94a34d,
+ 0x0dfef917,
+ 0x0e66e596,
+ 0x0ecc9e11,
+ 0x0f305145,
+ 0x0f92287b,
+ 0x0ff24861,
+ 0x1050d1c2,
+ 0x10ade211,
+ 0x110993e4,
+ 0x1163ff57,
+ 0x11bd3a58,
+ 0x121558f6,
+ 0x126c6d8f,
+ 0x12c2890a,
+ 0x1317bafe,
+ 0x136c11d2,
+ 0x13bf9ae1,
+ 0x1412628d,
+ 0x1464745b,
+ 0x14b5daff,
+ 0x1506a06c,
+ 0x1556cde0,
+ 0x15a66be8,
+ 0x15f58265,
+ 0x16441889,
+ 0x169234cd,
+ 0x16dfdce1,
+ 0x172d1591,
+ 0x1779e292,
+ 0x17c6463d,
+ 0x18124118,
+ 0x185dd11e,
+ 0x18a8f089,
+ 0x18f393ba,
+ 0x193da56a,
+ 0x1986ff34,
+ 0x19cf5991,
+ 0x1a1624e1,
+ 0x1a5a17bd,
+ 0x1a9772f8,
+ 0x1abc0000,
+ };
+
+ /**
+ * Helper factor for lattice.
+ */
+ private static final int SCALE = 14160;
+
+ /**
+ * Denominator of lattice L0.
+ */
+ private static final int L0 = SCALE << 17;
+
+ /**
+ * Latitude on L0.
+ */
+ private final int lat;
+
+ /**
+ * Whether latitude is valid.
+ */
+ private final boolean valid;
+
+ /**
+ * New value on lattice.
+ *
+ * @param lat latitude on lattice.
+ */
+ private L0Latitude(int lat) {
+ this.lat = lat;
+ valid = Math.abs(lat) <= L0 / 4;
+ }
+
+ /**
+ * Get latitude in degrees.
+ *
+ * @return latitude [°].
+ * @see #isValid() if not valid, returned latitude is outside valid range
+ */
+ public double toDegrees() {
+ return 360. * lat / L0;
+ }
+
+ /**
+ * Whether latitude is valid.
+ * Note: it is considered invalid if and only if it is outside range {@code [-90°,90°]}.
+ *
+ * @return true if latitude is valid, false otherwise
+ */
+ public boolean isValid() {
+ return valid;
+ }
+
+ /**
+ * Compute NL(toDegrees()), i.e. the number of longitude zones for this latitude.
+ * See DO-260B §A.1.7.2 for reference.
+ *
+ * @return number of longitude zones for this latitude.
+ */
+ public int NL() {
+ int idx = Arrays.binarySearch(T_LAT, Math.abs(lat));
+ if (idx < 0) idx = -idx - 1;
+ return T_LAT.length - idx + 1;
+ }
+
+ /**
+ * Constructs a new L0Latitude object based on a provided latitude in degrees.
+ *
+ * @param degrees latitude in degrees.
+ * @return latitude in L0
+ */
+ public static L0Latitude ofDegrees(double degrees) {
+ return new L0Latitude((int) (degrees * L0 / 360.));
+ }
+
+ /**
+ * Create new latitude for a given position message, using global decoding.
+ *
+ * @param cpr CPR encoded position
+ * @param zoneIndex zone index
+ * @param refLat reference latitude in degrees, needed if this is for a surface position
+ * @return latitude in L0 for given parameters
+ */
+ public static L0Latitude ofGlobal(CPREncodedPosition cpr, int zoneIndex, double refLat) {
+ int nBits = cpr.getNBits();
+
+ int f = cpr.isSurface() ? 4 : 1;
+ int zones = cpr.isOddFormat() ? 59 : 60;
+ int effectiveScale = SCALE / f / zones;
+ int nz = Util.mod(zoneIndex, zones) << nBits;
+ int lat0 = nz + cpr.yz();
+ int r = (lat0 << (17 - nBits)) * effectiveScale;
+
+ if (!cpr.isSurface()) {
+ if (r > L0 / 2) // Southern Hemisphere
+ r -= L0;
+ } else {
+ int l = ofDegrees(refLat).lat;
+ if (r == 0 && l > L0 / 8) // North Pole
+ r = L0 / 4;
+ else if (r - l > L0 / 8) // Southern Hemisphere
+ r -= L0 / 4;
+ }
+
+ return new L0Latitude(r);
+ }
+
+ /**
+ * Create new latitude for a given position message, using local decoding.
+ *
+ * @param cpr CPR encoded position
+ * @param referenceLatitude reference latitude
+ * @return latitude in L0 for given parameters
+ */
+ public static L0Latitude ofLocal(CPREncodedPosition cpr, double referenceLatitude) {
+ int nBits = cpr.getNBits();
+ int f = cpr.isSurface() ? 4 : 1;
+ int zones = cpr.isOddFormat() ? 59 : 60;
+ double D = 360. / f / zones;
+ double cprScale = 1 << nBits;
+ int zone = (int) Math.floor(.5 + referenceLatitude / D - cpr.yz() / cprScale);
+
+ int effectiveScale = SCALE / f / zones;
+ int nz = zone << nBits;
+ int lat0 = nz + cpr.yz();
+ int r = (lat0 << (17 - nBits)) * effectiveScale;
+
+ return new L0Latitude(r);
+ }
+}
diff --git a/src/main/java/de/serosystems/lib1090/cpr/StatefulPositionDecoder.java b/src/main/java/de/serosystems/lib1090/cpr/StatefulPositionDecoder.java
index 5ac1f5e..1c8a69c 100644
--- a/src/main/java/de/serosystems/lib1090/cpr/StatefulPositionDecoder.java
+++ b/src/main/java/de/serosystems/lib1090/cpr/StatefulPositionDecoder.java
@@ -64,7 +64,7 @@ public Position decodePosition(CPREncodedPosition cpr, Position receiver, boolea
//////// apply some additional (stateful) reasonableness tests //////////
// check if it's realistic that the target covered this distance (faster than 1000 knots?)
- if (!disableSpeedTest && last_pos != null && last_time != null && cpr.getTimestamp() != null) {
+ if (!disableSpeedTest && last_pos != null && last_time != null) {
double td = abs((cpr.getTimestamp() - last_time) / 1_000.);
double groundSpeed = newPos.haversine(last_pos) / td; // in meters per second
diff --git a/src/main/java/de/serosystems/lib1090/cpr/Util.java b/src/main/java/de/serosystems/lib1090/cpr/Util.java
new file mode 100644
index 0000000..b347ab7
--- /dev/null
+++ b/src/main/java/de/serosystems/lib1090/cpr/Util.java
@@ -0,0 +1,22 @@
+package de.serosystems.lib1090.cpr;
+
+final class Util {
+ private Util() {}
+
+ /**
+ * Euclidean modulo operator.
+ *
+ * Let {@code a, b} be given integers, then {@code a = b * k + r} for some integers {@code k, r} such that {@code 0 <= r < |b|}. Then this function returns {@code r}.
+ * An alternative definition is {@code r = a - floor(a/b)*b}.
+ *
+ * @param a some a
+ * @param b some b > 0
+ * @return a mod b following Euclidean definition.
+ */
+ public static int mod(int a, int b) {
+ int m = a % b;
+ if (m < 0)
+ m += b;
+ return m;
+ }
+}
diff --git a/src/main/java/de/serosystems/lib1090/msgs/adsb/AirbornePositionV0Msg.java b/src/main/java/de/serosystems/lib1090/msgs/adsb/AirbornePositionV0Msg.java
index dfb6641..d88e988 100644
--- a/src/main/java/de/serosystems/lib1090/msgs/adsb/AirbornePositionV0Msg.java
+++ b/src/main/java/de/serosystems/lib1090/msgs/adsb/AirbornePositionV0Msg.java
@@ -100,8 +100,7 @@ public AirbornePositionV0Msg(ExtendedSquitter squitter, Long timestamp) throws B
boolean cpr_format = ((msg[2]>>>2)&0x1) == 1;
int cpr_encoded_lat = (((msg[2]&0x3)<<15) | ((msg[3]&0xFF)<<7) | ((msg[4]>>>1)&0x7F)) & 0x1FFFF;
int cpr_encoded_lon = (((msg[4]&0x1)<<16) | ((msg[5]&0xFF)<<8) | (msg[6]&0xFF)) & 0x1FFFF;
- position = new CPREncodedPosition(
- cpr_format, cpr_encoded_lat, cpr_encoded_lon, 17, false,
+ position = CPREncodedPosition.ofAirborne(17, cpr_format, cpr_encoded_lat, cpr_encoded_lon,
timestamp == null ? System.currentTimeMillis() : timestamp);
}
diff --git a/src/main/java/de/serosystems/lib1090/msgs/adsb/SurfacePositionV0Msg.java b/src/main/java/de/serosystems/lib1090/msgs/adsb/SurfacePositionV0Msg.java
index 58dd815..582536f 100644
--- a/src/main/java/de/serosystems/lib1090/msgs/adsb/SurfacePositionV0Msg.java
+++ b/src/main/java/de/serosystems/lib1090/msgs/adsb/SurfacePositionV0Msg.java
@@ -91,8 +91,8 @@ public SurfacePositionV0Msg(ExtendedSquitter squitter, Long timestamp) throws Ba
int cpr_encoded_lat = (((msg[2]&0x3)<<15) | ((msg[3]&0xFF)<<7) | ((msg[4]>>>1)&0x7F)) & 0x1FFFF;
int cpr_encoded_lon = (((msg[4]&0x1)<<16) | ((msg[5]&0xFF)<<8) | (msg[6]&0xFF)) & 0x1FFFF;
- position = new CPREncodedPosition(
- cpr_format, cpr_encoded_lat, cpr_encoded_lon, 17, true,
+ boolean highGroundSpeed = movement == 0 || movement > 49;
+ position = CPREncodedPosition.ofSurface(17, cpr_format, highGroundSpeed, cpr_encoded_lat, cpr_encoded_lon,
timestamp == null ? System.currentTimeMillis() : timestamp);
}
diff --git a/src/main/java/de/serosystems/lib1090/msgs/adsr/AirbornePositionV0Msg.java b/src/main/java/de/serosystems/lib1090/msgs/adsr/AirbornePositionV0Msg.java
index 75db394..8e17051 100644
--- a/src/main/java/de/serosystems/lib1090/msgs/adsr/AirbornePositionV0Msg.java
+++ b/src/main/java/de/serosystems/lib1090/msgs/adsr/AirbornePositionV0Msg.java
@@ -99,8 +99,7 @@ public AirbornePositionV0Msg(ExtendedSquitter squitter, Long timestamp) throws B
boolean cpr_format = ((msg[2]>>>2)&0x1) == 1;
int cpr_encoded_lat = (((msg[2]&0x3)<<15) | ((msg[3]&0xFF)<<7) | ((msg[4]>>>1)&0x7F)) & 0x1FFFF;
int cpr_encoded_lon = (((msg[4]&0x1)<<16) | ((msg[5]&0xFF)<<8) | (msg[6]&0xFF)) & 0x1FFFF;
- position = new CPREncodedPosition(
- cpr_format, cpr_encoded_lat, cpr_encoded_lon, 17, false,
+ position = CPREncodedPosition.ofAirborne(17, cpr_format, cpr_encoded_lat, cpr_encoded_lon,
timestamp == null ? System.currentTimeMillis() : timestamp);
}
diff --git a/src/main/java/de/serosystems/lib1090/msgs/adsr/SurfacePositionV0Msg.java b/src/main/java/de/serosystems/lib1090/msgs/adsr/SurfacePositionV0Msg.java
index 458157b..b3d0152 100644
--- a/src/main/java/de/serosystems/lib1090/msgs/adsr/SurfacePositionV0Msg.java
+++ b/src/main/java/de/serosystems/lib1090/msgs/adsr/SurfacePositionV0Msg.java
@@ -100,8 +100,8 @@ public SurfacePositionV0Msg(ExtendedSquitter squitter, Long timestamp) throws Ba
int cpr_encoded_lat = (((msg[2]&0x3)<<15) | ((msg[3]&0xFF)<<7) | ((msg[4]>>>1)&0x7F)) & 0x1FFFF;
int cpr_encoded_lon = (((msg[4]&0x1)<<16) | ((msg[5]&0xFF)<<8) | (msg[6]&0xFF)) & 0x1FFFF;
- position = new CPREncodedPosition(
- cpr_format, cpr_encoded_lat, cpr_encoded_lon, 17, true,
+ boolean highGroundSpeed = movement == 0 || movement > 49;
+ position = CPREncodedPosition.ofSurface(17, cpr_format, highGroundSpeed, cpr_encoded_lat, cpr_encoded_lon,
timestamp == null ? System.currentTimeMillis() : timestamp);
}
diff --git a/src/main/java/de/serosystems/lib1090/msgs/tisb/CoarsePositionMsg.java b/src/main/java/de/serosystems/lib1090/msgs/tisb/CoarsePositionMsg.java
index b1bf952..128a529 100644
--- a/src/main/java/de/serosystems/lib1090/msgs/tisb/CoarsePositionMsg.java
+++ b/src/main/java/de/serosystems/lib1090/msgs/tisb/CoarsePositionMsg.java
@@ -100,8 +100,7 @@ public CoarsePositionMsg(ExtendedSquitter squitter, Long timestamp) throws BadFo
short cpr_encoded_lat = (short) (((msg[4]&0xff)<<4) | ((msg[5]&0xff)>>4));
short cpr_encoded_lon = (short) (((msg[5]&0x0f)<<8) | (msg[6]&0xff));
- position = new CPREncodedPosition(
- cpr_format, cpr_encoded_lat, cpr_encoded_lon, 12, false,
+ position = CPREncodedPosition.ofAirborne(12, cpr_format, cpr_encoded_lat, cpr_encoded_lon,
timestamp == null ? System.currentTimeMillis() : timestamp);
}
diff --git a/src/main/java/de/serosystems/lib1090/msgs/tisb/FineAirbornePositionMsg.java b/src/main/java/de/serosystems/lib1090/msgs/tisb/FineAirbornePositionMsg.java
index 06bdb93..187dfec 100644
--- a/src/main/java/de/serosystems/lib1090/msgs/tisb/FineAirbornePositionMsg.java
+++ b/src/main/java/de/serosystems/lib1090/msgs/tisb/FineAirbornePositionMsg.java
@@ -107,8 +107,7 @@ public FineAirbornePositionMsg(ExtendedSquitter squitter, Long timestamp) throws
int cpr_encoded_lat = (((msg[2]&0x3)<<15) | ((msg[3]&0xFF)<<7) | ((msg[4]>>>1)&0x7F)) & 0x1FFFF;
int cpr_encoded_lon = (((msg[4]&0x1)<<16) | ((msg[5]&0xFF)<<8) | (msg[6]&0xFF)) & 0x1FFFF;
- position = new CPREncodedPosition(
- cpr_format, cpr_encoded_lat, cpr_encoded_lon, 17, false,
+ position = CPREncodedPosition.ofAirborne(17, cpr_format, cpr_encoded_lat, cpr_encoded_lon,
timestamp == null ? System.currentTimeMillis() : timestamp);
}
diff --git a/src/main/java/de/serosystems/lib1090/msgs/tisb/FineSurfacePositionMsg.java b/src/main/java/de/serosystems/lib1090/msgs/tisb/FineSurfacePositionMsg.java
index 3f81ca0..f0c40b9 100644
--- a/src/main/java/de/serosystems/lib1090/msgs/tisb/FineSurfacePositionMsg.java
+++ b/src/main/java/de/serosystems/lib1090/msgs/tisb/FineSurfacePositionMsg.java
@@ -104,8 +104,8 @@ public FineSurfacePositionMsg(ExtendedSquitter squitter, Long timestamp) throws
int cpr_encoded_lat = (((msg[2]&0x3)<<15) | ((msg[3]&0xFF)<<7) | ((msg[4]>>>1)&0x7F)) & 0x1FFFF;
int cpr_encoded_lon = (((msg[4]&0x1)<<16) | ((msg[5]&0xFF)<<8) | (msg[6]&0xFF)) & 0x1FFFF;
- position = new CPREncodedPosition(
- cpr_format, cpr_encoded_lat, cpr_encoded_lon, 17, true,
+ boolean highGroundSpeed = movement == 0 || movement > 49;
+ position = CPREncodedPosition.ofSurface(17, cpr_format, highGroundSpeed, cpr_encoded_lat, cpr_encoded_lon,
timestamp == null ? System.currentTimeMillis() : timestamp);
}
diff --git a/src/test/java/de/serosystems/lib1090/GlobalPositionDecodingTest.java b/src/test/java/de/serosystems/lib1090/GlobalPositionDecodingTest.java
index 28df07f..3241d5c 100644
--- a/src/test/java/de/serosystems/lib1090/GlobalPositionDecodingTest.java
+++ b/src/test/java/de/serosystems/lib1090/GlobalPositionDecodingTest.java
@@ -1,7 +1,6 @@
package de.serosystems.lib1090;
import de.serosystems.lib1090.cpr.CPREncodedPosition;
-import de.serosystems.lib1090.cpr.CompactPositionReporting;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@@ -19,12 +18,12 @@ public class GlobalPositionDecodingTest {
* @param ref reference position, may be null if not surface
* @param expect expected decoded position
*/
- private static void test(boolean surface, int latEven, int lonEven, int latOdd, int lonOdd, Position ref, Position expect) {
- CPREncodedPosition cprEven = new CPREncodedPosition(false, latEven, lonEven, 17, surface, null);
- CPREncodedPosition cprOdd = new CPREncodedPosition(true, latOdd, lonOdd, 17, surface, null);
+ private static void testAirborneTiming(boolean surface, int latEven, int lonEven, int latOdd, int lonOdd, Position ref, Position expect) {
+ CPREncodedPosition cprEven = surface ? CPREncodedPosition.ofSurface(17, false, false, latEven, lonEven, 0L) : CPREncodedPosition.ofAirborne(17, false, latEven, lonEven, 0L);
+ CPREncodedPosition cprOdd = surface ? CPREncodedPosition.ofSurface(17, true, false, latOdd, lonOdd, 0L) : CPREncodedPosition.ofAirborne(17, true, latOdd, lonOdd, 0L);
- Position even = CompactPositionReporting.decodeGlobalPosition(cprEven, cprOdd, ref);
- Position odd = CompactPositionReporting.decodeGlobalPosition(cprOdd, cprEven, ref);
+ Position even = cprEven.decodeGlobal(cprOdd, ref);
+ Position odd = cprOdd.decodeGlobal(cprEven, ref);
if (expect == null) {
assertNull(even);
@@ -46,8 +45,8 @@ private static void test(boolean surface, int latEven, int lonEven, int latOdd,
@Test
void testStraddle() {
// whether straddling positions are handled properly
- test(false, 0x0afd9, 0x0, 0x0d79c, 0x00000, null, null);
- test(true, 0x0bf7e, 0x0, 0x15e70, 0x00000, new Position(0., 0., 0.), null);
+ testAirborneTiming(false, 0x0afd9, 0x0, 0x0d79c, 0x00000, null, null);
+ testAirborneTiming(true, 0x0bf7e, 0x0, 0x15e70, 0x00000, new Position(0., 0., 0.), null);
}
@Test
@@ -56,22 +55,22 @@ void testAirborne() {
{
// DXB
Position expected = new Position(55.3657, 25.2532, 0.);
- test(false, 0x06AF1, 0x09C16, 0x04706, 0x04D58, null, expected);
+ testAirborneTiming(false, 0x06AF1, 0x09C16, 0x04706, 0x04D58, null, expected);
}
{
// LAX
Position expected = new Position(-118.4085, 33.9416, 0.);
- test(false, 0x1505A, 0x1C43E, 0x12014, 0x06CA5, null, expected);
+ testAirborneTiming(false, 0x1505A, 0x1C43E, 0x12014, 0x06CA5, null, expected);
}
{
// SYD
Position expected = new Position(151.1753, -33.9399, 0.);
- test(false, 0x0AFCC, 0x1273D, 0x0E011, 0x0503C, null, expected);
+ testAirborneTiming(false, 0x0AFCC, 0x1273D, 0x0E011, 0x0503C, null, expected);
}
{
// SCL
Position expected = new Position(-70.7858, -33.3928, 0.);
- test(false, 0x0DE7B, 0x05658, 0x10DF9, 0x0BB04, null, expected);
+ testAirborneTiming(false, 0x0DE7B, 0x05658, 0x10DF9, 0x0BB04, null, expected);
}
}
@@ -83,56 +82,137 @@ void testSurface() {
// DXB
Position expected = new Position(55.3657, 25.2532, 0.);
Position ref = new Position(30., 80., 0.);
- test(true, 0x1ABC2, 0x07058, 0x11C19, 0x13560, ref, expected);
+ testAirborneTiming(true, 0x1ABC2, 0x07058, 0x11C19, 0x13560, ref, expected);
}
{
// LAX
Position expected = new Position(-118.4085, 33.9416, 0.);
Position ref = new Position(-120., -10., 0.);
- test(true, 0x14166, 0x110F9, 0x0804F, 0x1B296, ref, expected);
+ testAirborneTiming(true, 0x14166, 0x110F9, 0x0804F, 0x1B296, ref, expected);
}
{
// SYD
Position expected = new Position(151.1753, -33.9399, 0.);
Position ref = new Position(120., 10., 0.);
- test(true, 0x0BF2E, 0x09CF4, 0x18043, 0x140EF, ref, expected);
+ testAirborneTiming(true, 0x0BF2E, 0x09CF4, 0x18043, 0x140EF, ref, expected);
}
{
// SCL
Position expected = new Position(-70.7858, -33.3928, 0.);
Position ref = new Position(-110., -10., 0.);
- test(true, 0x179ED, 0x1595F, 0x037E4, 0x0EC11, ref, expected);
+ testAirborneTiming(true, 0x179ED, 0x1595F, 0x037E4, 0x0EC11, ref, expected);
}
{
// Special cases: Equator vs Poles
- test(true, 0x00000, 0x00000, 0x00000, 0x00000, new Position(0.,10.,0.), new Position(0.,0.,0.));
- test(true, 0x00000, 0x00000, 0x00000, 0x00000, new Position(0.,60.,0.), new Position(0.,90.,0.));
- test(true, 0x00000, 0x00000, 0x00000, 0x00000, new Position(0.,-60.,0.), new Position(0.,-90.,0.));
+ testAirborneTiming(true, 0x00000, 0x00000, 0x00000, 0x00000, new Position(0., 10., 0.), new Position(0., 0., 0.));
+ testAirborneTiming(true, 0x00000, 0x00000, 0x00000, 0x00000, new Position(0., 60., 0.), new Position(0., 90., 0.));
+ testAirborneTiming(true, 0x00000, 0x00000, 0x00000, 0x00000, new Position(0., -60., 0.), new Position(0., -90., 0.));
}
}
@Test
void testSanityChecks() {
// Test same format (both even)
- CPREncodedPosition cprEven1 = new CPREncodedPosition(false, 0x06AF1, 0x09C16, 17, false, null);
- CPREncodedPosition cprEven2 = new CPREncodedPosition(false, 0x04706, 0x04D58, 17, false, null);
- assertNull(CompactPositionReporting.decodeGlobalPosition(cprEven1, cprEven2, null));
+ CPREncodedPosition cprEven1 = CPREncodedPosition.ofAirborne(17, false, 0x06AF1, 0x09C16, 0L);
+ CPREncodedPosition cprEven2 = CPREncodedPosition.ofAirborne(17, false, 0x04706, 0x04D58, 0L);
+ assertNull(cprEven1.decodeGlobal(cprEven2, null));
// Test same format (both odd)
- CPREncodedPosition cprOdd1 = new CPREncodedPosition(true, 0x06AF1, 0x09C16, 17, false, null);
- CPREncodedPosition cprOdd2 = new CPREncodedPosition(true, 0x04706, 0x04D58, 17, false, null);
- assertNull(CompactPositionReporting.decodeGlobalPosition(cprOdd1, cprOdd2, null));
+ CPREncodedPosition cprOdd1 = CPREncodedPosition.ofAirborne(17, true, 0x06AF1, 0x09C16, 0L);
+ CPREncodedPosition cprOdd2 = CPREncodedPosition.ofAirborne(17, true, 0x04706, 0x04D58, 0L);
+ assertNull(cprOdd1.decodeGlobal(cprOdd2, null));
// Test mixing airborne and surface positions
- CPREncodedPosition cprAirborne = new CPREncodedPosition(false, 0x06AF1, 0x09C16, 17, false, null);
- CPREncodedPosition cprSurface = new CPREncodedPosition(true, 0x11C19, 0x13560, 17, true, null);
- assertNull(CompactPositionReporting.decodeGlobalPosition(cprAirborne, cprSurface, null));
- assertNull(CompactPositionReporting.decodeGlobalPosition(cprSurface, cprAirborne, null));
+ CPREncodedPosition cprAirborne = CPREncodedPosition.ofAirborne(17, false, 0x06AF1, 0x09C16, 0L);
+ CPREncodedPosition cprSurface = CPREncodedPosition.ofSurface(17, true, false, 0x11C19, 0x13560, 0L);
+ assertNull(cprAirborne.decodeGlobal(cprSurface, null));
+ assertNull(cprSurface.decodeGlobal(cprAirborne, null));
// Test surface position without reference position
- CPREncodedPosition cprSurfaceEven = new CPREncodedPosition(false, 0x1ABC2, 0x07058, 17, true, null);
- CPREncodedPosition cprSurfaceOdd = new CPREncodedPosition(true, 0x11C19, 0x13560, 17, true, null);
- assertNull(CompactPositionReporting.decodeGlobalPosition(cprSurfaceEven, cprSurfaceOdd, null));
+ CPREncodedPosition cprSurfaceEven = CPREncodedPosition.ofSurface(17, false, false, 0x1ABC2, 0x07058, 0L);
+ CPREncodedPosition cprSurfaceOdd = CPREncodedPosition.ofSurface(17, true, false, 0x11C19, 0x13560, 0L);
+ assertNull(cprSurfaceEven.decodeGlobal(cprSurfaceOdd, null));
+ }
+
+ @Test
+ void testAirborneTimingGap() {
+ CPREncodedPosition cprEven = CPREncodedPosition.ofAirborne(17, false, 0x06AF1, 0x09C16, 0L);
+ CPREncodedPosition cprOdd = CPREncodedPosition.ofAirborne(17, true, 0x04706, 0x04D58, 0L);
+
+ assertEquals(10_000L, cprEven.maxGap(cprOdd));
+ assertEquals(10_000L, cprOdd.maxGap(cprEven));
+ }
+
+ private static void testAirborneTiming(long t1, long t2, boolean expectSuccess) {
+ CPREncodedPosition cprEven = CPREncodedPosition.ofAirborne(17, false, 0x06AF1, 0x09C16, t1);
+ CPREncodedPosition cprOdd = CPREncodedPosition.ofAirborne(17, true, 0x04706, 0x04D58, t2);
+
+ Position even = cprEven.decodeGlobal(cprOdd, null);
+ Position odd = cprOdd.decodeGlobal(cprEven, null);
+
+ if (expectSuccess) {
+ assertNotNull(even);
+ assertNotNull(odd);
+ } else {
+ assertNull(even);
+ assertNull(odd);
+ }
+ }
+
+ @Test
+ void testAirborneTimingChecks() {
+ testAirborneTiming(0L, 0L, true);
+ testAirborneTiming(0L, 10_000L, true);
+ testAirborneTiming(0, 10_001L, false);
+ testAirborneTiming(10_000L, 0L, true);
+ testAirborneTiming(10_001L, 0L, false);
+ }
+
+ private static void testSurfaceTimingGap(boolean hs1, boolean hs2, int expect) {
+ CPREncodedPosition cprEven = CPREncodedPosition.ofSurface(17, false, hs1, 0x1ABC2, 0x07058, 0L);
+ CPREncodedPosition cprOdd = CPREncodedPosition.ofSurface(17, true, hs2, 0x11C19, 0x13560, 0L);
+
+ assertEquals(expect, cprEven.maxGap(cprOdd));
+ assertEquals(expect, cprOdd.maxGap(cprEven));
+ }
+
+ @Test
+ void testSurfaceTimingGap() {
+ testSurfaceTimingGap(false, false, 50_000);
+ testSurfaceTimingGap(false, true, 25_000);
+ testSurfaceTimingGap(true, false, 25_000);
+ testSurfaceTimingGap(true, true, 25_000);
+ }
+
+ private static void testSurfaceTiming(long t1, boolean hs1, long t2, boolean hs2, boolean expectSuccess) {
+ Position ref = new Position(30., 80., 0.);
+
+ CPREncodedPosition cprEven = CPREncodedPosition.ofSurface(17, false, hs1, 0x1ABC2, 0x07058, t1);
+ CPREncodedPosition cprOdd = CPREncodedPosition.ofSurface(17, true, hs2, 0x11C19, 0x13560, t2);
+
+ Position even = cprEven.decodeGlobal(cprOdd, ref);
+ Position odd = cprOdd.decodeGlobal(cprEven, ref);
+
+ if (expectSuccess) {
+ assertNotNull(even);
+ assertNotNull(odd);
+ } else {
+ assertNull(even);
+ assertNull(odd);
+ }
+ }
+
+ @Test
+ void testSurfaceTimingChecks() {
+ testSurfaceTiming(0L, false, 0L, false, true);
+ testSurfaceTiming(50_001L, false, 0L, false, false);
+
+ testSurfaceTiming(0L, true, 0L, false, true);
+ testSurfaceTiming(0L, true, 25_000L, false, true);
+ testSurfaceTiming(0L, true, 25_001L, false, false);
+ testSurfaceTiming(0L, false, 25_001L, true, false);
+ testSurfaceTiming(25_0001L, false, 0L, true, false);
+ testSurfaceTiming(25_0001L, true, 0L, false, false);
}
}