From 9441ed331af3aa6b3ef45bf165e40faad18bc7fd Mon Sep 17 00:00:00 2001 From: Ben Burwell Date: Sat, 19 Nov 2016 17:39:05 -0500 Subject: Use gradle --- .../planes/gui/aircraftmap/AircraftMap.java | 174 +++++++++++++++++++++ .../gui/aircraftmap/AircraftMapComponent.java | 106 +++++++++++++ .../planes/gui/aircraftmap/Drawable.java | 10 ++ .../planes/gui/aircraftmap/GeoPoint.java | 36 +++++ .../benburwell/planes/gui/aircraftmap/Plane.java | 126 +++++++++++++++ 5 files changed, 452 insertions(+) create mode 100644 src/main/java/com/benburwell/planes/gui/aircraftmap/AircraftMap.java create mode 100644 src/main/java/com/benburwell/planes/gui/aircraftmap/AircraftMapComponent.java create mode 100644 src/main/java/com/benburwell/planes/gui/aircraftmap/Drawable.java create mode 100644 src/main/java/com/benburwell/planes/gui/aircraftmap/GeoPoint.java create mode 100644 src/main/java/com/benburwell/planes/gui/aircraftmap/Plane.java (limited to 'src/main/java/com/benburwell/planes/gui/aircraftmap') 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 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 getRangeRadii() { + int rangeRadius = this.getRangeRadius(); + List 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 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 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 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()); + } + } +} -- cgit v1.2.3