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
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
110 zoom(p, 2.5);
111 } else {
112
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
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
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 }