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

Last change on this file since 1890 was 1890, checked in by Gubaer, 15 years ago

update: rewrite of layer dialog
new: allows multiple selection of layers in the dialog
new: move up, move down, toggle visibility, and delete on multiple layers
new: merge from an arbitrary layer into another layer, not only from the first into the second
new: new action for merging of the currently selected primitives on an arbitrary layer
new: make "active" layer explicit (special icon); activating a layer automatically moves it in the first position
refactoring: public fields 'name' and 'visible' on Layer are @deprecated. Use the setter/getters instead, Layer now emits PropertyChangeEvents if name or visibility are changed.

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