package com.knutejohnson.aviation.vor; import java.awt.*; import java.awt.event.*; import java.awt.geom.*; import java.awt.image.*; import java.io.*; import java.util.*; import javax.imageio.*; import javax.swing.*; import javax.swing.event.*; public class Map extends JPanel implements MyObserver { private static final int WIDTH = 300; private static final int HEIGHT = 300; private final int XOFFSET,YOFFSET; private final Point center; private final MyObservable observable; private volatile int bearing; private Point mouse; private BufferedImage sectional,airplane; private int radial; public Map() { Dimension size = new Dimension(WIDTH,HEIGHT); setPreferredSize(size); setMaximumSize(size); setMinimumSize(size); try { sectional = ImageIO.read( getClass().getResourceAsStream("sectional.jpg")); airplane = ImageIO.read( getClass().getResourceAsStream("airplane.gif")); } catch (IOException ioe) { ioe.printStackTrace(); } XOFFSET = airplane.getWidth() / 2; YOFFSET = airplane.getHeight() / 2; center = new Point(150,150); mouse = new Point(center.x,center.y - 100); MouseAdapter ma = new MouseAdapter() { public void mousePressed(MouseEvent me) { mouse = me.getPoint(); radial = calculateRadial(mouse); repaint(); observable.setChanged(); observable.notifyObservers(radial); } public void mouseDragged(MouseEvent me) { mouse = me.getPoint(); if (mouse.x < 0) mouse.x = 0; if (mouse.x > WIDTH) mouse.x = WIDTH; if (mouse.y < 0) mouse.y = 0; if (mouse.y > HEIGHT) mouse.y = HEIGHT; radial = calculateRadial(mouse); repaint(); observable.setChanged(); observable.notifyObservers(radial); } }; addMouseListener(ma); addMouseMotionListener(ma); observable = new MyObservable(); } public void update(MyObservable o, Object arg) { bearing = ((Integer)arg).intValue(); repaint(); } public void addObserver(MyObserver observer) { observable.addObserver(observer); } private int calculateRadial(Point here) { int rad = (int)Math.round(Math.toDegrees( Math.PI + Math.atan2((double)center.x - here.x, (double)center.y-(HEIGHT-here.y)))); rad %= 360; return rad; } public void paintComponent(Graphics g2d) { Graphics2D g = (Graphics2D)g2d; g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.drawImage(sectional,0,0,null); AffineTransform at = g.getTransform(); g.rotate(Math.toRadians(bearing),center.x,center.y); g.setStroke(new BasicStroke(2f)); g.setColor(new Color(255,0,0,100)); g.drawLine(30,HEIGHT/2,WIDTH-30,HEIGHT/2); g.drawLine(WIDTH/2,30,WIDTH/2,HEIGHT-30); g.drawLine(WIDTH/2,30,WIDTH/2-4,38); g.drawLine(WIDTH/2,30,WIDTH/2+4,38); g.drawLine(WIDTH/2-4,38,WIDTH/2+4,38); g.setTransform(at); g.drawImage(airplane,mouse.x-XOFFSET,mouse.y-YOFFSET,null); } } package com.knutejohnson.aviation.vor; import java.util.*; public class MyObservable { protected boolean changed = false; protected List obs; public MyObservable() { obs = new ArrayList(); } public synchronized void addObserver(MyObserver o) { if (o == null) throw new NullPointerException(); if (!obs.contains(o)) { obs.add(o); } } public synchronized void deleteObserver(MyObserver o) { obs.remove(o); } public void notifyObservers() { notifyObservers(null); } public synchronized void notifyObservers(Object arg) { if (!changed) return; clearChanged(); for (MyObserver o : obs) o.update(this,arg); } public synchronized void deleteObservers() { obs.clear(); } protected synchronized void setChanged() { changed = true; } protected synchronized void clearChanged() { changed = false; } public synchronized boolean hasChanged() { return changed; } public synchronized int countObservers() { return obs.size(); } public synchronized List getObservers() { return obs; } } package com.knutejohnson.aviation.vor; public interface MyObserver { void update(MyObservable o, Object arg); } package com.knutejohnson.aviation.vor; import java.awt.*; import java.awt.event.*; import java.awt.geom.*; import java.awt.image.*; import java.io.*; import java.util.*; import javax.imageio.*; import javax.swing.*; public class VORHead extends JPanel implements MyObserver { private static final int WIDTH = 300; private static final int HEIGHT = 300; private static final String[] labels = {"N","3","6","E","12","15","S","21","24","W","30","33"}; private static final int[] headX = {WIDTH/2-5,WIDTH/2-2,WIDTH/2,WIDTH/2+2,WIDTH/2+5}; private static final int[] headY = {70,65,50,65,70}; private static final Polygon head = new Polygon(headX,headY,5); private static final int[] tailX = {WIDTH/2-5,WIDTH/2+5,WIDTH/2}; private static final int[] tailY = {HEIGHT-60,HEIGHT-60,HEIGHT-50}; private static final Polygon tail = new Polygon(tailX,tailY,3); private final MyObservable observable; // synchronized on this private int bearing; private int radial; private int deflection; private String toFrom = "FROM"; // thread confined private BufferedImage obs; public VORHead() { super(new GridBagLayout()); Dimension size = new Dimension(300,300); setPreferredSize(size); setMinimumSize(size); setMaximumSize(size); try { obs = ImageIO.read(getClass().getResourceAsStream("obs.gif")); } catch (IOException ioe) { ioe.printStackTrace(); } GridBagConstraints c = new GridBagConstraints(); c.weighty = 1.0; c.insets = new Insets(3,3,3,3); c.anchor = GridBagConstraints.NORTHWEST; c.weightx = 0.0; add(new ArrowButton(true),c); c.weightx = 1.0; add(new ArrowButton(false),c); observable = new MyObservable(); } public synchronized void update(MyObservable o, Object arg) { radial = ((Integer)arg).intValue(); calculate(); repaint(); } public void addObserver(MyObserver observer) { observable.addObserver(observer); } public void paintComponent(Graphics g2d) { Graphics2D g = (Graphics2D)g2d; g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); int localBearing,localDeflection; String localToFrom; synchronized (this) { localBearing = getBearing(); localDeflection = deflection; localToFrom = toFrom; } g.setFont(new Font("Arial",Font.BOLD,15)); g.setStroke(new BasicStroke(1.5f)); // draw background g.setColor(new Color(200,200,200)); g.fillRect(0,0,WIDTH,HEIGHT); // draw outer ring g.setColor(Color.BLACK); g.fillOval(20,20,WIDTH-40,HEIGHT-40); // draw inner ring g.setColor(new Color(0,0,40)); g.fillOval(55,55,WIDTH-110,HEIGHT-110); AffineTransform t1 = g.getTransform(); g.rotate(Math.toRadians(-localBearing),WIDTH/2.0,HEIGHT/2.0); // draw ticks AffineTransform t2 = g.getTransform(); g.setColor(Color.WHITE); for (int i=0; i<36; ++i) { g.drawLine(WIDTH/2,HEIGHT/2-100, WIDTH/2,HEIGHT/2-110); g.rotate(Math.toRadians(5.0),WIDTH/2.0,HEIGHT/2.0); g.drawLine(WIDTH/2,HEIGHT/2-100, WIDTH/2,HEIGHT/2-105); g.rotate(Math.toRadians(5.0),WIDTH/2.0,HEIGHT/2.0); } g.setTransform(t2); // draw labels t2 = g.getTransform(); FontMetrics fm = g.getFontMetrics(); for (int i=0; i<12; i++) { int offset = fm.stringWidth(labels[i]) / 2; g.drawString(labels[i],WIDTH/2-offset,HEIGHT/2-115); g.rotate(Math.toRadians(30.0),WIDTH/2.0,HEIGHT/2.0); } g.setTransform(t2); g.setTransform(t1); // draw obs knob t1 = g.getTransform(); g.rotate(Math.toRadians(-localBearing * 2),45,HEIGHT-45); g.drawImage(obs,20,HEIGHT-70,null); g.setTransform(t1); // draw head and tail pointers g.setColor(Color.YELLOW); g.fill(head); g.fill(tail); // draw center circle g.setColor(Color.WHITE); g.fillOval(WIDTH/2-5,HEIGHT/2-5,10,10); g.setColor(Color.BLACK); g.fillOval(WIDTH/2-3,HEIGHT/2-3,6,6); // draw dots t1 = g.getTransform(); g.rotate(Math.toRadians(-30),WIDTH/2,70); g.setColor(Color.WHITE); for (int i=0; i<11; i++) { g.rotate(Math.toRadians(5.0),WIDTH/2,70); g.fillOval(WIDTH/2-2,HEIGHT-80,4,4); } g.setTransform(t1); // draw cdi t1 = g.getTransform(); g.setColor(Color.WHITE); g.rotate(Math.toRadians(localDeflection * 2.5),WIDTH/2,72); g.setStroke( new BasicStroke(3.5f,BasicStroke.CAP_ROUND,BasicStroke.JOIN_ROUND)); g.drawLine(WIDTH/2,72,WIDTH/2,HEIGHT-70); g.setTransform(t1); // draw to/from g.setColor(Color.WHITE); g.drawString(localToFrom,180,HEIGHT/2+5); } private void calculate() { double rad = Math.toRadians(getBearing() - radial); toFrom = Math.cos(rad) > 0 ? "FROM" : "TO"; double tan = Math.tan(rad); int diff = (int)Math.abs(Math.round((Math.toDegrees(Math.atan(tan))))); if (diff > 11) diff = 11; deflection = (int)-Math.copySign(diff,Math.sin(rad)); } private synchronized int getBearing() { return bearing; } private synchronized void incrementBearing() { setBearing(bearing + 1); } private synchronized void decrementBearing() { setBearing(bearing - 1); } private void setBearing(int b) { while (b >= 360) b -= 360; if (b < 0) b += 360; bearing = b; calculate(); repaint(); observable.setChanged(); observable.notifyObservers(bearing); } private class ArrowButton extends JPanel implements Runnable { private final boolean left; private final Dimension size; private final int[] x,y; private final Polygon arrow; public ArrowButton(boolean left) { this.left = left; size = new Dimension(20,20); setPreferredSize(size); setMaximumSize(size); setMinimumSize(size); if (left) { x = new int[] {0,size.width,size.width}; y = new int[] {size.height/2,size.height,0}; } else { x = new int[] {0,size.width,0}; y = new int[] {size.height,size.height/2,0}; } arrow = new Polygon(x,y,3); addMouseListener(new MouseAdapter() { Thread thread; public void mousePressed(MouseEvent me) { thread = new Thread(ArrowButton.this); thread.start(); } public void mouseReleased(MouseEvent me) { thread.interrupt(); } }); } public void run() { try { while (true) { if (left) incrementBearing(); else decrementBearing(); Thread.sleep(50); } } catch (InterruptedException ie) { // normal exit - nothing to do } } public void paintComponent(Graphics g2d) { Graphics2D g = (Graphics2D)g2d; g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setColor(Color.WHITE); g.fillRect(0,0,size.width,size.height); g.setColor(new Color(0,0,255,100)); g.fillRect(0,0,size.width,size.height); g.setColor(Color.BLUE); g.fill(arrow); } } } // // // VORTrainer // // An interactive program to demonstrate a VOR instrument // // Copyright (c) 2010 Knute Johnson. All rights reserved. // // Version Date Modification // ---------------------------------------------------------------------------- // 0.10 10 Mar 2010 incept // 0.20 10 Mar 2010 clean up and verify bug free with findbugs // 0.30 11 Mar 2010 change to ThreadSafe Observable/Observer // 0.40 12 Mar 2010 redo the Observable/Observer and clean up code to // ensure thread safety - thanks to Peter Duniho for // many great suggestions // // package com.knutejohnson.aviation.vor; import java.awt.*; import java.awt.event.*; import java.io.*; import javax.swing.*; import javax.swing.text.html.*; public class VORTrainer extends JPanel { public static final String VERSION = "0.40a"; public static final String COPYRIGHT = "VOR Trainer\nVersion: " + VERSION + "\nCopyright \u00a9 2010 Knute Johnson. All rights reserved."; public VORTrainer() { super(new GridBagLayout()); Map map = new Map(); VORHead head = new VORHead(); map.addObserver(head); // used to send radial to vor head head.addObserver(map); // used to send obs bearing to map add(map); add(head); } private void showDocs(JFrame f) { InputStreamReader isr = null; try { isr = new InputStreamReader( getClass().getResourceAsStream("doc.html")); JTextPane pane = new JTextPane(); pane.setEditorKit(new HTMLEditorKit()); pane.setEditable(false); pane.read(isr,null); JOptionPane.showMessageDialog(f,pane,"VOR Trainer Instructions", JOptionPane.INFORMATION_MESSAGE); } catch (IOException ioe) { ioe.printStackTrace(); } finally { if (isr != null) try { isr.close(); } catch (IOException ioe) { ioe.printStackTrace(); } } } public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { public void run() { final JFrame f = new JFrame("VOR Trainer"); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); final VORTrainer trainer = new VORTrainer(); JMenuBar mb = new JMenuBar(); f.setJMenuBar(mb); JMenu help = new JMenu("Help"); mb.add(help); JMenuItem instructions = new JMenuItem("Instructions"); instructions.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ae) { trainer.showDocs(f); } }); help.add(instructions); JMenuItem about = new JMenuItem("About"); about.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ae) { JOptionPane.showMessageDialog(f,COPYRIGHT, "About",JOptionPane.INFORMATION_MESSAGE); } }); help.add(about); f.add(trainer); f.pack(); f.setLocationRelativeTo(null); f.setVisible(true); } }); } } package com.knutejohnson.aviation.vor; import java.awt.*; import javax.swing.*; public class VORTrainerJApplet extends JApplet { private VORTrainer trainer; public void init() { EventQueue.invokeLater(new Runnable() { public void run() { trainer = new VORTrainer(); add(trainer); } }); } public String getAppletInfo() { return trainer.COPYRIGHT; } }