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..555a839 100644 --- a/build.gradle +++ b/build.gradle @@ -55,12 +55,29 @@ sourceSets { dependencies { implementation 'com.github.jitpack:gradle-simple:1.1' - implementation 'com.google.protobuf:protobuf-java:3.20.1' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + 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' + 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' + + 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' } test { useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } } afterEvaluate { diff --git a/src/main/java/Program.java b/src/main/java/Program.java new file mode 100644 index 0000000..3df5867 --- /dev/null +++ b/src/main/java/Program.java @@ -0,0 +1,71 @@ +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.function.BiConsumer; + +@Component +@ComponentScan(basePackages = "io.wtmsb.nxf.manager") +public class Program { + @Autowired + private TrackManager manager; + + public static void main(String[] args) { + try (GenericApplicationContext ctx = new AnnotationConfigApplicationContext(Program.class)) { + var bean = ctx.getBean(Program.class); + bean.run(); + } + } + + public void run() { + for (int i = 0; i < 10; i++) { + 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"); + } + + System.out.println("Showing all RadarTarget->Track..."); + 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) -> { + 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/ControllingUnit.java b/src/main/java/io/wtmsb/nxf/domain/ControllingUnit.java new file mode 100644 index 0000000..1fc8e8c --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/domain/ControllingUnit.java @@ -0,0 +1,23 @@ +package io.wtmsb.nxf.domain; + +import io.wtmsb.nxf.manager.ControllerManager; +import io.wtmsb.nxf.message.radar.NxfRadar; +import io.wtmsb.nxf.validation.AlphanumericString; +import lombok.*; + +/** + * Immutable, shared object managed by {@link ControllerManager} + */ +@Getter @AllArgsConstructor @EqualsAndHashCode +public final class ControllingUnit { + @NonNull @AlphanumericString(maxLength = 3) + private final String facility; + + @NonNull @AlphanumericString(maxLength = 3) + private final String sector; + + public ControllingUnit(NxfRadar.ControllingUnit cuMessage) { + facility = cuMessage.getFacility(); + sector = cuMessage.getSector(); + } +} 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..3474f67 --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/domain/FlightData.java @@ -0,0 +1,95 @@ +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.*; +import lombok.*; +import org.springframework.util.StringUtils; + +import javax.validation.constraints.Max; +import javax.validation.constraints.PastOrPresent; +import java.time.Instant; + +/** + * Mutable object managed by {@link io.wtmsb.nxf.manager.TrackManager} + */ +@Getter @Setter +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +public class FlightData implements IRadarComponent { + @Callsign + @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 @PastOrPresent + @EqualsAndHashCode.Include + private Instant lastUpdated = Instant.now(); + + @AlphanumericString(maxLength = 4) + private String aircraftType = ""; + + @AlphanumericString(maxLength = 1) + private String equipmentSuffix = ""; + + @NonNull + private FlightRule flightRule = FlightRule.INSTRUMENT; + + @AlphanumericString(maxLength = 4) + private String departurePoint = ""; + + @AlphanumericString(maxLength = 4) + private String destination = ""; + + @NonNull @Max(MAX_ALTITUDE) + private Integer requestedAltitude = 0; + + @RouteString(maxLength = 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(@Callsign String _callsign) { + callsign = _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..10ad311 --- /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 io.wtmsb.nxf.validation.RouteString; +import lombok.*; + +import javax.validation.constraints.Max; + +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 @RouteString(maxLength = 5) + private String pad1; + + @NonNull @RouteString(maxLength = 5) + private String pad2; + + @NonNull @RouteString(maxLength = 3) + private String runway; + + @NonNull @RouteString(maxLength = 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/IRadarComponent.java b/src/main/java/io/wtmsb/nxf/domain/IRadarComponent.java new file mode 100644 index 0000000..4bd9690 --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/domain/IRadarComponent.java @@ -0,0 +1,44 @@ +package io.wtmsb.nxf.domain; + +import com.google.protobuf.ByteString; +import io.wtmsb.nxf.validation.IcaoAddress; + +public interface IRadarComponent { + ByteString DEFAULT_BEACON_CODE = ByteString.fromHex("0000"); + ByteString DEFAULT_ICAO_ADDRESS = ByteString.fromHex("000000"); + int MAX_ALTITUDE = 100000; + + enum TransponderMode { + PRIMARY, 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 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; + } +} 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..85ea26c --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/domain/RadarTarget.java @@ -0,0 +1,99 @@ +package io.wtmsb.nxf.domain; + +import com.google.protobuf.ByteString; +import io.wtmsb.nxf.message.radar.NxfRadar; +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 javax.validation.constraints.PastOrPresent; +import java.time.Instant; + +@Getter +@EqualsAndHashCode @ToString +public final class RadarTarget implements IRadarComponent { + @Min(-90) @Max(90) + private final double lat; + + @Min(-180) @Max(180) + private final double lon; + + @NonNull @PastOrPresent + private final Instant returnTime; + + @BeaconCode + private final ByteString beaconCode; + + @Max(MAX_ALTITUDE) + private final int reportedAltitude; + + @IcaoAddress + private final ByteString modeSAddress; + + private final IRadarComponent.TransponderMode transponderMode; + + private final int groundSpeed; + + @Max(359) + private final int groundTrack; + + + 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); + } + + /** + * 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; + 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 new file mode 100644 index 0000000..afd75f6 --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/domain/Track.java @@ -0,0 +1,51 @@ +package io.wtmsb.nxf.domain; + +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 FlightData flightData; + + @Getter(value = AccessLevel.NONE) @Setter(value = AccessLevel.NONE) + private boolean isPrimary = true; + + @Getter(value = AccessLevel.NONE) @Setter(value = AccessLevel.NONE) + private boolean isCorrelated = false; + + /** + * 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/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/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/ControllerManager.java b/src/main/java/io/wtmsb/nxf/manager/ControllerManager.java new file mode 100644 index 0000000..2af56a4 --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/manager/ControllerManager.java @@ -0,0 +1,39 @@ +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.Service; + +@Service +public final class ControllerManager { + 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); + } + + @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.@NonNull ControllingUnit nxfCu) { + String facility = nxfCu.getFacility(), sector = nxfCu.getSector(); + return getControllingUnit(facility, sector); + } + + 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 new file mode 100644 index 0000000..8d8c683 --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/manager/TrackManager.java @@ -0,0 +1,161 @@ +package io.wtmsb.nxf.manager; + +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.ListMultimap; +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.Synchronized; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +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; + +@Component +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; + } + + @Synchronized + public Map getTargetTrackMap() { + return targetTrackMap; + } + + @Synchronized + public ListMultimap getTrackRadarTargetMultiMap() { + return trackTargetMultiMap; + } + + @Synchronized + public Map getTrackByCallsignMap() { + return trackByCallsignMap; + } + + public void addTarget(RadarTarget target, @Callsign String callsign) { + if (callsign.length() == 0) { + addTargetAndAutoCorrelate(target); + } else { + if (target.getTransponderMode() == TransponderMode.MODE_C || + target.getTransponderMode() == TransponderMode.MODE_S) { + // TODO: implement mode S correlation methods + addCorrelatedTarget(target, callsign); + } else { + throw new IllegalArgumentException(); + } + } + } + + /** + * 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"); + } + + /** + * 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, @Callsign String callsignHint) { + Track correlatedTrack = trackByCallsignMap.computeIfAbsent(callsignHint, Track::new); + trackTargetMultiMap.put(correlatedTrack, target); + targetTrackMap.put(target, correlatedTrack); + trimHistoryRadarTarget(correlatedTrack); + } + + /** + * 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 + 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()) { + RadarTarget closestTarget = _closestTarget.get(); + // associate the new target with the track associated with the closest target + 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(target, associatedTrack); + trackTargetMultiMap.put(associatedTrack, target); + + trimHistoryRadarTarget(associatedTrack); + } +} 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 9b5b19e..0000000 --- a/src/main/java/io/wtmsb/nxf/object/ControllingUnit.java +++ /dev/null @@ -1,21 +0,0 @@ -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 ControllingUnit { - @NonNull @Builder.Default - String facility = ""; - - @NonNull @Builder.Default - String sector = ""; - - public static ControllingUnit getDefault() { - return ControllingUnit.builder().build(); - } -} 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 83aef02..0000000 --- a/src/main/java/io/wtmsb/nxf/object/DataBlockSupplement.java +++ /dev/null @@ -1,40 +0,0 @@ -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 - } - - @NonNull @Builder.Default - LeaderLineDirection leaderLineDirection = LeaderLineDirection.DEFAULT; - - @NonNull - Integer assignedTemporaryAltitude; - - @NonNull @Builder.Default - String pad1 = ""; - - @NonNull @Builder.Default - String pad2 = ""; - - @NonNull @Builder.Default - String runway = ""; - - @NonNull @Builder.Default - String exitFix = ""; - - public static DataBlockSupplement getDefault() { - return DataBlockSupplement.builder().build(); - } -} 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 f2df099..0000000 --- a/src/main/java/io/wtmsb/nxf/object/FlightStrip.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.wtmsb.nxf.object; - -import lombok.*; - -import java.nio.ByteBuffer; - -@Getter -@Setter -@Builder(toBuilder = true) -public class FlightStrip { - @NonNull - private Integer id; - - @NonNull @Builder.Default - private String aircraftType = ""; - - @NonNull @Builder.Default - 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 @Builder.Default - private WakeCategory wakeCategory = WakeCategory.NO_WEIGHT; - - public enum FlightRule { - INSTRUMENT, VISUAL, SPECIAL_VISUAL, DEFENSE_VISUAL - } - - @NonNull @Builder.Default - private FlightRule flightRule = FlightRule.INSTRUMENT; - - @NonNull @Builder.Default - private String destination = ""; - - @NonNull - private Integer requestedAltitude; - - @NonNull @Builder.Default - private ByteBuffer assignedBeaconCode = ByteBuffer.allocate(2); - - @NonNull @Builder.Default - private String routeString = ""; - - public static FlightStrip getDefault() { - return FlightStrip.builder().build(); - } -} 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 edb26b6..0000000 --- a/src/main/java/io/wtmsb/nxf/object/RadarTarget.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.wtmsb.nxf.object; - -import lombok.Getter; -import lombok.NonNull; -import lombok.Setter; - -import java.nio.ByteBuffer; - -@Getter -@Setter -public final class RadarTarget { - @NonNull String lat; - @NonNull String lon; - - enum TransponderMode { - NO_MODE, MODE_A, MODE_C, MODE_S - } - - @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); - } -} 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 a160523..0000000 --- a/src/main/java/io/wtmsb/nxf/object/Track.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.wtmsb.nxf.object; - -import lombok.*; - -import java.util.List; -import java.util.Vector; - -@Getter -@Setter -@Builder(toBuilder = true) -public class Track { - @NonNull - private Integer id; - - @NonNull @Builder.Default - private List radarTargets = new Vector<>(); - - @NonNull @Builder.Default - private FlightStrip flightStrip = FlightStrip.getDefault(); - - @NonNull @Builder.Default - private ControllingUnit currentController = ControllingUnit.getDefault(); - - @NonNull @Builder.Default - private ControllingUnit nextController = ControllingUnit.getDefault(); - - // reserved 6 to 10 - @NonNull @Builder.Default - private DataBlockSupplement dataBlockSupplement = DataBlockSupplement.getDefault(); -} 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..6c96855 --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/utility/ClientMessenger.java @@ -0,0 +1,12 @@ +package io.wtmsb.nxf.utility; + +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(); + + 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..ecff1ec --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/utility/GeoCalculator.java @@ -0,0 +1,15 @@ +package io.wtmsb.nxf.utility; + +import io.wtmsb.nxf.domain.RadarTarget; +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(), + lon1 = tgt1.getLon(), lon2 = 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..38bf310 --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/utility/ServerMessenger.java @@ -0,0 +1,12 @@ +package io.wtmsb.nxf.utility; + +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(); + + return sb.build(); + } +} 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..d52893c --- /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\\d]"); + + 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/BeaconCode.java b/src/main/java/io/wtmsb/nxf/validation/BeaconCode.java new file mode 100644 index 0000000..0007f31 --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/validation/BeaconCode.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 = BeaconCodeValidator.class) +public @interface BeaconCode { + String message() default "{io.wtmsb.nxf.validation.BeaconCode.message}"; + Class[] groups() default {}; + Class[] payload() default {}; +} 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..3da8b96 --- /dev/null +++ b/src/main/java/io/wtmsb/nxf/validation/BeaconCodeValidator.java @@ -0,0 +1,26 @@ +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) { + return bs != null && bs.size() == BEACON_CODE_SIZE && checkBeaconCodeValue(bs); + } +} 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/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..be79493 --- /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/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/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; } 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() + ); + } + } +} 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() + ); + } + } +}