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

Last change on this file since 1722 was 1722, checked in by stoecker, 15 years ago

Large rework in projection handling - now allows only switching and more specific projections
TODO:

  • allow subprojections (i.e. settings for projections)
  • setup preferences for subprojections
  • better support of the new projection depending world bounds (how to handle valid data outside of world)
  • do not allow to zoom out of the world - zoom should stop when whole world is displayed
  • fix Lambert and SwissGrid to handle new OutOfWorld style and subprojections
  • fix new UTM projection
  • handle layers with fixed projection on projection change
  • allow easier projection switching (e.g. in menu)

NOTE:
This checkin very likely will cause problems. Please report or fix them. Older plugins may have trouble. The SVN plugins
have been fixed but may have problems nevertheless. This is a BIG change, but will make JOSMs internal structure much cleaner
and reduce lots of projection related problems.

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