summaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
authorBen Burwell <ben.burwell@trifecta.com>2016-11-19 17:39:05 -0500
committerBen Burwell <ben.burwell@trifecta.com>2016-11-19 17:39:05 -0500
commit9441ed331af3aa6b3ef45bf165e40faad18bc7fd (patch)
treee4f9155ccce81e3cb2145a41a68dacff6671950a /src/main/java
parent4a7ae2831563622ebb4a1d893764afd0e0e0dfbe (diff)
Use gradle
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/com/benburwell/planes/data/Aircraft.java90
-rw-r--r--src/main/java/com/benburwell/planes/data/AircraftStore.java34
-rw-r--r--src/main/java/com/benburwell/planes/data/AircraftStoreListener.java9
-rw-r--r--src/main/java/com/benburwell/planes/data/NavigationAid.java187
-rw-r--r--src/main/java/com/benburwell/planes/data/Position.java46
-rw-r--r--src/main/java/com/benburwell/planes/gui/AircraftTableComponent.java26
-rw-r--r--src/main/java/com/benburwell/planes/gui/AircraftTableModel.java85
-rw-r--r--src/main/java/com/benburwell/planes/gui/GraphicsTheme.java27
-rw-r--r--src/main/java/com/benburwell/planes/gui/Main1090.java93
-rw-r--r--src/main/java/com/benburwell/planes/gui/MenuBarProvider.java41
-rw-r--r--src/main/java/com/benburwell/planes/gui/TCPConnectionOptionDialog.java45
-rw-r--r--src/main/java/com/benburwell/planes/gui/ViewComponent.java10
-rw-r--r--src/main/java/com/benburwell/planes/gui/aircraftmap/AircraftMap.java174
-rw-r--r--src/main/java/com/benburwell/planes/gui/aircraftmap/AircraftMapComponent.java106
-rw-r--r--src/main/java/com/benburwell/planes/gui/aircraftmap/Drawable.java10
-rw-r--r--src/main/java/com/benburwell/planes/gui/aircraftmap/GeoPoint.java36
-rw-r--r--src/main/java/com/benburwell/planes/gui/aircraftmap/Plane.java126
-rw-r--r--src/main/java/com/benburwell/planes/sbs/AggregateDataSource.java48
-rw-r--r--src/main/java/com/benburwell/planes/sbs/DataListener.java8
-rw-r--r--src/main/java/com/benburwell/planes/sbs/DataSource.java10
-rw-r--r--src/main/java/com/benburwell/planes/sbs/MalformedPacketException.java15
-rw-r--r--src/main/java/com/benburwell/planes/sbs/MessageType.java31
-rw-r--r--src/main/java/com/benburwell/planes/sbs/SBSPacket.java216
-rw-r--r--src/main/java/com/benburwell/planes/sbs/TCPDataSource.java99
-rw-r--r--src/main/java/com/benburwell/planes/sbs/TransmissionType.java34
-rw-r--r--src/main/java/com/benburwell/planes/sbs/UnrecognizedMessageTypeException.java20
-rw-r--r--src/main/java/com/benburwell/planes/sbs/UnrecognizedTransmissionTypeException.java20
27 files changed, 1646 insertions, 0 deletions
diff --git a/src/main/java/com/benburwell/planes/data/Aircraft.java b/src/main/java/com/benburwell/planes/data/Aircraft.java
new file mode 100644
index 0000000..66a7a46
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/data/Aircraft.java
@@ -0,0 +1,90 @@
+package com.benburwell.planes.data;
+
+import com.benburwell.planes.sbs.SBSPacket;
+
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * Created by ben on 11/15/16.
+ */
+public class Aircraft implements Comparable<Aircraft> {
+ private final String hexIdent;
+ private Position currentPosition = new Position();
+ private List<Position> positionHistory = new ArrayList<>();
+ private String callsign = "";
+ private String squawk = "";
+ private long packetCount = 0;
+ private double track;
+ private double groundSpeed;
+ private double verticalRate;
+
+ public Aircraft(String hexIdent) {
+ this.hexIdent = hexIdent;
+ }
+
+ public void handleUpdate(SBSPacket packet) {
+ this.packetCount++;
+ if (packet.getAltitude() != null) {
+ this.currentPosition.setAltitude(packet.getAltitude());
+ }
+ if (packet.getLatitude() != null) {
+ this.currentPosition.setLatitude(packet.getLatitude());
+ }
+ if (packet.getLongitude() != null) {
+ this.currentPosition.setLongitude(packet.getLongitude());
+ }
+ if (packet.getCallsign() != null && !packet.getCallsign().isEmpty()) {
+ this.callsign = packet.getCallsign();
+ }
+ if (packet.getSquawk() != null && !packet.getSquawk().isEmpty()) {
+ this.callsign = packet.getSquawk();
+ }
+ if (packet.getTrack() != null) {
+ this.track = packet.getTrack();
+ }
+ if (packet.getGroundSpeed() != null) {
+ this.groundSpeed = packet.getGroundSpeed();
+ }
+ if (packet.getVerticalRate() != null) {
+ this.verticalRate = packet.getVerticalRate();
+ }
+ }
+
+ public Position getCurrentPosition() {
+ return currentPosition;
+ }
+
+ public String getCallsign() {
+ return callsign;
+ }
+
+ public String getSquawk() {
+ return squawk;
+ }
+
+ public Long getPacketCount() {
+ return packetCount;
+ }
+
+ public String getHexIdent() {
+ return this.hexIdent;
+ }
+
+ public double getTrack() {
+ return this.track;
+ }
+
+ public double getGroundSpeed() {
+ return this.groundSpeed;
+ }
+
+ public double getVerticalRate() {
+ return this.verticalRate;
+ }
+
+ @Override
+ public int compareTo(Aircraft that) {
+ return this.getHexIdent().compareTo(that.getHexIdent());
+ }
+}
diff --git a/src/main/java/com/benburwell/planes/data/AircraftStore.java b/src/main/java/com/benburwell/planes/data/AircraftStore.java
new file mode 100644
index 0000000..076701d
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/data/AircraftStore.java
@@ -0,0 +1,34 @@
+package com.benburwell.planes.data;
+
+import com.benburwell.planes.sbs.SBSPacket;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * Created by ben on 11/17/16.
+ */
+public class AircraftStore {
+ private Map<String,Aircraft> aircraftMap = new HashMap<>();
+ private List<AircraftStoreListener> aircraftListeners = new ArrayList<>();
+
+ public Map<String,Aircraft> getAircraft() {
+ return this.aircraftMap;
+ }
+
+ public void addPacket(SBSPacket packet) {
+ if (!this.aircraftMap.containsKey(packet.getHexIdent())) {
+ this.aircraftMap.put(packet.getHexIdent(), new Aircraft(packet.getHexIdent()));
+ }
+ this.aircraftMap.get(packet.getHexIdent()).handleUpdate(packet);
+ this.aircraftListeners.stream()
+ .filter(listener -> listener.respondTo(packet.getHexIdent()))
+ .forEach(AircraftStoreListener::aircraftStoreChanged);
+ }
+
+ public void subscribe(AircraftStoreListener listener) {
+ this.aircraftListeners.add(listener);
+ }
+}
diff --git a/src/main/java/com/benburwell/planes/data/AircraftStoreListener.java b/src/main/java/com/benburwell/planes/data/AircraftStoreListener.java
new file mode 100644
index 0000000..2ef635f
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/data/AircraftStoreListener.java
@@ -0,0 +1,9 @@
+package com.benburwell.planes.data;
+
+/**
+ * Created by ben on 11/17/16.
+ */
+public interface AircraftStoreListener {
+ void aircraftStoreChanged();
+ boolean respondTo(String aircraftId);
+}
diff --git a/src/main/java/com/benburwell/planes/data/NavigationAid.java b/src/main/java/com/benburwell/planes/data/NavigationAid.java
new file mode 100644
index 0000000..09521a0
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/data/NavigationAid.java
@@ -0,0 +1,187 @@
+package com.benburwell.planes.data;
+
+/**
+ * Frequencies in kHz, elevations in ft
+ *
+ * Created by ben on 11/19/16.
+ */
+public class NavigationAid {
+ private int id;
+ private String filename;
+ private String ident;
+ private String type;
+ private int frequency;
+ private double latitude;
+ private double longitude;
+ private int elevation;
+ private String isoCountry;
+ private double dmeFrequency;
+ private String dmeChannel;
+ private double dmeLatitude;
+ private double dmeLongitude;
+ private int dmeElevation;
+ private double slavedVariation;
+ private double magneticVariation;
+ private String usageType;
+ private String power;
+ private String associatedAirport;
+
+ public NavigationAid() {}
+
+ public static NavigationAid fromCSV(String row) {
+ NavigationAid aid = new NavigationAid();
+ return aid;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getFilename() {
+ return filename;
+ }
+
+ public void setFilename(String filename) {
+ this.filename = filename;
+ }
+
+ public String getIdent() {
+ return ident;
+ }
+
+ public void setIdent(String ident) {
+ this.ident = ident;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public int getFrequency() {
+ return frequency;
+ }
+
+ public void setFrequency(int frequency) {
+ this.frequency = frequency;
+ }
+
+ public double getLatitude() {
+ return latitude;
+ }
+
+ public void setLatitude(double latitude) {
+ this.latitude = latitude;
+ }
+
+ public double getLongitude() {
+ return longitude;
+ }
+
+ public void setLongitude(double longitude) {
+ this.longitude = longitude;
+ }
+
+ public int getElevation() {
+ return elevation;
+ }
+
+ public void setElevation(int elevation) {
+ this.elevation = elevation;
+ }
+
+ public String getIsoCountry() {
+ return isoCountry;
+ }
+
+ public void setIsoCountry(String isoCountry) {
+ this.isoCountry = isoCountry;
+ }
+
+ public double getDmeFrequency() {
+ return dmeFrequency;
+ }
+
+ public void setDmeFrequency(double dmeFrequency) {
+ this.dmeFrequency = dmeFrequency;
+ }
+
+ public String getDmeChannel() {
+ return dmeChannel;
+ }
+
+ public void setDmeChannel(String dmeChannel) {
+ this.dmeChannel = dmeChannel;
+ }
+
+ public double getDmeLatitude() {
+ return dmeLatitude;
+ }
+
+ public void setDmeLatitude(double dmeLatitude) {
+ this.dmeLatitude = dmeLatitude;
+ }
+
+ public double getDmeLongitude() {
+ return dmeLongitude;
+ }
+
+ public void setDmeLongitude(double dmeLongitude) {
+ this.dmeLongitude = dmeLongitude;
+ }
+
+ public int getDmeElevation() {
+ return dmeElevation;
+ }
+
+ public void setDmeElevation(int dmeElevation) {
+ this.dmeElevation = dmeElevation;
+ }
+
+ public double getSlavedVariation() {
+ return slavedVariation;
+ }
+
+ public void setSlavedVariation(double slavedVariation) {
+ this.slavedVariation = slavedVariation;
+ }
+
+ public double getMagneticVariation() {
+ return magneticVariation;
+ }
+
+ public void setMagneticVariation(double magneticVariation) {
+ this.magneticVariation = magneticVariation;
+ }
+
+ public String getUsageType() {
+ return usageType;
+ }
+
+ public void setUsageType(String usageType) {
+ this.usageType = usageType;
+ }
+
+ public String getPower() {
+ return power;
+ }
+
+ public void setPower(String power) {
+ this.power = power;
+ }
+
+ public String getAssociatedAirport() {
+ return associatedAirport;
+ }
+
+ public void setAssociatedAirport(String associatedAirport) {
+ this.associatedAirport = associatedAirport;
+ }
+}
diff --git a/src/main/java/com/benburwell/planes/data/Position.java b/src/main/java/com/benburwell/planes/data/Position.java
new file mode 100644
index 0000000..4b37235
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/data/Position.java
@@ -0,0 +1,46 @@
+package com.benburwell.planes.data;
+
+import java.util.Date;
+
+/**
+ * Created by ben on 11/15/16.
+ */
+public class Position {
+ private Date timestamp = new Date(System.currentTimeMillis());
+ private Double latitude = 0.0;
+ private Double longitude = 0.0;
+
+ public Date getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(Date timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public double getLatitude() {
+ return latitude;
+ }
+
+ public void setLatitude(double latitude) {
+ this.latitude = latitude;
+ }
+
+ public double getLongitude() {
+ return longitude;
+ }
+
+ public void setLongitude(double longitude) {
+ this.longitude = longitude;
+ }
+
+ public double getAltitude() {
+ return altitude;
+ }
+
+ public void setAltitude(double altitude) {
+ this.altitude = altitude;
+ }
+
+ private double altitude = 0;
+}
diff --git a/src/main/java/com/benburwell/planes/gui/AircraftTableComponent.java b/src/main/java/com/benburwell/planes/gui/AircraftTableComponent.java
new file mode 100644
index 0000000..221bdda
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/gui/AircraftTableComponent.java
@@ -0,0 +1,26 @@
+package com.benburwell.planes.gui;
+
+import com.benburwell.planes.data.AircraftStore;
+
+import javax.swing.*;
+
+/**
+ * Created by ben on 11/17/16.
+ */
+public class AircraftTableComponent implements ViewComponent{
+ private JTable table;
+ private AircraftTableModel tableModel;
+ private JScrollPane scrollPane;
+
+ public AircraftTableComponent(AircraftStore store) {
+ this.tableModel = new AircraftTableModel(store);
+ this.table = new JTable(this.tableModel);
+ this.table.setFillsViewportHeight(true);
+ this.scrollPane = new JScrollPane(table);
+ }
+
+ @Override
+ public JComponent getComponent() {
+ return this.scrollPane;
+ }
+}
diff --git a/src/main/java/com/benburwell/planes/gui/AircraftTableModel.java b/src/main/java/com/benburwell/planes/gui/AircraftTableModel.java
new file mode 100644
index 0000000..3931893
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/gui/AircraftTableModel.java
@@ -0,0 +1,85 @@
+package com.benburwell.planes.gui;
+
+import com.benburwell.planes.data.Aircraft;
+import com.benburwell.planes.data.AircraftStore;
+import com.benburwell.planes.data.AircraftStoreListener;
+
+import javax.swing.table.AbstractTableModel;
+import java.util.Map;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * Created by ben on 11/15/16.
+ */
+public class AircraftTableModel extends AbstractTableModel {
+ private Map<String,Aircraft> aircraftMap;
+ private String[] columnNames = { "Hex", "Callsign", "Squawk", "Latitude", "Longitude", "Altitude", "Vertical Rate", "Track", "Ground Speed", "Packets" };
+
+ public AircraftTableModel(AircraftStore store) {
+ this.aircraftMap = store.getAircraft();
+ store.subscribe(new AircraftStoreListener() {
+ @Override
+ public void aircraftStoreChanged() {
+ AircraftTableModel.super.fireTableDataChanged();
+ }
+
+ @Override
+ public boolean respondTo(String aircraftId) {
+ // listen for all changes
+ return true;
+ }
+ });
+ }
+
+ @Override
+ public int getRowCount() {
+ return this.aircraftMap.keySet().size();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return this.columnNames.length;
+ }
+
+ @Override
+ public String getColumnName(int col) {
+ return this.columnNames[col];
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ List<Aircraft> aircraftList = this.getAircraftList();
+ Aircraft aircraft = aircraftList.get(rowIndex);
+ switch (columnIndex) {
+ case 0:
+ return aircraft.getHexIdent();
+ case 1:
+ return aircraft.getCallsign();
+ case 2:
+ return aircraft.getSquawk();
+ case 3:
+ return aircraft.getCurrentPosition().getLatitude();
+ case 4:
+ return aircraft.getCurrentPosition().getLongitude();
+ case 5:
+ return aircraft.getCurrentPosition().getAltitude();
+ case 6:
+ return aircraft.getVerticalRate();
+ case 7:
+ return aircraft.getTrack();
+ case 8:
+ return aircraft.getGroundSpeed();
+ case 9:
+ return aircraft.getPacketCount();
+ }
+ return "";
+ }
+
+ private List<Aircraft> getAircraftList() {
+ List<Aircraft> aircraftList = new ArrayList<>(this.aircraftMap.values());
+ Collections.sort(aircraftList);
+ return aircraftList;
+ }
+}
diff --git a/src/main/java/com/benburwell/planes/gui/GraphicsTheme.java b/src/main/java/com/benburwell/planes/gui/GraphicsTheme.java
new file mode 100644
index 0000000..cece138
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/gui/GraphicsTheme.java
@@ -0,0 +1,27 @@
+package com.benburwell.planes.gui;
+
+import java.awt.*;
+
+/**
+ * Created by ben on 11/18/16.
+ */
+public class GraphicsTheme {
+ public static class Colors {
+ public static final Color BASE_0 = new Color(12, 16, 20);
+ public static final Color BASE_1 = new Color(17, 21, 28);
+ public static final Color BASE_2 = new Color(9, 31, 46);
+ public static final Color BASE_3 = new Color(10, 55, 73);
+ public static final Color BASE_4 = new Color(36, 83, 97);
+ public static final Color BASE_5 = new Color(89, 156, 171);
+ public static final Color BASE_6 = new Color(153, 209, 206);
+ public static final Color BASE_7 = new Color(211, 235, 233);
+ public static final Color RED = new Color(194, 49, 39);
+ public static final Color ORANGE = new Color(210, 105, 55);
+ public static final Color YELLOW = new Color(237, 180, 67);
+ public static final Color MAGENTA = new Color(136, 140, 166);
+ public static final Color VIOLET = new Color(78, 81, 102);
+ public static final Color BLUE = new Color(25, 84, 102);
+ public static final Color CYAN = new Color(51, 133, 158);
+ public static final Color GREEN = new Color(42, 168, 137);
+ }
+}
diff --git a/src/main/java/com/benburwell/planes/gui/Main1090.java b/src/main/java/com/benburwell/planes/gui/Main1090.java
new file mode 100644
index 0000000..d7fc830
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/gui/Main1090.java
@@ -0,0 +1,93 @@
+/**
+ * Created by ben on 11/15/16.
+ */
+
+package com.benburwell.planes.gui;
+
+import com.benburwell.planes.sbs.*;
+import com.benburwell.planes.data.*;
+import com.benburwell.planes.gui.aircraftmap.*;
+
+import java.awt.*;
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+
+public class Main1090 extends JFrame {
+ private AggregateDataSource sbsDataSource = new AggregateDataSource();
+ private AircraftStore aircraft = new AircraftStore();
+ private int currentTcpConnection = 0;
+ private JTabbedPane tabbedPane = new JTabbedPane();
+
+ public Main1090() {
+ this.initUI();
+ }
+
+ private void initUI() {
+ this.createMenuBar();
+
+ this.setTitle("1090");
+ this.setSize(600, 400);
+ this.setLocationRelativeTo(null);
+ this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+
+ this.openDataSource();
+
+ this.createTabs();
+ }
+
+ private void createTabs() {
+ AircraftTableComponent aircraftData = new AircraftTableComponent(this.aircraft);
+ this.tabbedPane.addTab("Aircraft Data", aircraftData.getComponent());
+
+ AircraftMapComponent aircraftMap = new AircraftMapComponent(this.aircraft);
+ this.tabbedPane.addTab("Live Map", aircraftMap.getComponent());
+
+ this.add(this.tabbedPane);
+ this.tabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
+
+ this.tabbedPane.setSelectedIndex(1);
+ }
+
+ private void createMenuBar() {
+ MenuBarProvider provider = new MenuBarProvider();
+ this.setJMenuBar(provider.getMenuBar());
+
+ provider.getDataConnectItem().addActionListener((ActionEvent event) -> {
+ if (this.currentTcpConnection == 0) {
+ TCPConnectionOptionDialog dialog = new TCPConnectionOptionDialog();
+ JOptionPane.showMessageDialog(this, dialog.getComponent(), "New Network Data Source", JOptionPane.PLAIN_MESSAGE);
+ this.currentTcpConnection = this.addTcpSource(dialog.getHost(), dialog.getPort());
+ provider.getDataConnectItem().setEnabled(false);
+ provider.getDataDisconnectItem().setEnabled(true);
+ }
+ });
+ provider.getDataDisconnectItem().addActionListener((ActionEvent event) -> {
+ if (this.currentTcpConnection != 0) {
+ this.sbsDataSource.closeSource(this.currentTcpConnection);
+ provider.getDataConnectItem().setEnabled(true);
+ provider.getDataDisconnectItem().setEnabled(false);
+ this.currentTcpConnection = 0;
+ }
+ });
+ }
+
+ private void openDataSource() {
+ this.sbsDataSource.subscribe((SBSPacket packet) -> {
+ this.aircraft.addPacket(packet);
+ });
+ this.sbsDataSource.open();
+ }
+
+ private int addTcpSource(String host, int port) {
+ TCPDataSource source = new TCPDataSource(host, port);
+ source.open();
+ return this.sbsDataSource.addSource(source);
+ }
+
+ public static void main(String[] args) {
+ EventQueue.invokeLater(() -> {
+ Main1090 app = new Main1090();
+ app.setVisible(true);
+ });
+ }
+}
diff --git a/src/main/java/com/benburwell/planes/gui/MenuBarProvider.java b/src/main/java/com/benburwell/planes/gui/MenuBarProvider.java
new file mode 100644
index 0000000..add77ce
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/gui/MenuBarProvider.java
@@ -0,0 +1,41 @@
+package com.benburwell.planes.gui;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+
+/**
+ * Created by ben on 11/17/16.
+ */
+public class MenuBarProvider {
+ private JMenuBar menubar = new JMenuBar();
+ private JMenu file = new JMenu("1090");
+ private JMenuItem fileQuitMenuItem = new JMenuItem("Quit");
+ private JMenu data = new JMenu("Data Source");
+ private JMenuItem dataConnectItem = new JMenuItem("Connect to Remote...");
+ private JMenuItem dataDisconnectItem = new JMenuItem("Disconnect");
+
+ public MenuBarProvider() {
+ fileQuitMenuItem.addActionListener((ActionEvent event) -> {
+ System.exit(0);
+ });
+ file.add(fileQuitMenuItem);
+ menubar.add(file);
+
+ dataDisconnectItem.setEnabled(false);
+ data.add(dataConnectItem);
+ data.add(dataDisconnectItem);
+ menubar.add(data);
+ }
+
+ public JMenuBar getMenuBar() {
+ return this.menubar;
+ }
+
+ public JMenuItem getDataConnectItem() {
+ return this.dataConnectItem;
+ }
+
+ public JMenuItem getDataDisconnectItem() {
+ return this.dataDisconnectItem;
+ }
+}
diff --git a/src/main/java/com/benburwell/planes/gui/TCPConnectionOptionDialog.java b/src/main/java/com/benburwell/planes/gui/TCPConnectionOptionDialog.java
new file mode 100644
index 0000000..609a70a
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/gui/TCPConnectionOptionDialog.java
@@ -0,0 +1,45 @@
+package com.benburwell.planes.gui;
+
+import javax.swing.*;
+
+/**
+ * Created by ben on 11/17/16.
+ */
+public class TCPConnectionOptionDialog implements ViewComponent {
+ public static final String DEFAULT_HOSTNAME = "10.0.0.111";
+ public static final int DEFAULT_TCP_PORT = 30003;
+
+ private JPanel dialog = new JPanel();
+ private JTextField hostField = new JTextField(10);
+ private JTextField portField = new JTextField(5);
+ private JLabel descriptionLabel = new JLabel("Add a network data source that provides data in the SBS-1 format");
+
+ @Override
+ public JComponent getComponent() {
+ // set properties
+ hostField.setText(DEFAULT_HOSTNAME);
+ hostField.setToolTipText("Hostname or IP address");
+
+ portField.setText(String.valueOf(DEFAULT_TCP_PORT));
+ portField.setToolTipText("Port number");
+
+ // create layout
+ dialog.add(descriptionLabel);
+ dialog.add(hostField);
+ dialog.add(portField);
+
+ return dialog;
+ }
+
+ public String getHost() {
+ return this.hostField.getText();
+ }
+
+ public int getPort() {
+ try {
+ return Integer.valueOf(this.portField.getText());
+ } catch (NumberFormatException e) {
+ return DEFAULT_TCP_PORT;
+ }
+ }
+}
diff --git a/src/main/java/com/benburwell/planes/gui/ViewComponent.java b/src/main/java/com/benburwell/planes/gui/ViewComponent.java
new file mode 100644
index 0000000..91c21cd
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/gui/ViewComponent.java
@@ -0,0 +1,10 @@
+package com.benburwell.planes.gui;
+
+import javax.swing.*;
+
+/**
+ * Created by ben on 11/17/16.
+ */
+public interface ViewComponent {
+ JComponent getComponent();
+}
diff --git a/src/main/java/com/benburwell/planes/gui/aircraftmap/AircraftMap.java b/src/main/java/com/benburwell/planes/gui/aircraftmap/AircraftMap.java
new file mode 100644
index 0000000..fad1082
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/gui/aircraftmap/AircraftMap.java
@@ -0,0 +1,174 @@
+package com.benburwell.planes.gui.aircraftmap;
+
+import com.benburwell.planes.gui.GraphicsTheme;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by ben on 11/19/16.
+ */
+public class AircraftMap extends JPanel {
+ // geographic constants
+ public final double MAX_LATITUDE = 90.0;
+ public final double MIN_LATITUDE = -90.0;
+ public final double MAX_LONGITUDE = 180.0;
+ public final double MIN_LONGITUDE = -180.0;
+ public final double NAUTICAL_MILES_PER_DEGREE_LATITUDE = 60.0;
+
+ // drawing constants
+ public final float FONT_SIZE = 12;
+ public final int TEXT_PADDING = 5;
+ public final int NUMBER_OF_RANGE_RINGS = 5;
+
+ // map manipulation constants
+ public final int MIN_ZOOM_PIXELS_PER_MILE = 1;
+ public final int MAX_ZOOM_PIXELS_PER_MILE = 2000;
+ public final double PAN_INTERVAL_MILES = 1.0;
+
+ // instance fields
+ private List<Drawable> planes = new ArrayList<>();
+ private double centerLatitude;
+ private double centerLongitude;
+ private int pixelsPerNauticalMile = 10;
+
+ /**
+ * Construct a map
+ */
+ public AircraftMap() {
+ super();
+ this.setBackground(GraphicsTheme.Colors.BASE_1);
+ this.setBorder(BorderFactory.createEmptyBorder());
+ this.setCenter(0, 0);
+ }
+
+ /**
+ * Paint the ViewComponent on a Graphics instance
+ *
+ * @param g the graphics context to paint on
+ */
+ @Override
+ public void paintComponent(Graphics g) {
+ super.paintComponent(g);
+ this.drawPositionAndScale(g);
+ this.drawRange(g);
+ this.planes.forEach(item -> item.drawOn(g, this));
+ }
+
+ public void drawPositionAndScale(Graphics g) {
+ Font currentFont = g.getFont();
+ Font newFont = currentFont.deriveFont(FONT_SIZE);
+ g.setFont(newFont);
+ g.setColor(GraphicsTheme.Colors.BLUE);
+ g.drawString(String.format("%08.5f N", this.centerLatitude), TEXT_PADDING, (int) FONT_SIZE + TEXT_PADDING);
+ g.drawString(String.format("%08.5f E", this.centerLongitude), TEXT_PADDING, (int) FONT_SIZE * 2 + TEXT_PADDING);
+ g.drawString(String.format("%d nm", this.getRangeRadius()), TEXT_PADDING, (int) FONT_SIZE * 3 + TEXT_PADDING);
+ }
+
+ public int getRangeRadius() {
+ double milesHigh = this.getHeight() / this.getPixelsPerNauticalMile();
+ double milesWide = this.getWidth() / this.getPixelsPerNauticalMile();
+ double screenMiles = Math.min(milesHigh, milesWide);
+ int milesPerRing = (int) screenMiles / NUMBER_OF_RANGE_RINGS;
+ return milesPerRing;
+ }
+
+ public List<Integer> getRangeRadii() {
+ int rangeRadius = this.getRangeRadius();
+ List<Integer> radii = new ArrayList<>();
+ for (int ringNumber = 1; ringNumber <= NUMBER_OF_RANGE_RINGS; ringNumber++) {
+ radii.add(rangeRadius * ringNumber);
+ }
+ return radii;
+ }
+
+ public void drawRange(Graphics g) {
+ int centerX = this.getWidth() / 2;
+ int centerY = this.getHeight() / 2;
+ g.setColor(GraphicsTheme.Colors.BASE_3);
+ for (Integer radius : this.getRangeRadii()) {
+ int pixelRadius = (int) (this.getPixelsPerNauticalMile() * radius);
+ g.drawOval(centerX - pixelRadius, centerY - pixelRadius, pixelRadius * 2, pixelRadius * 2);
+ }
+ g.drawLine(centerX, 0, centerX, this.getHeight());
+ g.drawLine(0, centerY, this.getWidth(), centerY);
+ }
+
+ public void setPlanes(List<Drawable> planes) {
+ this.planes = planes;
+ this.redraw();
+ }
+
+ public void redraw() {
+ this.invalidate();
+ this.validate();
+ this.repaint();
+ }
+
+ public void setCenter(double latitude, double longitude) {
+ this.centerLatitude = latitude;
+ this.centerLongitude = longitude;
+ this.redraw();
+ }
+
+ public double getCenterLatitude() {
+ return this.centerLatitude;
+ }
+
+ public double getCenterLongitude() {
+ return this.centerLongitude;
+ }
+
+ public double getPixelsPerDegreeLatitude() {
+ return this.pixelsPerNauticalMile * NAUTICAL_MILES_PER_DEGREE_LATITUDE;
+ }
+
+ public double getPixelsPerDegreeLongitude() {
+ return this.pixelsPerNauticalMile * this.getNauticalMilesPerDegreeLongitude();
+ }
+
+ public double getNauticalMilesPerDegreeLongitude() {
+ double milesPerDegree = Math.abs(Math.cos(Math.toRadians(this.centerLatitude)) * NAUTICAL_MILES_PER_DEGREE_LATITUDE);
+ return milesPerDegree;
+ }
+
+ public double getPixelsPerNauticalMile() {
+ return this.pixelsPerNauticalMile;
+ }
+
+ public void zoomIn() {
+ this.pixelsPerNauticalMile = Math.min(MAX_ZOOM_PIXELS_PER_MILE, this.pixelsPerNauticalMile * 2);
+ this.redraw();
+ }
+
+ public void zoomOut() {
+ this.pixelsPerNauticalMile = Math.max(MIN_ZOOM_PIXELS_PER_MILE, this.pixelsPerNauticalMile / 2);
+ this.redraw();
+ }
+
+ public void moveEast() {
+ double degreesToMove = PAN_INTERVAL_MILES / this.getNauticalMilesPerDegreeLongitude();
+ this.centerLongitude = Math.min(this.centerLongitude + degreesToMove, MAX_LONGITUDE);
+ this.redraw();
+ }
+
+ public void moveWest() {
+ double degreesToMove = PAN_INTERVAL_MILES / this.getNauticalMilesPerDegreeLongitude();
+ this.centerLongitude = Math.max(this.centerLongitude - degreesToMove, MIN_LONGITUDE);
+ this.redraw();
+ }
+
+ public void moveNorth() {
+ double degreesToMove = PAN_INTERVAL_MILES / NAUTICAL_MILES_PER_DEGREE_LATITUDE;
+ this.centerLatitude = Math.min(this.centerLatitude + degreesToMove, MAX_LATITUDE);
+ this.redraw();
+ }
+
+ public void moveSouth() {
+ double degreesToMove = PAN_INTERVAL_MILES / NAUTICAL_MILES_PER_DEGREE_LATITUDE;
+ this.centerLatitude = Math.max(this.centerLatitude - degreesToMove, MIN_LATITUDE);
+ this.redraw();
+ }
+}
diff --git a/src/main/java/com/benburwell/planes/gui/aircraftmap/AircraftMapComponent.java b/src/main/java/com/benburwell/planes/gui/aircraftmap/AircraftMapComponent.java
new file mode 100644
index 0000000..e490dae
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/gui/aircraftmap/AircraftMapComponent.java
@@ -0,0 +1,106 @@
+package com.benburwell.planes.gui.aircraftmap;
+
+import com.benburwell.planes.data.AircraftStore;
+import com.benburwell.planes.data.AircraftStoreListener;
+import com.benburwell.planes.data.Position;
+import com.benburwell.planes.gui.ViewComponent;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.KeyEvent;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * Created by ben on 11/18/16.
+ */
+public class AircraftMapComponent implements ViewComponent {
+ private AircraftStore store;
+ private AircraftMap mapPanel;
+ private String focusedAircraftIdentifier = null;
+
+ public AircraftMapComponent(AircraftStore store) {
+ this.store = store;
+ this.setupMap();
+ this.bindKeys();
+ this.subscribeToChanges();
+ }
+
+ public void focusNextAircraft() {
+ // List<String> aircraftIdentifiers = new ArrayList<>(this.store.getAircraft().keySet());
+ // Collections.sort(aircraftIdentifiers);
+ // if (this.focusedAircraftIdentifier == null && aircraftIdentifiers.size() > 0) {
+ // this.focusedAircraftIdentifier = aircraftIdentifiers.get(0);
+ // } else {
+ // int idx = aircraftIdentifiers.indexOf(this.focusedAircraftIdentifier);
+ // if (idx > 0 && idx < aircraftIdentifiers.size() - 1) {
+ // this.focusedAircraftIdentifier = aircraftIdentifiers.get(idx++);
+ // } else if (aircraftIdentifiers.size() > 0) {
+ // this.focusedAircraftIdentifier = aircraftIdentifiers.get(0);
+ // } else {
+ // this.focusedAircraftIdentifier = null;
+ // }
+ // }
+ }
+
+ private void setupMap() {
+ this.mapPanel = new AircraftMap();
+ this.mapPanel.setCenter(40.6188942, -75.4947205);
+ }
+
+ private void bindKeys() {
+ KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(e -> {
+ if (e.getKeyCode() == KeyEvent.VK_EQUALS && e.isShiftDown() && e.getID() == KeyEvent.KEY_PRESSED) {
+ this.mapPanel.zoomIn();
+ } else if (e.getKeyCode() == KeyEvent.VK_MINUS && e.getID() == KeyEvent.KEY_PRESSED) {
+ this.mapPanel.zoomOut();
+ } else if (e.getKeyCode() == KeyEvent.VK_L && e.getID() == KeyEvent.KEY_PRESSED) {
+ this.mapPanel.moveEast();
+ } else if (e.getKeyCode() == KeyEvent.VK_H && e.getID() == KeyEvent.KEY_PRESSED) {
+ this.mapPanel.moveWest();
+ } else if (e.getKeyCode() == KeyEvent.VK_J && e.getID() == KeyEvent.KEY_PRESSED) {
+ this.mapPanel.moveSouth();
+ } else if (e.getKeyCode() == KeyEvent.VK_K && e.getID() == KeyEvent.KEY_PRESSED) {
+ this.mapPanel.moveNorth();
+ } else if (e.getKeyCode() == KeyEvent.VK_0 && e.getID() == KeyEvent.KEY_PRESSED) {
+ this.mapPanel.setCenter(40.6188942, -75.4947205);
+ } else if (e.getKeyCode() == KeyEvent.VK_TAB && e.getID() == KeyEvent.KEY_PRESSED) {
+ this.focusNextAircraft();
+ this.centerMapOnPlane(this.focusedAircraftIdentifier);
+ }
+ return false;
+ });
+ }
+
+ private void centerMapOnPlane(String identifier) {
+ if (identifier != null) {
+ Position pos = this.store.getAircraft().get(identifier).getCurrentPosition();
+ this.mapPanel.setCenter(pos.getLatitude(), pos.getLongitude());
+ }
+ }
+
+ private void subscribeToChanges() {
+ this.store.subscribe(new AircraftStoreListener() {
+ @Override
+ public void aircraftStoreChanged() {
+ List<Drawable> planes = new ArrayList<>();
+ store.getAircraft().values().forEach(aircraft -> planes.add(new Plane(aircraft)));
+ mapPanel.setPlanes(planes);
+ mapPanel.validate();
+ mapPanel.repaint();
+ }
+
+ @Override
+ public boolean respondTo(String aircraftId) {
+ return true;
+ }
+ });
+ }
+
+ @Override
+ public JComponent getComponent() {
+ return this.mapPanel;
+ }
+
+}
diff --git a/src/main/java/com/benburwell/planes/gui/aircraftmap/Drawable.java b/src/main/java/com/benburwell/planes/gui/aircraftmap/Drawable.java
new file mode 100644
index 0000000..01c16ba
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/gui/aircraftmap/Drawable.java
@@ -0,0 +1,10 @@
+package com.benburwell.planes.gui.aircraftmap;
+
+import java.awt.*;
+
+/**
+ * Created by ben on 11/19/16.
+ */
+public interface Drawable {
+ void drawOn(Graphics graphicsContext, AircraftMap map);
+}
diff --git a/src/main/java/com/benburwell/planes/gui/aircraftmap/GeoPoint.java b/src/main/java/com/benburwell/planes/gui/aircraftmap/GeoPoint.java
new file mode 100644
index 0000000..d3eda40
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/gui/aircraftmap/GeoPoint.java
@@ -0,0 +1,36 @@
+package com.benburwell.planes.gui.aircraftmap;
+
+/**
+ * Created by ben on 11/19/16.
+ */
+public class GeoPoint {
+ private double latitude;
+ private double longitude;
+ private double altitude;
+
+ public GeoPoint(double latitude, double longitude, double altitude) {
+ this.latitude = latitude;
+ this.longitude = longitude;
+ this.altitude = altitude;
+ }
+
+ public int getX(AircraftMap map) {
+ double degreesFromCenter = map.getCenterLongitude() - this.longitude;
+ double pixelsFromCenter = degreesFromCenter * map.getPixelsPerDegreeLongitude();
+ double centerPixels = map.getSize().getWidth() / 2;
+ int xPosition = (int) (centerPixels - pixelsFromCenter);
+ return xPosition;
+ }
+
+ public int getY(AircraftMap map) {
+ double degreesFromCenter = map.getCenterLatitude() - this.latitude;
+ double pixelsFromCenter = degreesFromCenter * map.getPixelsPerDegreeLatitude();
+ double centerPixels = map.getSize().getHeight() / 2;
+ int yPosition = (int) (centerPixels + pixelsFromCenter);
+ return yPosition;
+ }
+
+ public double getAltitude() {
+ return this.altitude;
+ }
+}
diff --git a/src/main/java/com/benburwell/planes/gui/aircraftmap/Plane.java b/src/main/java/com/benburwell/planes/gui/aircraftmap/Plane.java
new file mode 100644
index 0000000..2f98bab
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/gui/aircraftmap/Plane.java
@@ -0,0 +1,126 @@
+package com.benburwell.planes.gui.aircraftmap;
+
+import com.benburwell.planes.data.Aircraft;
+import com.benburwell.planes.data.Position;
+import com.benburwell.planes.gui.GraphicsTheme;
+
+import java.awt.*;
+import java.awt.geom.AffineTransform;
+
+/**
+ * Created by ben on 11/19/16.
+ */
+public class Plane extends GeoPoint implements Drawable {
+ public final int TRIANGLE_HEIGHT = 6;
+ public final int TRIANGLE_WIDTH = 4;
+ public final int TEXT_OFFSET_X = 10;
+ public final int TEXT_OFFSET_Y = 15;
+ public final int MIN_COLOR_HEIGHT = 0;
+ public final int MAX_COLOR_HEIGHT = 50000;
+ private String name;
+ private double heading;
+ private double speed;
+ private double verticalRate;
+
+ public Plane(Aircraft ac) {
+ this(ac.getCallsign(), ac.getCurrentPosition(), ac.getTrack(), ac.getGroundSpeed(), ac.getVerticalRate());
+ }
+
+ public Plane(String name, Position position, double heading, double speed, double verticalRate) {
+ this(name, position.getLatitude(), position.getLongitude(), position.getAltitude(), heading, speed, verticalRate);
+ }
+
+ public Plane(String name, double latitude, double longitude, double altitude, double heading, double speed, double verticalRate) {
+ super(latitude, longitude, altitude);
+ this.name = name;
+ this.heading = heading;
+ this.speed = speed;
+ this.verticalRate = verticalRate;
+ }
+
+ public int getFlightLevel() {
+ return (int) this.getAltitude() / 100;
+ }
+
+ public Color getPlaneColor() {
+ Color minColor = GraphicsTheme.Colors.RED;
+ Color maxColor = GraphicsTheme.Colors.GREEN;
+
+ float[] minHsb = Color.RGBtoHSB(minColor.getRed(), minColor.getGreen(), minColor.getBlue(), null);
+ float[] maxHsb = Color.RGBtoHSB(maxColor.getRed(), maxColor.getGreen(), maxColor.getBlue(), null);
+
+ float minHue = minHsb[0];
+ float maxHue = maxHsb[0];
+ float minSat = minHsb[1];
+ float maxSat = maxHsb[1];
+ float minBright = minHsb[2];
+ float maxBright = maxHsb[2];
+
+ double planePosition = (this.getAltitude() / (MAX_COLOR_HEIGHT - MIN_COLOR_HEIGHT)) + MIN_COLOR_HEIGHT;
+ float huePosition = (float) (planePosition * (maxHue - minHue) + minHue);
+ float satPosition = (float) (planePosition * (maxSat - minSat) + minSat);
+ float brightPosition = (float) (planePosition * (maxBright - minBright) + minBright);
+
+ Color c = Color.getHSBColor(huePosition, satPosition, brightPosition);
+ return c;
+ }
+
+ public double getAngle() {
+ return Math.toRadians(this.heading);
+ }
+
+ public double getSpeed() {
+ return this.speed;
+ }
+
+ public void drawTriangle(Graphics2D ctx, int x, int y, int predictionLength) {
+ AffineTransform at = new AffineTransform();
+ at.setToRotation(this.getAngle(), x, y);
+ ctx.setTransform(at);
+ int[] xs = new int[]{ x - TRIANGLE_WIDTH, x, x + TRIANGLE_WIDTH, x - TRIANGLE_WIDTH };
+ int[] ys = new int[]{ y + TRIANGLE_HEIGHT, y - TRIANGLE_HEIGHT, y + TRIANGLE_HEIGHT, y + TRIANGLE_HEIGHT };
+ ctx.fillPolygon(xs, ys, 4);
+ ctx.drawLine(x, y, x, y - predictionLength);
+ }
+
+ public String getVerticalRateIndicator() {
+ if (this.verticalRate > 0) {
+ return "\u2191"; // ↑
+ } else if (this.verticalRate < 0) {
+ return "\u2193"; // ↓
+ }
+ return "";
+ }
+
+ public String getDisplayName() {
+ if (this.name == null || this.name.isEmpty()) {
+ return "-----";
+ }
+ return this.name;
+ }
+
+ public int getPredictionLength(double pixelsPerNauticalMile) {
+ return (int) (this.speed / 60.0 * pixelsPerNauticalMile);
+ }
+
+ public void drawOn(Graphics g, AircraftMap map) {
+ int x = this.getX(map);
+ int y = this.getY(map);
+
+ if (x >= 0 && x <= map.getSize().getWidth() && y >= 0 && y <= map.getSize().getHeight()) {
+ // draw the plane dot
+ Graphics2D g2d = (Graphics2D) g.create();
+ g2d.setColor(this.getPlaneColor());
+ int predictedTrack = this.getPredictionLength(map.getPixelsPerNauticalMile());
+ this.drawTriangle(g2d, x, y, predictedTrack);
+ g2d.dispose();
+
+
+ // draw the name of the plane
+ g.setColor(GraphicsTheme.Colors.BASE_5);
+ g.drawString(this.getDisplayName(), x + TEXT_OFFSET_X, y + TEXT_OFFSET_Y);
+ String infoString = String.format("%d%s %.1f", this.getFlightLevel(), this.getVerticalRateIndicator(), this.getSpeed());
+ g.drawString(infoString, x + TEXT_OFFSET_X, y + TEXT_OFFSET_Y + g.getFontMetrics().getHeight());
+ }
+ }
+}
diff --git a/src/main/java/com/benburwell/planes/sbs/AggregateDataSource.java b/src/main/java/com/benburwell/planes/sbs/AggregateDataSource.java
new file mode 100644
index 0000000..2850404
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/sbs/AggregateDataSource.java
@@ -0,0 +1,48 @@
+package com.benburwell.planes.sbs;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.HashMap;
+
+/**
+ * Created by ben on 11/15/16.
+ */
+public class AggregateDataSource implements DataSource {
+ private List<DataListener> subscribers = new ArrayList<>();
+ private boolean isOpen = false;
+ private int nextSourceNumber = 1;
+ private Map<Integer,DataSource> sources = new HashMap<>();
+
+ public int addSource(DataSource source) {
+ int thisSourceNumber = this.nextSourceNumber++;
+ this.sources.put(thisSourceNumber, source);
+ source.subscribe((SBSPacket packet) -> {
+ if (isOpen) {
+ for (DataListener listener : subscribers) {
+ listener.handleMessage(packet);
+ }
+ }
+ });
+ return thisSourceNumber;
+ }
+
+ public void subscribe(DataListener listener) {
+ this.subscribers.add(listener);
+ }
+
+ public void open() {
+ this.isOpen = true;
+ }
+
+ public void close() {
+ this.isOpen = false;
+ }
+
+ public void closeSource(int sourceNumber) {
+ if (this.sources.containsKey(sourceNumber)) {
+ this.sources.get(sourceNumber).close();
+ this.sources.remove(sourceNumber);
+ }
+ }
+}
diff --git a/src/main/java/com/benburwell/planes/sbs/DataListener.java b/src/main/java/com/benburwell/planes/sbs/DataListener.java
new file mode 100644
index 0000000..b0da2ef
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/sbs/DataListener.java
@@ -0,0 +1,8 @@
+package com.benburwell.planes.sbs;
+
+/**
+ * Created by ben on 11/15/16.
+ */
+public interface DataListener {
+ void handleMessage(SBSPacket packet);
+}
diff --git a/src/main/java/com/benburwell/planes/sbs/DataSource.java b/src/main/java/com/benburwell/planes/sbs/DataSource.java
new file mode 100644
index 0000000..961c6e2
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/sbs/DataSource.java
@@ -0,0 +1,10 @@
+package com.benburwell.planes.sbs;
+
+/**
+ * Created by ben on 11/15/16.
+ */
+public interface DataSource {
+ void subscribe(DataListener listener);
+ void open();
+ void close();
+}
diff --git a/src/main/java/com/benburwell/planes/sbs/MalformedPacketException.java b/src/main/java/com/benburwell/planes/sbs/MalformedPacketException.java
new file mode 100644
index 0000000..6cbd1a3
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/sbs/MalformedPacketException.java
@@ -0,0 +1,15 @@
+package com.benburwell.planes.sbs;
+
+/**
+ * Created by ben on 11/15/16.
+ */
+public class MalformedPacketException extends Exception {
+ private String message;
+ public MalformedPacketException(String message) {
+ this.message = message;
+ }
+
+ public String getMessage() {
+ return this.message;
+ }
+}
diff --git a/src/main/java/com/benburwell/planes/sbs/MessageType.java b/src/main/java/com/benburwell/planes/sbs/MessageType.java
new file mode 100644
index 0000000..a2ecc66
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/sbs/MessageType.java
@@ -0,0 +1,31 @@
+package com.benburwell.planes.sbs;
+
+/**
+ * Created by ben on 11/15/16.
+ */
+public enum MessageType {
+ SELECTION_CHANGE("SEL"),
+ NEW_ID("ID"),
+ NEW_AIRCRAFT("AIR"),
+ STATUS_CHANGE("STA"),
+ CLICK("CLK"),
+ TRANSMISSION("MSG");
+
+ private String code;
+ MessageType(String code) {
+ this.code = code;
+ }
+
+ public String getCode() {
+ return this.code;
+ }
+
+ public static MessageType parse(String messageType) throws UnrecognizedMessageTypeException {
+ for (MessageType type : MessageType.values()) {
+ if (type.getCode().equals(messageType)) {
+ return type;
+ }
+ }
+ throw new UnrecognizedMessageTypeException(messageType);
+ }
+}
diff --git a/src/main/java/com/benburwell/planes/sbs/SBSPacket.java b/src/main/java/com/benburwell/planes/sbs/SBSPacket.java
new file mode 100644
index 0000000..5684803
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/sbs/SBSPacket.java
@@ -0,0 +1,216 @@
+package com.benburwell.planes.sbs;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+
+/**
+ * Created by ben on 11/15/16.
+ */
+public class SBSPacket {
+ private MessageType messageType;
+ private TransmissionType transmissionType = null;
+ private String sessionId = null;
+ private String aircraftId = null;
+ private String hexIdent = null;
+ private String flightId = null;
+ private Date dateGenerated = null;
+ private Date dateLogged = null;
+ private String callsign = null;
+ private Double altitude = null;
+ private Double groundSpeed = null;
+ private Double track = null;
+ private Double latitude = null;
+ private Double longitude = null;
+ private Double verticalRate = null;
+ private String squawk = null;
+ private Boolean alert = null;
+ private Boolean emergency = null;
+ private Boolean spi = null;
+ private Boolean isOnGround = null;
+
+ public SBSPacket(String packet) throws MalformedPacketException {
+ this.parse(packet);
+ }
+
+ public void parse(String packet) throws MalformedPacketException {
+ String[] segments = packet.split(",", -1);
+ if (segments.length < 11) {
+ throw new MalformedPacketException("Packet must have at least 11 fields, but has only " + segments.length);
+ }
+
+ // get the message type
+ try {
+ this.messageType = MessageType.parse(segments[0]);
+ } catch (UnrecognizedMessageTypeException e) {
+ throw new MalformedPacketException("Packet has an unrecognized message type: " + e.getType());
+ }
+
+ // get the transmission type
+ if (this.messageType.equals(MessageType.TRANSMISSION)) {
+ try {
+ this.transmissionType = TransmissionType.parse(segments[1]);
+ } catch (UnrecognizedTransmissionTypeException e) {
+ throw new MalformedPacketException("Packet has an unrecognized transmission type code: " + e.getCode());
+ }
+ }
+
+ this.sessionId = segments[2];
+ this.aircraftId = segments[3];
+ this.hexIdent = segments[4];
+ this.flightId = segments[5];
+ this.dateGenerated = this.parseDateAndTime(segments[6], segments[7]);
+ this.dateLogged = this.parseDateAndTime(segments[8], segments[9]);
+ this.callsign = segments[10];
+
+ if (this.messageType.equals(MessageType.TRANSMISSION)) {
+ if (segments.length < 22) {
+ throw new MalformedPacketException("Packet is a message (22 fields), but only has " + segments.length);
+ }
+ if (segments[11].length() > 0) {
+ this.altitude = Double.parseDouble(segments[11]);
+ }
+ if (segments[12].length() > 0) {
+ this.groundSpeed = Double.parseDouble(segments[12]);
+ }
+ if (segments[13].length() > 0) {
+ this.track = Double.parseDouble(segments[13]);
+ }
+ if (segments[14].length() > 0) {
+ this.latitude = Double.parseDouble(segments[14]);
+ }
+ if (segments[15].length() > 0) {
+ this.longitude = Double.parseDouble(segments[15]);
+ }
+ if (segments[16].length() > 0) {
+ this.verticalRate = Double.parseDouble(segments[16]);
+ }
+ this.squawk = segments[17];
+ if (segments[18].length() > 0) {
+ this.alert = segments[18].equals("1");
+ }
+ if (segments[19].length() > 0) {
+ this.emergency = segments[19].equals("1");
+ }
+ if (segments[20].length() > 0) {
+ this.spi = segments[20].equals("1");
+ }
+ if (segments[21].length() > 0) {
+ this.isOnGround = segments[21].equals("1");
+ }
+ }
+ }
+
+ public Date parseDateAndTime(String date, String time) {
+ String combined = "";
+ Calendar now = Calendar.getInstance();
+ if (date == null || date.isEmpty()) {
+ combined += now.get(Calendar.YEAR) + "/" + now.get(Calendar.MONTH) + "/" + now.get(Calendar.DAY_OF_MONTH);
+ } else {
+ combined += date;
+ }
+
+ combined += " ";
+
+ if (time == null || time.isEmpty()) {
+ combined += now.get(Calendar.HOUR_OF_DAY) + ":" +
+ now.get(Calendar.MINUTE) + ":" +
+ now.get(Calendar.SECOND) + "." +
+ now.get(Calendar.MILLISECOND);
+ } else {
+ combined += time;
+ }
+
+ SimpleDateFormat fmt = new SimpleDateFormat("yyyy/MM/dd hh:mm:ss.sss");
+ try {
+ return fmt.parse(combined);
+ } catch (ParseException e) {
+ return null;
+ }
+ }
+
+ public String toString() {
+ return this.messageType.name();
+ }
+
+ public MessageType getMessageType() {
+ return messageType;
+ }
+
+ public TransmissionType getTransmissionType() {
+ return transmissionType;
+ }
+
+ public String getSessionId() {
+ return sessionId;
+ }
+
+ public String getAircraftId() {
+ return aircraftId;
+ }
+
+ public String getHexIdent() {
+ return hexIdent;
+ }
+
+ public String getFlightId() {
+ return flightId;
+ }
+
+ public Date getDateGenerated() {
+ return dateGenerated;
+ }
+
+ public Date getDateLogged() {
+ return dateLogged;
+ }
+
+ public String getCallsign() {
+ return callsign;
+ }
+
+ public Double getAltitude() {
+ return altitude;
+ }
+
+ public Double getGroundSpeed() {
+ return groundSpeed;
+ }
+
+ public Double getTrack() {
+ return track;
+ }
+
+ public Double getLatitude() {
+ return latitude;
+ }
+
+ public Double getLongitude() {
+ return longitude;
+ }
+
+ public Double getVerticalRate() {
+ return verticalRate;
+ }
+
+ public String getSquawk() {
+ return squawk;
+ }
+
+ public Boolean isAlert() {
+ return alert;
+ }
+
+ public Boolean isEmergency() {
+ return emergency;
+ }
+
+ public Boolean isSpi() {
+ return spi;
+ }
+
+ public Boolean isOnGround() {
+ return isOnGround;
+ }
+}
diff --git a/src/main/java/com/benburwell/planes/sbs/TCPDataSource.java b/src/main/java/com/benburwell/planes/sbs/TCPDataSource.java
new file mode 100644
index 0000000..2224d36
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/sbs/TCPDataSource.java
@@ -0,0 +1,99 @@
+package com.benburwell.planes.sbs;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.io.*;
+import java.net.*;
+
+/**
+ * Created by ben on 11/15/16.
+ */
+public class TCPDataSource implements DataSource {
+ private List<DataListener> subscribers = new ArrayList<>();
+ private String host;
+ private int port;
+ private Thread clientThread = null;
+ private SocketClient client = null;
+
+ public TCPDataSource(String host, int port) {
+ this.host = host;
+ this.port = port;
+ }
+
+ public void subscribe(DataListener listener) {
+ this.subscribers.add(listener);
+ }
+
+ public void open() {
+ this.client = new SocketClient(this.host, this.port);
+ this.clientThread = new Thread(this.client);
+ this.clientThread.start();
+ }
+
+ public void close() {
+ if (this.client != null) {
+ this.client.terminate();
+ try {
+ this.clientThread.join();
+ } catch (InterruptedException ignored) {}
+ }
+ }
+
+ private class SocketClient implements Runnable {
+ private String host;
+ private int port;
+ private Socket clientSocket = null;
+
+ public SocketClient(String host, int port) {
+ this.host = host;
+ this.port = port;
+ }
+
+ public void terminate() {
+ if (this.clientSocket != null) {
+ try {
+ this.clientSocket.close();
+ } catch (IOException e) {
+ System.out.println("Got exception closing socket: " + e.getMessage());
+ }
+ }
+ }
+
+ @Override
+ public void run() {
+ System.out.println("Starting socket client");
+ BufferedReader socketReader;
+ try {
+ this.clientSocket = new Socket(this.host, this.port);
+ } catch (IOException e) {
+ System.out.println("Could not connect to " + this.host + " on port " + this.port + ": " + e.getMessage());
+ return;
+ }
+ try {
+ socketReader = new BufferedReader(new InputStreamReader(this.clientSocket.getInputStream()));
+ } catch (IOException e) {
+ System.out.println("Could not create socket reader: " + e.getMessage());
+ return;
+ }
+
+ String receivedMessage;
+ while (true) {
+ try {
+ receivedMessage = socketReader.readLine();
+ } catch (IOException e) {
+ System.out.println("Error reading from socket: " + e.getMessage());
+ return;
+ }
+ try {
+ SBSPacket packet = new SBSPacket(receivedMessage);
+ for (DataListener subscriber : subscribers) {
+ subscriber.handleMessage(packet);
+ }
+ } catch (MalformedPacketException e) {
+ System.out.println("Discarding malformed packet: " + receivedMessage);
+ System.out.println(e.getMessage());
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/benburwell/planes/sbs/TransmissionType.java b/src/main/java/com/benburwell/planes/sbs/TransmissionType.java
new file mode 100644
index 0000000..fb1761e
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/sbs/TransmissionType.java
@@ -0,0 +1,34 @@
+package com.benburwell.planes.sbs;
+
+/**
+ * Created by ben on 11/15/16.
+ */
+public enum TransmissionType {
+ ES_IDENTIFICATION(1),
+ ES_SURFACE_POSITION(2),
+ ES_AIRBORNE_POSITION(3),
+ ES_AIRBORNE_VELOCITY(4),
+ SURVEILLANCE_ALT(5),
+ SURVEILLANCE_ID(6),
+ AIR_TO_AIR(7),
+ ALL_CALL_REPLY(8);
+
+ private int id;
+ TransmissionType(int id) {
+ this.id = id;
+ }
+
+ public int getId() {
+ return this.id;
+ }
+
+ public static TransmissionType parse(String codeString) throws UnrecognizedTransmissionTypeException {
+ int code = Integer.parseInt(codeString);
+ for (TransmissionType transmissionType : TransmissionType.values()) {
+ if (transmissionType.getId() == code) {
+ return transmissionType;
+ }
+ }
+ throw new UnrecognizedTransmissionTypeException(code);
+ }
+}
diff --git a/src/main/java/com/benburwell/planes/sbs/UnrecognizedMessageTypeException.java b/src/main/java/com/benburwell/planes/sbs/UnrecognizedMessageTypeException.java
new file mode 100644
index 0000000..ee30a87
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/sbs/UnrecognizedMessageTypeException.java
@@ -0,0 +1,20 @@
+package com.benburwell.planes.sbs;
+
+/**
+ * Created by ben on 11/15/16.
+ */
+public class UnrecognizedMessageTypeException extends Exception {
+ private String type;
+
+ public UnrecognizedMessageTypeException(String type) {
+ this.type = type;
+ }
+
+ public String getType() {
+ return this.type;
+ }
+
+ public String getMessage() {
+ return "Unrecognized message type: " + this.getType();
+ }
+}
diff --git a/src/main/java/com/benburwell/planes/sbs/UnrecognizedTransmissionTypeException.java b/src/main/java/com/benburwell/planes/sbs/UnrecognizedTransmissionTypeException.java
new file mode 100644
index 0000000..abab067
--- /dev/null
+++ b/src/main/java/com/benburwell/planes/sbs/UnrecognizedTransmissionTypeException.java
@@ -0,0 +1,20 @@
+package com.benburwell.planes.sbs;
+
+/**
+ * Created by ben on 11/15/16.
+ */
+public class UnrecognizedTransmissionTypeException extends Exception {
+ private int code;
+
+ public UnrecognizedTransmissionTypeException(int code) {
+ this.code = code;
+ }
+
+ public int getCode() {
+ return this.code;
+ }
+
+ public String getMessage() {
+ return "Unrecognized transmission type: " + this.getCode();
+ }
+}