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

Last change on this file since 2322 was 2322, checked in by Gubaer, 16 years ago

Added canceling of DownloadOsmTaskLists
Removed error remembering in the progress dialog

  • Property svn:eol-style set to native
File size: 30.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.Toolkit;
19import java.awt.event.ActionEvent;
20import java.awt.event.ActionListener;
21import java.awt.event.ComponentEvent;
22import java.awt.event.ComponentListener;
23import java.awt.event.KeyEvent;
24import java.awt.event.MouseAdapter;
25import java.awt.event.MouseEvent;
26import java.awt.image.BufferedImage;
27import java.awt.image.ImageObserver;
28import java.io.File;
29import java.io.IOException;
30import java.lang.ref.SoftReference;
31import java.text.ParseException;
32import java.text.SimpleDateFormat;
33import java.util.ArrayList;
34import java.util.Collection;
35import java.util.Collections;
36import java.util.Date;
37import java.util.LinkedList;
38import java.util.List;
39import java.util.WeakHashMap;
40
41import javax.swing.BorderFactory;
42import javax.swing.DefaultListCellRenderer;
43import javax.swing.Icon;
44import javax.swing.ImageIcon;
45import javax.swing.JButton;
46import javax.swing.JDialog;
47import javax.swing.JFileChooser;
48import javax.swing.JLabel;
49import javax.swing.JList;
50import javax.swing.JMenuItem;
51import javax.swing.JOptionPane;
52import javax.swing.JPanel;
53import javax.swing.JScrollPane;
54import javax.swing.JSeparator;
55import javax.swing.JTextField;
56import javax.swing.JToggleButton;
57import javax.swing.JViewport;
58import javax.swing.ScrollPaneConstants;
59import javax.swing.border.BevelBorder;
60import javax.swing.border.Border;
61import javax.swing.filechooser.FileFilter;
62
63import org.openstreetmap.josm.Main;
64import org.openstreetmap.josm.actions.RenameLayerAction;
65import org.openstreetmap.josm.data.coor.CachedLatLon;
66import org.openstreetmap.josm.data.coor.LatLon;
67import org.openstreetmap.josm.data.gpx.GpxTrack;
68import org.openstreetmap.josm.data.gpx.WayPoint;
69import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
70import org.openstreetmap.josm.gui.MapFrame;
71import org.openstreetmap.josm.gui.MapView;
72import org.openstreetmap.josm.gui.PleaseWaitRunnable;
73import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
74import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
75import org.openstreetmap.josm.gui.layer.GeoImageLayer.ImageLoader.ImageLoadedListener;
76import org.openstreetmap.josm.gui.progress.ProgressMonitor;
77import org.openstreetmap.josm.tools.DateParser;
78import org.openstreetmap.josm.tools.ExifReader;
79import org.openstreetmap.josm.tools.GBC;
80import org.openstreetmap.josm.tools.ImageProvider;
81
82/**
83 * A layer which imports several photos from disk and read EXIF time information from them.
84 *
85 * @author Imi
86 */
87public class GeoImageLayer extends Layer {
88
89 /**
90 * Allows to load and scale images. Loaded images are kept in cache (using soft reference). Both
91 * synchronous and asynchronous loading is supported
92 *
93 */
94 public static class ImageLoader implements ImageObserver {
95
96 public static class Entry {
97 final File file;
98 final int width;
99 final int height;
100 final int maxSize;
101 private final ImageLoadedListener listener;
102
103 volatile Image scaledImage;
104
105
106 public Entry(File file, int width, int height, int maxSize, ImageLoadedListener listener) {
107 this.file = file;
108 this.height = height;
109 this.width = width;
110 this.maxSize = maxSize;
111 this.listener = listener;
112 }
113 }
114
115 public interface ImageLoadedListener {
116 void imageLoaded();
117 }
118
119 private final List<ImageLoader.Entry> queue = new ArrayList<ImageLoader.Entry>();
120 private final WeakHashMap<File, SoftReference<Image>> loadedImageCache = new WeakHashMap<File, SoftReference<Image>>();
121 private ImageLoader.Entry currentEntry;
122
123 private Image getOrLoadImage(File file) {
124 SoftReference<Image> cachedImageRef = loadedImageCache.get(file);
125 if (cachedImageRef != null) {
126 Image cachedImage = cachedImageRef.get();
127 if (cachedImage != null)
128 return cachedImage;
129 }
130 return Toolkit.getDefaultToolkit().createImage(currentEntry.file.getAbsolutePath());
131 }
132
133 private BufferedImage createResizedCopy(Image originalImage,
134 int scaledWidth, int scaledHeight)
135 {
136 BufferedImage scaledBI = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_RGB);
137 Graphics2D g = scaledBI.createGraphics();
138 while (!g.drawImage(originalImage, 0, 0, scaledWidth, scaledHeight, null))
139 {
140 try {
141 Thread.sleep(10);
142 } catch(InterruptedException ie) {}
143 }
144 g.dispose();
145 return scaledBI;
146 }
147 private void loadImage() {
148 if (currentEntry != null)
149 return;
150 while (!queue.isEmpty()) {
151 currentEntry = queue.get(0);
152 queue.remove(0);
153
154 Image newImage = getOrLoadImage(currentEntry.file);
155 if (newImage.getWidth(this) == -1) {
156 break;
157 } else {
158 finishImage(newImage, currentEntry);
159 currentEntry = null;
160 }
161 }
162 }
163
164 private void finishImage(Image img, ImageLoader.Entry entry) {
165 loadedImageCache.put(entry.file, new SoftReference<Image>(img));
166 if (entry.maxSize != -1) {
167 int w = img.getWidth(null);
168 int h = img.getHeight(null);
169 if (w>h) {
170 h = Math.round(entry.maxSize*((float)h/w));
171 w = entry.maxSize;
172 } else {
173 w = Math.round(entry.maxSize*((float)w/h));
174 h = entry.maxSize;
175 }
176 entry.scaledImage = createResizedCopy(img, w, h);
177 } else if (entry.width != -1 && entry.height != -1) {
178 entry.scaledImage = createResizedCopy(img, entry.width, entry.height);
179 } else {
180 entry.scaledImage = img;
181 }
182 if (entry.listener != null) {
183 entry.listener.imageLoaded();
184 }
185 }
186
187 public synchronized ImageLoader.Entry loadImage(File file, int width, int height, int maxSize, ImageLoadedListener listener) {
188 ImageLoader.Entry e = new Entry(file, width, height, maxSize, listener);
189 queue.add(e);
190 loadImage();
191 return e;
192 }
193
194 public Image waitForImage(File file, int width, int height) {
195 return waitForImage(file, width, height, -1);
196 }
197
198 public Image waitForImage(File file, int maxSize) {
199 return waitForImage(file, -1, -1, maxSize);
200 }
201
202 public Image waitForImage(File file) {
203 return waitForImage(file, -1, -1, -1);
204 }
205
206 private synchronized Image waitForImage(File file, int width, int height, int maxSize) {
207 ImageLoader.Entry entry;
208 if (currentEntry != null && currentEntry.file.equals(file)) {
209 entry = currentEntry;
210 } else {
211 entry = new Entry(file, width, height, maxSize, null);
212 queue.add(0, entry);
213 }
214 loadImage();
215
216 while (true) {
217 if (entry.scaledImage != null)
218 return entry.scaledImage;
219 try {
220 wait();
221 } catch (InterruptedException e) {}
222 }
223 }
224
225
226 public synchronized boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
227 if ((infoflags & ImageObserver.ALLBITS) != 0) {
228 finishImage(img, currentEntry);
229 currentEntry = null;
230 loadImage();
231 notifyAll();
232 } else if ((infoflags & ImageObserver.ERROR) != 0) {
233 currentEntry.scaledImage = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_BINARY);
234 currentEntry = null;
235 }
236 return true;
237 }
238 }
239
240 private static final int ICON_SIZE = 16;
241 private static ImageLoader imageLoader = new ImageLoader();
242
243 private static final class ImageEntry implements Comparable<ImageEntry>, ImageLoadedListener {
244
245 private static final Image EMPTY_IMAGE = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_BINARY);
246
247 final File image;
248 ImageLoader.Entry icon;
249 Date time;
250 CachedLatLon pos;
251
252 public ImageEntry(File image) {
253 this.image = image;
254 icon = imageLoader.loadImage(image, ICON_SIZE, ICON_SIZE, -1, this);
255 }
256
257 public int compareTo(ImageEntry image) {
258 return time.compareTo(image.time);
259 }
260
261 public Image getIcon() {
262 if (icon.scaledImage == null)
263 return EMPTY_IMAGE;
264 else
265 return icon.scaledImage;
266 }
267
268 public void imageLoaded() {
269 MapFrame frame = Main.map;
270 if (frame != null) {
271 frame.mapView.repaint();
272 }
273 }
274 }
275
276 private static final class Loader extends PleaseWaitRunnable {
277 private GeoImageLayer layer;
278 private final Collection<File> files;
279 private final GpxLayer gpxLayer;
280 private LinkedList<TimedPoint> gps;
281
282 public Loader(Collection<File> files, GpxLayer gpxLayer) {
283 super(tr("Images for {0}", gpxLayer.getName()));
284 this.files = files;
285 this.gpxLayer = gpxLayer;
286 }
287 @Override protected void realRun() throws IOException {
288 progressMonitor.subTask(tr("Read GPX..."));
289 progressMonitor.setTicksCount(10 + files.size());
290 gps = new LinkedList<TimedPoint>();
291
292 // Extract dates and locations from GPX input
293
294 ProgressMonitor gpxSubTask = progressMonitor.createSubTaskMonitor(10, true);
295 int size = 0;
296 for (GpxTrack trk:gpxLayer.data.tracks) {
297 for (Collection<WayPoint> segment : trk.trackSegs) {
298 size += segment.size();
299 }
300 }
301 gpxSubTask.beginTask(null, size);
302
303 try {
304 for (GpxTrack trk : gpxLayer.data.tracks) {
305 for (Collection<WayPoint> segment : trk.trackSegs) {
306 for (WayPoint p : segment) {
307 LatLon c = p.getCoor();
308 if (!p.attr.containsKey("time"))
309 throw new IOException(tr("No time for point {0} x {1}",c.lat(),c.lon()));
310 Date d = null;
311 try {
312 d = DateParser.parse((String) p.attr.get("time"));
313 } catch (ParseException e) {
314 throw new IOException(tr("Cannot read time \"{0}\" from point {1} x {2}",p.attr.get("time"),c.lat(),c.lon()));
315 }
316 gps.add(new TimedPoint(d, c));
317 gpxSubTask.worked(1);
318 }
319 }
320 }
321 } finally {
322 gpxSubTask.finishTask();
323 }
324
325
326 if (gps.isEmpty())
327 return;
328
329 // read the image files
330 ArrayList<ImageEntry> data = new ArrayList<ImageEntry>(files.size());
331 for (File f : files) {
332 if (progressMonitor.isCancelled()) {
333 break;
334 }
335 progressMonitor.subTask(tr("Reading {0}...",f.getName()));
336
337 ImageEntry e = new ImageEntry(f);
338 try {
339 e.time = ExifReader.readTime(f);
340 progressMonitor.worked(1);
341 } catch (ParseException e1) {
342 continue;
343 }
344 if (e.time == null) {
345 continue;
346 }
347
348 data.add(e);
349 }
350 layer = new GeoImageLayer(data, gps);
351 layer.calculatePosition();
352 }
353 @Override protected void finish() {
354 if (gps.isEmpty()) {
355 JOptionPane.showMessageDialog(
356 Main.parent,
357 tr("No images with readable timestamps found."),
358 tr("Warning"),
359 JOptionPane.WARNING_MESSAGE
360 );
361 return;
362 }
363 if (layer != null) {
364 Main.main.addLayer(layer);
365 }
366 }
367
368 @Override
369 protected void cancel() {
370
371 }
372 }
373
374 public ArrayList<ImageEntry> data;
375 private LinkedList<TimedPoint> gps = new LinkedList<TimedPoint>();
376
377 /**
378 * The delta added to all timestamps in files from the camera
379 * to match to the timestamp from the gps receivers tracklog.
380 */
381 private long delta = Long.parseLong(Main.pref.get("tagimages.delta", "0"));
382 private long gpstimezone = Long.parseLong(Main.pref.get("tagimages.gpstimezone", "0"))*60*60*1000;
383 private boolean mousePressed = false;
384 private static final SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");
385 private MouseAdapter mouseAdapter;
386 private ImageViewerDialog imageViewerDialog;
387
388 public static final class GpsTimeIncorrect extends Exception {
389 public GpsTimeIncorrect(String message, Throwable cause) {
390 super(message, cause);
391 }
392 public GpsTimeIncorrect(String message) {
393 super(message);
394 }
395 }
396
397 private static final class TimedPoint implements Comparable<TimedPoint> {
398 Date time;
399 CachedLatLon pos;
400
401 public TimedPoint(Date time, LatLon pos) {
402 this.time = time;
403 this.pos = new CachedLatLon(pos);
404 }
405 public int compareTo(TimedPoint point) {
406 return time.compareTo(point.time);
407 }
408 }
409
410 public static void create(Collection<File> files, GpxLayer gpxLayer) {
411 Loader loader = new Loader(files, gpxLayer);
412 Main.worker.execute(loader);
413 }
414
415 private GeoImageLayer(final ArrayList<ImageEntry> data, LinkedList<TimedPoint> gps) {
416 super(tr("Geotagged Images"));
417 Collections.sort(data);
418 Collections.sort(gps);
419 this.data = data;
420 this.gps = gps;
421 final Layer self = this;
422 mouseAdapter = new MouseAdapter(){
423 @Override public void mousePressed(MouseEvent e) {
424 if (e.getButton() != MouseEvent.BUTTON1)
425 return;
426 mousePressed = true;
427 if (isVisible()) {
428 Main.map.mapView.repaint();
429 }
430 }
431 @Override public void mouseReleased(MouseEvent ev) {
432 if (ev.getButton() != MouseEvent.BUTTON1)
433 return;
434 mousePressed = false;
435 if (!isVisible())
436 return;
437 for (int i = data.size(); i > 0; --i) {
438 ImageEntry e = data.get(i-1);
439 if (e.pos == null) {
440 continue;
441 }
442 Point p = Main.map.mapView.getPoint(e.pos);
443 Rectangle r = new Rectangle(p.x-ICON_SIZE/2, p.y-ICON_SIZE/2, ICON_SIZE, ICON_SIZE);
444 if (r.contains(ev.getPoint())) {
445 showImage(i-1);
446 break;
447 }
448 }
449 Main.map.mapView.repaint();
450 }
451 };
452 Main.map.mapView.addMouseListener(mouseAdapter);
453 Layer.listeners.add(new LayerChangeListener(){
454 public void activeLayerChange(Layer oldLayer, Layer newLayer) {}
455 public void layerAdded(Layer newLayer) {}
456 public void layerRemoved(Layer oldLayer) {
457 if (oldLayer == self) {
458 Main.map.mapView.removeMouseListener(mouseAdapter);
459 }
460 }
461 });
462 }
463
464 private class ImageViewerDialog {
465
466 private int currentImage;
467 private ImageEntry currentImageEntry;
468
469 private final JDialog dlg;
470 private final JButton nextButton;
471 private final JButton prevButton;
472 private final JToggleButton scaleToggle;
473 private final JToggleButton centerToggle;
474 private final JViewport imageViewport;
475 private final JLabel imageLabel;
476
477 private class ImageAction implements ActionListener {
478
479 private final int offset;
480
481 public ImageAction(int offset) {
482 this.offset = offset;
483 }
484
485 public void actionPerformed(ActionEvent e) {
486 showImage(currentImage + offset);
487 }
488
489 }
490
491 public ImageViewerDialog(ImageEntry firstImage) {
492 final JPanel p = new JPanel(new BorderLayout());
493 imageLabel = new JLabel(new ImageIcon(imageLoader.waitForImage(firstImage.image, 580)));
494 final JScrollPane scroll = new JScrollPane(imageLabel);
495 scroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
496 scroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
497 imageViewport = scroll.getViewport();
498 p.add(scroll, BorderLayout.CENTER);
499
500 scaleToggle = new JToggleButton(ImageProvider.get("dialogs", "zoom-best-fit"));
501 nextButton = new JButton(ImageProvider.get("dialogs", "next"));
502 prevButton = new JButton(ImageProvider.get("dialogs", "previous"));
503 centerToggle = new JToggleButton(ImageProvider.get("dialogs", "centreview"));
504
505 JPanel p2 = new JPanel();
506 p2.add(prevButton);
507 p2.add(scaleToggle);
508 p2.add(centerToggle);
509 p2.add(nextButton);
510 p.add(p2, BorderLayout.SOUTH);
511 final JOptionPane pane = new JOptionPane(p, JOptionPane.PLAIN_MESSAGE);
512 dlg = pane.createDialog(Main.parent, "");
513 scaleToggle.addActionListener(new ImageAction(0));
514 scaleToggle.setSelected(true);
515 centerToggle.addActionListener(new ImageAction(0));
516
517 nextButton.setActionCommand("Next");
518 prevButton.setActionCommand("Previous");
519 nextButton.setMnemonic(KeyEvent.VK_RIGHT);
520 prevButton.setMnemonic(KeyEvent.VK_LEFT);
521 scaleToggle.setMnemonic(KeyEvent.VK_F);
522 centerToggle.setMnemonic(KeyEvent.VK_C);
523 nextButton.setToolTipText("Show next image");
524 prevButton.setToolTipText("Show previous image");
525 centerToggle.setToolTipText("Centre image location in main display");
526 scaleToggle.setToolTipText("Scale image to fit");
527
528 prevButton.addActionListener(new ImageAction(-1));
529 nextButton.addActionListener(new ImageAction(1));
530 centerToggle.setSelected(false);
531
532 dlg.addComponentListener(new ComponentListener() {
533 boolean ignoreEvent = true;
534 public void componentHidden(ComponentEvent e) {}
535 public void componentMoved(ComponentEvent e) {}
536 public void componentResized(ComponentEvent ev) {
537 // we ignore the first resize event, as the picture is scaled already on load:
538 if (scaleToggle.getModel().isSelected() && !ignoreEvent) {
539 imageLabel.setIcon(new ImageIcon(imageLoader.waitForImage(currentImageEntry.image,
540 Math.max(imageViewport.getWidth(), imageViewport.getHeight()))));
541 }
542 ignoreEvent = false;
543 }
544 public void componentShown(ComponentEvent e) {}
545
546 });
547 dlg.setModal(false);
548 dlg.setResizable(true);
549 dlg.pack();
550 }
551
552 public void showImage(int index) {
553 dlg.setVisible(true);
554 dlg.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
555
556 if (index < 0) {
557 index = 0;
558 } else if (index >= data.size() - 1) {
559 index = data.size() - 1;
560 }
561
562 currentImage = index;
563 currentImageEntry = data.get(currentImage);
564
565 prevButton.setEnabled(currentImage > 0);
566 nextButton.setEnabled(currentImage < data.size() - 1);
567
568 if (scaleToggle.getModel().isSelected()) {
569 imageLabel.setIcon(new ImageIcon(imageLoader.waitForImage(currentImageEntry.image,
570 Math.max(imageViewport.getWidth(), imageViewport.getHeight()))));
571 } else {
572 imageLabel.setIcon(new ImageIcon(imageLoader.waitForImage(currentImageEntry.image)));
573 }
574
575 if (centerToggle.getModel().isSelected()) {
576 Main.map.mapView.zoomTo(currentImageEntry.pos);
577 }
578
579 dlg.setTitle(currentImageEntry.image +
580 " (" + currentImageEntry.pos.toDisplayString() + ")");
581 dlg.setCursor(Cursor.getDefaultCursor());
582 }
583
584 }
585
586 private void showImage(int i) {
587 if (imageViewerDialog == null) {
588 imageViewerDialog = new ImageViewerDialog(data.get(i));
589 }
590 imageViewerDialog.showImage(i);
591 }
592
593 @Override public Icon getIcon() {
594 return ImageProvider.get("layer", "tagimages_small");
595 }
596
597 @Override public Object getInfoComponent() {
598 JPanel p = new JPanel(new GridBagLayout());
599 p.add(new JLabel(getToolTipText()), GBC.eop());
600
601 p.add(new JLabel(tr("GPS start: {0}",dateFormat.format(gps.getFirst().time))), GBC.eol());
602 p.add(new JLabel(tr("GPS end: {0}",dateFormat.format(gps.getLast().time))), GBC.eop());
603
604 p.add(new JLabel(tr("current delta: {0}s",(delta/1000.0))), GBC.eol());
605 p.add(new JLabel(tr("timezone difference: ")+(gpstimezone>0?"+":"")+(gpstimezone/1000/60/60)), GBC.eop());
606
607 JList img = new JList(data.toArray());
608 img.setCellRenderer(new DefaultListCellRenderer(){
609 @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
610 super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
611 ImageEntry e = (ImageEntry)value;
612 setIcon(new ImageIcon(e.getIcon()));
613 setText(e.image.getName()+" ("+dateFormat.format(new Date(e.time.getTime()+(delta+gpstimezone)))+")");
614 if (e.pos == null) {
615 setForeground(Color.red);
616 }
617 return this;
618 }
619 });
620 img.setVisibleRowCount(5);
621 p.add(new JScrollPane(img), GBC.eop().fill(GBC.BOTH));
622 return p;
623 }
624
625 @Override public String getToolTipText() {
626 int i = 0;
627 for (ImageEntry e : data)
628 if (e.pos != null) {
629 i++;
630 }
631 return data.size()+" "+trn("image","images",data.size())+". "+tr("{0} within the track.",i);
632 }
633
634 @Override public boolean isMergable(Layer other) {
635 return other instanceof GeoImageLayer;
636 }
637
638 @Override public void mergeFrom(Layer from) {
639 GeoImageLayer l = (GeoImageLayer)from;
640 data.addAll(l.data);
641 }
642
643 @Override public void paint(Graphics g, MapView mv) {
644 int clickedIndex = -1;
645
646 // First select beveled icon (for cases where are more icons on the same spot)
647 Point mousePosition = mv.getMousePosition();
648 if (mousePosition != null && mousePressed) {
649 for (int i = data.size() - 1; i >= 0; i--) {
650 ImageEntry e = data.get(i);
651 if (e.pos == null) {
652 continue;
653 }
654
655 Point p = mv.getPoint(e.pos);
656 Rectangle r = new Rectangle(p.x-ICON_SIZE / 2, p.y-ICON_SIZE / 2, ICON_SIZE, ICON_SIZE);
657 if (r.contains(mousePosition)) {
658 clickedIndex = i;
659 break;
660 }
661 }
662 }
663
664 for (int i = 0; i < data.size(); i++) {
665 ImageEntry e = data.get(i);
666 if (e.pos != null) {
667 Point p = mv.getPoint(e.pos);
668 Rectangle r = new Rectangle(p.x-ICON_SIZE / 2, p.y-ICON_SIZE / 2, ICON_SIZE, ICON_SIZE);
669 g.drawImage(e.getIcon(), r.x, r.y, null);
670 Border b = null;
671 if (i == clickedIndex) {
672 b = BorderFactory.createBevelBorder(BevelBorder.LOWERED);
673 } else {
674 b = BorderFactory.createBevelBorder(BevelBorder.RAISED);
675 }
676 Insets inset = b.getBorderInsets(mv);
677 r.grow((inset.top+inset.bottom)/2, (inset.left+inset.right)/2);
678 b.paintBorder(mv, g, r.x, r.y, r.width, r.height);
679 }
680 }
681 }
682
683 @Override public void visitBoundingBox(BoundingXYVisitor v) {
684 for (ImageEntry e : data) {
685 v.visit(e.pos);
686 }
687 }
688
689 @Override public Component[] getMenuEntries() {
690 JMenuItem sync = new JMenuItem(tr("Sync clock"), ImageProvider.get("clock"));
691 sync.addActionListener(new ActionListener(){
692 public void actionPerformed(ActionEvent e) {
693 JFileChooser fc = new JFileChooser(Main.pref.get("tagimages.lastdirectory"));
694 fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
695 fc.setAcceptAllFileFilterUsed(false);
696 fc.setFileFilter(new FileFilter(){
697 @Override public boolean accept(File f) {
698 return f.isDirectory() || f.getName().toLowerCase().endsWith(".jpg");
699 }
700 @Override public String getDescription() {
701 return tr("JPEG images (*.jpg)");
702 }
703 });
704 fc.showOpenDialog(Main.parent);
705 File sel = fc.getSelectedFile();
706 if (sel == null)
707 return;
708 Main.pref.put("tagimages.lastdirectory", sel.getPath());
709 sync(sel);
710 Main.map.repaint();
711 }
712 });
713 return new Component[]{
714 new JMenuItem(LayerListDialog.getInstance().createShowHideLayerAction(this)),
715 new JMenuItem(LayerListDialog.getInstance().createDeleteLayerAction(this)),
716 new JSeparator(),
717 sync,
718 new JSeparator(),
719 new JMenuItem(new RenameLayerAction(null, this)),
720 new JSeparator(),
721 new JMenuItem(new LayerListPopup.InfoAction(this))};
722 }
723
724 private void calculatePosition() {
725 for (ImageEntry e : data) {
726 TimedPoint lastTP = null;
727 for (TimedPoint tp : gps) {
728 Date time = new Date(tp.time.getTime() - (delta+gpstimezone));
729 if (time.after(e.time) && lastTP != null) {
730 e.pos = new CachedLatLon(lastTP.pos.getCenter(tp.pos));
731 break;
732 }
733 lastTP = tp;
734 }
735 if (e.pos == null) {
736 e.pos = gps.getLast().pos;
737 }
738 }
739 }
740
741 private void sync(File f) {
742 Date exifDate;
743 try {
744 exifDate = ExifReader.readTime(f);
745 } catch (ParseException e) {
746 JOptionPane.showMessageDialog(
747 Main.parent,
748 tr("The date in file \"{0}\" could not be parsed.", f.getName()),
749 tr("Error"),
750 JOptionPane.ERROR_MESSAGE
751 );
752 return;
753 }
754 if (exifDate == null) {
755 JOptionPane.showMessageDialog(
756 Main.parent,
757 tr("There is no EXIF time within the file \"{0}\".", f.getName()),
758 tr("Error"),
759 JOptionPane.ERROR_MESSAGE
760 );
761 return;
762 }
763 JPanel p = new JPanel(new GridBagLayout());
764 p.add(new JLabel(tr("Image")), GBC.eol());
765 p.add(new JLabel(new ImageIcon(imageLoader.waitForImage(f, 300))), GBC.eop());
766 p.add(new JLabel(tr("Enter shown date (mm/dd/yyyy HH:MM:SS)")), GBC.eol());
767 JTextField gpsText = new JTextField(dateFormat.format(new Date(exifDate.getTime()+delta)));
768 p.add(gpsText, GBC.eol().fill(GBC.HORIZONTAL));
769 p.add(new JLabel(tr("GPS unit timezone (difference to photo)")), GBC.eol());
770 String t = Main.pref.get("tagimages.gpstimezone", "0");
771 if (t.charAt(0) != '-') {
772 t = "+"+t;
773 }
774 JTextField gpsTimezone = new JTextField(t);
775 p.add(gpsTimezone, GBC.eol().fill(GBC.HORIZONTAL));
776
777 while (true) {
778 int answer = JOptionPane.showConfirmDialog(
779 Main.parent,
780 p,
781 tr("Synchronize Time with GPS Unit"),
782 JOptionPane.OK_CANCEL_OPTION,
783 JOptionPane.QUESTION_MESSAGE
784 );
785 if (answer != JOptionPane.OK_OPTION || gpsText.getText().equals(""))
786 return;
787 try {
788 delta = DateParser.parse(gpsText.getText()).getTime() - exifDate.getTime();
789 String time = gpsTimezone.getText();
790 if (!time.equals("") && time.charAt(0) == '+') {
791 time = time.substring(1);
792 }
793 if (time.equals("")) {
794 time = "0";
795 }
796 gpstimezone = Long.valueOf(time)*60*60*1000;
797 Main.pref.put("tagimages.delta", ""+delta);
798 Main.pref.put("tagimages.gpstimezone", time);
799 calculatePosition();
800 return;
801 } catch (NumberFormatException x) {
802 JOptionPane.showMessageDialog(
803 Main.parent,
804 tr("Time entered could not be parsed."),
805 tr("Error"),
806 JOptionPane.ERROR_MESSAGE
807 );
808 } catch (ParseException x) {
809 JOptionPane.showMessageDialog(
810 Main.parent,
811 tr("Time entered could not be parsed."),
812 tr("Error"),
813 JOptionPane.ERROR_MESSAGE
814 );
815 }
816 }
817 }
818
819}
Note: See TracBrowser for help on using the repository browser.