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

Last change on this file since 2186 was 2186, checked in by jttt, 15 years ago

Fix #3549

  • Property svn:eol-style set to native
File size: 30.2 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 public Loader(Collection<File> files, GpxLayer gpxLayer) {
281 super(tr("Images for {0}", gpxLayer.getName()));
282 this.files = files;
283 this.gpxLayer = gpxLayer;
284 }
285 @Override protected void realRun() throws IOException {
286 progressMonitor.subTask(tr("Read GPX..."));
287 progressMonitor.setTicksCount(10 + files.size());
288 LinkedList<TimedPoint> gps = new LinkedList<TimedPoint>();
289
290 // Extract dates and locations from GPX input
291
292 ProgressMonitor gpxSubTask = progressMonitor.createSubTaskMonitor(10, true);
293 int size = 0;
294 for (GpxTrack trk:gpxLayer.data.tracks) {
295 for (Collection<WayPoint> segment : trk.trackSegs) {
296 size += segment.size();
297 }
298 }
299 gpxSubTask.beginTask(null, size);
300
301 try {
302 for (GpxTrack trk : gpxLayer.data.tracks) {
303 for (Collection<WayPoint> segment : trk.trackSegs) {
304 for (WayPoint p : segment) {
305 LatLon c = p.getCoor();
306 if (!p.attr.containsKey("time"))
307 throw new IOException(tr("No time for point {0} x {1}",c.lat(),c.lon()));
308 Date d = null;
309 try {
310 d = DateParser.parse((String) p.attr.get("time"));
311 } catch (ParseException e) {
312 throw new IOException(tr("Cannot read time \"{0}\" from point {1} x {2}",p.attr.get("time"),c.lat(),c.lon()));
313 }
314 gps.add(new TimedPoint(d, c));
315 gpxSubTask.worked(1);
316 }
317 }
318 }
319 } finally {
320 gpxSubTask.finishTask();
321 }
322
323
324 if (gps.isEmpty()) {
325 progressMonitor.setErrorMessage(tr("No images with readable timestamps found."));
326 return;
327 }
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 (layer != null) {
355 Main.main.addLayer(layer);
356 }
357 }
358 @Override
359 protected void cancel() {
360
361 }
362 }
363
364 public ArrayList<ImageEntry> data;
365 private LinkedList<TimedPoint> gps = new LinkedList<TimedPoint>();
366
367 /**
368 * The delta added to all timestamps in files from the camera
369 * to match to the timestamp from the gps receivers tracklog.
370 */
371 private long delta = Long.parseLong(Main.pref.get("tagimages.delta", "0"));
372 private long gpstimezone = Long.parseLong(Main.pref.get("tagimages.gpstimezone", "0"))*60*60*1000;
373 private boolean mousePressed = false;
374 private static final SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");
375 private MouseAdapter mouseAdapter;
376 private ImageViewerDialog imageViewerDialog;
377
378 public static final class GpsTimeIncorrect extends Exception {
379 public GpsTimeIncorrect(String message, Throwable cause) {
380 super(message, cause);
381 }
382 public GpsTimeIncorrect(String message) {
383 super(message);
384 }
385 }
386
387 private static final class TimedPoint implements Comparable<TimedPoint> {
388 Date time;
389 CachedLatLon pos;
390
391 public TimedPoint(Date time, LatLon pos) {
392 this.time = time;
393 this.pos = new CachedLatLon(pos);
394 }
395 public int compareTo(TimedPoint point) {
396 return time.compareTo(point.time);
397 }
398 }
399
400 public static void create(Collection<File> files, GpxLayer gpxLayer) {
401 Loader loader = new Loader(files, gpxLayer);
402 Main.worker.execute(loader);
403 }
404
405 private GeoImageLayer(final ArrayList<ImageEntry> data, LinkedList<TimedPoint> gps) {
406 super(tr("Geotagged Images"));
407 Collections.sort(data);
408 Collections.sort(gps);
409 this.data = data;
410 this.gps = gps;
411 final Layer self = this;
412 mouseAdapter = new MouseAdapter(){
413 @Override public void mousePressed(MouseEvent e) {
414 if (e.getButton() != MouseEvent.BUTTON1)
415 return;
416 mousePressed = true;
417 if (isVisible()) {
418 Main.map.mapView.repaint();
419 }
420 }
421 @Override public void mouseReleased(MouseEvent ev) {
422 if (ev.getButton() != MouseEvent.BUTTON1)
423 return;
424 mousePressed = false;
425 if (!isVisible())
426 return;
427 for (int i = data.size(); i > 0; --i) {
428 ImageEntry e = data.get(i-1);
429 if (e.pos == null) {
430 continue;
431 }
432 Point p = Main.map.mapView.getPoint(e.pos);
433 Rectangle r = new Rectangle(p.x-ICON_SIZE/2, p.y-ICON_SIZE/2, ICON_SIZE, ICON_SIZE);
434 if (r.contains(ev.getPoint())) {
435 showImage(i-1);
436 break;
437 }
438 }
439 Main.map.mapView.repaint();
440 }
441 };
442 Main.map.mapView.addMouseListener(mouseAdapter);
443 Layer.listeners.add(new LayerChangeListener(){
444 public void activeLayerChange(Layer oldLayer, Layer newLayer) {}
445 public void layerAdded(Layer newLayer) {}
446 public void layerRemoved(Layer oldLayer) {
447 if (oldLayer == self) {
448 Main.map.mapView.removeMouseListener(mouseAdapter);
449 }
450 }
451 });
452 }
453
454 private class ImageViewerDialog {
455
456 private int currentImage;
457 private ImageEntry currentImageEntry;
458
459 private final JDialog dlg;
460 private final JButton nextButton;
461 private final JButton prevButton;
462 private final JToggleButton scaleToggle;
463 private final JToggleButton centerToggle;
464 private final JViewport imageViewport;
465 private final JLabel imageLabel;
466
467 private class ImageAction implements ActionListener {
468
469 private final int offset;
470
471 public ImageAction(int offset) {
472 this.offset = offset;
473 }
474
475 public void actionPerformed(ActionEvent e) {
476 showImage(currentImage + offset);
477 }
478
479 }
480
481 public ImageViewerDialog(ImageEntry firstImage) {
482 final JPanel p = new JPanel(new BorderLayout());
483 imageLabel = new JLabel(new ImageIcon(imageLoader.waitForImage(firstImage.image, 580)));
484 final JScrollPane scroll = new JScrollPane(imageLabel);
485 scroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
486 scroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
487 imageViewport = scroll.getViewport();
488 p.add(scroll, BorderLayout.CENTER);
489
490 scaleToggle = new JToggleButton(ImageProvider.get("dialogs", "zoom-best-fit"));
491 nextButton = new JButton(ImageProvider.get("dialogs", "next"));
492 prevButton = new JButton(ImageProvider.get("dialogs", "previous"));
493 centerToggle = new JToggleButton(ImageProvider.get("dialogs", "centreview"));
494
495 JPanel p2 = new JPanel();
496 p2.add(prevButton);
497 p2.add(scaleToggle);
498 p2.add(centerToggle);
499 p2.add(nextButton);
500 p.add(p2, BorderLayout.SOUTH);
501 final JOptionPane pane = new JOptionPane(p, JOptionPane.PLAIN_MESSAGE);
502 dlg = pane.createDialog(Main.parent, "");
503 scaleToggle.addActionListener(new ImageAction(0));
504 scaleToggle.setSelected(true);
505 centerToggle.addActionListener(new ImageAction(0));
506
507 nextButton.setActionCommand("Next");
508 prevButton.setActionCommand("Previous");
509 nextButton.setMnemonic(KeyEvent.VK_RIGHT);
510 prevButton.setMnemonic(KeyEvent.VK_LEFT);
511 scaleToggle.setMnemonic(KeyEvent.VK_F);
512 centerToggle.setMnemonic(KeyEvent.VK_C);
513 nextButton.setToolTipText("Show next image");
514 prevButton.setToolTipText("Show previous image");
515 centerToggle.setToolTipText("Centre image location in main display");
516 scaleToggle.setToolTipText("Scale image to fit");
517
518 prevButton.addActionListener(new ImageAction(-1));
519 nextButton.addActionListener(new ImageAction(1));
520 centerToggle.setSelected(false);
521
522 dlg.addComponentListener(new ComponentListener() {
523 boolean ignoreEvent = true;
524 public void componentHidden(ComponentEvent e) {}
525 public void componentMoved(ComponentEvent e) {}
526 public void componentResized(ComponentEvent ev) {
527 // we ignore the first resize event, as the picture is scaled already on load:
528 if (scaleToggle.getModel().isSelected() && !ignoreEvent) {
529 imageLabel.setIcon(new ImageIcon(imageLoader.waitForImage(currentImageEntry.image,
530 Math.max(imageViewport.getWidth(), imageViewport.getHeight()))));
531 }
532 ignoreEvent = false;
533 }
534 public void componentShown(ComponentEvent e) {}
535
536 });
537 dlg.setModal(false);
538 dlg.setResizable(true);
539 dlg.pack();
540 }
541
542 public void showImage(int index) {
543 dlg.setVisible(true);
544 dlg.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
545
546 if (index < 0) {
547 index = 0;
548 } else if (index >= data.size() - 1) {
549 index = data.size() - 1;
550 }
551
552 currentImage = index;
553 currentImageEntry = data.get(currentImage);
554
555 prevButton.setEnabled(currentImage > 0);
556 nextButton.setEnabled(currentImage < data.size() - 1);
557
558 if (scaleToggle.getModel().isSelected()) {
559 imageLabel.setIcon(new ImageIcon(imageLoader.waitForImage(currentImageEntry.image,
560 Math.max(imageViewport.getWidth(), imageViewport.getHeight()))));
561 } else {
562 imageLabel.setIcon(new ImageIcon(imageLoader.waitForImage(currentImageEntry.image)));
563 }
564
565 if (centerToggle.getModel().isSelected()) {
566 Main.map.mapView.zoomTo(currentImageEntry.pos);
567 }
568
569 dlg.setTitle(currentImageEntry.image +
570 " (" + currentImageEntry.pos.toDisplayString() + ")");
571 dlg.setCursor(Cursor.getDefaultCursor());
572 }
573
574 }
575
576 private void showImage(int i) {
577 if (imageViewerDialog == null) {
578 imageViewerDialog = new ImageViewerDialog(data.get(i));
579 }
580 imageViewerDialog.showImage(i);
581 }
582
583 @Override public Icon getIcon() {
584 return ImageProvider.get("layer", "tagimages_small");
585 }
586
587 @Override public Object getInfoComponent() {
588 JPanel p = new JPanel(new GridBagLayout());
589 p.add(new JLabel(getToolTipText()), GBC.eop());
590
591 p.add(new JLabel(tr("GPS start: {0}",dateFormat.format(gps.getFirst().time))), GBC.eol());
592 p.add(new JLabel(tr("GPS end: {0}",dateFormat.format(gps.getLast().time))), GBC.eop());
593
594 p.add(new JLabel(tr("current delta: {0}s",(delta/1000.0))), GBC.eol());
595 p.add(new JLabel(tr("timezone difference: ")+(gpstimezone>0?"+":"")+(gpstimezone/1000/60/60)), GBC.eop());
596
597 JList img = new JList(data.toArray());
598 img.setCellRenderer(new DefaultListCellRenderer(){
599 @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
600 super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
601 ImageEntry e = (ImageEntry)value;
602 setIcon(new ImageIcon(e.getIcon()));
603 setText(e.image.getName()+" ("+dateFormat.format(new Date(e.time.getTime()+(delta+gpstimezone)))+")");
604 if (e.pos == null) {
605 setForeground(Color.red);
606 }
607 return this;
608 }
609 });
610 img.setVisibleRowCount(5);
611 p.add(new JScrollPane(img), GBC.eop().fill(GBC.BOTH));
612 return p;
613 }
614
615 @Override public String getToolTipText() {
616 int i = 0;
617 for (ImageEntry e : data)
618 if (e.pos != null) {
619 i++;
620 }
621 return data.size()+" "+trn("image","images",data.size())+". "+tr("{0} within the track.",i);
622 }
623
624 @Override public boolean isMergable(Layer other) {
625 return other instanceof GeoImageLayer;
626 }
627
628 @Override public void mergeFrom(Layer from) {
629 GeoImageLayer l = (GeoImageLayer)from;
630 data.addAll(l.data);
631 }
632
633 @Override public void paint(Graphics g, MapView mv) {
634 int clickedIndex = -1;
635
636 // First select beveled icon (for cases where are more icons on the same spot)
637 Point mousePosition = mv.getMousePosition();
638 if (mousePosition != null && mousePressed) {
639 for (int i = data.size() - 1; i >= 0; i--) {
640 ImageEntry e = data.get(i);
641 if (e.pos == null) {
642 continue;
643 }
644
645 Point p = mv.getPoint(e.pos);
646 Rectangle r = new Rectangle(p.x-ICON_SIZE / 2, p.y-ICON_SIZE / 2, ICON_SIZE, ICON_SIZE);
647 if (r.contains(mousePosition)) {
648 clickedIndex = i;
649 break;
650 }
651 }
652 }
653
654 for (int i = 0; i < data.size(); i++) {
655 ImageEntry e = data.get(i);
656 if (e.pos != null) {
657 Point p = mv.getPoint(e.pos);
658 Rectangle r = new Rectangle(p.x-ICON_SIZE / 2, p.y-ICON_SIZE / 2, ICON_SIZE, ICON_SIZE);
659 g.drawImage(e.getIcon(), r.x, r.y, null);
660 Border b = null;
661 if (i == clickedIndex) {
662 b = BorderFactory.createBevelBorder(BevelBorder.LOWERED);
663 } else {
664 b = BorderFactory.createBevelBorder(BevelBorder.RAISED);
665 }
666 Insets inset = b.getBorderInsets(mv);
667 r.grow((inset.top+inset.bottom)/2, (inset.left+inset.right)/2);
668 b.paintBorder(mv, g, r.x, r.y, r.width, r.height);
669 }
670 }
671 }
672
673 @Override public void visitBoundingBox(BoundingXYVisitor v) {
674 for (ImageEntry e : data) {
675 v.visit(e.pos);
676 }
677 }
678
679 @Override public Component[] getMenuEntries() {
680 JMenuItem sync = new JMenuItem(tr("Sync clock"), ImageProvider.get("clock"));
681 sync.addActionListener(new ActionListener(){
682 public void actionPerformed(ActionEvent e) {
683 JFileChooser fc = new JFileChooser(Main.pref.get("tagimages.lastdirectory"));
684 fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
685 fc.setAcceptAllFileFilterUsed(false);
686 fc.setFileFilter(new FileFilter(){
687 @Override public boolean accept(File f) {
688 return f.isDirectory() || f.getName().toLowerCase().endsWith(".jpg");
689 }
690 @Override public String getDescription() {
691 return tr("JPEG images (*.jpg)");
692 }
693 });
694 fc.showOpenDialog(Main.parent);
695 File sel = fc.getSelectedFile();
696 if (sel == null)
697 return;
698 Main.pref.put("tagimages.lastdirectory", sel.getPath());
699 sync(sel);
700 Main.map.repaint();
701 }
702 });
703 return new Component[]{
704 new JMenuItem(LayerListDialog.getInstance().createShowHideLayerAction(this)),
705 new JMenuItem(LayerListDialog.getInstance().createDeleteLayerAction(this)),
706 new JSeparator(),
707 sync,
708 new JSeparator(),
709 new JMenuItem(new RenameLayerAction(null, this)),
710 new JSeparator(),
711 new JMenuItem(new LayerListPopup.InfoAction(this))};
712 }
713
714 private void calculatePosition() {
715 for (ImageEntry e : data) {
716 TimedPoint lastTP = null;
717 for (TimedPoint tp : gps) {
718 Date time = new Date(tp.time.getTime() - (delta+gpstimezone));
719 if (time.after(e.time) && lastTP != null) {
720 e.pos = new CachedLatLon(lastTP.pos.getCenter(tp.pos));
721 break;
722 }
723 lastTP = tp;
724 }
725 if (e.pos == null) {
726 e.pos = gps.getLast().pos;
727 }
728 }
729 }
730
731 private void sync(File f) {
732 Date exifDate;
733 try {
734 exifDate = ExifReader.readTime(f);
735 } catch (ParseException e) {
736 JOptionPane.showMessageDialog(
737 Main.parent,
738 tr("The date in file \"{0}\" could not be parsed.", f.getName()),
739 tr("Error"),
740 JOptionPane.ERROR_MESSAGE
741 );
742 return;
743 }
744 if (exifDate == null) {
745 JOptionPane.showMessageDialog(
746 Main.parent,
747 tr("There is no EXIF time within the file \"{0}\".", f.getName()),
748 tr("Error"),
749 JOptionPane.ERROR_MESSAGE
750 );
751 return;
752 }
753 JPanel p = new JPanel(new GridBagLayout());
754 p.add(new JLabel(tr("Image")), GBC.eol());
755 p.add(new JLabel(new ImageIcon(imageLoader.waitForImage(f, 300))), GBC.eop());
756 p.add(new JLabel(tr("Enter shown date (mm/dd/yyyy HH:MM:SS)")), GBC.eol());
757 JTextField gpsText = new JTextField(dateFormat.format(new Date(exifDate.getTime()+delta)));
758 p.add(gpsText, GBC.eol().fill(GBC.HORIZONTAL));
759 p.add(new JLabel(tr("GPS unit timezone (difference to photo)")), GBC.eol());
760 String t = Main.pref.get("tagimages.gpstimezone", "0");
761 if (t.charAt(0) != '-') {
762 t = "+"+t;
763 }
764 JTextField gpsTimezone = new JTextField(t);
765 p.add(gpsTimezone, GBC.eol().fill(GBC.HORIZONTAL));
766
767 while (true) {
768 int answer = JOptionPane.showConfirmDialog(
769 Main.parent,
770 p,
771 tr("Synchronize Time with GPS Unit"),
772 JOptionPane.OK_CANCEL_OPTION,
773 JOptionPane.QUESTION_MESSAGE
774 );
775 if (answer != JOptionPane.OK_OPTION || gpsText.getText().equals(""))
776 return;
777 try {
778 delta = DateParser.parse(gpsText.getText()).getTime() - exifDate.getTime();
779 String time = gpsTimezone.getText();
780 if (!time.equals("") && time.charAt(0) == '+') {
781 time = time.substring(1);
782 }
783 if (time.equals("")) {
784 time = "0";
785 }
786 gpstimezone = Long.valueOf(time)*60*60*1000;
787 Main.pref.put("tagimages.delta", ""+delta);
788 Main.pref.put("tagimages.gpstimezone", time);
789 calculatePosition();
790 return;
791 } catch (NumberFormatException x) {
792 JOptionPane.showMessageDialog(
793 Main.parent,
794 tr("Time entered could not be parsed."),
795 tr("Error"),
796 JOptionPane.ERROR_MESSAGE
797 );
798 } catch (ParseException x) {
799 JOptionPane.showMessageDialog(
800 Main.parent,
801 tr("Time entered could not be parsed."),
802 tr("Error"),
803 JOptionPane.ERROR_MESSAGE
804 );
805 }
806 }
807 }
808
809}
Note: See TracBrowser for help on using the repository browser.