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

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

Moved the code from agpifoj plugin to JOSM core. Thanks to Christian Gallioz for this great (ex-)plugin.
In the current state it might be a little unstable.

  • Did a view modification so it fits in better.
  • Added the Thumbnail feature, but still work to be done.

New in JOSM core: Possibility to add toggle dialogs not only on startup, but also later.

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