From fa2c0a321c762f01570e5558a563e0d38a55c876 Mon Sep 17 00:00:00 2001 From: Yi Zhang Date: Sat, 28 May 2022 06:24:05 -0400 Subject: [PATCH 01/10] initial target-correlation commit --- README.md | 7 +- build.gradle | 2 + src/main/java/Program.java | 39 +++++++ .../wtmsb/nxf/handler/HandlerException.java | 12 ++ .../io/wtmsb/nxf/manager/TrackManager.java | 110 ++++++++++++++++++ .../io/wtmsb/nxf/object/ControllingUnit.java | 26 +++-- .../wtmsb/nxf/object/DataBlockSupplement.java | 47 ++++---- .../java/io/wtmsb/nxf/object/FlightStrip.java | 60 +++++----- .../io/wtmsb/nxf/object/IRadarComponent.java | 83 +++++++++++++ .../java/io/wtmsb/nxf/object/RadarTarget.java | 60 ++++++---- src/main/java/io/wtmsb/nxf/object/Track.java | 53 ++++++--- .../io/wtmsb/nxf/utility/ClientMessenger.java | 11 ++ .../io/wtmsb/nxf/utility/GeoCalculator.java | 17 +++ .../io/wtmsb/nxf/utility/ServerMessenger.java | 11 ++ 14 files changed, 430 insertions(+), 108 deletions(-) create mode 100644 src/main/java/Program.java create mode 100644 src/main/java/io/wtmsb/nxf/handler/HandlerException.java create mode 100644 src/main/java/io/wtmsb/nxf/manager/TrackManager.java create mode 100644 src/main/java/io/wtmsb/nxf/object/IRadarComponent.java create mode 100644 src/main/java/io/wtmsb/nxf/utility/ClientMessenger.java create mode 100644 src/main/java/io/wtmsb/nxf/utility/GeoCalculator.java create mode 100644 src/main/java/io/wtmsb/nxf/utility/ServerMessenger.java diff --git a/README.md b/README.md index d3a8404..b0f9b85 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,15 @@ # New York Exchange Format (NXF) +___ [![](https://jitpack.io/v/io.wtmsb/NXF-Java.svg)](https://jitpack.io/#io.wtmsb/NXF-Java) A new mechanism for exchanging (simulated) air traffic radar data using [Protocol Buffers](https://developers.google.com/protocol-buffers). ## How to use ### Java -Note: use `1.0-dev-SNAPSHOT` as the tag in your application +Requires: Java 11 + +Development branch is subject to constant change. * Using jitpack: follow instructions on https://jitpack.io/#io.wtmsb/NXF-Java + * Using Maven Local: download project and run gradle task `publishToMavenLocal` + * Use tag `1.0-dev-SNAPSHOT` in your app. \ No newline at end of file diff --git a/build.gradle b/build.gradle index e441a96..31ec46b 100644 --- a/build.gradle +++ b/build.gradle @@ -56,6 +56,8 @@ sourceSets { dependencies { implementation 'com.github.jitpack:gradle-simple:1.1' implementation 'com.google.protobuf:protobuf-java:3.20.1' + implementation 'org.locationtech.spatial4j:spatial4j:0.8' + implementation 'com.google.guava:guava:31.1-jre' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' } diff --git a/src/main/java/Program.java b/src/main/java/Program.java new file mode 100644 index 0000000..d63d970 --- /dev/null +++ b/src/main/java/Program.java @@ -0,0 +1,39 @@ +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.Multimaps; +import io.wtmsb.nxf.message.radar.NxfRadar; +import io.wtmsb.nxf.object.DataBlockSupplement; +import io.wtmsb.nxf.object.RadarTarget; +import io.wtmsb.nxf.object.Track; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public class Program { + public static void main(String[] args) { + Map forwardMap = Collections.synchronizedMap(new LinkedHashMap<>()); + LinkedListMultimap reverseMap = + Multimaps.invertFrom(Multimaps.forMap(forwardMap), LinkedListMultimap.create()); + + Track t1 = new Track(1); + Track t2 = new Track(2); + assert !t1.equals(t2); + + for (int i = 0; i < 10; i++){ + RadarTarget rt1 = new RadarTarget(i); + RadarTarget rt2 = new RadarTarget(i + 10); + forwardMap.put(rt1, t1); + reverseMap.put(t1, rt1); + forwardMap.put(rt2, t2); + reverseMap.put(t2, rt2); + + List targetsList = reverseMap.get(t1); + while (targetsList.size() > 6) { + forwardMap.remove(targetsList.remove(0)); + } + } + + forwardMap.forEach((key, value) -> System.out.println(key + ": " + value)); + reverseMap.asMap().forEach((key, value) -> System.out.println(key + ": " + value)); + } +} diff --git a/src/main/java/io/wtmsb/nxf/handler/HandlerException.java b/src/main/java/io/wtmsb/nxf/handler/HandlerException.java new file mode 100644 index 0000000..cfa2f7a --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/handler/HandlerException.java @@ -0,0 +1,12 @@ +package io.wtmsb.nxf.handler; + +public class HandlerException extends RuntimeException { + + public HandlerException() { + super(); + } + + public HandlerException(String message) { + super(message); + } +} diff --git a/src/main/java/io/wtmsb/nxf/manager/TrackManager.java b/src/main/java/io/wtmsb/nxf/manager/TrackManager.java new file mode 100644 index 0000000..9a00c5a --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/manager/TrackManager.java @@ -0,0 +1,110 @@ +package io.wtmsb.nxf.manager; + +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimaps; +import com.google.protobuf.ByteString; +import io.wtmsb.nxf.object.FlightStrip; +import io.wtmsb.nxf.object.IRadarComponent; +import io.wtmsb.nxf.object.RadarTarget; +import io.wtmsb.nxf.object.Track; +import io.wtmsb.nxf.utility.GeoCalculator; +import lombok.Synchronized; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class TrackManager { + private final Map targetTrackMap; + private final ListMultimap trackTargetMultiMap; + + private final Map beaconTrackMap; + + private final Map addressTrackMap; + + private final int radarReturnCount; + + + public TrackManager() { + this(6); + } + + public TrackManager(int customRadarReturnCount) { + targetTrackMap = new ConcurrentHashMap<>(); + trackTargetMultiMap = Multimaps.invertFrom(Multimaps.forMap(targetTrackMap), + LinkedListMultimap.create()); + + beaconTrackMap = new ConcurrentHashMap<>(); + addressTrackMap = new ConcurrentHashMap<>(); + radarReturnCount = customRadarReturnCount; + } + + public void addTarget(RadarTarget target) { + if (target.getTransponderMode() == IRadarComponent.TransponderMode.MODE_A) { + autoCorrelateTarget(target); + } else if (target.getTransponderMode() == IRadarComponent.TransponderMode.MODE_C) { + autoCorrelateTarget(target); + } else if (target.getTransponderMode() == IRadarComponent.TransponderMode.MODE_S) { + autoCorrelateTarget(target); + } else { + addPrimaryTarget(target); + } + } + + public boolean autoCorrelateTarget(RadarTarget target) { + + return false; + } + + public boolean autoCorrelateFlightStrip(FlightStrip strip) { + + return false; + } + + @Synchronized + public void addPrimaryTarget(RadarTarget newTarget) { + // find the closest track from the target + Optional _closestTarget = targetTrackMap.keySet().stream() + .min(Comparator.comparingDouble(o -> GeoCalculator.calculateDistance(o, newTarget))); + + // generate a new track by default + Track associatedTrack = new Track(); + + if (_closestTarget.isPresent()) { + RadarTarget closestTarget = _closestTarget.get(); + LocalDateTime closestTargetTime = + LocalDateTime.ofEpochSecond(closestTarget.getReturnTime(), 0, ZoneOffset.UTC); + LocalDateTime newTargetTime = + LocalDateTime.ofEpochSecond(newTarget.getReturnTime(), 0, ZoneOffset.UTC); + + // associate the new target with the track associated with the closest target + if (GeoCalculator.calculateDistance(closestTarget, newTarget) <= 16.0 && + Duration.between(closestTargetTime, newTargetTime).getSeconds() < 45) + { + // get the existing track if track's latest return isn't stale or not within 16 miles of the new target + associatedTrack = targetTrackMap.get(closestTarget); + } + } + + targetTrackMap.put(newTarget, associatedTrack); + trackTargetMultiMap.put(associatedTrack, newTarget); + + List targetsList = trackTargetMultiMap.get(associatedTrack); + while (targetsList.size() > radarReturnCount) { + targetTrackMap.remove(targetsList.remove(0)); + } + } + + @Synchronized + public void pruneStaleTracks() { + + } +} diff --git a/src/main/java/io/wtmsb/nxf/object/ControllingUnit.java b/src/main/java/io/wtmsb/nxf/object/ControllingUnit.java index 9b5b19e..b222e85 100644 --- a/src/main/java/io/wtmsb/nxf/object/ControllingUnit.java +++ b/src/main/java/io/wtmsb/nxf/object/ControllingUnit.java @@ -1,21 +1,23 @@ package io.wtmsb.nxf.object; -import lombok.Builder; -import lombok.Getter; -import lombok.NonNull; -import lombok.Setter; +import io.wtmsb.nxf.message.radar.NxfRadar; +import lombok.*; -@Getter -@Setter -@Builder(toBuilder = true) +@Getter @Setter @NoArgsConstructor +@EqualsAndHashCode public final class ControllingUnit { - @NonNull @Builder.Default - String facility = ""; + @NonNull + String facility = "NoFacility"; - @NonNull @Builder.Default + @NonNull String sector = ""; - public static ControllingUnit getDefault() { - return ControllingUnit.builder().build(); + public ControllingUnit(NxfRadar.Track.ControllingUnit controllingUnitMessage) { + this.setFacility(controllingUnitMessage.getFacility()); + this.setSector(controllingUnitMessage.getSector()); + } + + public boolean isDefault() { + return facility.equals("NoFacility"); } } diff --git a/src/main/java/io/wtmsb/nxf/object/DataBlockSupplement.java b/src/main/java/io/wtmsb/nxf/object/DataBlockSupplement.java index 83aef02..69011d0 100644 --- a/src/main/java/io/wtmsb/nxf/object/DataBlockSupplement.java +++ b/src/main/java/io/wtmsb/nxf/object/DataBlockSupplement.java @@ -1,40 +1,41 @@ package io.wtmsb.nxf.object; -import lombok.Builder; -import lombok.Getter; -import lombok.NonNull; -import lombok.Setter; - -@Getter -@Setter -@Builder(toBuilder = true) -public final class DataBlockSupplement { - public enum LeaderLineDirection { - DEFAULT, - NW, N, NE, W, - HIDE, - E, SW, S, SE - } +import io.wtmsb.nxf.message.radar.NxfRadar; +import lombok.*; + +import static io.wtmsb.nxf.object.IRadarComponent.LeaderLineDirection; +import static io.wtmsb.nxf.object.IRadarComponent.getLeaderLineDirectionOrDefault; - @NonNull @Builder.Default +@Getter @Setter @NoArgsConstructor @EqualsAndHashCode +public final class DataBlockSupplement { + @NonNull LeaderLineDirection leaderLineDirection = LeaderLineDirection.DEFAULT; @NonNull - Integer assignedTemporaryAltitude; + Integer assignedTemporaryAltitude = 0; - @NonNull @Builder.Default + @NonNull String pad1 = ""; - @NonNull @Builder.Default + @NonNull String pad2 = ""; - @NonNull @Builder.Default + @NonNull String runway = ""; - @NonNull @Builder.Default + @NonNull String exitFix = ""; - public static DataBlockSupplement getDefault() { - return DataBlockSupplement.builder().build(); + public DataBlockSupplement(NxfRadar.Track.DataBlockSupplement dsbMsg) { + leaderLineDirection = getLeaderLineDirectionOrDefault(dsbMsg.getLeaderLineDirectionValue()); + assignedTemporaryAltitude = dsbMsg.getAssignedTemporaryAltitude(); + pad1 = dsbMsg.getPad1(); + pad2 = dsbMsg.getPad2(); + runway = dsbMsg.getRunway(); + exitFix = dsbMsg.getExitFix(); + } + + public boolean isDefault() { + return leaderLineDirection == LeaderLineDirection.DEFAULT; } } diff --git a/src/main/java/io/wtmsb/nxf/object/FlightStrip.java b/src/main/java/io/wtmsb/nxf/object/FlightStrip.java index f2df099..a5acf85 100644 --- a/src/main/java/io/wtmsb/nxf/object/FlightStrip.java +++ b/src/main/java/io/wtmsb/nxf/object/FlightStrip.java @@ -1,52 +1,56 @@ package io.wtmsb.nxf.object; +import com.google.protobuf.ByteString; +import io.wtmsb.nxf.message.radar.NxfRadar; import lombok.*; -import java.nio.ByteBuffer; - -@Getter -@Setter -@Builder(toBuilder = true) -public class FlightStrip { +@Getter @Setter @NoArgsConstructor +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +public class FlightStrip implements IRadarComponent { @NonNull - private Integer id; + private Integer id = Integer.MIN_VALUE; - @NonNull @Builder.Default + @NonNull private String aircraftType = ""; - @NonNull @Builder.Default + @NonNull @EqualsAndHashCode.Include private String aircraftCallsign = ""; - @NonNull @Builder.Default - private ByteBuffer aircraftAddress = ByteBuffer.allocate(3); - - public enum WakeCategory { - NO_WEIGHT, CAT_A, CAT_B, CAT_C, CAT_D, CAT_E, CAT_F - } + @NonNull @EqualsAndHashCode.Include + private ByteString aircraftAddress = DEFAULT_AIRCRAFT_ADDRESS; - @NonNull @Builder.Default + @NonNull private WakeCategory wakeCategory = WakeCategory.NO_WEIGHT; - public enum FlightRule { - INSTRUMENT, VISUAL, SPECIAL_VISUAL, DEFENSE_VISUAL - } - - @NonNull @Builder.Default + @NonNull private FlightRule flightRule = FlightRule.INSTRUMENT; - @NonNull @Builder.Default + @NonNull private String destination = ""; @NonNull - private Integer requestedAltitude; + private Integer requestedAltitude = 0; - @NonNull @Builder.Default - private ByteBuffer assignedBeaconCode = ByteBuffer.allocate(2); + @NonNull + private ByteString assignedBeaconCode = DEFAULT_BEACON_CODE; - @NonNull @Builder.Default + @NonNull private String routeString = ""; - public static FlightStrip getDefault() { - return FlightStrip.builder().build(); + public FlightStrip(NxfRadar.FlightStrip fsMsg) { + id = fsMsg.getId(); + aircraftType = fsMsg.getAircraftType(); + aircraftCallsign = fsMsg.getAircraftCallsign(); + aircraftAddress = IRadarComponent.getAircraftAddressOrDefault(fsMsg.getAircraftAddress()); + wakeCategory = IRadarComponent.getWakeCategoryOrDefault(fsMsg.getWakeCategoryValue()); + flightRule = IRadarComponent.getFlightRuleOrDefault(fsMsg.getFlightRuleValue()); + destination = fsMsg.getDestination(); + requestedAltitude = fsMsg.getRequestedAltitude(); + assignedBeaconCode = IRadarComponent.getBeaconCodeOrDefault(fsMsg.getAssignedBeaconCode()); + routeString = fsMsg.getRouteString(); + } + + public boolean isDefault() { + return id == Integer.MIN_VALUE; } } diff --git a/src/main/java/io/wtmsb/nxf/object/IRadarComponent.java b/src/main/java/io/wtmsb/nxf/object/IRadarComponent.java new file mode 100644 index 0000000..eebb5df --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/object/IRadarComponent.java @@ -0,0 +1,83 @@ +package io.wtmsb.nxf.object; + +import com.google.protobuf.ByteString; + +import java.nio.ByteBuffer; + +public interface IRadarComponent { + int BEACON_CODE_SIZE = 2; + ByteString DEFAULT_BEACON_CODE = ByteString.fromHex("0000"); + int AIRCRAFT_ADDRESS_SIZE = 3; + ByteString DEFAULT_AIRCRAFT_ADDRESS = ByteString.fromHex("000000"); + + enum TransponderMode { + NO_MODE, MODE_A, MODE_C, MODE_S + } + + enum FlightRule { + INSTRUMENT, VISUAL, SPECIAL_VISUAL, DEFENSE_VISUAL + } + + enum WakeCategory { + NO_WEIGHT, CAT_A, CAT_B, CAT_C, CAT_D, CAT_E, CAT_F + } + + enum LeaderLineDirection { + DEFAULT, + NW, N, NE, W, + HIDE, + E, SW, S, SE + } + + static ByteString getBeaconCodeOrDefault(ByteString src) { + return isBeaconCodeLegit(src) ? src : DEFAULT_BEACON_CODE; + } + + static ByteString getAircraftAddressOrDefault(ByteString src) { + return isAircraftAddressLegit(src) ? src : DEFAULT_AIRCRAFT_ADDRESS; + } + + static TransponderMode getTransponderModeOrDefault(int ordinal) { + return (0 < ordinal && ordinal < TransponderMode.values().length) ? + TransponderMode.values()[ordinal] : TransponderMode.NO_MODE; + } + + static FlightRule getFlightRuleOrDefault(int ordinal) { + return (0 < ordinal && ordinal < FlightRule.values().length) ? + FlightRule.values()[ordinal] : FlightRule.INSTRUMENT; + } + + static WakeCategory getWakeCategoryOrDefault(int ordinal) { + return (0 < ordinal && ordinal < WakeCategory.values().length) ? + WakeCategory.values()[ordinal] : WakeCategory.NO_WEIGHT; + } + + static LeaderLineDirection getLeaderLineDirectionOrDefault(int ordinal) { + return (0 < ordinal && ordinal < LeaderLineDirection.values().length) ? + LeaderLineDirection.values()[ordinal] : LeaderLineDirection.DEFAULT; + } + + static boolean isBeaconCodeLegit(ByteString octal) { + if (octal.size() != BEACON_CODE_SIZE) + return false; + + int value = fromBeaconCodeArray(octal.toByteArray()); + return 0 <= value && value <= 0xFFF; + } + + static boolean isAircraftAddressLegit(ByteString octal) { + if (octal.size() != AIRCRAFT_ADDRESS_SIZE) + return false; + + int value = fromAircraftAddressArray(octal.toByteArray()); + return 0 <= value && value <= 0xFFFFFF; + } + + static int fromBeaconCodeArray(byte[] bytes) { + return ((bytes[0] & 0xFF) << 8) | (bytes[1] & 0xFF); + } + + static int fromAircraftAddressArray(byte[] bytes) { + return ((bytes[0] & 0xFF) << 16) | ((bytes[1] & 0xFF) << 8) | (bytes[2] & 0xFF); + } +} diff --git a/src/main/java/io/wtmsb/nxf/object/RadarTarget.java b/src/main/java/io/wtmsb/nxf/object/RadarTarget.java index edb26b6..59bba5c 100644 --- a/src/main/java/io/wtmsb/nxf/object/RadarTarget.java +++ b/src/main/java/io/wtmsb/nxf/object/RadarTarget.java @@ -1,34 +1,44 @@ package io.wtmsb.nxf.object; -import lombok.Getter; -import lombok.NonNull; -import lombok.Setter; +import com.google.protobuf.ByteString; +import io.wtmsb.nxf.message.radar.NxfRadar; +import lombok.*; -import java.nio.ByteBuffer; +@Getter @Setter +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +@ToString(onlyExplicitlyIncluded = true) +public final class RadarTarget implements IRadarComponent { + @NonNull + String lat; + @NonNull + String lon; -@Getter -@Setter -public final class RadarTarget { - @NonNull String lat; - @NonNull String lon; + @NonNull @EqualsAndHashCode.Include + TransponderMode transponderMode = TransponderMode.NO_MODE; - enum TransponderMode { - NO_MODE, MODE_A, MODE_C, MODE_S + @NonNull @EqualsAndHashCode.Include + ByteString beaconCode = DEFAULT_BEACON_CODE; + + @NonNull @EqualsAndHashCode.Include @ToString.Include + Long returnTime; + + @NonNull + Integer reportedAltitude; + + @NonNull @EqualsAndHashCode.Include + ByteString modeSAddress = DEFAULT_AIRCRAFT_ADDRESS; + + public RadarTarget(long returnTime) { + this.returnTime = returnTime; } - @NonNull TransponderMode transponderMode; - @NonNull ByteBuffer beaconCode; - @NonNull Long returnTime; - @NonNull Integer reportedAltitude; - @NonNull ByteBuffer modeSAddress; - - public RadarTarget() { - lat = ""; - lon = ""; - transponderMode = TransponderMode.NO_MODE; - beaconCode = ByteBuffer.allocate(2); - returnTime = 0L; - reportedAltitude = 0; - modeSAddress = ByteBuffer.allocate(3); + public RadarTarget(NxfRadar.Track.RadarTarget rtMsg) { + lat = rtMsg.getLat(); + lon = rtMsg.getLon(); + transponderMode = IRadarComponent.getTransponderModeOrDefault(rtMsg.getTransponderModeValue()); + beaconCode = IRadarComponent.getBeaconCodeOrDefault(rtMsg.getBeaconCode()); + returnTime = rtMsg.getReturnTime(); + reportedAltitude = rtMsg.getReportedAltitude(); + modeSAddress = IRadarComponent.getAircraftAddressOrDefault(rtMsg.getModeSAddress()); } } diff --git a/src/main/java/io/wtmsb/nxf/object/Track.java b/src/main/java/io/wtmsb/nxf/object/Track.java index a160523..5a63501 100644 --- a/src/main/java/io/wtmsb/nxf/object/Track.java +++ b/src/main/java/io/wtmsb/nxf/object/Track.java @@ -1,30 +1,45 @@ package io.wtmsb.nxf.object; +import io.wtmsb.nxf.message.radar.NxfRadar; import lombok.*; -import java.util.List; -import java.util.Vector; - -@Getter -@Setter -@Builder(toBuilder = true) +@Getter @Setter @NoArgsConstructor public class Track { @NonNull - private Integer id; - - @NonNull @Builder.Default - private List radarTargets = new Vector<>(); + private Integer id = Integer.MIN_VALUE; - @NonNull @Builder.Default - private FlightStrip flightStrip = FlightStrip.getDefault(); + @NonNull + private FlightStrip flightStrip = new FlightStrip(); - @NonNull @Builder.Default - private ControllingUnit currentController = ControllingUnit.getDefault(); + @NonNull + private ControllingUnit currentController = new ControllingUnit(); - @NonNull @Builder.Default - private ControllingUnit nextController = ControllingUnit.getDefault(); + @NonNull + private ControllingUnit nextController = new ControllingUnit(); - // reserved 6 to 10 - @NonNull @Builder.Default - private DataBlockSupplement dataBlockSupplement = DataBlockSupplement.getDefault(); + @NonNull + private DataBlockSupplement dataBlockSupplement = new DataBlockSupplement(); + + private boolean isPrimary = true; + + public Track(int id) { + this.id = id; + } + + public Track(NxfRadar.Track tMsg) { + id = tMsg.getId(); + //radarTargets = new Vector<>(); + flightStrip = new FlightStrip(tMsg.getFlightStrip()); + currentController = new ControllingUnit(tMsg.getCurrentController()); + nextController = new ControllingUnit(tMsg.getNextController()); + dataBlockSupplement = new DataBlockSupplement(tMsg.getDataBlockSupplement()); + } + + public boolean isPrimaryTrack() { + return isPrimary; + } + + public boolean isUncorrelatedTrack() { + return flightStrip.isDefault(); + } } diff --git a/src/main/java/io/wtmsb/nxf/utility/ClientMessenger.java b/src/main/java/io/wtmsb/nxf/utility/ClientMessenger.java new file mode 100644 index 0000000..fe195df --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/utility/ClientMessenger.java @@ -0,0 +1,11 @@ +package io.wtmsb.nxf.utility; + +import io.wtmsb.nxf.message.radar.NxfRadarClient.*; + +public final class ClientMessenger { + public static ClientRadarEvent createRadarEvent(Object msg, int eventFieldNumber) { + ClientRadarEvent.Builder cb = ClientRadarEvent.newBuilder(); + + return cb.build(); + } +} diff --git a/src/main/java/io/wtmsb/nxf/utility/GeoCalculator.java b/src/main/java/io/wtmsb/nxf/utility/GeoCalculator.java new file mode 100644 index 0000000..aa6465e --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/utility/GeoCalculator.java @@ -0,0 +1,17 @@ +package io.wtmsb.nxf.utility; + +import io.wtmsb.nxf.message.radar.NxfRadar; +import io.wtmsb.nxf.object.RadarTarget; +import org.locationtech.spatial4j.distance.DistanceUtils; + +public final class GeoCalculator { + + public static double calculateDistance(RadarTarget tgt1, RadarTarget tgt2) { + double lat1 = Double.parseDouble(tgt1.getLat()), + lat2 = Double.parseDouble(tgt2.getLat()), + lon1 = Double.parseDouble(tgt1.getLon()), + lon2 = Double.parseDouble(tgt2.getLon()); + + return DistanceUtils.distVincentyRAD(lat1, lon1, lat2, lon2); + } +} diff --git a/src/main/java/io/wtmsb/nxf/utility/ServerMessenger.java b/src/main/java/io/wtmsb/nxf/utility/ServerMessenger.java new file mode 100644 index 0000000..b642052 --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/utility/ServerMessenger.java @@ -0,0 +1,11 @@ +package io.wtmsb.nxf.utility; + +import io.wtmsb.nxf.message.radar.NxfRadarServer.*; + +public final class ServerMessenger { + public static ServerRadarEvent createRadarEvent(Object msg, int eventFieldNumber) { + ServerRadarEvent.Builder sb = ServerRadarEvent.newBuilder(); + + return sb.build(); + } +} From 2ab83cab9c325d76a30f2a589273b1e1e84b799c Mon Sep 17 00:00:00 2001 From: Yi Zhang Date: Sun, 29 May 2022 07:09:52 -0400 Subject: [PATCH 02/10] update --- build.gradle | 3 + src/main/java/Program.java | 25 ++++--- .../io/wtmsb/nxf/domain/ControllingUnit.java | 24 +++++++ .../wtmsb/nxf/domain/DataBlockSupplement.java | 49 +++++++++++++ .../java/io/wtmsb/nxf/domain/FlightStrip.java | 71 +++++++++++++++++++ .../{object => domain}/IRadarComponent.java | 28 ++------ .../java/io/wtmsb/nxf/domain/RadarTarget.java | 52 ++++++++++++++ src/main/java/io/wtmsb/nxf/domain/Track.java | 67 +++++++++++++++++ .../wtmsb/nxf/manager/ControllerManager.java | 37 ++++++++++ .../io/wtmsb/nxf/manager/TrackManager.java | 57 ++++++--------- .../io/wtmsb/nxf/object/ControllingUnit.java | 23 ------ .../wtmsb/nxf/object/DataBlockSupplement.java | 41 ----------- .../java/io/wtmsb/nxf/object/FlightStrip.java | 56 --------------- .../java/io/wtmsb/nxf/object/RadarTarget.java | 44 ------------ src/main/java/io/wtmsb/nxf/object/Track.java | 45 ------------ .../io/wtmsb/nxf/utility/GeoCalculator.java | 9 +-- .../wtmsb/nxf/validator/ByteStringSize.java | 19 +++++ .../validator/ByteStringSizeValidator.java | 19 +++++ 18 files changed, 387 insertions(+), 282 deletions(-) create mode 100644 src/main/java/io/wtmsb/nxf/domain/ControllingUnit.java create mode 100644 src/main/java/io/wtmsb/nxf/domain/DataBlockSupplement.java create mode 100644 src/main/java/io/wtmsb/nxf/domain/FlightStrip.java rename src/main/java/io/wtmsb/nxf/{object => domain}/IRadarComponent.java (70%) create mode 100644 src/main/java/io/wtmsb/nxf/domain/RadarTarget.java create mode 100644 src/main/java/io/wtmsb/nxf/domain/Track.java create mode 100644 src/main/java/io/wtmsb/nxf/manager/ControllerManager.java delete mode 100644 src/main/java/io/wtmsb/nxf/object/ControllingUnit.java delete mode 100644 src/main/java/io/wtmsb/nxf/object/DataBlockSupplement.java delete mode 100644 src/main/java/io/wtmsb/nxf/object/FlightStrip.java delete mode 100644 src/main/java/io/wtmsb/nxf/object/RadarTarget.java delete mode 100644 src/main/java/io/wtmsb/nxf/object/Track.java create mode 100644 src/main/java/io/wtmsb/nxf/validator/ByteStringSize.java create mode 100644 src/main/java/io/wtmsb/nxf/validator/ByteStringSizeValidator.java diff --git a/build.gradle b/build.gradle index 31ec46b..da5f625 100644 --- a/build.gradle +++ b/build.gradle @@ -58,6 +58,9 @@ dependencies { implementation 'com.google.protobuf:protobuf-java:3.20.1' implementation 'org.locationtech.spatial4j:spatial4j:0.8' implementation 'com.google.guava:guava:31.1-jre' + implementation 'org.springframework:spring-core:5.3.20' + implementation 'org.springframework:spring-context:5.3.20' + implementation 'jakarta.validation:jakarta.validation-api:2.0.2' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' } diff --git a/src/main/java/Program.java b/src/main/java/Program.java index d63d970..e6cde1a 100644 --- a/src/main/java/Program.java +++ b/src/main/java/Program.java @@ -1,13 +1,9 @@ import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.Multimaps; -import io.wtmsb.nxf.message.radar.NxfRadar; -import io.wtmsb.nxf.object.DataBlockSupplement; -import io.wtmsb.nxf.object.RadarTarget; -import io.wtmsb.nxf.object.Track; +import io.wtmsb.nxf.domain.RadarTarget; +import io.wtmsb.nxf.domain.Track; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; public class Program { public static void main(String[] args) { @@ -15,8 +11,8 @@ public static void main(String[] args) { LinkedListMultimap reverseMap = Multimaps.invertFrom(Multimaps.forMap(forwardMap), LinkedListMultimap.create()); - Track t1 = new Track(1); - Track t2 = new Track(2); + Track t1 = new Track("N1"); + Track t2 = new Track("N2"); assert !t1.equals(t2); for (int i = 0; i < 10; i++){ @@ -33,7 +29,16 @@ public static void main(String[] args) { } } - forwardMap.forEach((key, value) -> System.out.println(key + ": " + value)); - reverseMap.asMap().forEach((key, value) -> System.out.println(key + ": " + value)); + forwardMap.forEach((key, value) -> + System.out.println(key.getReturnTime().toEpochMilli()/1000 + ": " + value.getFlightStrip().getAircraftCallsign())); + + reverseMap.asMap().forEach( + (key, value) -> { + System.out.print(key.getFlightStrip().getAircraftCallsign() + ": "); + value.forEach(x -> { + System.out.print(x.getReturnTime().toEpochMilli()/1000 + " "); + }); + System.out.println(); + }); } } diff --git a/src/main/java/io/wtmsb/nxf/domain/ControllingUnit.java b/src/main/java/io/wtmsb/nxf/domain/ControllingUnit.java new file mode 100644 index 0000000..7127e2d --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/domain/ControllingUnit.java @@ -0,0 +1,24 @@ +package io.wtmsb.nxf.domain; + +import io.wtmsb.nxf.manager.ControllerManager; +import io.wtmsb.nxf.message.radar.NxfRadar; +import lombok.*; + +import javax.validation.constraints.Size; + +/** + * Immutable, shared object managed by {@link ControllerManager} + */ +@Getter @AllArgsConstructor @EqualsAndHashCode +public final class ControllingUnit { + @NonNull @Size(max = 3) + private final String facility; + + @NonNull @Size(max = 5) + private final String sector; + + public ControllingUnit(NxfRadar.Track.ControllingUnit cuMessage) { + facility = cuMessage.getFacility(); + sector = cuMessage.getSector(); + } +} diff --git a/src/main/java/io/wtmsb/nxf/domain/DataBlockSupplement.java b/src/main/java/io/wtmsb/nxf/domain/DataBlockSupplement.java new file mode 100644 index 0000000..e5a50ca --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/domain/DataBlockSupplement.java @@ -0,0 +1,49 @@ +package io.wtmsb.nxf.domain; + +import io.wtmsb.nxf.message.radar.NxfRadar; +import lombok.*; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Size; + +import static io.wtmsb.nxf.domain.IRadarComponent.LeaderLineDirection; +import static io.wtmsb.nxf.domain.IRadarComponent.getLeaderLineDirectionOrDefault; + +@Getter @Setter @NoArgsConstructor @EqualsAndHashCode +public final class DataBlockSupplement { + private static final DataBlockSupplement DEFAULT = new DataBlockSupplement(); + @NonNull + private LeaderLineDirection leaderLineDirection = LeaderLineDirection.DEFAULT; + + @NonNull @Max(60000) + private Integer assignedTemporaryAltitude = 0; + + @NonNull @Size(max = 5) + private String pad1 = ""; + + @NonNull @Size(max = 5) + private String pad2 = ""; + + @NonNull @Size(max = 3) + private String runway = ""; + + @NonNull @Size(max = 5) + private String exitFix = ""; + + public DataBlockSupplement(NxfRadar.Track.DataBlockSupplement dsbMsg) { + leaderLineDirection = getLeaderLineDirectionOrDefault(dsbMsg.getLeaderLineDirectionValue()); + assignedTemporaryAltitude = dsbMsg.getAssignedTemporaryAltitude(); + pad1 = dsbMsg.getPad1(); + pad2 = dsbMsg.getPad2(); + runway = dsbMsg.getRunway(); + exitFix = dsbMsg.getExitFix(); + } + + public boolean isDefault() { + return this == DEFAULT; + } + + public static DataBlockSupplement getDefault() { + return DEFAULT; + } +} diff --git a/src/main/java/io/wtmsb/nxf/domain/FlightStrip.java b/src/main/java/io/wtmsb/nxf/domain/FlightStrip.java new file mode 100644 index 0000000..77f990d --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/domain/FlightStrip.java @@ -0,0 +1,71 @@ +package io.wtmsb.nxf.domain; + +import com.google.protobuf.ByteString; +import io.wtmsb.nxf.message.radar.NxfRadar; +import io.wtmsb.nxf.validator.ByteStringSize; +import lombok.*; +import org.springframework.util.StringUtils; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Size; +import java.time.Instant; + +/** + * Mutable object managed by {@link io.wtmsb.nxf.manager.TrackManager} + */ +@Getter @Setter +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +public class FlightStrip implements IRadarComponent { + @NonNull @Size(min = 2, max = 10) + @EqualsAndHashCode.Include + private String aircraftCallsign; + + @NonNull @ByteStringSize(AIRCRAFT_ADDRESS_SIZE) + @EqualsAndHashCode.Include + private ByteString aircraftAddress = DEFAULT_AIRCRAFT_ADDRESS; + + @NonNull @Size(max = 10) + private String aircraftType = ""; + + @NonNull + private WakeCategory wakeCategory = WakeCategory.NO_WEIGHT; + + @NonNull + private FlightRule flightRule = FlightRule.INSTRUMENT; + + @NonNull @Size(max = 4) + private String destination = ""; + + @NonNull @Max(100000) + private Integer requestedAltitude = 0; + + @NonNull @ByteStringSize(BEACON_CODE_SIZE) + private ByteString assignedBeaconCode = DEFAULT_BEACON_CODE; + + @NonNull @Size(max = 2000) + private String routeString = ""; + + @NonNull + private Instant lastUpdated = Instant.EPOCH; + + public FlightStrip(@NonNull String _aircraftCallsign) { + aircraftCallsign = _aircraftCallsign; + } + + public FlightStrip(NxfRadar.FlightStrip fsMsg) { + if (!StringUtils.hasText(fsMsg.getAircraftCallsign())) { + throw new IllegalArgumentException("Malformed aircraftCallsign in FlightStrip message"); + } else { + aircraftCallsign = fsMsg.getAircraftCallsign(); + } + + aircraftAddress = fsMsg.getAircraftAddress(); + wakeCategory = IRadarComponent.getWakeCategoryOrDefault(fsMsg.getWakeCategoryValue()); + flightRule = IRadarComponent.getFlightRuleOrDefault(fsMsg.getFlightRuleValue()); + assignedBeaconCode = IRadarComponent.getBeaconCodeOrDefault(fsMsg.getAssignedBeaconCode()); + aircraftType = fsMsg.getAircraftType(); + destination = fsMsg.getDestination(); + requestedAltitude = fsMsg.getRequestedAltitude(); + routeString = StringUtils.trimWhitespace(fsMsg.getRouteString()); + } +} diff --git a/src/main/java/io/wtmsb/nxf/object/IRadarComponent.java b/src/main/java/io/wtmsb/nxf/domain/IRadarComponent.java similarity index 70% rename from src/main/java/io/wtmsb/nxf/object/IRadarComponent.java rename to src/main/java/io/wtmsb/nxf/domain/IRadarComponent.java index eebb5df..ef244f6 100644 --- a/src/main/java/io/wtmsb/nxf/object/IRadarComponent.java +++ b/src/main/java/io/wtmsb/nxf/domain/IRadarComponent.java @@ -1,8 +1,7 @@ -package io.wtmsb.nxf.object; +package io.wtmsb.nxf.domain; import com.google.protobuf.ByteString; - -import java.nio.ByteBuffer; +import io.wtmsb.nxf.validator.ByteStringSize; public interface IRadarComponent { int BEACON_CODE_SIZE = 2; @@ -30,11 +29,7 @@ enum LeaderLineDirection { } static ByteString getBeaconCodeOrDefault(ByteString src) { - return isBeaconCodeLegit(src) ? src : DEFAULT_BEACON_CODE; - } - - static ByteString getAircraftAddressOrDefault(ByteString src) { - return isAircraftAddressLegit(src) ? src : DEFAULT_AIRCRAFT_ADDRESS; + return checkBeaconCodeValue(src) ? src : DEFAULT_BEACON_CODE; } static TransponderMode getTransponderModeOrDefault(int ordinal) { @@ -57,27 +52,16 @@ static LeaderLineDirection getLeaderLineDirectionOrDefault(int ordinal) { LeaderLineDirection.values()[ordinal] : LeaderLineDirection.DEFAULT; } - static boolean isBeaconCodeLegit(ByteString octal) { - if (octal.size() != BEACON_CODE_SIZE) - return false; - + private static boolean checkBeaconCodeValue(ByteString octal) { int value = fromBeaconCodeArray(octal.toByteArray()); return 0 <= value && value <= 0xFFF; } - static boolean isAircraftAddressLegit(ByteString octal) { - if (octal.size() != AIRCRAFT_ADDRESS_SIZE) - return false; - - int value = fromAircraftAddressArray(octal.toByteArray()); - return 0 <= value && value <= 0xFFFFFF; - } - - static int fromBeaconCodeArray(byte[] bytes) { + private static int fromBeaconCodeArray(byte[] bytes) { return ((bytes[0] & 0xFF) << 8) | (bytes[1] & 0xFF); } - static int fromAircraftAddressArray(byte[] bytes) { + private static int fromAircraftAddressArray(byte[] bytes) { return ((bytes[0] & 0xFF) << 16) | ((bytes[1] & 0xFF) << 8) | (bytes[2] & 0xFF); } } diff --git a/src/main/java/io/wtmsb/nxf/domain/RadarTarget.java b/src/main/java/io/wtmsb/nxf/domain/RadarTarget.java new file mode 100644 index 0000000..c94f6e9 --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/domain/RadarTarget.java @@ -0,0 +1,52 @@ +package io.wtmsb.nxf.domain; + +import com.google.protobuf.ByteString; +import io.wtmsb.nxf.message.radar.NxfRadar; +import io.wtmsb.nxf.validator.ByteStringSize; +import lombok.*; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import java.time.Instant; + +@Getter @Setter +@EqualsAndHashCode @ToString +public final class RadarTarget implements IRadarComponent { + @NonNull @Min(-90) @Max(90) + private Double lat; + @NonNull @Min(-180) @Max(180) + private Double lon; + + @NonNull + private TransponderMode transponderMode = TransponderMode.NO_MODE; + + @NonNull @ByteStringSize(BEACON_CODE_SIZE) + private ByteString beaconCode = DEFAULT_BEACON_CODE; + + @NonNull + private Instant returnTime = Instant.EPOCH; + + @NonNull @Min(0) @Max(100000) + private Integer reportedAltitude; + + @NonNull + private ByteString modeSAddress = DEFAULT_AIRCRAFT_ADDRESS; + + public RadarTarget(long customReturnTime) { + returnTime = Instant.ofEpochSecond(customReturnTime); + } + + public RadarTarget(NxfRadar.Track.RadarTarget rtMsg) throws RuntimeException { + lat = Double.parseDouble(rtMsg.getLat()); + lon = Double.parseDouble(rtMsg.getLon()); + transponderMode = IRadarComponent.getTransponderModeOrDefault(rtMsg.getTransponderModeValue()); + beaconCode = IRadarComponent.getBeaconCodeOrDefault(rtMsg.getBeaconCode()); + returnTime = Instant.ofEpochSecond(rtMsg.getReturnTime()); + reportedAltitude = rtMsg.getReportedAltitude(); + modeSAddress = rtMsg.getModeSAddress(); + } + + public boolean isDefault() { + return returnTime == Instant.EPOCH; + } +} diff --git a/src/main/java/io/wtmsb/nxf/domain/Track.java b/src/main/java/io/wtmsb/nxf/domain/Track.java new file mode 100644 index 0000000..c408311 --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/domain/Track.java @@ -0,0 +1,67 @@ +package io.wtmsb.nxf.domain; + +import io.wtmsb.nxf.manager.ControllerManager; +import io.wtmsb.nxf.message.radar.NxfRadar; +import lombok.*; + +@Getter @Setter +public class Track { + @NonNull + private FlightStrip flightStrip; + + @NonNull + private ControllingUnit currentController; + + @NonNull + private ControllingUnit nextController; + + @NonNull + private DataBlockSupplement dataBlockSupplement; + + @Getter(value = AccessLevel.NONE) @Setter(value = AccessLevel.NONE) + private boolean isPrimary = true; + + @Getter(value = AccessLevel.NONE) @Setter(value = AccessLevel.NONE) + private boolean isCorrelated = false; + + public Track(String aircraftCallsign) { + flightStrip = new FlightStrip(aircraftCallsign); + dataBlockSupplement = new DataBlockSupplement(); + currentController = nextController = ControllerManager.getDefaultControllingUnit(); + } + + public Track(NxfRadar.Track tMsg) { + flightStrip = new FlightStrip(tMsg.getFlightStrip()); + dataBlockSupplement = new DataBlockSupplement(tMsg.getDataBlockSupplement()); + currentController = ControllerManager.getControllingUnit(tMsg.getCurrentController()); + nextController = ControllerManager.getControllingUnit(tMsg.getNextController()); + } + + public boolean isPrimaryTrack() { + return isPrimary; + } + + public boolean isUncorrelatedTrack() { + return isCorrelated; + } + + public void correlateFlightStrip(FlightStrip fs) { + flightStrip = fs; + isCorrelated = true; + isPrimary = false; + } + + public void detachFlightStrip() { + flightStrip.setAircraftCallsign("WHO"); + isCorrelated = false; + } + + public void setPrimary() { + isCorrelated = false; + isPrimary = true; + } + + public boolean isUncontrolledTrack() { + return currentController == ControllerManager.getDefaultControllingUnit(); + } +} diff --git a/src/main/java/io/wtmsb/nxf/manager/ControllerManager.java b/src/main/java/io/wtmsb/nxf/manager/ControllerManager.java new file mode 100644 index 0000000..6286ff8 --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/manager/ControllerManager.java @@ -0,0 +1,37 @@ +package io.wtmsb.nxf.manager; + +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.Table; +import io.wtmsb.nxf.domain.ControllingUnit; +import io.wtmsb.nxf.message.radar.NxfRadar; +import lombok.NonNull; +import lombok.Synchronized; +import org.springframework.stereotype.Component; + +@Component +public final class ControllerManager { + private static final ControllingUnit defaultControllingUnit = new ControllingUnit("", ""); + private static final Table cache = HashBasedTable.create(); + + static { + cache.put("", "", defaultControllingUnit); + } + + @Synchronized + public static ControllingUnit getControllingUnit(String facility, String sector) { + if (!cache.contains(facility, sector)) { + cache.put(facility, sector, new ControllingUnit(facility, sector)); + } + + return cache.get(facility, sector); + } + + public static ControllingUnit getControllingUnit(NxfRadar.Track.@NonNull ControllingUnit nxfCu) { + String facility = nxfCu.getFacility(), sector = nxfCu.getSector(); + return getControllingUnit(facility, sector); + } + + public static ControllingUnit getDefaultControllingUnit() { + return defaultControllingUnit; + } +} diff --git a/src/main/java/io/wtmsb/nxf/manager/TrackManager.java b/src/main/java/io/wtmsb/nxf/manager/TrackManager.java index 9a00c5a..0c50355 100644 --- a/src/main/java/io/wtmsb/nxf/manager/TrackManager.java +++ b/src/main/java/io/wtmsb/nxf/manager/TrackManager.java @@ -2,49 +2,38 @@ import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; -import com.google.common.collect.Multimaps; -import com.google.protobuf.ByteString; -import io.wtmsb.nxf.object.FlightStrip; -import io.wtmsb.nxf.object.IRadarComponent; -import io.wtmsb.nxf.object.RadarTarget; -import io.wtmsb.nxf.object.Track; +import io.wtmsb.nxf.domain.FlightStrip; +import io.wtmsb.nxf.domain.IRadarComponent; +import io.wtmsb.nxf.domain.RadarTarget; +import io.wtmsb.nxf.domain.Track; import io.wtmsb.nxf.utility.GeoCalculator; import lombok.Synchronized; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; import java.time.Duration; -import java.time.LocalDateTime; -import java.time.ZoneOffset; import java.util.*; -import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; +@Service public class TrackManager { + private ControllerManager controllerManager; private final Map targetTrackMap; private final ListMultimap trackTargetMultiMap; - private final Map beaconTrackMap; + private final int radarReturnCount = 6; - private final Map addressTrackMap; + private final int updateFrequency = 3; - private final int radarReturnCount; - - - public TrackManager() { - this(6); + @Autowired + public TrackManager(ControllerManager controllerManager) { + this(); + this.controllerManager = controllerManager; } - public TrackManager(int customRadarReturnCount) { + public TrackManager() { targetTrackMap = new ConcurrentHashMap<>(); - trackTargetMultiMap = Multimaps.invertFrom(Multimaps.forMap(targetTrackMap), - LinkedListMultimap.create()); - - beaconTrackMap = new ConcurrentHashMap<>(); - addressTrackMap = new ConcurrentHashMap<>(); - radarReturnCount = customRadarReturnCount; + trackTargetMultiMap = LinkedListMultimap.create(); } public void addTarget(RadarTarget target) { @@ -76,18 +65,13 @@ public void addPrimaryTarget(RadarTarget newTarget) { .min(Comparator.comparingDouble(o -> GeoCalculator.calculateDistance(o, newTarget))); // generate a new track by default - Track associatedTrack = new Track(); + Track associatedTrack = new Track("PRI"); if (_closestTarget.isPresent()) { RadarTarget closestTarget = _closestTarget.get(); - LocalDateTime closestTargetTime = - LocalDateTime.ofEpochSecond(closestTarget.getReturnTime(), 0, ZoneOffset.UTC); - LocalDateTime newTargetTime = - LocalDateTime.ofEpochSecond(newTarget.getReturnTime(), 0, ZoneOffset.UTC); - // associate the new target with the track associated with the closest target if (GeoCalculator.calculateDistance(closestTarget, newTarget) <= 16.0 && - Duration.between(closestTargetTime, newTargetTime).getSeconds() < 45) + Duration.between(closestTarget.getReturnTime(), newTarget.getReturnTime()).getSeconds() < 45) { // get the existing track if track's latest return isn't stale or not within 16 miles of the new target associatedTrack = targetTrackMap.get(closestTarget); @@ -103,8 +87,11 @@ public void addPrimaryTarget(RadarTarget newTarget) { } } + /** + * + */ @Synchronized public void pruneStaleTracks() { - + //trackTargetMultiMap } } diff --git a/src/main/java/io/wtmsb/nxf/object/ControllingUnit.java b/src/main/java/io/wtmsb/nxf/object/ControllingUnit.java deleted file mode 100644 index b222e85..0000000 --- a/src/main/java/io/wtmsb/nxf/object/ControllingUnit.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.wtmsb.nxf.object; - -import io.wtmsb.nxf.message.radar.NxfRadar; -import lombok.*; - -@Getter @Setter @NoArgsConstructor -@EqualsAndHashCode -public final class ControllingUnit { - @NonNull - String facility = "NoFacility"; - - @NonNull - String sector = ""; - - public ControllingUnit(NxfRadar.Track.ControllingUnit controllingUnitMessage) { - this.setFacility(controllingUnitMessage.getFacility()); - this.setSector(controllingUnitMessage.getSector()); - } - - public boolean isDefault() { - return facility.equals("NoFacility"); - } -} diff --git a/src/main/java/io/wtmsb/nxf/object/DataBlockSupplement.java b/src/main/java/io/wtmsb/nxf/object/DataBlockSupplement.java deleted file mode 100644 index 69011d0..0000000 --- a/src/main/java/io/wtmsb/nxf/object/DataBlockSupplement.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.wtmsb.nxf.object; - -import io.wtmsb.nxf.message.radar.NxfRadar; -import lombok.*; - -import static io.wtmsb.nxf.object.IRadarComponent.LeaderLineDirection; -import static io.wtmsb.nxf.object.IRadarComponent.getLeaderLineDirectionOrDefault; - -@Getter @Setter @NoArgsConstructor @EqualsAndHashCode -public final class DataBlockSupplement { - @NonNull - LeaderLineDirection leaderLineDirection = LeaderLineDirection.DEFAULT; - - @NonNull - Integer assignedTemporaryAltitude = 0; - - @NonNull - String pad1 = ""; - - @NonNull - String pad2 = ""; - - @NonNull - String runway = ""; - - @NonNull - String exitFix = ""; - - public DataBlockSupplement(NxfRadar.Track.DataBlockSupplement dsbMsg) { - leaderLineDirection = getLeaderLineDirectionOrDefault(dsbMsg.getLeaderLineDirectionValue()); - assignedTemporaryAltitude = dsbMsg.getAssignedTemporaryAltitude(); - pad1 = dsbMsg.getPad1(); - pad2 = dsbMsg.getPad2(); - runway = dsbMsg.getRunway(); - exitFix = dsbMsg.getExitFix(); - } - - public boolean isDefault() { - return leaderLineDirection == LeaderLineDirection.DEFAULT; - } -} diff --git a/src/main/java/io/wtmsb/nxf/object/FlightStrip.java b/src/main/java/io/wtmsb/nxf/object/FlightStrip.java deleted file mode 100644 index a5acf85..0000000 --- a/src/main/java/io/wtmsb/nxf/object/FlightStrip.java +++ /dev/null @@ -1,56 +0,0 @@ -package io.wtmsb.nxf.object; - -import com.google.protobuf.ByteString; -import io.wtmsb.nxf.message.radar.NxfRadar; -import lombok.*; - -@Getter @Setter @NoArgsConstructor -@EqualsAndHashCode(onlyExplicitlyIncluded = true) -public class FlightStrip implements IRadarComponent { - @NonNull - private Integer id = Integer.MIN_VALUE; - - @NonNull - private String aircraftType = ""; - - @NonNull @EqualsAndHashCode.Include - private String aircraftCallsign = ""; - - @NonNull @EqualsAndHashCode.Include - private ByteString aircraftAddress = DEFAULT_AIRCRAFT_ADDRESS; - - @NonNull - private WakeCategory wakeCategory = WakeCategory.NO_WEIGHT; - - @NonNull - private FlightRule flightRule = FlightRule.INSTRUMENT; - - @NonNull - private String destination = ""; - - @NonNull - private Integer requestedAltitude = 0; - - @NonNull - private ByteString assignedBeaconCode = DEFAULT_BEACON_CODE; - - @NonNull - private String routeString = ""; - - public FlightStrip(NxfRadar.FlightStrip fsMsg) { - id = fsMsg.getId(); - aircraftType = fsMsg.getAircraftType(); - aircraftCallsign = fsMsg.getAircraftCallsign(); - aircraftAddress = IRadarComponent.getAircraftAddressOrDefault(fsMsg.getAircraftAddress()); - wakeCategory = IRadarComponent.getWakeCategoryOrDefault(fsMsg.getWakeCategoryValue()); - flightRule = IRadarComponent.getFlightRuleOrDefault(fsMsg.getFlightRuleValue()); - destination = fsMsg.getDestination(); - requestedAltitude = fsMsg.getRequestedAltitude(); - assignedBeaconCode = IRadarComponent.getBeaconCodeOrDefault(fsMsg.getAssignedBeaconCode()); - routeString = fsMsg.getRouteString(); - } - - public boolean isDefault() { - return id == Integer.MIN_VALUE; - } -} diff --git a/src/main/java/io/wtmsb/nxf/object/RadarTarget.java b/src/main/java/io/wtmsb/nxf/object/RadarTarget.java deleted file mode 100644 index 59bba5c..0000000 --- a/src/main/java/io/wtmsb/nxf/object/RadarTarget.java +++ /dev/null @@ -1,44 +0,0 @@ -package io.wtmsb.nxf.object; - -import com.google.protobuf.ByteString; -import io.wtmsb.nxf.message.radar.NxfRadar; -import lombok.*; - -@Getter @Setter -@EqualsAndHashCode(onlyExplicitlyIncluded = true) -@ToString(onlyExplicitlyIncluded = true) -public final class RadarTarget implements IRadarComponent { - @NonNull - String lat; - @NonNull - String lon; - - @NonNull @EqualsAndHashCode.Include - TransponderMode transponderMode = TransponderMode.NO_MODE; - - @NonNull @EqualsAndHashCode.Include - ByteString beaconCode = DEFAULT_BEACON_CODE; - - @NonNull @EqualsAndHashCode.Include @ToString.Include - Long returnTime; - - @NonNull - Integer reportedAltitude; - - @NonNull @EqualsAndHashCode.Include - ByteString modeSAddress = DEFAULT_AIRCRAFT_ADDRESS; - - public RadarTarget(long returnTime) { - this.returnTime = returnTime; - } - - public RadarTarget(NxfRadar.Track.RadarTarget rtMsg) { - lat = rtMsg.getLat(); - lon = rtMsg.getLon(); - transponderMode = IRadarComponent.getTransponderModeOrDefault(rtMsg.getTransponderModeValue()); - beaconCode = IRadarComponent.getBeaconCodeOrDefault(rtMsg.getBeaconCode()); - returnTime = rtMsg.getReturnTime(); - reportedAltitude = rtMsg.getReportedAltitude(); - modeSAddress = IRadarComponent.getAircraftAddressOrDefault(rtMsg.getModeSAddress()); - } -} diff --git a/src/main/java/io/wtmsb/nxf/object/Track.java b/src/main/java/io/wtmsb/nxf/object/Track.java deleted file mode 100644 index 5a63501..0000000 --- a/src/main/java/io/wtmsb/nxf/object/Track.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.wtmsb.nxf.object; - -import io.wtmsb.nxf.message.radar.NxfRadar; -import lombok.*; - -@Getter @Setter @NoArgsConstructor -public class Track { - @NonNull - private Integer id = Integer.MIN_VALUE; - - @NonNull - private FlightStrip flightStrip = new FlightStrip(); - - @NonNull - private ControllingUnit currentController = new ControllingUnit(); - - @NonNull - private ControllingUnit nextController = new ControllingUnit(); - - @NonNull - private DataBlockSupplement dataBlockSupplement = new DataBlockSupplement(); - - private boolean isPrimary = true; - - public Track(int id) { - this.id = id; - } - - public Track(NxfRadar.Track tMsg) { - id = tMsg.getId(); - //radarTargets = new Vector<>(); - flightStrip = new FlightStrip(tMsg.getFlightStrip()); - currentController = new ControllingUnit(tMsg.getCurrentController()); - nextController = new ControllingUnit(tMsg.getNextController()); - dataBlockSupplement = new DataBlockSupplement(tMsg.getDataBlockSupplement()); - } - - public boolean isPrimaryTrack() { - return isPrimary; - } - - public boolean isUncorrelatedTrack() { - return flightStrip.isDefault(); - } -} diff --git a/src/main/java/io/wtmsb/nxf/utility/GeoCalculator.java b/src/main/java/io/wtmsb/nxf/utility/GeoCalculator.java index aa6465e..194e821 100644 --- a/src/main/java/io/wtmsb/nxf/utility/GeoCalculator.java +++ b/src/main/java/io/wtmsb/nxf/utility/GeoCalculator.java @@ -1,16 +1,13 @@ package io.wtmsb.nxf.utility; -import io.wtmsb.nxf.message.radar.NxfRadar; -import io.wtmsb.nxf.object.RadarTarget; +import io.wtmsb.nxf.domain.RadarTarget; import org.locationtech.spatial4j.distance.DistanceUtils; public final class GeoCalculator { public static double calculateDistance(RadarTarget tgt1, RadarTarget tgt2) { - double lat1 = Double.parseDouble(tgt1.getLat()), - lat2 = Double.parseDouble(tgt2.getLat()), - lon1 = Double.parseDouble(tgt1.getLon()), - lon2 = Double.parseDouble(tgt2.getLon()); + double lat1 = tgt1.getLat(), lat2 = tgt2.getLat(), + lon1 = tgt1.getLon(), lon2 = tgt2.getLon(); return DistanceUtils.distVincentyRAD(lat1, lon1, lat2, lon2); } diff --git a/src/main/java/io/wtmsb/nxf/validator/ByteStringSize.java b/src/main/java/io/wtmsb/nxf/validator/ByteStringSize.java new file mode 100644 index 0000000..0b1475f --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/validator/ByteStringSize.java @@ -0,0 +1,19 @@ +package io.wtmsb.nxf.validator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; + +@Target({METHOD, FIELD, ANNOTATION_TYPE, PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = ByteStringSizeValidator.class) +public @interface ByteStringSize { + String message() default "{io.wtmsb.nxf.constraint.ByteStringSize.message}"; + Class[] groups() default {}; + Class[] payload() default {}; + int value(); +} diff --git a/src/main/java/io/wtmsb/nxf/validator/ByteStringSizeValidator.java b/src/main/java/io/wtmsb/nxf/validator/ByteStringSizeValidator.java new file mode 100644 index 0000000..7502202 --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/validator/ByteStringSizeValidator.java @@ -0,0 +1,19 @@ +package io.wtmsb.nxf.validator; + +import com.google.protobuf.ByteString; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class ByteStringSizeValidator implements ConstraintValidator { + private int mandatorySize; + + public void initialize(ByteStringSize byteStringSize) { + mandatorySize = byteStringSize.value(); + } + + @Override + public boolean isValid(ByteString bs, ConstraintValidatorContext constraintValidatorContext) { + return bs.size() == mandatorySize; + } +} From 9b0c7ad9d4a48664041f55396cbaac756c80685a Mon Sep 17 00:00:00 2001 From: Yi Zhang Date: Tue, 31 May 2022 02:28:42 -0400 Subject: [PATCH 03/10] update --- src/main/java/Program.java | 4 +- .../io/wtmsb/nxf/domain/ControllingUnit.java | 2 +- .../wtmsb/nxf/domain/DataBlockSupplement.java | 49 ------ .../java/io/wtmsb/nxf/domain/FlightData.java | 100 +++++++++++++ .../nxf/domain/FlightDataSupplement.java | 57 +++++++ .../java/io/wtmsb/nxf/domain/FlightStrip.java | 71 --------- .../io/wtmsb/nxf/domain/IRadarComponent.java | 31 +--- .../java/io/wtmsb/nxf/domain/RadarTarget.java | 86 +++++++---- src/main/java/io/wtmsb/nxf/domain/Track.java | 86 +++++------ .../wtmsb/nxf/manager/ControllerManager.java | 11 +- .../io/wtmsb/nxf/manager/TrackManager.java | 58 ++++--- .../io/wtmsb/nxf/utility/ClientMessenger.java | 1 + .../io/wtmsb/nxf/utility/GeoCalculator.java | 1 + .../io/wtmsb/nxf/utility/ServerMessenger.java | 1 + .../BeaconCode.java} | 9 +- .../nxf/validation/BeaconCodeValidator.java | 29 ++++ .../io/wtmsb/nxf/validation/IcaoAddress.java | 18 +++ .../nxf/validation/IcaoAddressValidator.java | 17 +++ .../validator/ByteStringSizeValidator.java | 19 --- src/main/proto/radar/nxf_radar.proto | 141 ++++++++---------- src/main/proto/radar/nxf_radar_client.proto | 29 +--- src/main/proto/radar/nxf_radar_server.proto | 46 +++--- 22 files changed, 460 insertions(+), 406 deletions(-) delete mode 100644 src/main/java/io/wtmsb/nxf/domain/DataBlockSupplement.java create mode 100644 src/main/java/io/wtmsb/nxf/domain/FlightData.java create mode 100644 src/main/java/io/wtmsb/nxf/domain/FlightDataSupplement.java delete mode 100644 src/main/java/io/wtmsb/nxf/domain/FlightStrip.java rename src/main/java/io/wtmsb/nxf/{validator/ByteStringSize.java => validation/BeaconCode.java} (65%) create mode 100644 src/main/java/io/wtmsb/nxf/validation/BeaconCodeValidator.java create mode 100644 src/main/java/io/wtmsb/nxf/validation/IcaoAddress.java create mode 100644 src/main/java/io/wtmsb/nxf/validation/IcaoAddressValidator.java delete mode 100644 src/main/java/io/wtmsb/nxf/validator/ByteStringSizeValidator.java diff --git a/src/main/java/Program.java b/src/main/java/Program.java index e6cde1a..af31b2c 100644 --- a/src/main/java/Program.java +++ b/src/main/java/Program.java @@ -30,11 +30,11 @@ public static void main(String[] args) { } forwardMap.forEach((key, value) -> - System.out.println(key.getReturnTime().toEpochMilli()/1000 + ": " + value.getFlightStrip().getAircraftCallsign())); + System.out.println(key.getReturnTime().toEpochMilli()/1000 + ": " + value.getFlightData().getCallsign())); reverseMap.asMap().forEach( (key, value) -> { - System.out.print(key.getFlightStrip().getAircraftCallsign() + ": "); + System.out.print(key.getFlightData().getCallsign() + ": "); value.forEach(x -> { System.out.print(x.getReturnTime().toEpochMilli()/1000 + " "); }); diff --git a/src/main/java/io/wtmsb/nxf/domain/ControllingUnit.java b/src/main/java/io/wtmsb/nxf/domain/ControllingUnit.java index 7127e2d..87f1b3a 100644 --- a/src/main/java/io/wtmsb/nxf/domain/ControllingUnit.java +++ b/src/main/java/io/wtmsb/nxf/domain/ControllingUnit.java @@ -17,7 +17,7 @@ public final class ControllingUnit { @NonNull @Size(max = 5) private final String sector; - public ControllingUnit(NxfRadar.Track.ControllingUnit cuMessage) { + public ControllingUnit(NxfRadar.ControllingUnit cuMessage) { facility = cuMessage.getFacility(); sector = cuMessage.getSector(); } diff --git a/src/main/java/io/wtmsb/nxf/domain/DataBlockSupplement.java b/src/main/java/io/wtmsb/nxf/domain/DataBlockSupplement.java deleted file mode 100644 index e5a50ca..0000000 --- a/src/main/java/io/wtmsb/nxf/domain/DataBlockSupplement.java +++ /dev/null @@ -1,49 +0,0 @@ -package io.wtmsb.nxf.domain; - -import io.wtmsb.nxf.message.radar.NxfRadar; -import lombok.*; - -import javax.validation.constraints.Max; -import javax.validation.constraints.Size; - -import static io.wtmsb.nxf.domain.IRadarComponent.LeaderLineDirection; -import static io.wtmsb.nxf.domain.IRadarComponent.getLeaderLineDirectionOrDefault; - -@Getter @Setter @NoArgsConstructor @EqualsAndHashCode -public final class DataBlockSupplement { - private static final DataBlockSupplement DEFAULT = new DataBlockSupplement(); - @NonNull - private LeaderLineDirection leaderLineDirection = LeaderLineDirection.DEFAULT; - - @NonNull @Max(60000) - private Integer assignedTemporaryAltitude = 0; - - @NonNull @Size(max = 5) - private String pad1 = ""; - - @NonNull @Size(max = 5) - private String pad2 = ""; - - @NonNull @Size(max = 3) - private String runway = ""; - - @NonNull @Size(max = 5) - private String exitFix = ""; - - public DataBlockSupplement(NxfRadar.Track.DataBlockSupplement dsbMsg) { - leaderLineDirection = getLeaderLineDirectionOrDefault(dsbMsg.getLeaderLineDirectionValue()); - assignedTemporaryAltitude = dsbMsg.getAssignedTemporaryAltitude(); - pad1 = dsbMsg.getPad1(); - pad2 = dsbMsg.getPad2(); - runway = dsbMsg.getRunway(); - exitFix = dsbMsg.getExitFix(); - } - - public boolean isDefault() { - return this == DEFAULT; - } - - public static DataBlockSupplement getDefault() { - return DEFAULT; - } -} diff --git a/src/main/java/io/wtmsb/nxf/domain/FlightData.java b/src/main/java/io/wtmsb/nxf/domain/FlightData.java new file mode 100644 index 0000000..5bfcb3d --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/domain/FlightData.java @@ -0,0 +1,100 @@ +package io.wtmsb.nxf.domain; + +import com.google.protobuf.ByteString; +import io.wtmsb.nxf.manager.ControllerManager; +import io.wtmsb.nxf.message.radar.NxfRadar; +import io.wtmsb.nxf.validation.BeaconCode; +import io.wtmsb.nxf.validation.IcaoAddress; +import lombok.*; +import org.springframework.util.StringUtils; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Size; +import java.time.Instant; + +/** + * Mutable object managed by {@link io.wtmsb.nxf.manager.TrackManager} + */ +@Getter @Setter +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +public class FlightData implements IRadarComponent { + @NonNull @Size(min = 2, max = 10) + @EqualsAndHashCode.Include + private String callsign; + + @EqualsAndHashCode.Include + private boolean hasIcaoAddress = false; + + @IcaoAddress + @EqualsAndHashCode.Include + private ByteString icaoAddress = DEFAULT_ICAO_ADDRESS; + + @EqualsAndHashCode.Include + private boolean hasAssignedBeaconCode = false; + + @BeaconCode + @EqualsAndHashCode.Include + private ByteString assignedBeaconCode = DEFAULT_BEACON_CODE; + + @NonNull + @EqualsAndHashCode.Include + private Instant lastUpdated = Instant.now(); + + @NonNull @Size(max = 4) + private String aircraftType = ""; + + @NonNull @Size(max = 1) + private String equipmentSuffix = ""; + + @NonNull + private FlightRule flightRule = FlightRule.INSTRUMENT; + + @NonNull @Size(max = 4) + private String departurePoint = ""; + + @NonNull @Size(max = 4) + private String destination = ""; + + @NonNull @Max(MAX_ALTITUDE) + private Integer requestedAltitude = 0; + + @NonNull @Size(max = 2000) + private String routeString = ""; + + @NonNull + private ControllingUnit currentController = ControllerManager.getUncontrolledUnit(); + + @NonNull + private ControllingUnit nextController = ControllerManager.getUncontrolledUnit(); + + @NonNull @Max(MAX_ALTITUDE) + private Integer assignedTemporaryAltitude = 0; + + @NonNull @Max(MAX_ALTITUDE) + private Integer assignedFinalAltitude = 0; + + @NonNull + private FlightDataSupplement supplement = FlightDataSupplement.getDefault(); + + public FlightData(@NonNull String _callsign) { + if (!StringUtils.hasText(_callsign)) { + throw new IllegalArgumentException("Invalid callsign in FlightData message"); + } + // TODO: use {@link org.apache.commons.lang3.StringUtils} + callsign = StringUtils.trimWhitespace(_callsign); + } + + public FlightData(NxfRadar.FlightData fDataMessage) { + this(fDataMessage.getIdentification().getCallsign()); + if (fDataMessage.getIdentification().hasIcaoAddress()) { + icaoAddress = fDataMessage.getIdentification().getIcaoAddress(); + } + assignedBeaconCode = fDataMessage.getAssignedBeaconCode(); + flightRule = IRadarComponent.getFlightRuleOrDefault(fDataMessage.getFlightRuleValue()); + + aircraftType = fDataMessage.getAircraftType(); + departurePoint = fDataMessage.getDestination(); + requestedAltitude = fDataMessage.getRequestedAltitude(); + routeString = StringUtils.trimWhitespace(fDataMessage.getRouteString()); + } +} diff --git a/src/main/java/io/wtmsb/nxf/domain/FlightDataSupplement.java b/src/main/java/io/wtmsb/nxf/domain/FlightDataSupplement.java new file mode 100644 index 0000000..36625f2 --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/domain/FlightDataSupplement.java @@ -0,0 +1,57 @@ +package io.wtmsb.nxf.domain; + +import io.wtmsb.nxf.message.radar.NxfRadar; +import lombok.*; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Size; + +import static io.wtmsb.nxf.domain.IRadarComponent.*; + +@Getter @Setter @EqualsAndHashCode +public final class FlightDataSupplement { + private static final FlightDataSupplement DEFAULT = new FlightDataSupplement(); + + @NonNull @Max(MAX_ALTITUDE) + private Integer assignedTemporaryAltitude; + + @NonNull @Max(MAX_ALTITUDE) + private Integer assignedFinalAltitude; + + @NonNull @Size(max = 5) + private String pad1; + + @NonNull @Size(max = 5) + private String pad2; + + @NonNull @Size(max = 3) + private String runway; + + @NonNull @Size(max = 5) + private String exitFix; + + @NonNull + private LeaderLineDirection leaderLineDirection; + + private FlightDataSupplement() { + this(NxfRadar.FlightDataSupplement.getDefaultInstance()); + } + + public FlightDataSupplement(NxfRadar.FlightDataSupplement supplementMessage) { + assignedTemporaryAltitude = supplementMessage.getAssignedTemporaryAltitude(); + assignedFinalAltitude = supplementMessage.getAssignedFinalAltitude(); + pad1 = supplementMessage.getPad1(); + pad2 = supplementMessage.getPad2(); + runway = supplementMessage.getRunway(); + exitFix = supplementMessage.getExitFix(); + leaderLineDirection = getLeaderLineDirectionOrDefault(supplementMessage.getLeaderLineDirectionValue()); + } + + public boolean isDefault() { + return this.equals(DEFAULT); + } + + public static FlightDataSupplement getDefault() { + return new FlightDataSupplement(); + } +} diff --git a/src/main/java/io/wtmsb/nxf/domain/FlightStrip.java b/src/main/java/io/wtmsb/nxf/domain/FlightStrip.java deleted file mode 100644 index 77f990d..0000000 --- a/src/main/java/io/wtmsb/nxf/domain/FlightStrip.java +++ /dev/null @@ -1,71 +0,0 @@ -package io.wtmsb.nxf.domain; - -import com.google.protobuf.ByteString; -import io.wtmsb.nxf.message.radar.NxfRadar; -import io.wtmsb.nxf.validator.ByteStringSize; -import lombok.*; -import org.springframework.util.StringUtils; - -import javax.validation.constraints.Max; -import javax.validation.constraints.Size; -import java.time.Instant; - -/** - * Mutable object managed by {@link io.wtmsb.nxf.manager.TrackManager} - */ -@Getter @Setter -@EqualsAndHashCode(onlyExplicitlyIncluded = true) -public class FlightStrip implements IRadarComponent { - @NonNull @Size(min = 2, max = 10) - @EqualsAndHashCode.Include - private String aircraftCallsign; - - @NonNull @ByteStringSize(AIRCRAFT_ADDRESS_SIZE) - @EqualsAndHashCode.Include - private ByteString aircraftAddress = DEFAULT_AIRCRAFT_ADDRESS; - - @NonNull @Size(max = 10) - private String aircraftType = ""; - - @NonNull - private WakeCategory wakeCategory = WakeCategory.NO_WEIGHT; - - @NonNull - private FlightRule flightRule = FlightRule.INSTRUMENT; - - @NonNull @Size(max = 4) - private String destination = ""; - - @NonNull @Max(100000) - private Integer requestedAltitude = 0; - - @NonNull @ByteStringSize(BEACON_CODE_SIZE) - private ByteString assignedBeaconCode = DEFAULT_BEACON_CODE; - - @NonNull @Size(max = 2000) - private String routeString = ""; - - @NonNull - private Instant lastUpdated = Instant.EPOCH; - - public FlightStrip(@NonNull String _aircraftCallsign) { - aircraftCallsign = _aircraftCallsign; - } - - public FlightStrip(NxfRadar.FlightStrip fsMsg) { - if (!StringUtils.hasText(fsMsg.getAircraftCallsign())) { - throw new IllegalArgumentException("Malformed aircraftCallsign in FlightStrip message"); - } else { - aircraftCallsign = fsMsg.getAircraftCallsign(); - } - - aircraftAddress = fsMsg.getAircraftAddress(); - wakeCategory = IRadarComponent.getWakeCategoryOrDefault(fsMsg.getWakeCategoryValue()); - flightRule = IRadarComponent.getFlightRuleOrDefault(fsMsg.getFlightRuleValue()); - assignedBeaconCode = IRadarComponent.getBeaconCodeOrDefault(fsMsg.getAssignedBeaconCode()); - aircraftType = fsMsg.getAircraftType(); - destination = fsMsg.getDestination(); - requestedAltitude = fsMsg.getRequestedAltitude(); - routeString = StringUtils.trimWhitespace(fsMsg.getRouteString()); - } -} diff --git a/src/main/java/io/wtmsb/nxf/domain/IRadarComponent.java b/src/main/java/io/wtmsb/nxf/domain/IRadarComponent.java index ef244f6..4bd9690 100644 --- a/src/main/java/io/wtmsb/nxf/domain/IRadarComponent.java +++ b/src/main/java/io/wtmsb/nxf/domain/IRadarComponent.java @@ -1,16 +1,15 @@ package io.wtmsb.nxf.domain; import com.google.protobuf.ByteString; -import io.wtmsb.nxf.validator.ByteStringSize; +import io.wtmsb.nxf.validation.IcaoAddress; public interface IRadarComponent { - int BEACON_CODE_SIZE = 2; ByteString DEFAULT_BEACON_CODE = ByteString.fromHex("0000"); - int AIRCRAFT_ADDRESS_SIZE = 3; - ByteString DEFAULT_AIRCRAFT_ADDRESS = ByteString.fromHex("000000"); + ByteString DEFAULT_ICAO_ADDRESS = ByteString.fromHex("000000"); + int MAX_ALTITUDE = 100000; enum TransponderMode { - NO_MODE, MODE_A, MODE_C, MODE_S + PRIMARY, MODE_A, MODE_C, MODE_S } enum FlightRule { @@ -28,15 +27,6 @@ enum LeaderLineDirection { E, SW, S, SE } - static ByteString getBeaconCodeOrDefault(ByteString src) { - return checkBeaconCodeValue(src) ? src : DEFAULT_BEACON_CODE; - } - - static TransponderMode getTransponderModeOrDefault(int ordinal) { - return (0 < ordinal && ordinal < TransponderMode.values().length) ? - TransponderMode.values()[ordinal] : TransponderMode.NO_MODE; - } - static FlightRule getFlightRuleOrDefault(int ordinal) { return (0 < ordinal && ordinal < FlightRule.values().length) ? FlightRule.values()[ordinal] : FlightRule.INSTRUMENT; @@ -51,17 +41,4 @@ static LeaderLineDirection getLeaderLineDirectionOrDefault(int ordinal) { return (0 < ordinal && ordinal < LeaderLineDirection.values().length) ? LeaderLineDirection.values()[ordinal] : LeaderLineDirection.DEFAULT; } - - private static boolean checkBeaconCodeValue(ByteString octal) { - int value = fromBeaconCodeArray(octal.toByteArray()); - return 0 <= value && value <= 0xFFF; - } - - private static int fromBeaconCodeArray(byte[] bytes) { - return ((bytes[0] & 0xFF) << 8) | (bytes[1] & 0xFF); - } - - private static int fromAircraftAddressArray(byte[] bytes) { - return ((bytes[0] & 0xFF) << 16) | ((bytes[1] & 0xFF) << 8) | (bytes[2] & 0xFF); - } } diff --git a/src/main/java/io/wtmsb/nxf/domain/RadarTarget.java b/src/main/java/io/wtmsb/nxf/domain/RadarTarget.java index c94f6e9..b97cde6 100644 --- a/src/main/java/io/wtmsb/nxf/domain/RadarTarget.java +++ b/src/main/java/io/wtmsb/nxf/domain/RadarTarget.java @@ -2,51 +2,83 @@ import com.google.protobuf.ByteString; import io.wtmsb.nxf.message.radar.NxfRadar; -import io.wtmsb.nxf.validator.ByteStringSize; +import io.wtmsb.nxf.validation.BeaconCode; +import io.wtmsb.nxf.validation.IcaoAddress; import lombok.*; import javax.validation.constraints.Max; import javax.validation.constraints.Min; import java.time.Instant; -@Getter @Setter -@EqualsAndHashCode @ToString +@Getter +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +@ToString public final class RadarTarget implements IRadarComponent { - @NonNull @Min(-90) @Max(90) - private Double lat; - @NonNull @Min(-180) @Max(180) - private Double lon; + @Min(-90) @Max(90) + private final double lat; + @Min(-180) @Max(180) + private final double lon; - @NonNull - private TransponderMode transponderMode = TransponderMode.NO_MODE; + @NonNull @Min(0) + private final Instant returnTime; - @NonNull @ByteStringSize(BEACON_CODE_SIZE) - private ByteString beaconCode = DEFAULT_BEACON_CODE; + @BeaconCode + private final ByteString beaconCode; - @NonNull - private Instant returnTime = Instant.EPOCH; + @Max(MAX_ALTITUDE) + private final int reportedAltitude; - @NonNull @Min(0) @Max(100000) - private Integer reportedAltitude; + @IcaoAddress + private final ByteString modeSAddress; + + private final IRadarComponent.TransponderMode transponderMode; + + private final int groundSpeed; + + @Max(359) + private final int groundTrack; - @NonNull - private ByteString modeSAddress = DEFAULT_AIRCRAFT_ADDRESS; public RadarTarget(long customReturnTime) { + // TODO: for testing only, remove when possible + lat = lon = 0.0; + beaconCode = DEFAULT_BEACON_CODE; + reportedAltitude = 0; + modeSAddress = DEFAULT_ICAO_ADDRESS; + transponderMode = TransponderMode.PRIMARY; + groundSpeed = 0; + groundTrack = 0; returnTime = Instant.ofEpochSecond(customReturnTime); } - public RadarTarget(NxfRadar.Track.RadarTarget rtMsg) throws RuntimeException { - lat = Double.parseDouble(rtMsg.getLat()); - lon = Double.parseDouble(rtMsg.getLon()); - transponderMode = IRadarComponent.getTransponderModeOrDefault(rtMsg.getTransponderModeValue()); - beaconCode = IRadarComponent.getBeaconCodeOrDefault(rtMsg.getBeaconCode()); + public RadarTarget(NxfRadar.RadarTarget rtMsg) { + lat = rtMsg.getLat(); + lon = rtMsg.getLon(); returnTime = Instant.ofEpochSecond(rtMsg.getReturnTime()); - reportedAltitude = rtMsg.getReportedAltitude(); - modeSAddress = rtMsg.getModeSAddress(); - } - public boolean isDefault() { - return returnTime == Instant.EPOCH; + if (rtMsg.hasModeSAddress() && rtMsg.hasReportedAltitude() && rtMsg.hasBeaconCode()) { + transponderMode = TransponderMode.MODE_S; + beaconCode = rtMsg.getBeaconCode(); + reportedAltitude = rtMsg.getReportedAltitude(); + modeSAddress = rtMsg.getModeSAddress(); + } else if (rtMsg.hasReportedAltitude() && rtMsg.hasBeaconCode()) { + transponderMode = TransponderMode.MODE_C; + beaconCode = rtMsg.getBeaconCode(); + reportedAltitude = rtMsg.getReportedAltitude(); + modeSAddress = DEFAULT_ICAO_ADDRESS; + } else if (rtMsg.hasBeaconCode()) { + transponderMode = TransponderMode.MODE_A; + beaconCode = rtMsg.getBeaconCode(); + reportedAltitude = 0; + modeSAddress = DEFAULT_ICAO_ADDRESS; + } else { + transponderMode = TransponderMode.PRIMARY; + beaconCode = DEFAULT_BEACON_CODE; + reportedAltitude = 0; + modeSAddress = DEFAULT_ICAO_ADDRESS; + } + + groundSpeed = rtMsg.getGroundSpeed(); + groundTrack = rtMsg.getGroundTrack(); } } diff --git a/src/main/java/io/wtmsb/nxf/domain/Track.java b/src/main/java/io/wtmsb/nxf/domain/Track.java index c408311..afd75f6 100644 --- a/src/main/java/io/wtmsb/nxf/domain/Track.java +++ b/src/main/java/io/wtmsb/nxf/domain/Track.java @@ -1,22 +1,14 @@ package io.wtmsb.nxf.domain; -import io.wtmsb.nxf.manager.ControllerManager; -import io.wtmsb.nxf.message.radar.NxfRadar; +import com.google.protobuf.ByteString; +import io.wtmsb.nxf.validation.BeaconCode; +import io.wtmsb.nxf.validation.IcaoAddress; import lombok.*; @Getter @Setter public class Track { @NonNull - private FlightStrip flightStrip; - - @NonNull - private ControllingUnit currentController; - - @NonNull - private ControllingUnit nextController; - - @NonNull - private DataBlockSupplement dataBlockSupplement; + private FlightData flightData; @Getter(value = AccessLevel.NONE) @Setter(value = AccessLevel.NONE) private boolean isPrimary = true; @@ -24,44 +16,36 @@ public class Track { @Getter(value = AccessLevel.NONE) @Setter(value = AccessLevel.NONE) private boolean isCorrelated = false; - public Track(String aircraftCallsign) { - flightStrip = new FlightStrip(aircraftCallsign); - dataBlockSupplement = new DataBlockSupplement(); - currentController = nextController = ControllerManager.getDefaultControllingUnit(); - } - - public Track(NxfRadar.Track tMsg) { - flightStrip = new FlightStrip(tMsg.getFlightStrip()); - dataBlockSupplement = new DataBlockSupplement(tMsg.getDataBlockSupplement()); - currentController = ControllerManager.getControllingUnit(tMsg.getCurrentController()); - nextController = ControllerManager.getControllingUnit(tMsg.getNextController()); - } - - public boolean isPrimaryTrack() { - return isPrimary; - } - - public boolean isUncorrelatedTrack() { - return isCorrelated; - } - - public void correlateFlightStrip(FlightStrip fs) { - flightStrip = fs; - isCorrelated = true; - isPrimary = false; - } - - public void detachFlightStrip() { - flightStrip.setAircraftCallsign("WHO"); - isCorrelated = false; - } - - public void setPrimary() { - isCorrelated = false; - isPrimary = true; - } - - public boolean isUncontrolledTrack() { - return currentController == ControllerManager.getDefaultControllingUnit(); + /** + * Constructor for creating a new primary track + * + * @param callsign callsign for this track + */ + public Track(String callsign) { + flightData = new FlightData(callsign); + } + + /** + * For creating a new mode A or C track + * + * @param callsign callsign for this track + * @param beaconCode 12-bit octal beacon code enclosed in 2 byte + */ + public Track(String callsign, @BeaconCode ByteString beaconCode) { + flightData = new FlightData(callsign); + flightData.setHasAssignedBeaconCode(true); + flightData.setAssignedBeaconCode(beaconCode); + } + + /** + * For creating a new mode S track + * + * @param callsign callsign for this track + * @param icaoAddress 3 byte ICAO address associated with this track + */ + public Track(String callsign, @BeaconCode ByteString beaconCode, @IcaoAddress ByteString icaoAddress) { + this(callsign, beaconCode); + flightData.setHasIcaoAddress(true); + flightData.setIcaoAddress(icaoAddress); } } diff --git a/src/main/java/io/wtmsb/nxf/manager/ControllerManager.java b/src/main/java/io/wtmsb/nxf/manager/ControllerManager.java index 6286ff8..b7ad8ef 100644 --- a/src/main/java/io/wtmsb/nxf/manager/ControllerManager.java +++ b/src/main/java/io/wtmsb/nxf/manager/ControllerManager.java @@ -10,11 +10,12 @@ @Component public final class ControllerManager { - private static final ControllingUnit defaultControllingUnit = new ControllingUnit("", ""); + private static final ControllingUnit uncontrolledUnit = + new ControllingUnit(NxfRadar.ControllingUnit.getDefaultInstance()); private static final Table cache = HashBasedTable.create(); static { - cache.put("", "", defaultControllingUnit); + cache.put(uncontrolledUnit.getFacility(), uncontrolledUnit.getSector(), uncontrolledUnit); } @Synchronized @@ -26,12 +27,12 @@ public static ControllingUnit getControllingUnit(String facility, String sector) return cache.get(facility, sector); } - public static ControllingUnit getControllingUnit(NxfRadar.Track.@NonNull ControllingUnit nxfCu) { + public static ControllingUnit getControllingUnit(NxfRadar.@NonNull ControllingUnit nxfCu) { String facility = nxfCu.getFacility(), sector = nxfCu.getSector(); return getControllingUnit(facility, sector); } - public static ControllingUnit getDefaultControllingUnit() { - return defaultControllingUnit; + public static ControllingUnit getUncontrolledUnit() { + return uncontrolledUnit; } } diff --git a/src/main/java/io/wtmsb/nxf/manager/TrackManager.java b/src/main/java/io/wtmsb/nxf/manager/TrackManager.java index 0c50355..261cd1b 100644 --- a/src/main/java/io/wtmsb/nxf/manager/TrackManager.java +++ b/src/main/java/io/wtmsb/nxf/manager/TrackManager.java @@ -2,19 +2,24 @@ import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; -import io.wtmsb.nxf.domain.FlightStrip; -import io.wtmsb.nxf.domain.IRadarComponent; +import com.google.protobuf.ByteString; +import io.wtmsb.nxf.domain.FlightData; import io.wtmsb.nxf.domain.RadarTarget; import io.wtmsb.nxf.domain.Track; import io.wtmsb.nxf.utility.GeoCalculator; +import io.wtmsb.nxf.validation.IcaoAddress; import lombok.Synchronized; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; import java.time.Duration; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import static io.wtmsb.nxf.domain.IRadarComponent.TransponderMode; + @Service public class TrackManager { private ControllerManager controllerManager; @@ -23,8 +28,6 @@ public class TrackManager { private final int radarReturnCount = 6; - private final int updateFrequency = 3; - @Autowired public TrackManager(ControllerManager controllerManager) { this(); @@ -32,37 +35,42 @@ public TrackManager(ControllerManager controllerManager) { } public TrackManager() { + // TODO: make private targetTrackMap = new ConcurrentHashMap<>(); trackTargetMultiMap = LinkedListMultimap.create(); } - public void addTarget(RadarTarget target) { - if (target.getTransponderMode() == IRadarComponent.TransponderMode.MODE_A) { - autoCorrelateTarget(target); - } else if (target.getTransponderMode() == IRadarComponent.TransponderMode.MODE_C) { - autoCorrelateTarget(target); - } else if (target.getTransponderMode() == IRadarComponent.TransponderMode.MODE_S) { - autoCorrelateTarget(target); + public void addTarget(RadarTarget target, @Min(2) @Max(10) String callsign) { + if (target.getTransponderMode() == TransponderMode.MODE_C) { + addCorrelatedTarget(target, callsign); + } else if (target.getTransponderMode() == TransponderMode.MODE_S) { + addCorrelatedTarget(target, callsign, target.getModeSAddress()); } else { - addPrimaryTarget(target); + attemptTargetCorrelation(target); } } - public boolean autoCorrelateTarget(RadarTarget target) { - - return false; + public void attemptTargetCorrelation(RadarTarget target) { + if (target.getTransponderMode() == TransponderMode.PRIMARY) { + addPrimaryTarget(target); + } else { + addModeATarget(target); + } } - public boolean autoCorrelateFlightStrip(FlightStrip strip) { + public void addCorrelatedTarget(RadarTarget target, String callsign) { + throw new UnsupportedOperationException(); + } - return false; + public void addCorrelatedTarget(RadarTarget target, String callsign, @IcaoAddress ByteString address) { + throw new UnsupportedOperationException(); } @Synchronized - public void addPrimaryTarget(RadarTarget newTarget) { + public void addPrimaryTarget(RadarTarget target) { // find the closest track from the target Optional _closestTarget = targetTrackMap.keySet().stream() - .min(Comparator.comparingDouble(o -> GeoCalculator.calculateDistance(o, newTarget))); + .min(Comparator.comparingDouble(o -> GeoCalculator.calculateDistance(o, target))); // generate a new track by default Track associatedTrack = new Track("PRI"); @@ -70,16 +78,16 @@ public void addPrimaryTarget(RadarTarget newTarget) { if (_closestTarget.isPresent()) { RadarTarget closestTarget = _closestTarget.get(); // associate the new target with the track associated with the closest target - if (GeoCalculator.calculateDistance(closestTarget, newTarget) <= 16.0 && - Duration.between(closestTarget.getReturnTime(), newTarget.getReturnTime()).getSeconds() < 45) + if (GeoCalculator.calculateDistance(closestTarget, target) <= 16.0 && + Duration.between(closestTarget.getReturnTime(), target.getReturnTime()).getSeconds() < 45) { // get the existing track if track's latest return isn't stale or not within 16 miles of the new target associatedTrack = targetTrackMap.get(closestTarget); } } - targetTrackMap.put(newTarget, associatedTrack); - trackTargetMultiMap.put(associatedTrack, newTarget); + targetTrackMap.put(target, associatedTrack); + trackTargetMultiMap.put(associatedTrack, target); List targetsList = trackTargetMultiMap.get(associatedTrack); while (targetsList.size() > radarReturnCount) { @@ -87,6 +95,10 @@ public void addPrimaryTarget(RadarTarget newTarget) { } } + public void addModeATarget(RadarTarget target) { + Track associatedTrack = new Track("MODE_A"); + } + /** * */ diff --git a/src/main/java/io/wtmsb/nxf/utility/ClientMessenger.java b/src/main/java/io/wtmsb/nxf/utility/ClientMessenger.java index fe195df..6c96855 100644 --- a/src/main/java/io/wtmsb/nxf/utility/ClientMessenger.java +++ b/src/main/java/io/wtmsb/nxf/utility/ClientMessenger.java @@ -3,6 +3,7 @@ import io.wtmsb.nxf.message.radar.NxfRadarClient.*; public final class ClientMessenger { + private ClientMessenger() {} public static ClientRadarEvent createRadarEvent(Object msg, int eventFieldNumber) { ClientRadarEvent.Builder cb = ClientRadarEvent.newBuilder(); diff --git a/src/main/java/io/wtmsb/nxf/utility/GeoCalculator.java b/src/main/java/io/wtmsb/nxf/utility/GeoCalculator.java index 194e821..ecff1ec 100644 --- a/src/main/java/io/wtmsb/nxf/utility/GeoCalculator.java +++ b/src/main/java/io/wtmsb/nxf/utility/GeoCalculator.java @@ -4,6 +4,7 @@ import org.locationtech.spatial4j.distance.DistanceUtils; public final class GeoCalculator { + private GeoCalculator() {} public static double calculateDistance(RadarTarget tgt1, RadarTarget tgt2) { double lat1 = tgt1.getLat(), lat2 = tgt2.getLat(), diff --git a/src/main/java/io/wtmsb/nxf/utility/ServerMessenger.java b/src/main/java/io/wtmsb/nxf/utility/ServerMessenger.java index b642052..38bf310 100644 --- a/src/main/java/io/wtmsb/nxf/utility/ServerMessenger.java +++ b/src/main/java/io/wtmsb/nxf/utility/ServerMessenger.java @@ -3,6 +3,7 @@ import io.wtmsb.nxf.message.radar.NxfRadarServer.*; public final class ServerMessenger { + private ServerMessenger() {} public static ServerRadarEvent createRadarEvent(Object msg, int eventFieldNumber) { ServerRadarEvent.Builder sb = ServerRadarEvent.newBuilder(); diff --git a/src/main/java/io/wtmsb/nxf/validator/ByteStringSize.java b/src/main/java/io/wtmsb/nxf/validation/BeaconCode.java similarity index 65% rename from src/main/java/io/wtmsb/nxf/validator/ByteStringSize.java rename to src/main/java/io/wtmsb/nxf/validation/BeaconCode.java index 0b1475f..0007f31 100644 --- a/src/main/java/io/wtmsb/nxf/validator/ByteStringSize.java +++ b/src/main/java/io/wtmsb/nxf/validation/BeaconCode.java @@ -1,4 +1,4 @@ -package io.wtmsb.nxf.validator; +package io.wtmsb.nxf.validation; import javax.validation.Constraint; import javax.validation.Payload; @@ -10,10 +10,9 @@ @Target({METHOD, FIELD, ANNOTATION_TYPE, PARAMETER}) @Retention(RetentionPolicy.RUNTIME) -@Constraint(validatedBy = ByteStringSizeValidator.class) -public @interface ByteStringSize { - String message() default "{io.wtmsb.nxf.constraint.ByteStringSize.message}"; +@Constraint(validatedBy = BeaconCodeValidator.class) +public @interface BeaconCode { + String message() default "{io.wtmsb.nxf.validation.BeaconCode.message}"; Class[] groups() default {}; Class[] payload() default {}; - int value(); } diff --git a/src/main/java/io/wtmsb/nxf/validation/BeaconCodeValidator.java b/src/main/java/io/wtmsb/nxf/validation/BeaconCodeValidator.java new file mode 100644 index 0000000..d735615 --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/validation/BeaconCodeValidator.java @@ -0,0 +1,29 @@ +package io.wtmsb.nxf.validation; + +import com.google.protobuf.ByteString; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class BeaconCodeValidator implements ConstraintValidator { + private static final int BEACON_CODE_SIZE = 2; + + private static boolean checkBeaconCodeValue(ByteString octal) { + int value = fromBeaconCodeArray(octal.toByteArray()); + return 0 <= value && value <= 0xFFF; + } + + private static int fromBeaconCodeArray(byte[] bytes) { + return ((bytes[0] & 0xFF) << 8) | (bytes[1] & 0xFF); + } + + public void initialize(BeaconCode beaconCode) {} + + @Override + public boolean isValid(ByteString bs, ConstraintValidatorContext constraintValidatorContext) { + if (bs == null || bs.size() != BEACON_CODE_SIZE) + return false; + + return checkBeaconCodeValue(bs); + } +} diff --git a/src/main/java/io/wtmsb/nxf/validation/IcaoAddress.java b/src/main/java/io/wtmsb/nxf/validation/IcaoAddress.java new file mode 100644 index 0000000..579bb32 --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/validation/IcaoAddress.java @@ -0,0 +1,18 @@ +package io.wtmsb.nxf.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; + +@Target({METHOD, FIELD, ANNOTATION_TYPE, PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = IcaoAddressValidator.class) +public @interface IcaoAddress { + String message() default "{io.wtmsb.nxf.validation.IcaoAddress.message}"; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/io/wtmsb/nxf/validation/IcaoAddressValidator.java b/src/main/java/io/wtmsb/nxf/validation/IcaoAddressValidator.java new file mode 100644 index 0000000..2a416dc --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/validation/IcaoAddressValidator.java @@ -0,0 +1,17 @@ +package io.wtmsb.nxf.validation; + +import com.google.protobuf.ByteString; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class IcaoAddressValidator implements ConstraintValidator { + + public void initialize(IcaoAddress icaoAddress) {} + + @Override + public boolean isValid(ByteString bs, ConstraintValidatorContext constraintValidatorContext) { + int ICAO_ADDRESS_SIZE = 3; + return bs == null || bs.size() != ICAO_ADDRESS_SIZE; + } +} diff --git a/src/main/java/io/wtmsb/nxf/validator/ByteStringSizeValidator.java b/src/main/java/io/wtmsb/nxf/validator/ByteStringSizeValidator.java deleted file mode 100644 index 7502202..0000000 --- a/src/main/java/io/wtmsb/nxf/validator/ByteStringSizeValidator.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.wtmsb.nxf.validator; - -import com.google.protobuf.ByteString; - -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; - -public class ByteStringSizeValidator implements ConstraintValidator { - private int mandatorySize; - - public void initialize(ByteStringSize byteStringSize) { - mandatorySize = byteStringSize.value(); - } - - @Override - public boolean isValid(ByteString bs, ConstraintValidatorContext constraintValidatorContext) { - return bs.size() == mandatorySize; - } -} diff --git a/src/main/proto/radar/nxf_radar.proto b/src/main/proto/radar/nxf_radar.proto index 38c7a4c..1310d09 100644 --- a/src/main/proto/radar/nxf_radar.proto +++ b/src/main/proto/radar/nxf_radar.proto @@ -3,87 +3,35 @@ syntax = "proto3"; package io.wtmsb.nxf.message.radar; /** - Top level message used to draw objects on the scope. - Includes radar return and minimum information necessary to construct a data block. + NxfRadar.RadarTarget message: + This message is analogous to a radar blip. + Nxf data source should not attempt to modify an already-sent RadarTarget message. + Nxf data client should make the domain object for this message immutable. */ -message Track { - uint32 id = 1; - - // Radar targets must live inside a track - message RadarTarget { - string lat = 1; - string lon = 2; - enum TransponderMode { - NO_MODE = 0; - MODE_A = 1; - MODE_C = 2; - MODE_S = 3; - } - TransponderMode transponder_mode = 3; - bytes beacon_code = 4; - //uint32 calculated_ground_speed = 5; - //uint32 calculated_heading = 6; - uint64 return_time = 7; - optional uint32 reported_altitude = 8; // Needs at least Mode C - optional bytes mode_s_address = 9; // Mode S only - } - repeated RadarTarget radar_targets = 2; // client: store history dots, derive ground speed and heading - - optional FlightStrip flight_strip = 3; - - message ControllingUnit { - string facility = 1; - string sector = 2; - } - optional ControllingUnit current_controller = 4; - optional ControllingUnit next_controller = 5; - - reserved 6 to 10; - - message DataBlockSupplement { - // lld has a default type so it won't be empty - enum LeaderLineDirection { - DEFAULT = 0; - NW = 1; - N = 2; - NE = 3; - W = 4; - HIDE = 5; - E = 6; - SW = 7; - S = 8; - SE = 9; - } - LeaderLineDirection leader_line_direction = 1; - - optional uint32 assigned_temporary_altitude = 2; - optional string pad_1 = 3; - optional string pad_2 = 4; - optional string runway = 5; - optional string exit_fix = 6; - } - DataBlockSupplement data_block_supplement = 11; +message RadarTarget { + double lat = 1; // mandatory id component + double lon = 2; // mandatory id component + uint64 return_time = 3; // mandatory id component + optional bytes beacon_code = 4; // optional id component but will be used in almost all cases + optional uint32 reported_altitude = 5; + optional bytes mode_s_address = 6; // optional id component + uint32 ground_speed = 7; + uint32 ground_track = 8; } /** - Flight strip message that could be used to supplement data block display. + NxfRadar.FlightData message: + This message is analogous to a flight plan. */ -message FlightStrip { - uint32 id = 1; - string aircraft_type = 2; - string aircraft_callsign = 3; - optional bytes aircraft_address = 4; // Mode S only - - enum WakeCategory { - NO_WEIGHT = 0; - CAT_A = 1; - CAT_B = 2; - CAT_C = 3; - CAT_D = 4; - CAT_E = 5; - CAT_F = 6; +message FlightData { + message Key { + string callsign = 1; // mandatory key component + optional bytes icao_address = 2; // optional key component } - WakeCategory wake_category = 5; + Key identification = 1; + optional bytes assigned_beacon_code = 3; + optional string aircraft_type = 4; + optional string equipment_suffix = 5; enum FlightRule { INSTRUMENT = 0; @@ -91,10 +39,45 @@ message FlightStrip { SPECIAL_VISUAL = 2; DEFENSE_VISUAL = 3; } - FlightRule flight_rule = 6; + optional FlightRule flight_rule = 6; - string destination = 7; - uint32 requested_altitude = 8; - bytes assigned_beacon_code = 9; + optional string departure_point = 7; + optional string destination = 8; + optional uint32 requested_altitude = 9; optional string route_string = 10; + + reserved 11 to 19; + + //optional uint64 last_updated = 20; + + optional ControllingUnit current_controller = 21; + optional ControllingUnit next_controller = 22; +} + +message ControllingUnit { + string facility = 1; + string sector = 2; +} + +message FlightDataSupplement { + optional uint32 assigned_temporary_altitude = 23; + optional uint32 assigned_final_altitude = 24; + optional string pad_1 = 25; + optional string pad_2 = 26; + optional string runway = 27; + optional string exit_fix = 28; + + enum LeaderLineDirection { + DEFAULT = 0; + NW = 1; + N = 2; + NE = 3; + W = 4; + HIDE = 5; + E = 6; + SW = 7; + S = 8; + SE = 9; + } + optional LeaderLineDirection leader_line_direction = 29; } diff --git a/src/main/proto/radar/nxf_radar_client.proto b/src/main/proto/radar/nxf_radar_client.proto index 255bb3d..dd351df 100644 --- a/src/main/proto/radar/nxf_radar_client.proto +++ b/src/main/proto/radar/nxf_radar_client.proto @@ -6,32 +6,19 @@ import "radar/nxf_radar.proto"; package io.wtmsb.nxf.message.radar; message ClientRadarEvent { - oneof event_specific { - // Nested message edits - // Track - EditFlightStrip edit_flight_strip = 11; - EditInitiateHandoff edit_initiate_handoff = 12; - EditAcceptHandoff edit_accept_handoff = 13; - EditDataBlockSupplement edit_data_block_supplement = 14; + oneof event { + EditInitiateHandoff edit_initiate_handoff = 11; + EditAcceptHandoff edit_accept_handoff = 12; } } -message EditFlightStrip { - uint32 track_id = 1; - FlightStrip new_flight_strip = 2; -} - message EditInitiateHandoff { - uint32 track_id = 1; - Track.ControllingUnit next_controller = 2; + FlightData.Key key = 1; + ControllingUnit next_controller = 2; + optional ControllingUnit current_controller = 3; } message EditAcceptHandoff { - uint32 track_id = 1; - optional Track.ControllingUnit current_controller = 2; + FlightData.Key key = 1; + optional ControllingUnit current_controller = 2; } - -message EditDataBlockSupplement { - uint32 track_id = 1; - Track.DataBlockSupplement data_block_supplement = 2; -} \ No newline at end of file diff --git a/src/main/proto/radar/nxf_radar_server.proto b/src/main/proto/radar/nxf_radar_server.proto index d7cdf06..6c9ee4c 100644 --- a/src/main/proto/radar/nxf_radar_server.proto +++ b/src/main/proto/radar/nxf_radar_server.proto @@ -6,43 +6,37 @@ import "radar/nxf_radar.proto"; package io.wtmsb.nxf.message.radar; message ServerRadarEvent { - oneof event_specific { + oneof event { // New message pushes - Track push_new_track = 1; - FlightStrip push_new_flight_strip = 2; - - // Nested message pushes - // Track - PushRadarTarget push_radar_target = 11; - PushFlightStrip push_flight_strip = 12; - PushInitiateHandoff push_initiate_handoff = 13; - PushAcceptHandoff push_accept_handoff = 14; - PushDataBlockSupplement push_data_block_supplement = 15; + PushRadarTarget push_radar_target = 1; + FlightData push_flight_data = 2; + + // ControllingUnit pushes + PushInitiateHandoff push_initiate_handoff = 11; + PushAcceptHandoff push_accept_handoff = 12; + + // FlightDataSupplement push + PushFlightDataSupplement push_flight_data_supplement = 21; } } message PushRadarTarget { - uint32 track_id = 1; - Track.RadarTarget new_target = 2; -} - -message PushFlightStrip { - uint32 track_id = 1; - FlightStrip flight_strip = 2; + optional FlightData.Key correlated_flight_data = 1; + RadarTarget new_target = 2; } message PushInitiateHandoff { - uint32 track_id = 1; - Track.ControllingUnit next_controller = 2; - optional Track.ControllingUnit current_controller = 3; + FlightData.Key key = 1; + ControllingUnit next_controller = 2; + optional ControllingUnit current_controller = 3; } message PushAcceptHandoff { - uint32 track_id = 1; - optional Track.ControllingUnit current_controller = 2; + FlightData.Key key = 1; + optional ControllingUnit current_controller = 3; } -message PushDataBlockSupplement { - uint32 track_id = 1; - Track.DataBlockSupplement data_block_supplement = 2; +message PushFlightDataSupplement { + FlightData.Key key = 1; + FlightDataSupplement flight_data_supplement = 2; } From 3684aff3cf5a9a286244ce41810babe292b996f0 Mon Sep 17 00:00:00 2001 From: Yi Zhang Date: Tue, 31 May 2022 23:08:21 -0400 Subject: [PATCH 04/10] update TrackManager.java --- .../io/wtmsb/nxf/manager/TrackManager.java | 125 ++++++++++++------ 1 file changed, 81 insertions(+), 44 deletions(-) diff --git a/src/main/java/io/wtmsb/nxf/manager/TrackManager.java b/src/main/java/io/wtmsb/nxf/manager/TrackManager.java index 261cd1b..ec1ba1a 100644 --- a/src/main/java/io/wtmsb/nxf/manager/TrackManager.java +++ b/src/main/java/io/wtmsb/nxf/manager/TrackManager.java @@ -2,21 +2,19 @@ import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; -import com.google.protobuf.ByteString; -import io.wtmsb.nxf.domain.FlightData; import io.wtmsb.nxf.domain.RadarTarget; import io.wtmsb.nxf.domain.Track; import io.wtmsb.nxf.utility.GeoCalculator; -import io.wtmsb.nxf.validation.IcaoAddress; +import lombok.NonNull; import lombok.Synchronized; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import javax.validation.constraints.Max; -import javax.validation.constraints.Min; import java.time.Duration; +import java.time.Instant; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import static io.wtmsb.nxf.domain.IRadarComponent.TransponderMode; @@ -25,54 +23,108 @@ public class TrackManager { private ControllerManager controllerManager; private final Map targetTrackMap; private final ListMultimap trackTargetMultiMap; + private final Map trackByCallsignMap; private final int radarReturnCount = 6; + { + targetTrackMap = new ConcurrentHashMap<>(); + trackTargetMultiMap = LinkedListMultimap.create(); + trackByCallsignMap = new ConcurrentHashMap<>(); + } + + private TrackManager() {} + @Autowired public TrackManager(ControllerManager controllerManager) { this(); this.controllerManager = controllerManager; } - public TrackManager() { - // TODO: make private - targetTrackMap = new ConcurrentHashMap<>(); - trackTargetMultiMap = LinkedListMultimap.create(); - } - - public void addTarget(RadarTarget target, @Min(2) @Max(10) String callsign) { - if (target.getTransponderMode() == TransponderMode.MODE_C) { - addCorrelatedTarget(target, callsign); - } else if (target.getTransponderMode() == TransponderMode.MODE_S) { - addCorrelatedTarget(target, callsign, target.getModeSAddress()); + public void addTarget(RadarTarget target, @NonNull String callsign) { + if (callsign.length() == 0) { + addTargetAndAutoCorrelate(target); } else { - attemptTargetCorrelation(target); + if (target.getTransponderMode() == TransponderMode.MODE_C || + target.getTransponderMode() == TransponderMode.MODE_S) { + // TODO: implement mode S correlation methods + addCorrelatedTarget(target, callsign); + } else { + throw new IllegalArgumentException(); + } } } - public void attemptTargetCorrelation(RadarTarget target) { - if (target.getTransponderMode() == TransponderMode.PRIMARY) { - addPrimaryTarget(target); - } else { - addModeATarget(target); - } + /** + * Add a target and attempt to correlate with a track. + * + * @param target new target to add + */ + @Synchronized + private void addTargetAndAutoCorrelate(RadarTarget target) { + throw new UnsupportedOperationException("Auto correlation has not been implemented"); } - public void addCorrelatedTarget(RadarTarget target, String callsign) { - throw new UnsupportedOperationException(); + /** + * Add a Mode A/C target and correlate with the track with the given callsignHint + * + * @param target new target to add + * @param callsignHint hint from Nxf data source + */ + @Synchronized + private void addCorrelatedTarget(RadarTarget target, String callsignHint) { + Track correlatedTrack = trackByCallsignMap.computeIfAbsent(callsignHint, value -> new Track(callsignHint)); + trackTargetMultiMap.put(correlatedTrack, target); + targetTrackMap.put(target, correlatedTrack); + trimHistoryRadarTarget(correlatedTrack); } - public void addCorrelatedTarget(RadarTarget target, String callsign, @IcaoAddress ByteString address) { - throw new UnsupportedOperationException(); + /** + * Each add target method must call this method to remove excessive radar targets. + * + * @param track the track to operate on + */ + @Synchronized + private void trimHistoryRadarTarget(Track track) { + List targetsList = trackTargetMultiMap.get(track); + while (targetsList.size() > radarReturnCount) { + targetTrackMap.remove(targetsList.remove(0)); + } } + /** + * Prune track if the last radar target's return time is over 45 seconds ago. + * TODO: implement coast/suspended track management + */ @Synchronized - public void addPrimaryTarget(RadarTarget target) { + private void pruneStaleTracks() { + Instant currentInstant = Instant.now(); + + List>> toBeRemoved = trackTargetMultiMap.asMap().entrySet().stream() + .filter(entry -> { + Optional newestTarget = + entry.getValue().stream().max(Comparator.comparing(RadarTarget::getReturnTime)); + return newestTarget.filter(radarTarget -> + Duration.between(radarTarget.getReturnTime(), currentInstant).getSeconds() > 45 + ).isPresent(); + }) + .collect(Collectors.toList()); + + toBeRemoved.forEach(trackCollectionEntry -> { + targetTrackMap.keySet().removeAll(trackCollectionEntry.getValue()); + trackTargetMultiMap.removeAll(trackCollectionEntry.getKey()); + trackByCallsignMap.remove(trackCollectionEntry.getKey().getFlightData().getCallsign()); + }); + } + + @Synchronized + private void addPrimaryTarget(RadarTarget target) { // find the closest track from the target Optional _closestTarget = targetTrackMap.keySet().stream() .min(Comparator.comparingDouble(o -> GeoCalculator.calculateDistance(o, target))); // generate a new track by default + // TODO: does not work, Track requires unique callsign Track associatedTrack = new Track("PRI"); if (_closestTarget.isPresent()) { @@ -89,21 +141,6 @@ public void addPrimaryTarget(RadarTarget target) { targetTrackMap.put(target, associatedTrack); trackTargetMultiMap.put(associatedTrack, target); - List targetsList = trackTargetMultiMap.get(associatedTrack); - while (targetsList.size() > radarReturnCount) { - targetTrackMap.remove(targetsList.remove(0)); - } - } - - public void addModeATarget(RadarTarget target) { - Track associatedTrack = new Track("MODE_A"); - } - - /** - * - */ - @Synchronized - public void pruneStaleTracks() { - //trackTargetMultiMap + trimHistoryRadarTarget(associatedTrack); } } From fd6d85f32fd67b017b664510f47e66b273991019 Mon Sep 17 00:00:00 2001 From: Yi Zhang Date: Wed, 1 Jun 2022 01:16:50 -0400 Subject: [PATCH 05/10] fix RadarTarget hashing, modify (rudimentary) testing --- src/main/java/Program.java | 88 ++++++++++++------- .../java/io/wtmsb/nxf/domain/RadarTarget.java | 3 +- .../wtmsb/nxf/manager/ControllerManager.java | 3 +- .../io/wtmsb/nxf/manager/TrackManager.java | 21 ++++- 4 files changed, 78 insertions(+), 37 deletions(-) diff --git a/src/main/java/Program.java b/src/main/java/Program.java index af31b2c..6c96e89 100644 --- a/src/main/java/Program.java +++ b/src/main/java/Program.java @@ -1,44 +1,70 @@ -import com.google.common.collect.LinkedListMultimap; -import com.google.common.collect.Multimaps; +import com.google.common.collect.ListMultimap; +import com.google.protobuf.ByteString; import io.wtmsb.nxf.domain.RadarTarget; import io.wtmsb.nxf.domain.Track; +import io.wtmsb.nxf.manager.TrackManager; +import io.wtmsb.nxf.message.radar.NxfRadar; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.stereotype.Component; -import java.util.*; +import java.util.function.BiConsumer; +@Component +@ComponentScan(basePackages = "io.wtmsb.nxf.manager") public class Program { + @Autowired + private TrackManager manager; + public static void main(String[] args) { - Map forwardMap = Collections.synchronizedMap(new LinkedHashMap<>()); - LinkedListMultimap reverseMap = - Multimaps.invertFrom(Multimaps.forMap(forwardMap), LinkedListMultimap.create()); + try (GenericApplicationContext ctx = new AnnotationConfigApplicationContext(Program.class)) { + var bean = ctx.getBean(Program.class); + bean.run(); + } - Track t1 = new Track("N1"); - Track t2 = new Track("N2"); - assert !t1.equals(t2); + } + public void run() { for (int i = 0; i < 10; i++){ - RadarTarget rt1 = new RadarTarget(i); - RadarTarget rt2 = new RadarTarget(i + 10); - forwardMap.put(rt1, t1); - reverseMap.put(t1, rt1); - forwardMap.put(rt2, t2); - reverseMap.put(t2, rt2); - - List targetsList = reverseMap.get(t1); - while (targetsList.size() > 6) { - forwardMap.remove(targetsList.remove(0)); - } + RadarTarget rt1 = new RadarTarget( + NxfRadar.RadarTarget.newBuilder() + .setReportedAltitude(10000) + .setBeaconCode(ByteString.fromHex("0480")) + .setReturnTime(i) + .build() + ); + RadarTarget rt2 = new RadarTarget( + NxfRadar.RadarTarget.newBuilder() + .setReportedAltitude(12000) + .setBeaconCode(ByteString.fromHex("010481")) // should fail + .setReturnTime(i + 10) + .build() + ); + + manager.addTarget(rt1, "N1"); + manager.addTarget(rt2, "N2"); } - forwardMap.forEach((key, value) -> - System.out.println(key.getReturnTime().toEpochMilli()/1000 + ": " + value.getFlightData().getCallsign())); - - reverseMap.asMap().forEach( - (key, value) -> { - System.out.print(key.getFlightData().getCallsign() + ": "); - value.forEach(x -> { - System.out.print(x.getReturnTime().toEpochMilli()/1000 + " "); - }); - System.out.println(); - }); + System.out.println("Showing all RadarTarget->Track..."); + manager.getTargetTrackMap().forEach((radarTarget, track) -> + System.out.println(radarTarget.getReturnTime().toEpochMilli()/1000 + ": " + track.getFlightData().getCallsign()) + ); + + System.out.println("Showing all Track->List..."); + ListMultimap trackRadarTargetListMultimap = manager.getTrackRadarTargetMultiMap(); + trackRadarTargetListMultimap.asMap().forEach((track, radarTargetList) -> { + System.out.print(track.getFlightData().getCallsign() + ": "); + for (RadarTarget radarTarget : radarTargetList) { + System.out.print(radarTarget.getReturnTime().toEpochMilli()/1000 + " "); + } + System.out.println(); + }); + + System.out.println("Showing all String->Track..."); + manager.getTrackByCallsignMap().forEach((s, track) -> + System.out.println(s + ": " + track.getFlightData().getCallsign()) + ); } } diff --git a/src/main/java/io/wtmsb/nxf/domain/RadarTarget.java b/src/main/java/io/wtmsb/nxf/domain/RadarTarget.java index b97cde6..bb5ac9f 100644 --- a/src/main/java/io/wtmsb/nxf/domain/RadarTarget.java +++ b/src/main/java/io/wtmsb/nxf/domain/RadarTarget.java @@ -11,8 +11,7 @@ import java.time.Instant; @Getter -@EqualsAndHashCode(onlyExplicitlyIncluded = true) -@ToString +@EqualsAndHashCode @ToString public final class RadarTarget implements IRadarComponent { @Min(-90) @Max(90) private final double lat; diff --git a/src/main/java/io/wtmsb/nxf/manager/ControllerManager.java b/src/main/java/io/wtmsb/nxf/manager/ControllerManager.java index b7ad8ef..b375d38 100644 --- a/src/main/java/io/wtmsb/nxf/manager/ControllerManager.java +++ b/src/main/java/io/wtmsb/nxf/manager/ControllerManager.java @@ -7,8 +7,9 @@ import lombok.NonNull; import lombok.Synchronized; import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; -@Component +@Service public final class ControllerManager { private static final ControllingUnit uncontrolledUnit = new ControllingUnit(NxfRadar.ControllingUnit.getDefaultInstance()); diff --git a/src/main/java/io/wtmsb/nxf/manager/TrackManager.java b/src/main/java/io/wtmsb/nxf/manager/TrackManager.java index ec1ba1a..21343bb 100644 --- a/src/main/java/io/wtmsb/nxf/manager/TrackManager.java +++ b/src/main/java/io/wtmsb/nxf/manager/TrackManager.java @@ -8,7 +8,7 @@ import lombok.NonNull; import lombok.Synchronized; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; import java.time.Duration; import java.time.Instant; @@ -18,7 +18,7 @@ import static io.wtmsb.nxf.domain.IRadarComponent.TransponderMode; -@Service +@Component public class TrackManager { private ControllerManager controllerManager; private final Map targetTrackMap; @@ -41,6 +41,21 @@ public TrackManager(ControllerManager controllerManager) { this.controllerManager = controllerManager; } + @Synchronized + public Map getTargetTrackMap() { + return targetTrackMap; + } + + @Synchronized + public ListMultimap getTrackRadarTargetMultiMap() { + return trackTargetMultiMap; + } + + @Synchronized + public Map getTrackByCallsignMap() { + return trackByCallsignMap; + } + public void addTarget(RadarTarget target, @NonNull String callsign) { if (callsign.length() == 0) { addTargetAndAutoCorrelate(target); @@ -73,7 +88,7 @@ private void addTargetAndAutoCorrelate(RadarTarget target) { */ @Synchronized private void addCorrelatedTarget(RadarTarget target, String callsignHint) { - Track correlatedTrack = trackByCallsignMap.computeIfAbsent(callsignHint, value -> new Track(callsignHint)); + Track correlatedTrack = trackByCallsignMap.computeIfAbsent(callsignHint, Track::new); trackTargetMultiMap.put(correlatedTrack, target); targetTrackMap.put(target, correlatedTrack); trimHistoryRadarTarget(correlatedTrack); From 34ec2a68df084b5753d084735dd01db4eab6698a Mon Sep 17 00:00:00 2001 From: Yi Zhang Date: Wed, 1 Jun 2022 16:44:31 -0400 Subject: [PATCH 06/10] bruh --- .../java/io/wtmsb/nxf/validation/BeaconCodeValidator.java | 5 +---- .../java/io/wtmsb/nxf/validation/IcaoAddressValidator.java | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/wtmsb/nxf/validation/BeaconCodeValidator.java b/src/main/java/io/wtmsb/nxf/validation/BeaconCodeValidator.java index d735615..3da8b96 100644 --- a/src/main/java/io/wtmsb/nxf/validation/BeaconCodeValidator.java +++ b/src/main/java/io/wtmsb/nxf/validation/BeaconCodeValidator.java @@ -21,9 +21,6 @@ public void initialize(BeaconCode beaconCode) {} @Override public boolean isValid(ByteString bs, ConstraintValidatorContext constraintValidatorContext) { - if (bs == null || bs.size() != BEACON_CODE_SIZE) - return false; - - return checkBeaconCodeValue(bs); + return bs != null && bs.size() == BEACON_CODE_SIZE && checkBeaconCodeValue(bs); } } diff --git a/src/main/java/io/wtmsb/nxf/validation/IcaoAddressValidator.java b/src/main/java/io/wtmsb/nxf/validation/IcaoAddressValidator.java index 2a416dc..356faf4 100644 --- a/src/main/java/io/wtmsb/nxf/validation/IcaoAddressValidator.java +++ b/src/main/java/io/wtmsb/nxf/validation/IcaoAddressValidator.java @@ -12,6 +12,6 @@ public void initialize(IcaoAddress icaoAddress) {} @Override public boolean isValid(ByteString bs, ConstraintValidatorContext constraintValidatorContext) { int ICAO_ADDRESS_SIZE = 3; - return bs == null || bs.size() != ICAO_ADDRESS_SIZE; + return bs != null && bs.size() != ICAO_ADDRESS_SIZE; } } From 4ea1b1ec284b875c38f2301f26cf9621af545b30 Mon Sep 17 00:00:00 2001 From: Yi Zhang Date: Wed, 1 Jun 2022 17:35:35 -0400 Subject: [PATCH 07/10] bruh update --- src/main/java/Program.java | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/main/java/Program.java b/src/main/java/Program.java index 6c96e89..05247a3 100644 --- a/src/main/java/Program.java +++ b/src/main/java/Program.java @@ -27,20 +27,20 @@ public static void main(String[] args) { } public void run() { - for (int i = 0; i < 10; i++){ + for (int i = 0; i < 10; i++) { RadarTarget rt1 = new RadarTarget( - NxfRadar.RadarTarget.newBuilder() - .setReportedAltitude(10000) - .setBeaconCode(ByteString.fromHex("0480")) - .setReturnTime(i) - .build() + NxfRadar.RadarTarget.newBuilder() + .setReportedAltitude(10000) + .setBeaconCode(ByteString.fromHex("0480")) + .setReturnTime(i) + .build() ); RadarTarget rt2 = new RadarTarget( - NxfRadar.RadarTarget.newBuilder() - .setReportedAltitude(12000) - .setBeaconCode(ByteString.fromHex("010481")) // should fail - .setReturnTime(i + 10) - .build() + NxfRadar.RadarTarget.newBuilder() + .setReportedAltitude(12000) + .setBeaconCode(ByteString.fromHex("010481")) // should fail + .setReturnTime(i + 10) + .build() ); manager.addTarget(rt1, "N1"); @@ -48,23 +48,25 @@ public void run() { } System.out.println("Showing all RadarTarget->Track..."); - manager.getTargetTrackMap().forEach((radarTarget, track) -> - System.out.println(radarTarget.getReturnTime().toEpochMilli()/1000 + ": " + track.getFlightData().getCallsign()) + manager.getTargetTrackMap().forEach((radarTarget, track) -> { + System.out.println("radarTarget.getBeaconCode: " + radarTarget.getBeaconCode()); + System.out.println(radarTarget.getReturnTime().toEpochMilli() / 1000 + ": " + track.getFlightData().getCallsign()); + } ); System.out.println("Showing all Track->List..."); - ListMultimap trackRadarTargetListMultimap = manager.getTrackRadarTargetMultiMap(); - trackRadarTargetListMultimap.asMap().forEach((track, radarTargetList) -> { + ListMultimap trackRadarTargetListMultiMap = manager.getTrackRadarTargetMultiMap(); + trackRadarTargetListMultiMap.asMap().forEach((track, radarTargetList) -> { System.out.print(track.getFlightData().getCallsign() + ": "); for (RadarTarget radarTarget : radarTargetList) { - System.out.print(radarTarget.getReturnTime().toEpochMilli()/1000 + " "); + System.out.print(radarTarget.getReturnTime().toEpochMilli() / 1000 + " "); } System.out.println(); }); System.out.println("Showing all String->Track..."); manager.getTrackByCallsignMap().forEach((s, track) -> - System.out.println(s + ": " + track.getFlightData().getCallsign()) + System.out.println(s + ": " + track.getFlightData().getCallsign()) ); } } From 6635abbee39c458d8b8c18e7fc8dd0a1568e3a1a Mon Sep 17 00:00:00 2001 From: Yi Zhang Date: Wed, 1 Jun 2022 19:34:26 -0400 Subject: [PATCH 08/10] fix validation, add validation testing --- build.gradle | 10 +- src/main/java/Program.java | 1 - .../java/io/wtmsb/nxf/domain/RadarTarget.java | 17 +++- .../nxf/validation/IcaoAddressValidator.java | 2 +- .../io/wtmsb/nxf/domain/RadarTargetTest.java | 95 +++++++++++++++++++ 5 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 src/test/java/io/wtmsb/nxf/domain/RadarTargetTest.java diff --git a/build.gradle b/build.gradle index da5f625..44d7503 100644 --- a/build.gradle +++ b/build.gradle @@ -60,12 +60,20 @@ dependencies { implementation 'com.google.guava:guava:31.1-jre' implementation 'org.springframework:spring-core:5.3.20' implementation 'org.springframework:spring-context:5.3.20' + implementation 'org.hibernate.validator:hibernate-validator:6.2.3.Final' + implementation 'org.hibernate.validator:hibernate-validator-cdi:6.2.3.Final' + implementation 'org.glassfish:jakarta.el:3.0.3' implementation 'jakarta.validation:jakarta.validation-api:2.0.2' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + + testImplementation platform('org.junit:junit-bom:5.8.2') + testImplementation 'org.junit.jupiter:junit-jupiter' } test { useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } } afterEvaluate { diff --git a/src/main/java/Program.java b/src/main/java/Program.java index 05247a3..3df5867 100644 --- a/src/main/java/Program.java +++ b/src/main/java/Program.java @@ -23,7 +23,6 @@ public static void main(String[] args) { var bean = ctx.getBean(Program.class); bean.run(); } - } public void run() { diff --git a/src/main/java/io/wtmsb/nxf/domain/RadarTarget.java b/src/main/java/io/wtmsb/nxf/domain/RadarTarget.java index bb5ac9f..d7c614b 100644 --- a/src/main/java/io/wtmsb/nxf/domain/RadarTarget.java +++ b/src/main/java/io/wtmsb/nxf/domain/RadarTarget.java @@ -8,6 +8,7 @@ import javax.validation.constraints.Max; import javax.validation.constraints.Min; +import javax.validation.constraints.PastOrPresent; import java.time.Instant; @Getter @@ -18,7 +19,7 @@ public final class RadarTarget implements IRadarComponent { @Min(-180) @Max(180) private final double lon; - @NonNull @Min(0) + @NonNull @PastOrPresent private final Instant returnTime; @BeaconCode @@ -50,27 +51,41 @@ public RadarTarget(long customReturnTime) { returnTime = Instant.ofEpochSecond(customReturnTime); } + /** + * Create an immutable {@link RadarTarget}.
+ * + * Field {@link RadarTarget#transponderMode} is determined by checking if certain fields + * are set in {@link NxfRadar.RadarTarget} + * + * @param rtMsg {@link NxfRadar.RadarTarget} message + */ public RadarTarget(NxfRadar.RadarTarget rtMsg) { lat = rtMsg.getLat(); lon = rtMsg.getLon(); returnTime = Instant.ofEpochSecond(rtMsg.getReturnTime()); if (rtMsg.hasModeSAddress() && rtMsg.hasReportedAltitude() && rtMsg.hasBeaconCode()) { + // must have all three set to be made a mode S target transponderMode = TransponderMode.MODE_S; beaconCode = rtMsg.getBeaconCode(); reportedAltitude = rtMsg.getReportedAltitude(); modeSAddress = rtMsg.getModeSAddress(); } else if (rtMsg.hasReportedAltitude() && rtMsg.hasBeaconCode()) { + // must have reported altitude and beacon code set to be made a mode C target transponderMode = TransponderMode.MODE_C; beaconCode = rtMsg.getBeaconCode(); reportedAltitude = rtMsg.getReportedAltitude(); modeSAddress = DEFAULT_ICAO_ADDRESS; } else if (rtMsg.hasBeaconCode()) { + // must have beacon code set to be made a mode A target transponderMode = TransponderMode.MODE_A; beaconCode = rtMsg.getBeaconCode(); reportedAltitude = 0; modeSAddress = DEFAULT_ICAO_ADDRESS; } else { + // if message has none of the three fields, + // or if the fields are set incorrectly (e.g. reported altitude and address are set but beacon code is not) + // the target will be made a primary target with all fields set to default transponderMode = TransponderMode.PRIMARY; beaconCode = DEFAULT_BEACON_CODE; reportedAltitude = 0; diff --git a/src/main/java/io/wtmsb/nxf/validation/IcaoAddressValidator.java b/src/main/java/io/wtmsb/nxf/validation/IcaoAddressValidator.java index 356faf4..be79493 100644 --- a/src/main/java/io/wtmsb/nxf/validation/IcaoAddressValidator.java +++ b/src/main/java/io/wtmsb/nxf/validation/IcaoAddressValidator.java @@ -12,6 +12,6 @@ public void initialize(IcaoAddress icaoAddress) {} @Override public boolean isValid(ByteString bs, ConstraintValidatorContext constraintValidatorContext) { int ICAO_ADDRESS_SIZE = 3; - return bs != null && bs.size() != ICAO_ADDRESS_SIZE; + return bs != null && bs.size() == ICAO_ADDRESS_SIZE; } } diff --git a/src/test/java/io/wtmsb/nxf/domain/RadarTargetTest.java b/src/test/java/io/wtmsb/nxf/domain/RadarTargetTest.java new file mode 100644 index 0000000..fd0e34b --- /dev/null +++ b/src/test/java/io/wtmsb/nxf/domain/RadarTargetTest.java @@ -0,0 +1,95 @@ +package io.wtmsb.nxf.domain; + +import com.google.protobuf.ByteString; +import io.wtmsb.nxf.message.radar.NxfRadar; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RadarTargetTest { + private static Validator validator; + + @BeforeAll + public static void setUp() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + validator = factory.getValidator(); + } + } + + /** + * Testing malformed beacon code.
+ * + * @see RadarTarget#RadarTarget(NxfRadar.RadarTarget) + */ + @Test + public void malformedBeaconCode() { + /* + If NxfRadar.RadarTarget#hasBeaconCode() == false, RadarTarget#RadarTarget(NxfRadar.RadarTarget) + creates a primary target with default beacon code (i.e. validation will pass). + */ + List malformedBeaconCodeTargets = new LinkedList<>(Arrays.asList( + new RadarTarget(NxfRadar.RadarTarget.newBuilder() + .setBeaconCode(ByteString.fromHex("")).build()), + new RadarTarget(NxfRadar.RadarTarget.newBuilder() + .setBeaconCode(ByteString.fromHex("FF")).build()), + new RadarTarget(NxfRadar.RadarTarget.newBuilder() + .setBeaconCode(ByteString.fromHex("FFFF")).build()), + new RadarTarget(NxfRadar.RadarTarget.newBuilder() + .setBeaconCode(ByteString.fromHex("123456")).build()) + )); + + for (RadarTarget target : malformedBeaconCodeTargets) { + Set> violationSet = validator.validate(target); + + assertEquals(1, violationSet.size()); + assertEquals( + "{io.wtmsb.nxf.validation.BeaconCode.message}", + violationSet.iterator().next().getMessage() + ); + } + } + + /** + * Testing malformed icao 24-bit address.
+ * + * @see RadarTarget#RadarTarget(NxfRadar.RadarTarget) + */ + @Test + public void malformedIcaoAddress() { + List malformedIcaoAddressTargets = new LinkedList<>(Arrays.asList( + new RadarTarget(NxfRadar.RadarTarget.newBuilder() + .setBeaconCode(ByteString.fromHex("0480")) + .setReportedAltitude(10000) + .setModeSAddress(ByteString.fromHex("")).build()), + new RadarTarget(NxfRadar.RadarTarget.newBuilder() + .setBeaconCode(ByteString.fromHex("0480")) + .setReportedAltitude(10000) + .setModeSAddress(ByteString.fromHex("00")).build()), + new RadarTarget(NxfRadar.RadarTarget.newBuilder() + .setBeaconCode(ByteString.fromHex("0480")) + .setReportedAltitude(10000) + .setModeSAddress(ByteString.fromHex("0000")).build()), + new RadarTarget(NxfRadar.RadarTarget.newBuilder() + .setBeaconCode(ByteString.fromHex("0480")) + .setReportedAltitude(10000) + .setModeSAddress(ByteString.fromHex("00000000")).build()) + )); + + for (RadarTarget target : malformedIcaoAddressTargets) { + Set> violationSet = validator.validate(target); + + assertEquals(1, violationSet.size()); + assertEquals( + "{io.wtmsb.nxf.validation.IcaoAddress.message}", + violationSet.iterator().next().getMessage() + ); + } + } +} From f24c34caae8cc3bb0a5be7daaa0c1a55fc4c29b9 Mon Sep 17 00:00:00 2001 From: Yi Zhang Date: Thu, 2 Jun 2022 01:05:10 -0400 Subject: [PATCH 09/10] add more validation --- .../io/wtmsb/nxf/domain/ControllingUnit.java | 7 ++- .../java/io/wtmsb/nxf/domain/FlightData.java | 27 +++++----- .../nxf/domain/FlightDataSupplement.java | 10 ++-- .../io/wtmsb/nxf/manager/TrackManager.java | 5 +- .../nxf/validation/AlphanumericString.java | 24 +++++++++ .../AlphanumericStringValidator.java | 28 +++++++++++ .../io/wtmsb/nxf/validation/Callsign.java | 24 +++++++++ .../nxf/validation/CallsignValidator.java | 26 ++++++++++ .../io/wtmsb/nxf/validation/RouteString.java | 22 +++++++++ .../nxf/validation/RouteStringValidator.java | 26 ++++++++++ .../io/wtmsb/nxf/domain/FlightDataTest.java | 49 +++++++++++++++++++ 11 files changed, 221 insertions(+), 27 deletions(-) create mode 100644 src/main/java/io/wtmsb/nxf/validation/AlphanumericString.java create mode 100644 src/main/java/io/wtmsb/nxf/validation/AlphanumericStringValidator.java create mode 100644 src/main/java/io/wtmsb/nxf/validation/Callsign.java create mode 100644 src/main/java/io/wtmsb/nxf/validation/CallsignValidator.java create mode 100644 src/main/java/io/wtmsb/nxf/validation/RouteString.java create mode 100644 src/main/java/io/wtmsb/nxf/validation/RouteStringValidator.java create mode 100644 src/test/java/io/wtmsb/nxf/domain/FlightDataTest.java diff --git a/src/main/java/io/wtmsb/nxf/domain/ControllingUnit.java b/src/main/java/io/wtmsb/nxf/domain/ControllingUnit.java index 87f1b3a..1fc8e8c 100644 --- a/src/main/java/io/wtmsb/nxf/domain/ControllingUnit.java +++ b/src/main/java/io/wtmsb/nxf/domain/ControllingUnit.java @@ -2,19 +2,18 @@ import io.wtmsb.nxf.manager.ControllerManager; import io.wtmsb.nxf.message.radar.NxfRadar; +import io.wtmsb.nxf.validation.AlphanumericString; import lombok.*; -import javax.validation.constraints.Size; - /** * Immutable, shared object managed by {@link ControllerManager} */ @Getter @AllArgsConstructor @EqualsAndHashCode public final class ControllingUnit { - @NonNull @Size(max = 3) + @NonNull @AlphanumericString(maxLength = 3) private final String facility; - @NonNull @Size(max = 5) + @NonNull @AlphanumericString(maxLength = 3) private final String sector; public ControllingUnit(NxfRadar.ControllingUnit cuMessage) { diff --git a/src/main/java/io/wtmsb/nxf/domain/FlightData.java b/src/main/java/io/wtmsb/nxf/domain/FlightData.java index 5bfcb3d..3474f67 100644 --- a/src/main/java/io/wtmsb/nxf/domain/FlightData.java +++ b/src/main/java/io/wtmsb/nxf/domain/FlightData.java @@ -3,13 +3,12 @@ import com.google.protobuf.ByteString; import io.wtmsb.nxf.manager.ControllerManager; import io.wtmsb.nxf.message.radar.NxfRadar; -import io.wtmsb.nxf.validation.BeaconCode; -import io.wtmsb.nxf.validation.IcaoAddress; +import io.wtmsb.nxf.validation.*; import lombok.*; import org.springframework.util.StringUtils; import javax.validation.constraints.Max; -import javax.validation.constraints.Size; +import javax.validation.constraints.PastOrPresent; import java.time.Instant; /** @@ -18,7 +17,7 @@ @Getter @Setter @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class FlightData implements IRadarComponent { - @NonNull @Size(min = 2, max = 10) + @Callsign @EqualsAndHashCode.Include private String callsign; @@ -36,29 +35,29 @@ public class FlightData implements IRadarComponent { @EqualsAndHashCode.Include private ByteString assignedBeaconCode = DEFAULT_BEACON_CODE; - @NonNull + @NonNull @PastOrPresent @EqualsAndHashCode.Include private Instant lastUpdated = Instant.now(); - @NonNull @Size(max = 4) + @AlphanumericString(maxLength = 4) private String aircraftType = ""; - @NonNull @Size(max = 1) + @AlphanumericString(maxLength = 1) private String equipmentSuffix = ""; @NonNull private FlightRule flightRule = FlightRule.INSTRUMENT; - @NonNull @Size(max = 4) + @AlphanumericString(maxLength = 4) private String departurePoint = ""; - @NonNull @Size(max = 4) + @AlphanumericString(maxLength = 4) private String destination = ""; @NonNull @Max(MAX_ALTITUDE) private Integer requestedAltitude = 0; - @NonNull @Size(max = 2000) + @RouteString(maxLength = 2000) private String routeString = ""; @NonNull @@ -76,12 +75,8 @@ public class FlightData implements IRadarComponent { @NonNull private FlightDataSupplement supplement = FlightDataSupplement.getDefault(); - public FlightData(@NonNull String _callsign) { - if (!StringUtils.hasText(_callsign)) { - throw new IllegalArgumentException("Invalid callsign in FlightData message"); - } - // TODO: use {@link org.apache.commons.lang3.StringUtils} - callsign = StringUtils.trimWhitespace(_callsign); + public FlightData(@Callsign String _callsign) { + callsign = _callsign; } public FlightData(NxfRadar.FlightData fDataMessage) { diff --git a/src/main/java/io/wtmsb/nxf/domain/FlightDataSupplement.java b/src/main/java/io/wtmsb/nxf/domain/FlightDataSupplement.java index 36625f2..10ad311 100644 --- a/src/main/java/io/wtmsb/nxf/domain/FlightDataSupplement.java +++ b/src/main/java/io/wtmsb/nxf/domain/FlightDataSupplement.java @@ -1,10 +1,10 @@ package io.wtmsb.nxf.domain; import io.wtmsb.nxf.message.radar.NxfRadar; +import io.wtmsb.nxf.validation.RouteString; import lombok.*; import javax.validation.constraints.Max; -import javax.validation.constraints.Size; import static io.wtmsb.nxf.domain.IRadarComponent.*; @@ -18,16 +18,16 @@ public final class FlightDataSupplement { @NonNull @Max(MAX_ALTITUDE) private Integer assignedFinalAltitude; - @NonNull @Size(max = 5) + @NonNull @RouteString(maxLength = 5) private String pad1; - @NonNull @Size(max = 5) + @NonNull @RouteString(maxLength = 5) private String pad2; - @NonNull @Size(max = 3) + @NonNull @RouteString(maxLength = 3) private String runway; - @NonNull @Size(max = 5) + @NonNull @RouteString(maxLength = 5) private String exitFix; @NonNull diff --git a/src/main/java/io/wtmsb/nxf/manager/TrackManager.java b/src/main/java/io/wtmsb/nxf/manager/TrackManager.java index 21343bb..5a652e6 100644 --- a/src/main/java/io/wtmsb/nxf/manager/TrackManager.java +++ b/src/main/java/io/wtmsb/nxf/manager/TrackManager.java @@ -5,6 +5,7 @@ import io.wtmsb.nxf.domain.RadarTarget; import io.wtmsb.nxf.domain.Track; import io.wtmsb.nxf.utility.GeoCalculator; +import io.wtmsb.nxf.validation.Callsign; import lombok.NonNull; import lombok.Synchronized; import org.springframework.beans.factory.annotation.Autowired; @@ -56,7 +57,7 @@ public Map getTrackByCallsignMap() { return trackByCallsignMap; } - public void addTarget(RadarTarget target, @NonNull String callsign) { + public void addTarget(RadarTarget target, @Callsign String callsign) { if (callsign.length() == 0) { addTargetAndAutoCorrelate(target); } else { @@ -87,7 +88,7 @@ private void addTargetAndAutoCorrelate(RadarTarget target) { * @param callsignHint hint from Nxf data source */ @Synchronized - private void addCorrelatedTarget(RadarTarget target, String callsignHint) { + private void addCorrelatedTarget(RadarTarget target, @Callsign String callsignHint) { Track correlatedTrack = trackByCallsignMap.computeIfAbsent(callsignHint, Track::new); trackTargetMultiMap.put(correlatedTrack, target); targetTrackMap.put(target, correlatedTrack); diff --git a/src/main/java/io/wtmsb/nxf/validation/AlphanumericString.java b/src/main/java/io/wtmsb/nxf/validation/AlphanumericString.java new file mode 100644 index 0000000..635a1ff --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/validation/AlphanumericString.java @@ -0,0 +1,24 @@ +package io.wtmsb.nxf.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; + +@Constraint(validatedBy = AlphanumericStringValidator.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({METHOD, FIELD, ANNOTATION_TYPE, PARAMETER}) +public @interface AlphanumericString { + String message() default "{io.wtmsb.nxf.validation.AlphanumericString.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + int minLength() default 0; + + int maxLength() default Integer.MAX_VALUE; +} diff --git a/src/main/java/io/wtmsb/nxf/validation/AlphanumericStringValidator.java b/src/main/java/io/wtmsb/nxf/validation/AlphanumericStringValidator.java new file mode 100644 index 0000000..e3b0456 --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/validation/AlphanumericStringValidator.java @@ -0,0 +1,28 @@ +package io.wtmsb.nxf.validation; + +import org.springframework.util.StringUtils; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.regex.Pattern; + +public class AlphanumericStringValidator implements ConstraintValidator { + private static final Pattern illegalCharacterPattern = Pattern.compile("[^A-Za-z]"); + + private int minLength, maxLength; + + public void initialize(AlphanumericString constraint) { + minLength = constraint.minLength(); + maxLength = constraint.maxLength(); + } + + @Override + public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { + if (StringUtils.hasText(s)) + return minLength <= s.length() && s.length() <= maxLength && + !illegalCharacterPattern.asPredicate().test(s); + + // otherwise minLength must be 0 and s must be empty + return minLength == 0 && s != null && s.length() == 0; + } +} diff --git a/src/main/java/io/wtmsb/nxf/validation/Callsign.java b/src/main/java/io/wtmsb/nxf/validation/Callsign.java new file mode 100644 index 0000000..819cfd8 --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/validation/Callsign.java @@ -0,0 +1,24 @@ +package io.wtmsb.nxf.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; + +@Constraint(validatedBy = CallsignValidator.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({METHOD, FIELD, ANNOTATION_TYPE, PARAMETER}) +public @interface Callsign { + String message() default "{io.wtmsb.nxf.validation.Callsign.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + int minLength() default 2; + + int maxLength() default 10; +} diff --git a/src/main/java/io/wtmsb/nxf/validation/CallsignValidator.java b/src/main/java/io/wtmsb/nxf/validation/CallsignValidator.java new file mode 100644 index 0000000..2a67d2d --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/validation/CallsignValidator.java @@ -0,0 +1,26 @@ +package io.wtmsb.nxf.validation; + +import org.springframework.util.StringUtils; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.regex.Pattern; + +public class CallsignValidator implements ConstraintValidator { + + public static final Pattern illegalCharacterPattern = Pattern.compile("[^a-zA-Z\\d]"); + + private int minLength, maxLength; + + public void initialize(Callsign constraint) { + minLength = constraint.minLength(); + maxLength = constraint.maxLength(); + } + + @Override + public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { + return StringUtils.hasText(s) && + minLength <= s.length() && s.length() <= maxLength && + !illegalCharacterPattern.asPredicate().test(s); + } +} diff --git a/src/main/java/io/wtmsb/nxf/validation/RouteString.java b/src/main/java/io/wtmsb/nxf/validation/RouteString.java new file mode 100644 index 0000000..702524a --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/validation/RouteString.java @@ -0,0 +1,22 @@ +package io.wtmsb.nxf.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; + +@Constraint(validatedBy = RouteStringValidator.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({METHOD, FIELD, ANNOTATION_TYPE, PARAMETER}) +public @interface RouteString { + String message() default "{io.wtmsb.nxf.validation.RouteString.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + int maxLength() default Integer.MAX_VALUE; +} diff --git a/src/main/java/io/wtmsb/nxf/validation/RouteStringValidator.java b/src/main/java/io/wtmsb/nxf/validation/RouteStringValidator.java new file mode 100644 index 0000000..34b7200 --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/validation/RouteStringValidator.java @@ -0,0 +1,26 @@ +package io.wtmsb.nxf.validation; + +import org.springframework.util.StringUtils; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.regex.Pattern; + +public class RouteStringValidator implements ConstraintValidator { + private static final Pattern illegalCharacterPattern = Pattern.compile("/[^A-Za-z\\d. +-]/"); + + private int maxLength; + + public void initialize(RouteString constraint) { + maxLength = constraint.maxLength(); + } + + @Override + public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { + if (StringUtils.hasText(s)) + return s.length() < maxLength && !illegalCharacterPattern.asPredicate().test(s); + + // otherwise s must be empty + return s != null && s.length() == 0; + } +} diff --git a/src/test/java/io/wtmsb/nxf/domain/FlightDataTest.java b/src/test/java/io/wtmsb/nxf/domain/FlightDataTest.java new file mode 100644 index 0000000..0e53ad1 --- /dev/null +++ b/src/test/java/io/wtmsb/nxf/domain/FlightDataTest.java @@ -0,0 +1,49 @@ +package io.wtmsb.nxf.domain; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import static io.wtmsb.nxf.validation.CallsignValidator.illegalCharacterPattern; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class FlightDataTest { + private static Validator validator; + + @BeforeAll + public static void setUp() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + validator = factory.getValidator(); + } + } + + @Test + public void malformedCallsign() { + List malformedCallsignFDs = new LinkedList<>(Arrays.asList( + new FlightData((String) null), // null + new FlightData(""), // empty + new FlightData(" "), // contains no text + new FlightData("N"), // under length + new FlightData("N1234567890123"), // over length + new FlightData("\u00a0JBU1\u2000") // contains illegal character + )); + + for (FlightData data : malformedCallsignFDs) { + Set> violationSet = validator.validate(data); + + assertEquals(1, violationSet.size()); + assertEquals( + "{io.wtmsb.nxf.validation.Callsign.message}", + violationSet.iterator().next().getMessage() + ); + } + } +} From 1b1b3fdf756a79fca9eee2294ae93f3202bfa42b Mon Sep 17 00:00:00 2001 From: Yi Zhang Date: Wed, 6 Jul 2022 15:37:47 -0400 Subject: [PATCH 10/10] restarting project... --- build.gradle | 6 ++++- .../java/io/wtmsb/nxf/domain/RadarTarget.java | 1 + .../java/io/wtmsb/nxf/gateway/NxfGateway.java | 22 +++++++++++++++++++ .../wtmsb/nxf/manager/ControllerManager.java | 8 +++---- .../io/wtmsb/nxf/manager/TrackManager.java | 1 - .../AlphanumericStringValidator.java | 2 +- 6 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 src/main/java/io/wtmsb/nxf/gateway/NxfGateway.java diff --git a/build.gradle b/build.gradle index 44d7503..555a839 100644 --- a/build.gradle +++ b/build.gradle @@ -55,7 +55,7 @@ sourceSets { dependencies { implementation 'com.github.jitpack:gradle-simple:1.1' - implementation 'com.google.protobuf:protobuf-java:3.20.1' + implementation 'com.google.protobuf:protobuf-java:3.21.1' implementation 'org.locationtech.spatial4j:spatial4j:0.8' implementation 'com.google.guava:guava:31.1-jre' implementation 'org.springframework:spring-core:5.3.20' @@ -65,6 +65,10 @@ dependencies { implementation 'org.glassfish:jakarta.el:3.0.3' implementation 'jakarta.validation:jakarta.validation-api:2.0.2' + implementation 'org.slf4j:jul-to-slf4j:1.7.36' + implementation 'org.apache.logging.log4j:log4j-to-slf4j:2.18.0' + implementation 'ch.qos.logback:logback-classic:1.2.11' + testImplementation platform('org.junit:junit-bom:5.8.2') testImplementation 'org.junit.jupiter:junit-jupiter' } diff --git a/src/main/java/io/wtmsb/nxf/domain/RadarTarget.java b/src/main/java/io/wtmsb/nxf/domain/RadarTarget.java index d7c614b..85ea26c 100644 --- a/src/main/java/io/wtmsb/nxf/domain/RadarTarget.java +++ b/src/main/java/io/wtmsb/nxf/domain/RadarTarget.java @@ -16,6 +16,7 @@ public final class RadarTarget implements IRadarComponent { @Min(-90) @Max(90) private final double lat; + @Min(-180) @Max(180) private final double lon; diff --git a/src/main/java/io/wtmsb/nxf/gateway/NxfGateway.java b/src/main/java/io/wtmsb/nxf/gateway/NxfGateway.java new file mode 100644 index 0000000..b0f05d2 --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/gateway/NxfGateway.java @@ -0,0 +1,22 @@ +package io.wtmsb.nxf.gateway; + +import io.wtmsb.nxf.manager.ControllerManager; +import io.wtmsb.nxf.manager.TrackManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class NxfGateway { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @Autowired + private TrackManager trackManager; + + @Autowired + private ControllerManager controllerManager; + + +} diff --git a/src/main/java/io/wtmsb/nxf/manager/ControllerManager.java b/src/main/java/io/wtmsb/nxf/manager/ControllerManager.java index b375d38..2af56a4 100644 --- a/src/main/java/io/wtmsb/nxf/manager/ControllerManager.java +++ b/src/main/java/io/wtmsb/nxf/manager/ControllerManager.java @@ -6,16 +6,16 @@ import io.wtmsb.nxf.message.radar.NxfRadar; import lombok.NonNull; import lombok.Synchronized; -import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; @Service public final class ControllerManager { - private static final ControllingUnit uncontrolledUnit = - new ControllingUnit(NxfRadar.ControllingUnit.getDefaultInstance()); - private static final Table cache = HashBasedTable.create(); + private static final ControllingUnit uncontrolledUnit; + private static final Table cache; static { + uncontrolledUnit = new ControllingUnit(NxfRadar.ControllingUnit.getDefaultInstance()); + cache = HashBasedTable.create(); cache.put(uncontrolledUnit.getFacility(), uncontrolledUnit.getSector(), uncontrolledUnit); } diff --git a/src/main/java/io/wtmsb/nxf/manager/TrackManager.java b/src/main/java/io/wtmsb/nxf/manager/TrackManager.java index 5a652e6..8d8c683 100644 --- a/src/main/java/io/wtmsb/nxf/manager/TrackManager.java +++ b/src/main/java/io/wtmsb/nxf/manager/TrackManager.java @@ -6,7 +6,6 @@ import io.wtmsb.nxf.domain.Track; import io.wtmsb.nxf.utility.GeoCalculator; import io.wtmsb.nxf.validation.Callsign; -import lombok.NonNull; import lombok.Synchronized; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; diff --git a/src/main/java/io/wtmsb/nxf/validation/AlphanumericStringValidator.java b/src/main/java/io/wtmsb/nxf/validation/AlphanumericStringValidator.java index e3b0456..d52893c 100644 --- a/src/main/java/io/wtmsb/nxf/validation/AlphanumericStringValidator.java +++ b/src/main/java/io/wtmsb/nxf/validation/AlphanumericStringValidator.java @@ -7,7 +7,7 @@ import java.util.regex.Pattern; public class AlphanumericStringValidator implements ConstraintValidator { - private static final Pattern illegalCharacterPattern = Pattern.compile("[^A-Za-z]"); + private static final Pattern illegalCharacterPattern = Pattern.compile("[^A-Za-z\\d]"); private int minLength, maxLength;