source: josm/trunk/src/org/openstreetmap/josm/gui/layer/GeoImageLayer.java@ 323

Last change on this file since 323 was 323, checked in by framm, 18 years ago
  • fixed GeoImageLayer to release memory after thubmnails are built. This was always the intended behavoour but did not work properly, leading to memory shortage when working with large/many images.
File size: 17.5 KB
Line 
1//License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.gui.layer;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.BorderLayout;
8import java.awt.Color;
9import java.awt.Component;
10import java.awt.Cursor;
11import java.awt.Graphics;
12import java.awt.Graphics2D;
13import java.awt.GridBagLayout;
14import java.awt.Image;
15import java.awt.Insets;
16import java.awt.Point;
17import java.awt.Rectangle;
18import java.awt.event.ActionEvent;
19import java.awt.event.ActionListener;
20import java.awt.event.MouseAdapter;
21import java.awt.event.MouseEvent;
22import java.awt.event.KeyEvent;
23import java.awt.image.BufferedImage;
24import java.io.File;
25import java.io.IOException;
26import java.text.ParseException;
27import java.text.SimpleDateFormat;
28import java.util.ArrayList;
29import java.util.Collection;
30import java.util.Collections;
31import java.util.Date;
32import java.util.LinkedList;
33
34import javax.swing.BorderFactory;
35import javax.swing.DefaultListCellRenderer;
36import javax.swing.Icon;
37import javax.swing.ImageIcon;
38import javax.swing.JDialog;
39import javax.swing.JFileChooser;
40import javax.swing.JLabel;
41import javax.swing.JList;
42import javax.swing.JMenuItem;
43import javax.swing.JOptionPane;
44import javax.swing.JPanel;
45import javax.swing.JScrollPane;
46import javax.swing.JSeparator;
47import javax.swing.JTextField;
48import javax.swing.JToggleButton;
49import javax.swing.JButton;
50import javax.swing.JViewport;
51import javax.swing.border.BevelBorder;
52import javax.swing.border.Border;
53import javax.swing.filechooser.FileFilter;
54
55import org.openstreetmap.josm.Main;
56import org.openstreetmap.josm.actions.RenameLayerAction;
57import org.openstreetmap.josm.data.coor.EastNorth;
58import org.openstreetmap.josm.data.coor.LatLon;
59import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
60import org.openstreetmap.josm.gui.MapView;
61import org.openstreetmap.josm.gui.PleaseWaitRunnable;
62import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
63import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
64import org.openstreetmap.josm.gui.layer.RawGpsLayer.GpsPoint;
65import org.openstreetmap.josm.tools.DateParser;
66import org.openstreetmap.josm.tools.ExifReader;
67import org.openstreetmap.josm.tools.GBC;
68import org.openstreetmap.josm.tools.ImageProvider;
69
70/**
71 * A layer which imports several photos from disk and read EXIF time information from them.
72 *
73 * @author Imi
74 */
75public class GeoImageLayer extends Layer {
76
77 private static final class ImageEntry implements Comparable<ImageEntry> {
78 File image;
79 Date time;
80 LatLon coor;
81 EastNorth pos;
82 Icon icon;
83 public int compareTo(ImageEntry image) {
84 return time.compareTo(image.time);
85 }
86 }
87
88 private static final class Loader extends PleaseWaitRunnable {
89 boolean cancelled = false;
90 private GeoImageLayer layer;
91 private final Collection<File> files;
92 private final RawGpsLayer gpsLayer;
93 public Loader(Collection<File> files, RawGpsLayer gpsLayer) {
94 super(tr("Images for {0}", gpsLayer.name));
95 this.files = files;
96 this.gpsLayer = gpsLayer;
97 }
98 @Override protected void realRun() throws IOException {
99 Main.pleaseWaitDlg.currentAction.setText(tr("Read GPS..."));
100 LinkedList<TimedPoint> gps = new LinkedList<TimedPoint>();
101
102 // Extract dates and locations from GPS input
103 for (Collection<GpsPoint> c : gpsLayer.data) {
104 for (GpsPoint p : c) {
105 if (p.time == null)
106 throw new IOException(tr("No time for point {0} x {1}",p.latlon.lat(),p.latlon.lon()));
107 Date d = null;
108 try {
109 d = DateParser.parse(p.time);
110 } catch (ParseException e) {
111 throw new IOException(tr("Cannot read time \"{0}\" from point {1} x {2}",p.time,p.latlon.lat(),p.latlon.lon()));
112 }
113 gps.add(new TimedPoint(d, p.eastNorth));
114 }
115 }
116
117 if (gps.isEmpty()) {
118 errorMessage = tr("No images with readable timestamps found.");
119 return;
120 }
121
122 // read the image files
123 ArrayList<ImageEntry> data = new ArrayList<ImageEntry>(files.size());
124 int i = 0;
125 Main.pleaseWaitDlg.progress.setMaximum(files.size());
126 for (File f : files) {
127 if (cancelled)
128 break;
129 Main.pleaseWaitDlg.currentAction.setText(tr("Reading {0}...",f.getName()));
130 Main.pleaseWaitDlg.progress.setValue(i++);
131
132 ImageEntry e = new ImageEntry();
133 try {
134 e.time = ExifReader.readTime(f);
135 } catch (ParseException e1) {
136 continue;
137 }
138 if (e.time == null)
139 continue;
140 e.image = f;
141 e.icon = loadScaledImage(f, 16);
142
143 data.add(e);
144 }
145 layer = new GeoImageLayer(data, gps);
146 layer.calculatePosition();
147 }
148 @Override protected void finish() {
149 if (layer != null)
150 Main.main.addLayer(layer);
151 }
152 @Override protected void cancel() {cancelled = true;}
153 }
154
155 public ArrayList<ImageEntry> data;
156 private LinkedList<TimedPoint> gps = new LinkedList<TimedPoint>();
157
158 /**
159 * The delta added to all timestamps in files from the camera
160 * to match to the timestamp from the gps receivers tracklog.
161 */
162 private long delta = Long.parseLong(Main.pref.get("tagimages.delta", "0"));
163 private long gpstimezone = Long.parseLong(Main.pref.get("tagimages.gpstimezone", "0"))*60*60*1000;
164 private boolean mousePressed = false;
165 private static final SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");
166 private MouseAdapter mouseAdapter;
167 private int currentImage;
168
169 public static final class GpsTimeIncorrect extends Exception {
170 public GpsTimeIncorrect(String message, Throwable cause) {
171 super(message, cause);
172 }
173 public GpsTimeIncorrect(String message) {
174 super(message);
175 }
176 }
177
178 private static final class TimedPoint implements Comparable<TimedPoint> {
179 Date time;
180 EastNorth pos;
181 public TimedPoint(Date time, EastNorth pos) {
182 this.time = time;
183 this.pos = pos;
184 }
185 public int compareTo(TimedPoint point) {
186 return time.compareTo(point.time);
187 }
188 }
189
190 public static void create(Collection<File> files, RawGpsLayer gpsLayer) {
191 Loader loader = new Loader(files, gpsLayer);
192 Main.worker.execute(loader);
193 }
194
195 private GeoImageLayer(final ArrayList<ImageEntry> data, LinkedList<TimedPoint> gps) {
196 super(tr("Geotagged Images"));
197 Collections.sort(data);
198 Collections.sort(gps);
199 this.data = data;
200 this.gps = gps;
201 mouseAdapter = new MouseAdapter(){
202 @Override public void mousePressed(MouseEvent e) {
203 if (e.getButton() != MouseEvent.BUTTON1)
204 return;
205 mousePressed = true;
206 if (visible)
207 Main.map.mapView.repaint();
208 }
209 @Override public void mouseReleased(MouseEvent ev) {
210 if (ev.getButton() != MouseEvent.BUTTON1)
211 return;
212 mousePressed = false;
213 if (!visible)
214 return;
215 for (int i = data.size(); i > 0; --i) {
216 ImageEntry e = data.get(i-1);
217 if (e.pos == null)
218 continue;
219 Point p = Main.map.mapView.getPoint(e.pos);
220 Rectangle r = new Rectangle(p.x-e.icon.getIconWidth()/2, p.y-e.icon.getIconHeight()/2, e.icon.getIconWidth(), e.icon.getIconHeight());
221 if (r.contains(ev.getPoint())) {
222 showImage(i-1);
223 break;
224 }
225 }
226 Main.map.mapView.repaint();
227 }
228 };
229 Main.map.mapView.addMouseListener(mouseAdapter);
230 Layer.listeners.add(new LayerChangeListener(){
231 public void activeLayerChange(Layer oldLayer, Layer newLayer) {}
232 public void layerAdded(Layer newLayer) {}
233 public void layerRemoved(Layer oldLayer) {
234 Main.map.mapView.removeMouseListener(mouseAdapter);
235 }
236 });
237 }
238
239 private void showImage(int i) {
240 currentImage = i;
241 final JPanel p = new JPanel(new BorderLayout());
242 final ImageEntry e = data.get(currentImage);
243 final JScrollPane scroll = new JScrollPane(new JLabel(loadScaledImage(e.image, 580)));
244 final JViewport vp = scroll.getViewport();
245 p.add(scroll, BorderLayout.CENTER);
246
247 final JToggleButton scale = new JToggleButton(ImageProvider.get("dialogs", "zoom-best-fit"));
248 final JButton next = new JButton(ImageProvider.get("dialogs", "next"));
249 final JButton prev = new JButton(ImageProvider.get("dialogs", "previous"));
250 final JToggleButton cent = new JToggleButton(ImageProvider.get("dialogs", "centreview"));
251
252 JPanel p2 = new JPanel();
253 p2.add(prev);
254 p2.add(scale);
255 p2.add(cent);
256 p2.add(next);
257 prev.setEnabled(currentImage>0?true:false);
258 next.setEnabled(currentImage<data.size()-1?true:false);
259 p.add(p2, BorderLayout.SOUTH);
260 final JOptionPane pane = new JOptionPane(p, JOptionPane.PLAIN_MESSAGE);
261 final JDialog dlg = pane.createDialog(Main.parent, e.image+" ("+e.coor.toDisplayString()+")");
262 scale.addActionListener(new ActionListener(){
263 public void actionPerformed(ActionEvent ev) {
264 p.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
265 if (scale.getModel().isSelected())
266 ((JLabel)vp.getView()).setIcon(loadScaledImage(e.image, Math.max(vp.getWidth(), vp.getHeight())));
267 else
268 ((JLabel)vp.getView()).setIcon(new ImageIcon(e.image.getPath()));
269 p.setCursor(Cursor.getDefaultCursor());
270 }
271 });
272 scale.setSelected(true);
273 cent.addActionListener(new ActionListener(){
274 public void actionPerformed(ActionEvent ev) {
275 final ImageEntry e = data.get(currentImage);
276 if (cent.getModel().isSelected())
277 Main.map.mapView.zoomTo(e.pos, Main.map.mapView.getScale());
278 }
279 });
280
281 ActionListener nextprevAction = new ActionListener(){
282 public void actionPerformed(ActionEvent ev) {
283 p.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
284 if (ev.getActionCommand().equals("Next")) {
285 currentImage++;
286 if(currentImage>=data.size()-1) next.setEnabled(false);
287 prev.setEnabled(true);
288 } else {
289 currentImage--;
290 if(currentImage<=0) prev.setEnabled(false);
291 next.setEnabled(true);
292 }
293
294 final ImageEntry e = data.get(currentImage);
295 if (scale.getModel().isSelected())
296 ((JLabel)vp.getView()).setIcon(loadScaledImage(e.image, Math.max(vp.getWidth(), vp.getHeight())));
297 else
298 ((JLabel)vp.getView()).setIcon(new ImageIcon(e.image.getPath()));
299 dlg.setTitle(e.image+" ("+e.coor.toDisplayString()+")");
300 if (cent.getModel().isSelected())
301 Main.map.mapView.zoomTo(e.pos, Main.map.mapView.getScale());
302 p.setCursor(Cursor.getDefaultCursor());
303 }
304 };
305 next.setActionCommand("Next");
306 prev.setActionCommand("Previous");
307 next.setMnemonic(KeyEvent.VK_RIGHT);
308 prev.setMnemonic(KeyEvent.VK_LEFT);
309 scale.setMnemonic(KeyEvent.VK_F);
310 cent.setMnemonic(KeyEvent.VK_C);
311 next.setToolTipText("Show next image");
312 prev.setToolTipText("Show previous image");
313 cent.setToolTipText("Centre image location in main display");
314 scale.setToolTipText("Scale image to fit");
315
316 prev.addActionListener(nextprevAction);
317 next.addActionListener(nextprevAction);
318 cent.setSelected(false);
319
320 dlg.setModal(false);
321 dlg.setVisible(true);
322 }
323
324 @Override public Icon getIcon() {
325 return ImageProvider.get("layer", "tagimages_small");
326 }
327
328 @Override public Object getInfoComponent() {
329 JPanel p = new JPanel(new GridBagLayout());
330 p.add(new JLabel(getToolTipText()), GBC.eop());
331
332 p.add(new JLabel(tr("GPS start: {0}",dateFormat.format(gps.getFirst().time))), GBC.eol());
333 p.add(new JLabel(tr("GPS end: {0}",dateFormat.format(gps.getLast().time))), GBC.eop());
334
335 p.add(new JLabel(tr("current delta: {0}s",(delta/1000.0))), GBC.eol());
336 p.add(new JLabel(tr("timezone difference: ")+(gpstimezone>0?"+":"")+(gpstimezone/1000/60/60)), GBC.eop());
337
338 JList img = new JList(data.toArray());
339 img.setCellRenderer(new DefaultListCellRenderer(){
340 @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
341 super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
342 ImageEntry e = (ImageEntry)value;
343 setIcon(e.icon);
344 setText(e.image.getName()+" ("+dateFormat.format(new Date(e.time.getTime()+(delta+gpstimezone)))+")");
345 if (e.pos == null)
346 setForeground(Color.red);
347 return this;
348 }
349 });
350 img.setVisibleRowCount(5);
351 p.add(new JScrollPane(img), GBC.eop().fill(GBC.BOTH));
352 return p;
353 }
354
355 @Override public String getToolTipText() {
356 int i = 0;
357 for (ImageEntry e : data)
358 if (e.pos != null)
359 i++;
360 return data.size()+" "+trn("image","images",data.size())+". "+tr("{0} within the track.",i);
361 }
362
363 @Override public boolean isMergable(Layer other) {
364 return other instanceof GeoImageLayer;
365 }
366
367 @Override public void mergeFrom(Layer from) {
368 GeoImageLayer l = (GeoImageLayer)from;
369 data.addAll(l.data);
370 }
371
372 @Override public void paint(Graphics g, MapView mv) {
373 boolean clickedFound = false;
374 for (ImageEntry e : data) {
375 if (e.pos != null) {
376 Point p = mv.getPoint(e.pos);
377 Rectangle r = new Rectangle(p.x-e.icon.getIconWidth()/2, p.y-e.icon.getIconHeight()/2, e.icon.getIconWidth(), e.icon.getIconHeight());
378 e.icon.paintIcon(mv, g, r.x, r.y);
379 Border b = null;
380 Point mousePosition = mv.getMousePosition();
381 if (mousePosition == null)
382 continue; // mouse outside the whole window
383 if (!clickedFound && mousePressed && r.contains(mousePosition)) {
384 b = BorderFactory.createBevelBorder(BevelBorder.LOWERED);
385 clickedFound = true;
386 } else
387 b = BorderFactory.createBevelBorder(BevelBorder.RAISED);
388 Insets inset = b.getBorderInsets(mv);
389 r.grow((inset.top+inset.bottom)/2, (inset.left+inset.right)/2);
390 b.paintBorder(mv, g, r.x, r.y, r.width, r.height);
391 }
392 }
393 }
394
395 @Override public void visitBoundingBox(BoundingXYVisitor v) {
396 for (ImageEntry e : data)
397 v.visit(e.pos);
398 }
399
400 @Override public Component[] getMenuEntries() {
401 JMenuItem sync = new JMenuItem(tr("Sync clock"), ImageProvider.get("clock"));
402 sync.addActionListener(new ActionListener(){
403 public void actionPerformed(ActionEvent e) {
404 JFileChooser fc = new JFileChooser(Main.pref.get("tagimages.lastdirectory"));
405 fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
406 fc.setAcceptAllFileFilterUsed(false);
407 fc.setFileFilter(new FileFilter(){
408 @Override public boolean accept(File f) {
409 return f.isDirectory() || f.getName().toLowerCase().endsWith(".jpg");
410 }
411 @Override public String getDescription() {
412 return tr("JPEG images (*.jpg)");
413 }
414 });
415 fc.showOpenDialog(Main.parent);
416 File sel = fc.getSelectedFile();
417 if (sel == null)
418 return;
419 Main.pref.put("tagimages.lastdirectory", sel.getPath());
420 sync(sel);
421 Main.map.repaint();
422 }
423 });
424 return new Component[]{
425 new JMenuItem(new LayerListDialog.ShowHideLayerAction(this)),
426 new JMenuItem(new LayerListDialog.DeleteLayerAction(this)),
427 new JSeparator(),
428 sync,
429 new JSeparator(),
430 new JMenuItem(new RenameLayerAction(null, this)),
431 new JSeparator(),
432 new JMenuItem(new LayerListPopup.InfoAction(this))};
433 }
434
435 private void calculatePosition() {
436 for (ImageEntry e : data) {
437 TimedPoint lastTP = null;
438 for (TimedPoint tp : gps) {
439 Date time = new Date(tp.time.getTime() - (delta+gpstimezone));
440 if (time.after(e.time) && lastTP != null) {
441 double x = (lastTP.pos.east()+tp.pos.east())/2;
442 double y = (lastTP.pos.north()+tp.pos.north())/2;
443 e.pos = new EastNorth(x,y);
444 break;
445 }
446 lastTP = tp;
447 }
448 if (e.pos != null)
449 e.coor = Main.proj.eastNorth2latlon(e.pos);
450 }
451 }
452
453 private void sync(File f) {
454 Date exifDate;
455 try {
456 exifDate = ExifReader.readTime(f);
457 } catch (ParseException e) {
458 JOptionPane.showMessageDialog(Main.parent, tr("The date in file \"{0}\" could not be parsed.", f.getName()));
459 return;
460 }
461 if (exifDate == null) {
462 JOptionPane.showMessageDialog(Main.parent, tr("There is no EXIF time within the file \"{0}\".", f.getName()));
463 return;
464 }
465 JPanel p = new JPanel(new GridBagLayout());
466 p.add(new JLabel(tr("Image")), GBC.eol());
467 p.add(new JLabel(loadScaledImage(f, 300)), GBC.eop());
468 p.add(new JLabel(tr("Enter shown date (mm/dd/yyyy HH:MM:SS)")), GBC.eol());
469 JTextField gpsText = new JTextField(dateFormat.format(new Date(exifDate.getTime()+delta)));
470 p.add(gpsText, GBC.eol().fill(GBC.HORIZONTAL));
471 p.add(new JLabel(tr("GPS unit timezome (difference to photo)")), GBC.eol());
472 String t = Main.pref.get("tagimages.gpstimezone", "0");
473 if (t.charAt(0) != '-')
474 t = "+"+t;
475 JTextField gpsTimezone = new JTextField(t);
476 p.add(gpsTimezone, GBC.eol().fill(GBC.HORIZONTAL));
477
478 while (true) {
479 int answer = JOptionPane.showConfirmDialog(Main.parent, p, tr("Syncronize Time with GPS Unit"), JOptionPane.OK_CANCEL_OPTION);
480 if (answer != JOptionPane.OK_OPTION || gpsText.getText().equals(""))
481 return;
482 try {
483 delta = DateParser.parse(gpsText.getText()).getTime() - exifDate.getTime();
484 String time = gpsTimezone.getText();
485 if (!time.equals("") && time.charAt(0) == '+')
486 time = time.substring(1);
487 if (time.equals(""))
488 time = "0";
489 gpstimezone = Long.valueOf(time)*60*60*1000;
490 Main.pref.put("tagimages.delta", ""+delta);
491 Main.pref.put("tagimages.gpstimezone", time);
492 calculatePosition();
493 return;
494 } catch (NumberFormatException x) {
495 JOptionPane.showMessageDialog(Main.parent, tr("Time entered could not be parsed."));
496 } catch (ParseException x) {
497 JOptionPane.showMessageDialog(Main.parent, tr("Time entered could not be parsed."));
498 }
499 }
500 }
501
502 private static Icon loadScaledImage(File f, int maxSize) {
503 Image img = new ImageIcon(f.getPath()).getImage();
504 int w = img.getWidth(null);
505 int h = img.getHeight(null);
506 if (w>h) {
507 h = Math.round(maxSize*((float)h/w));
508 w = maxSize;
509 } else {
510 w = Math.round(maxSize*((float)w/h));
511 h = maxSize;
512 }
513 return new ImageIcon(createResizedCopy(img, w, h));
514 }
515
516 private static BufferedImage createResizedCopy(Image originalImage,
517 int scaledWidth, int scaledHeight)
518 {
519 BufferedImage scaledBI = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_RGB);
520 Graphics2D g = scaledBI.createGraphics();
521
522 g.drawImage(originalImage, 0, 0, scaledWidth, scaledHeight, null);
523 g.dispose();
524 return scaledBI;
525 }
526}
Note: See TracBrowser for help on using the repository browser.