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

Last change on this file since 2617 was 2617, checked in by bastiK, 14 years ago

geoimage: improved thumbnails (closes #4101)

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