View Javadoc
1   package au.gov.amsa.animator;
2   
3   import java.awt.Color;
4   import java.awt.Graphics;
5   import java.awt.Graphics2D;
6   import java.awt.Rectangle;
7   import java.awt.RenderingHints;
8   import java.awt.event.ComponentAdapter;
9   import java.awt.event.ComponentEvent;
10  import java.awt.event.MouseAdapter;
11  import java.awt.event.MouseEvent;
12  import java.awt.event.MouseWheelEvent;
13  import java.awt.event.WindowAdapter;
14  import java.awt.event.WindowEvent;
15  import java.awt.geom.AffineTransform;
16  import java.awt.geom.NoninvertibleTransformException;
17  import java.awt.geom.Point2D;
18  import java.awt.image.BufferedImage;
19  import java.util.concurrent.TimeUnit;
20  import java.util.concurrent.atomic.AtomicBoolean;
21  import java.util.concurrent.atomic.AtomicInteger;
22  
23  import javax.swing.JFrame;
24  import javax.swing.JPanel;
25  
26  import org.geotools.geometry.jts.ReferencedEnvelope;
27  import org.geotools.map.MapContent;
28  import org.geotools.referencing.crs.DefaultGeographicCRS;
29  import org.geotools.renderer.lite.RendererUtilities;
30  import org.geotools.renderer.lite.StreamingRenderer;
31  
32  import au.gov.amsa.util.swing.FramePreferences;
33  import rx.Scheduler.Worker;
34  import rx.internal.util.SubscriptionList;
35  import rx.schedulers.Schedulers;
36  import rx.schedulers.SwingScheduler;
37  
38  public class Animator {
39  
40      private final Model model;
41      private final View view;
42      private volatile BufferedImage image;
43      private volatile BufferedImage backgroundImage;
44      private volatile ReferencedEnvelope bounds;
45      final JPanel panel = createMapPanel();
46      final MapContent map;
47      private final SubscriptionList subscriptions;
48      private final Worker worker;
49      private volatile BufferedImage offScreenImage;
50      private volatile AffineTransform worldToScreen;
51  
52      public Animator(MapContent map, Model model, View view) {
53          this.map = map;
54          this.model = model;
55          this.view = view;
56          // default to Australia centred region
57          bounds = new ReferencedEnvelope(90, 175, -50, 0, DefaultGeographicCRS.WGS84);
58          subscriptions = new SubscriptionList();
59          worker = Schedulers.newThread().createWorker();
60          subscriptions.add(worker);
61      }
62  
63      ReferencedEnvelope getBounds() {
64          return bounds;
65      }
66  
67      private JPanel createMapPanel() {
68          final JPanel panel = new JPanel() {
69              private static final long serialVersionUID = 3824694997015022298L;
70  
71              @Override
72              protected void paintComponent(Graphics g) {
73                  super.paintComponent(g);
74                  g.drawImage(image, 0, 0, null);
75              }
76          };
77          MouseAdapter listener = createMouseListener();
78          panel.addMouseListener(listener);
79          panel.addMouseWheelListener(listener);
80          return panel;
81      }
82  
83      private MouseAdapter createMouseListener() {
84          return new MouseAdapter() {
85  
86              @Override
87              public void mouseWheelMoved(MouseWheelEvent e) {
88                  int notches = e.getWheelRotation();
89                  Point2D.Float p = toWorld(e);
90                  boolean zoomIn = notches < 0;
91                  for (int i = 0; i < Math.min(Math.abs(notches), 8); i++) {
92                      if (zoomIn)
93                          zoom(p, 0.9);
94                      else
95                          zoom(p, 1.1);
96                  }
97                  worker.schedule(() -> {
98                      redrawAll();
99                  } , 50, TimeUnit.MILLISECONDS);
100             }
101 
102             @Override
103             public void mouseClicked(MouseEvent e) {
104                 boolean shiftDown = (e.getModifiersEx()
105                         & MouseEvent.SHIFT_DOWN_MASK) == MouseEvent.SHIFT_DOWN_MASK;
106                 Point2D.Float p = toWorld(e);
107                 if (e.getClickCount() == 2) {
108                     if (shiftDown) {
109                         // zoom out centred on p
110                         zoom(p, 2.5);
111                     } else {
112                         // zoom in centred on p
113                         zoom(p, 0.4);
114                     }
115                     redrawAll();
116                 } else if (e.getClickCount() == 1 && e.getButton() == MouseEvent.BUTTON1) {
117                     System.out.println(p.getX() + " " + p.getY());
118                 }
119             }
120 
121             private void zoom(Point2D.Float p, double factor) {
122                 double w = bounds.getWidth() * factor;
123                 double h = bounds.getHeight() * factor;
124                 if (w >= map.getMaxBounds().getWidth() || h >= map.getMaxBounds().getHeight())
125                     bounds = map.getMaxBounds();
126                 bounds = new ReferencedEnvelope(p.getX() - w / 2, p.getX() + w / 2,
127                         p.getY() - h / 2, p.getY() + h / 2, bounds.getCoordinateReferenceSystem());
128             }
129 
130             private Point2D.Float toWorld(MouseEvent e) {
131                 Point2D.Float a = new Point2D.Float(e.getX(), e.getY());
132                 Point2D.Float b = new Point2D.Float();
133                 try {
134                     worldToScreen.inverseTransform(a, b);
135                 } catch (NoninvertibleTransformException e1) {
136                     throw new RuntimeException(e1);
137                 }
138                 return b;
139             }
140 
141         };
142     }
143 
144     public void start() {
145         SwingScheduler.getInstance().createWorker().schedule(() -> {
146             JFrame frame = new JFrame();
147             frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
148             synchronized (panel) {
149                 frame.setContentPane(panel);
150             }
151             FramePreferences.restoreLocationAndSize(frame, 100, 100, 800, 600, Animator.class);
152             bounds = AnimatorPreferences.restoreBounds(90, 175, -50, 0, frame, Animator.this);
153             frame.addComponentListener(new ComponentAdapter() {
154 
155                 @Override
156                 public void componentResized(ComponentEvent e) {
157                     super.componentResized(e);
158                     redrawAll();
159                 }
160 
161                 @Override
162                 public void componentShown(ComponentEvent e) {
163                     super.componentShown(e);
164                     redrawAll();
165                 }
166             });
167             frame.addWindowListener(new WindowAdapter() {
168 
169                 @Override
170                 public void windowClosing(WindowEvent e) {
171                     Animator.this.close();
172                 }
173             });
174             frame.setVisible(true);
175         });
176         final AtomicInteger timeStep = new AtomicInteger();
177         worker.schedulePeriodically(() -> {
178             model.updateModel(timeStep.getAndIncrement());
179             redrawAnimationLayer();
180         } , 50, 50, TimeUnit.MILLISECONDS);
181     }
182 
183     private void redrawAll() {
184         backgroundImage = null;
185         redraw();
186     }
187 
188     private synchronized void redraw() {
189 
190         if (backgroundImage == null) {
191             // get the frame width and height
192             int width = panel.getParent().getWidth();
193             double ratio = bounds.getHeight() / bounds.getWidth();
194             int proportionalHeight = (int) Math.round(width * ratio);
195             Rectangle imageBounds = new Rectangle(0, 0, width, proportionalHeight);
196             image = createImage(imageBounds);
197             BufferedImage backgroundImage = createImage(imageBounds);
198             Graphics2D gr = backgroundImage.createGraphics();
199             gr.setPaint(Color.WHITE);
200             gr.fill(imageBounds);
201             StreamingRenderer renderer = new StreamingRenderer();
202             renderer.setMapContent(map);
203             renderer.paint(gr, imageBounds, bounds);
204             this.backgroundImage = backgroundImage;
205             this.offScreenImage = createImage(imageBounds);
206             worldToScreen = RendererUtilities.worldToScreenTransform(bounds,
207                     new Rectangle(0, 0, backgroundImage.getWidth(), backgroundImage.getHeight()));
208         }
209         redrawAnimationLayer();
210 
211     }
212 
213     private static BufferedImage createImage(Rectangle imageBounds) {
214         BufferedImage img = new BufferedImage(imageBounds.width, imageBounds.height,
215                 BufferedImage.TYPE_INT_RGB);
216         Graphics2D g = img.createGraphics();
217         g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
218         g.setBackground(Color.white);
219         return img;
220     }
221 
222     private final AtomicBoolean redrawing = new AtomicBoolean(false);
223 
224     private void redrawAnimationLayer() {
225         if (redrawing.compareAndSet(false, true)) {
226             // if (backgroundImage != null && offscreenImage != null) {
227             if (offScreenImage != null) {
228                 offScreenImage.getGraphics().drawImage(backgroundImage, 0, 0, null);
229                 view.draw(model, (Graphics2D) offScreenImage.getGraphics(), worldToScreen);
230                 BufferedImage temp = offScreenImage;
231                 offScreenImage = image;
232                 image = temp;
233             }
234             panel.repaint();
235             redrawing.set(false);
236         }
237     }
238 
239     public void close() {
240         System.out.println("unsubscribing");
241         subscriptions.unsubscribe();
242         System.out.println("unsubscribed");
243     }
244 
245 }