summaryrefslogtreecommitdiff
path: root/src/main/java/com/benburwell/planes/gui/aircraftmap
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/benburwell/planes/gui/aircraftmap')
-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
5 files changed, 452 insertions, 0 deletions
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());
+ }
+ }
+}