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

Last change on this file since 2733 was 2733, checked in by framm, 14 years ago

message fixes

  • Property svn:eol-style set to native
File size: 25.0 KB
Line 
1// License: GPL. See LICENSE file for details.
2// Copyright 2007 by Christian Gallioz (aka khris78)
3// Parts of code from Geotagged plugin (by Rob Neild)
4// and the core JOSM source code (by Immanuel Scholz and others)
5
6package org.openstreetmap.josm.gui.layer.geoimage;
7
8import static org.openstreetmap.josm.tools.I18n.tr;
9import static org.openstreetmap.josm.tools.I18n.trn;
10
11import java.awt.AlphaComposite;
12import java.awt.BorderLayout;
13import java.awt.Color;
14import java.awt.Component;
15import java.awt.Composite;
16import java.awt.Dimension;
17import java.awt.Graphics2D;
18import java.awt.Image;
19import java.awt.Point;
20import java.awt.Rectangle;
21import java.awt.event.MouseAdapter;
22import java.awt.event.MouseEvent;
23import java.awt.image.BufferedImage;
24import java.beans.PropertyChangeEvent;
25import java.beans.PropertyChangeListener;
26import java.io.File;
27import java.io.IOException;
28import java.text.ParseException;
29import java.util.ArrayList;
30import java.util.Arrays;
31import java.util.Collection;
32import java.util.Collections;
33import java.util.Date;
34import java.util.HashSet;
35import java.util.LinkedHashSet;
36import java.util.List;
37
38import javax.swing.Icon;
39import javax.swing.JLabel;
40import javax.swing.JMenuItem;
41import javax.swing.JOptionPane;
42import javax.swing.JPanel;
43import javax.swing.JSeparator;
44import javax.swing.SwingConstants;
45
46import org.openstreetmap.josm.Main;
47import org.openstreetmap.josm.actions.RenameLayerAction;
48import org.openstreetmap.josm.actions.mapmode.MapMode;
49import org.openstreetmap.josm.data.Bounds;
50import org.openstreetmap.josm.data.coor.CachedLatLon;
51import org.openstreetmap.josm.data.coor.LatLon;
52import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
53import org.openstreetmap.josm.gui.ExtendedDialog;
54import org.openstreetmap.josm.gui.MapFrame;
55import org.openstreetmap.josm.gui.MapFrame.MapModeChangeListener;
56import org.openstreetmap.josm.gui.MapView;
57import org.openstreetmap.josm.gui.PleaseWaitRunnable;
58import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
59import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
60import org.openstreetmap.josm.gui.layer.GpxLayer;
61import org.openstreetmap.josm.gui.layer.Layer;
62import org.openstreetmap.josm.tools.ExifReader;
63import org.openstreetmap.josm.tools.ImageProvider;
64
65import com.drew.imaging.jpeg.JpegMetadataReader;
66import com.drew.lang.Rational;
67import com.drew.metadata.Directory;
68import com.drew.metadata.Metadata;
69import com.drew.metadata.exif.GpsDirectory;
70
71public class GeoImageLayer extends Layer implements PropertyChangeListener {
72
73 List<ImageEntry> data;
74 GpxLayer gpxLayer;
75
76 private Icon icon = ImageProvider.get("dialogs/geoimage/photo-marker");
77 private Icon selectedIcon = ImageProvider.get("dialogs/geoimage/photo-marker-selected");
78
79 private int currentPhoto = -1;
80
81 boolean useThumbs = false;
82 ThumbsLoader thumbsloader;
83 boolean thumbsLoaded = false;
84 private BufferedImage offscreenBuffer;
85 boolean updateOffscreenBuffer = true;
86
87 /** Loads a set of images, while displaying a dialog that indicates what the plugin is currently doing.
88 * In facts, this object is instantiated with a list of files. These files may be JPEG files or
89 * directories. In case of directories, they are scanned to find all the images they contain.
90 * Then all the images that have be found are loaded as ImageEntry instances.
91 */
92 private static final class Loader extends PleaseWaitRunnable {
93
94 private boolean cancelled = false;
95 private GeoImageLayer layer;
96 private Collection<File> selection;
97 private HashSet<String> loadedDirectories = new HashSet<String>();
98 private LinkedHashSet<String> errorMessages;
99 private GpxLayer gpxLayer;
100
101 protected void rememberError(String message) {
102 this.errorMessages.add(message);
103 }
104
105 public Loader(Collection<File> selection, GpxLayer gpxLayer) {
106 super(tr("Extracting GPS locations from EXIF"));
107 this.selection = selection;
108 this.gpxLayer = gpxLayer;
109 errorMessages = new LinkedHashSet<String>();
110 }
111
112 @Override protected void realRun() throws IOException {
113
114 progressMonitor.subTask(tr("Starting directory scan"));
115 Collection<File> files = new ArrayList<File>();
116 try {
117 addRecursiveFiles(files, selection);
118 } catch(NullPointerException npe) {
119 rememberError(tr("One of the selected files was null"));
120 }
121
122 if (cancelled)
123 return;
124 progressMonitor.subTask(tr("Read photos..."));
125 progressMonitor.setTicksCount(files.size());
126
127 progressMonitor.subTask(tr("Read photos..."));
128 progressMonitor.setTicksCount(files.size());
129
130 // read the image files
131 List<ImageEntry> data = new ArrayList<ImageEntry>(files.size());
132
133 for (File f : files) {
134
135 if (cancelled) {
136 break;
137 }
138
139 progressMonitor.subTask(tr("Reading {0}...", f.getName()));
140 progressMonitor.worked(1);
141
142 ImageEntry e = new ImageEntry();
143
144 // Changed to silently cope with no time info in exif. One case
145 // of person having time that couldn't be parsed, but valid GPS info
146
147 try {
148 e.time = ExifReader.readTime(f);
149 } catch (ParseException e1) {
150 e.time = null;
151 }
152 e.file = f;
153 extractExif(e);
154 data.add(e);
155 }
156 layer = new GeoImageLayer(data, gpxLayer);
157 files.clear();
158 }
159
160 private void addRecursiveFiles(Collection<File> files, Collection<File> sel) {
161 boolean nullFile = false;
162
163 for (File f : sel) {
164
165 if(cancelled) {
166 break;
167 }
168
169 if (f == null) {
170 nullFile = true;
171
172 } else if (f.isDirectory()) {
173 String canonical = null;
174 try {
175 canonical = f.getCanonicalPath();
176 } catch (IOException e) {
177 e.printStackTrace();
178 rememberError(tr("Unable to get canonical path for directory {0}\n",
179 f.getAbsolutePath()));
180 }
181
182 if (canonical == null || loadedDirectories.contains(canonical)) {
183 continue;
184 } else {
185 loadedDirectories.add(canonical);
186 }
187
188 Collection<File> children = Arrays.asList(f.listFiles(JpegFileFilter.getInstance()));
189 if (children != null) {
190 progressMonitor.subTask(tr("Scanning directory {0}", f.getPath()));
191 try {
192 addRecursiveFiles(files, children);
193 } catch(NullPointerException npe) {
194 npe.printStackTrace();
195 rememberError(tr("Found null file in directory {0}\n", f.getPath()));
196 }
197 } else {
198 rememberError(tr("Error while getting files from directory {0}\n", f.getPath()));
199 }
200
201 } else {
202 files.add(f);
203 }
204 }
205
206 if (nullFile)
207 throw new NullPointerException();
208 }
209
210 protected String formatErrorMessages() {
211 StringBuffer sb = new StringBuffer();
212 sb.append("<html>");
213 if (errorMessages.size() == 1) {
214 sb.append(errorMessages.iterator().next());
215 } else {
216 sb.append("<ul>");
217 for (String msg: errorMessages) {
218 sb.append("<li>").append(msg).append("</li>");
219 }
220 sb.append("/ul>");
221 }
222 sb.append("</html>");
223 return sb.toString();
224 }
225
226 @Override protected void finish() {
227 if (!errorMessages.isEmpty()) {
228 JOptionPane.showMessageDialog(
229 Main.parent,
230 formatErrorMessages(),
231 tr("Error"),
232 JOptionPane.ERROR_MESSAGE
233 );
234 }
235 if (layer != null) {
236 Main.main.addLayer(layer);
237 layer.hook_up_mouse_events(); // Main.map.mapView should exist
238 // now. Can add mouse listener
239 Main.map.mapView.addPropertyChangeListener(layer);
240 if (!addedToggleDialog) {
241 Main.map.addToggleDialog(ImageViewerDialog.getInstance());
242 addedToggleDialog = true;
243 }
244
245 if (! cancelled && layer.data.size() > 0) {
246 boolean noGeotagFound = true;
247 for (ImageEntry e : layer.data) {
248 if (e.getPos() != null) {
249 noGeotagFound = false;
250 }
251 }
252 if (noGeotagFound) {
253 new CorrelateGpxWithImages(layer).actionPerformed(null);
254 }
255 }
256 }
257 }
258
259 @Override protected void cancel() {
260 cancelled = true;
261 }
262 }
263
264 private static boolean addedToggleDialog = false;
265
266 public static void create(Collection<File> files, GpxLayer gpxLayer) {
267 Loader loader = new Loader(files, gpxLayer);
268 Main.worker.execute(loader);
269 }
270
271 private GeoImageLayer(final List<ImageEntry> data, GpxLayer gpxLayer) {
272
273 super(tr("Geotagged Images"));
274
275 Collections.sort(data);
276 this.data = data;
277 this.gpxLayer = gpxLayer;
278 }
279
280 @Override
281 public Icon getIcon() {
282 return ImageProvider.get("dialogs/geoimage");
283 }
284
285 @Override
286 public Object getInfoComponent() {
287 // TODO Auto-generated method stub
288 return null;
289 }
290
291 @Override
292 public Component[] getMenuEntries() {
293
294 JMenuItem correlateItem = new JMenuItem(tr("Correlate to GPX"), ImageProvider.get("dialogs/geoimage/gpx2img"));
295 correlateItem.addActionListener(new CorrelateGpxWithImages(this));
296
297 return new Component[] {
298 new JMenuItem(LayerListDialog.getInstance().createShowHideLayerAction(this)),
299 new JMenuItem(LayerListDialog.getInstance().createDeleteLayerAction(this)),
300 new JMenuItem(new RenameLayerAction(null, this)),
301 new JSeparator(),
302 correlateItem
303 };
304 }
305
306 @Override
307 public String getToolTipText() {
308 int i = 0;
309 for (ImageEntry e : data)
310 if (e.getPos() != null) {
311 i++;
312 }
313 return data.size() + " " + trn("image", "images", data.size())
314 + " loaded. " + tr("{0} were found to be gps tagged.", i);
315 }
316
317 @Override
318 public boolean isMergable(Layer other) {
319 return other instanceof GeoImageLayer;
320 }
321
322 @Override
323 public void mergeFrom(Layer from) {
324 GeoImageLayer l = (GeoImageLayer) from;
325
326 ImageEntry selected = null;
327 if (l.currentPhoto >= 0) {
328 selected = l.data.get(l.currentPhoto);
329 }
330
331 data.addAll(l.data);
332 Collections.sort(data);
333
334 // Supress the double photos.
335 if (data.size() > 1) {
336 ImageEntry cur;
337 ImageEntry prev = data.get(data.size() - 1);
338 for (int i = data.size() - 2; i >= 0; i--) {
339 cur = data.get(i);
340 if (cur.file.equals(prev.file)) {
341 data.remove(i);
342 } else {
343 prev = cur;
344 }
345 }
346 }
347
348 if (selected != null) {
349 for (int i = 0; i < data.size() ; i++) {
350 if (data.get(i) == selected) {
351 currentPhoto = i;
352 ImageViewerDialog.showImage(GeoImageLayer.this, data.get(i));
353 break;
354 }
355 }
356 }
357
358 setName(l.getName());
359 }
360
361 private Dimension scaledDimension(Image thumb) {
362 final double d = Main.map.mapView.getDist100Pixel();
363 final double size = 10 /*meter*/; /* size of the photo on the map */
364 double s = size * 100 /*px*/ / d;
365
366 final double sMin = ThumbsLoader.minSize;
367 final double sMax = ThumbsLoader.maxSize;
368
369 if (s < sMin) {
370 s = sMin;
371 }
372 if (s > sMax) {
373 s = sMax;
374 }
375 final double f = s / sMax; /* scale factor */
376
377 if (thumb == null)
378 return null;
379
380 return new Dimension(
381 (int) Math.round(f * thumb.getWidth(null)),
382 (int) Math.round(f * thumb.getHeight(null)));
383 }
384
385 @Override
386 public void paint(Graphics2D g, MapView mv, Bounds bounds) {
387 int width = Main.map.mapView.getWidth();
388 int height = Main.map.mapView.getHeight();
389 Rectangle clip = g.getClipBounds();
390 if (useThumbs) {
391 if (null == offscreenBuffer || offscreenBuffer.getWidth() != width // reuse the old buffer if possible
392 || offscreenBuffer.getHeight() != height) {
393 offscreenBuffer = new BufferedImage(width, height,
394 BufferedImage.TYPE_INT_ARGB);
395 updateOffscreenBuffer = true;
396 }
397
398 if (updateOffscreenBuffer) {
399 Graphics2D tempG = offscreenBuffer.createGraphics();
400 tempG.setColor(new Color(0,0,0,0));
401 Composite saveComp = tempG.getComposite();
402 tempG.setComposite(AlphaComposite.Clear); // remove the old images
403 tempG.fillRect(0, 0, width, height);
404 tempG.setComposite(saveComp);
405
406 for (ImageEntry e : data) {
407 if (e.getPos() == null) {
408 continue;
409 }
410 Point p = mv.getPoint(e.getPos());
411 if (e.thumbnail != null) {
412 Dimension d = scaledDimension(e.thumbnail);
413 Rectangle target = new Rectangle(p.x - d.width / 2, p.y - d.height / 2, d.width, d.height);
414 if (clip.intersects(target)) {
415 tempG.drawImage(e.thumbnail, target.x, target.y, target.width, target.height, null);
416 }
417 }
418 else { // thumbnail not loaded yet
419 icon.paintIcon(mv, tempG,
420 p.x - icon.getIconWidth() / 2,
421 p.y - icon.getIconHeight() / 2);
422 }
423 }
424 updateOffscreenBuffer = false;
425 }
426 g.drawImage(offscreenBuffer, 0, 0, null);
427 }
428 else {
429 for (ImageEntry e : data) {
430 if (e.getPos() == null) {
431 continue;
432 }
433 Point p = mv.getPoint(e.getPos());
434 icon.paintIcon(mv, g,
435 p.x - icon.getIconWidth() / 2,
436 p.y - icon.getIconHeight() / 2);
437 }
438 }
439
440 if (currentPhoto >= 0 && currentPhoto < data.size()) {
441 ImageEntry e = data.get(currentPhoto);
442
443 if (e.getPos() != null) {
444 Point p = mv.getPoint(e.getPos());
445
446 if (e.thumbnail != null) {
447 Dimension d = scaledDimension(e.thumbnail);
448 g.setColor(new Color(128, 0, 0, 122));
449 g.fillRect(p.x - d.width / 2, p.y - d.height / 2, d.width, d.height);
450 } else {
451 selectedIcon.paintIcon(mv, g,
452 p.x - selectedIcon.getIconWidth() / 2,
453 p.y - selectedIcon.getIconHeight() / 2);
454 }
455 }
456 }
457 }
458
459 @Override
460 public void visitBoundingBox(BoundingXYVisitor v) {
461 for (ImageEntry e : data) {
462 v.visit(e.getPos());
463 }
464 }
465
466 /*
467 * Extract gps from image exif
468 *
469 * If successful, fills in the LatLon and EastNorth attributes of passed in
470 * image;
471 */
472
473 private static void extractExif(ImageEntry e) {
474
475 try {
476 int deg;
477 float min, sec;
478 double lon, lat;
479
480 Metadata metadata = JpegMetadataReader.readMetadata(e.file);
481 Directory dir = metadata.getDirectory(GpsDirectory.class);
482
483 // longitude
484
485 Rational[] components = dir
486 .getRationalArray(GpsDirectory.TAG_GPS_LONGITUDE);
487
488 deg = components[0].intValue();
489 min = components[1].floatValue();
490 sec = components[2].floatValue();
491
492 lon = (deg + (min / 60) + (sec / 3600));
493
494 if (dir.getString(GpsDirectory.TAG_GPS_LONGITUDE_REF).charAt(0) == 'W') {
495 lon = -lon;
496 }
497
498 // latitude
499
500 components = dir.getRationalArray(GpsDirectory.TAG_GPS_LATITUDE);
501
502 deg = components[0].intValue();
503 min = components[1].floatValue();
504 sec = components[2].floatValue();
505
506 lat = (deg + (min / 60) + (sec / 3600));
507
508 if (dir.getString(GpsDirectory.TAG_GPS_LATITUDE_REF).charAt(0) == 'S') {
509 lat = -lat;
510 }
511
512 // Store values
513
514 e.setCoor(new LatLon(lat, lon));
515 e.exifCoor = e.getPos();
516
517 } catch (Exception p) {
518 e.exifCoor = null;
519 e.setPos(null);
520 }
521 }
522
523 public void showNextPhoto() {
524 if (data != null && data.size() > 0) {
525 currentPhoto++;
526 if (currentPhoto >= data.size()) {
527 currentPhoto = data.size() - 1;
528 }
529 ImageViewerDialog.showImage(this, data.get(currentPhoto));
530 } else {
531 currentPhoto = -1;
532 }
533 Main.main.map.repaint();
534 }
535
536 public void showPreviousPhoto() {
537 if (data != null && data.size() > 0) {
538 currentPhoto--;
539 if (currentPhoto < 0) {
540 currentPhoto = 0;
541 }
542 ImageViewerDialog.showImage(this, data.get(currentPhoto));
543 } else {
544 currentPhoto = -1;
545 }
546 Main.main.map.repaint();
547 }
548
549 public void checkPreviousNextButtons() {
550 // System.err.println("showing image " + currentPhoto);
551 ImageViewerDialog.setNextEnabled(currentPhoto < data.size() - 1);
552 ImageViewerDialog.setPreviousEnabled(currentPhoto > 0);
553 }
554
555 public void removeCurrentPhoto() {
556 if (data != null && data.size() > 0 && currentPhoto >= 0 && currentPhoto < data.size()) {
557 data.remove(currentPhoto);
558 if (currentPhoto >= data.size()) {
559 currentPhoto = data.size() - 1;
560 }
561 if (currentPhoto >= 0) {
562 ImageViewerDialog.showImage(this, data.get(currentPhoto));
563 } else {
564 ImageViewerDialog.showImage(this, null);
565 }
566 updateOffscreenBuffer = true;
567 Main.main.map.repaint();
568 }
569 }
570
571 public void removeCurrentPhotoFromDisk() {
572 ImageEntry toDelete = null;
573 if (data != null && data.size() > 0 && currentPhoto >= 0 && currentPhoto < data.size()) {
574 toDelete = data.get(currentPhoto);
575
576 int result = new ExtendedDialog(
577 Main.parent,
578 tr("Delete image file from disk"),
579 new String[] {tr("Cancel"), tr("Delete")})
580 .setButtonIcons(new String[] {"cancel.png", "dialogs/delete.png"})
581 .setContent(new JLabel(tr("<html><h3>Delete the file {0} from disk?<p>The image file will be permanently lost!</h3></html>"
582 ,toDelete.file.getName()), ImageProvider.get("dialogs/geoimage/deletefromdisk"),SwingConstants.LEFT))
583 .toggleEnable("geoimage.deleteimagefromdisk")
584 .setCancelButton(1)
585 .setDefaultButton(2)
586 .showDialog()
587 .getValue();
588
589 if(result == 2)
590 {
591 data.remove(currentPhoto);
592 if (currentPhoto >= data.size()) {
593 currentPhoto = data.size() - 1;
594 }
595 if (currentPhoto >= 0) {
596 ImageViewerDialog.showImage(this, data.get(currentPhoto));
597 } else {
598 ImageViewerDialog.showImage(this, null);
599 }
600
601 if (toDelete.file.delete()) {
602 System.out.println("File "+toDelete.file.toString()+" deleted. ");
603 } else {
604 JOptionPane.showMessageDialog(
605 Main.parent,
606 tr("Image file could not be deleted."),
607 tr("Error"),
608 JOptionPane.ERROR_MESSAGE
609 );
610 }
611
612 updateOffscreenBuffer = true;
613 Main.main.map.repaint();
614 }
615 }
616 }
617
618 private MouseAdapter mouseAdapter = null;
619 private MapModeChangeListener mapModeListener = null;
620
621 private void hook_up_mouse_events() {
622 mouseAdapter = new MouseAdapter() {
623 @Override public void mousePressed(MouseEvent e) {
624
625 if (e.getButton() != MouseEvent.BUTTON1)
626 return;
627 if (isVisible()) {
628 Main.map.mapView.repaint();
629 }
630 }
631
632 @Override public void mouseReleased(MouseEvent ev) {
633 if (ev.getButton() != MouseEvent.BUTTON1)
634 return;
635 if (!isVisible())
636 return;
637
638 for (int i = data.size() - 1; i >= 0; --i) {
639 ImageEntry e = data.get(i);
640 if (e.getPos() == null) {
641 continue;
642 }
643 Point p = Main.map.mapView.getPoint(e.getPos());
644 Rectangle r;
645 if (e.thumbnail != null) {
646 Dimension d = scaledDimension(e.thumbnail);
647 r = new Rectangle(p.x - d.width / 2, p.y - d.height / 2, d.width, d.height);
648 } else {
649 r = new Rectangle(p.x - icon.getIconWidth() / 2,
650 p.y - icon.getIconHeight() / 2,
651 icon.getIconWidth(),
652 icon.getIconHeight());
653 }
654 if (r.contains(ev.getPoint())) {
655 currentPhoto = i;
656 ImageViewerDialog.showImage(GeoImageLayer.this, e);
657 Main.main.map.repaint();
658 break;
659 }
660 }
661 }
662 };
663
664 mapModeListener = new MapModeChangeListener() {
665 public void mapModeChange(MapMode oldMapMode, MapMode newMapMode) {
666 if (newMapMode instanceof org.openstreetmap.josm.actions.mapmode.SelectAction) {
667 Main.map.mapView.addMouseListener(mouseAdapter);
668 } else {
669 Main.map.mapView.removeMouseListener(mouseAdapter);
670 }
671 }
672 };
673
674 Main.map.addMapModeChangeListener(mapModeListener);
675 mapModeListener.mapModeChange(null, Main.map.mapMode);
676
677 MapView.addLayerChangeListener(new LayerChangeListener() {
678 public void activeLayerChange(Layer oldLayer, Layer newLayer) {
679 if (newLayer == GeoImageLayer.this) {
680 // only in select mode it is possible to click the images
681 Main.map.selectSelectTool(false);
682 }
683 }
684
685 public void layerAdded(Layer newLayer) {
686 }
687
688 public void layerRemoved(Layer oldLayer) {
689 if (oldLayer == GeoImageLayer.this) {
690 if (thumbsloader != null) {
691 thumbsloader.stop = true;
692 }
693 Main.map.mapView.removeMouseListener(mouseAdapter);
694 Main.map.removeMapModeChangeListener(mapModeListener);
695 currentPhoto = -1;
696 data.clear();
697 data = null;
698 // stop listening to layer change events
699 MapView.removeLayerChangeListener(this);
700 }
701 }
702 });
703 }
704
705 public void propertyChange(PropertyChangeEvent evt) {
706 if ("center".equals(evt.getPropertyName()) || "scale".equals(evt.getPropertyName())) {
707 updateOffscreenBuffer = true;
708 }
709 }
710
711 public void loadThumbs() {
712 if (useThumbs && !thumbsLoaded) {
713 thumbsLoaded = true;
714 thumbsloader = new ThumbsLoader(this);
715 Thread t = new Thread(thumbsloader);
716 t.setPriority(Thread.MIN_PRIORITY);
717 t.start();
718 }
719 }
720
721 public void updateBufferAndRepaint() {
722 updateOffscreenBuffer = true;
723 Main.map.mapView.repaint();
724 }
725}
Note: See TracBrowser for help on using the repository browser.