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

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

geoimage: make thumbnails optional + cosmetics (see #4101)

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