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); } }