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

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

geoimage: add thumbnail caching (fixes #4116, see #4101)

  • Property svn:eol-style set to native
File size: 19.3 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 ThumbsLoader thumbsloader;
75
76 /*
77 * Stores info about each image
78 */
79
80 static final class ImageEntry implements Comparable<ImageEntry> {
81 File file;
82 Date time;
83 LatLon exifCoor;
84 CachedLatLon pos;
85 Image thumbnail;
86 /** Speed in kilometer per second */
87 Double speed;
88 /** Elevation (altitude) in meters */
89 Double elevation;
90
91 public void setCoor(LatLon latlon)
92 {
93 pos = new CachedLatLon(latlon);
94 }
95 public int compareTo(ImageEntry image) {
96 if (time != null && image.time != null) {
97 return time.compareTo(image.time);
98 } else if (time == null && image.time == null) {
99 return 0;
100 } else if (time == null) {
101 return -1;
102 } else {
103 return 1;
104 }
105 }
106 }
107
108 /** Loads a set of images, while displaying a dialog that indicates what the plugin is currently doing.
109 * In facts, this object is instantiated with a list of files. These files may be JPEG files or
110 * directories. In case of directories, they are scanned to find all the images they contain.
111 * Then all the images that have be found are loaded as ImageEntry instances.
112 */
113 private static final class Loader extends PleaseWaitRunnable {
114
115 private boolean cancelled = false;
116 private GeoImageLayer layer;
117 private Collection<File> selection;
118 private HashSet<String> loadedDirectories = new HashSet<String>();
119 private LinkedHashSet<String> errorMessages;
120
121 protected void rememberError(String message) {
122 this.errorMessages.add(message);
123 }
124
125 public Loader(Collection<File> selection, GpxLayer gpxLayer) {
126 super(tr("Extracting GPS locations from EXIF"));
127 this.selection = selection;
128 errorMessages = new LinkedHashSet<String>();
129 }
130
131 @Override protected void realRun() throws IOException {
132
133 progressMonitor.subTask(tr("Starting directory scan"));
134 Collection<File> files = new ArrayList<File>();
135 try {
136 addRecursiveFiles(files, selection);
137 } catch(NullPointerException npe) {
138 rememberError(tr("One of the selected files was null"));
139 }
140
141 if (cancelled) {
142 return;
143 }
144 progressMonitor.subTask(tr("Read photos..."));
145 progressMonitor.setTicksCount(files.size());
146
147 progressMonitor.subTask(tr("Read photos..."));
148 progressMonitor.setTicksCount(files.size());
149
150 // read the image files
151 List<ImageEntry> data = new ArrayList<ImageEntry>(files.size());
152
153 for (File f : files) {
154
155 if (cancelled) {
156 break;
157 }
158
159 progressMonitor.subTask(tr("Reading {0}...", f.getName()));
160 progressMonitor.worked(1);
161
162 ImageEntry e = new ImageEntry();
163
164 // Changed to silently cope with no time info in exif. One case
165 // of person having time that couldn't be parsed, but valid GPS info
166
167 try {
168 e.time = ExifReader.readTime(f);
169 } catch (ParseException e1) {
170 e.time = null;
171 }
172 e.file = f;
173 extractExif(e);
174 data.add(e);
175 }
176 layer = new GeoImageLayer(data);
177 files.clear();
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
280 private static boolean addedToggleDialog = false;
281
282 public static void create(Collection<File> files, GpxLayer gpxLayer) {
283 Loader loader = new Loader(files, gpxLayer);
284 Main.worker.execute(loader);
285 if (!addedToggleDialog) {
286 Main.map.addToggleDialog(ImageViewerDialog.getInstance());
287 addedToggleDialog = true;
288 }
289 }
290
291 private GeoImageLayer(final List<ImageEntry> data) {
292
293 super(tr("Geotagged Images"));
294
295 Collections.sort(data);
296 this.data = data;
297 }
298
299 @Override
300 public Icon getIcon() {
301 return ImageProvider.get("dialogs/geoimage");
302 }
303
304 @Override
305 public Object getInfoComponent() {
306 // TODO Auto-generated method stub
307 return null;
308 }
309
310 @Override
311 public Component[] getMenuEntries() {
312
313 JMenuItem correlateItem = new JMenuItem(tr("Correlate to GPX"), ImageProvider.get("dialogs/geoimage/gpx2img"));
314 correlateItem.addActionListener(new CorrelateGpxWithImages(this));
315
316 return new Component[] {
317 new JMenuItem(LayerListDialog.getInstance().createShowHideLayerAction(this)),
318 new JMenuItem(LayerListDialog.getInstance().createDeleteLayerAction(this)),
319 new JMenuItem(new RenameLayerAction(null, this)),
320 new JSeparator(),
321 correlateItem
322 };
323 }
324
325 @Override
326 public String getToolTipText() {
327 int i = 0;
328 for (ImageEntry e : data)
329 if (e.pos != null)
330 i++;
331 return data.size() + " " + trn("image", "images", data.size())
332 + " loaded. " + tr("{0} were found to be gps tagged.", i);
333 }
334
335 @Override
336 public boolean isMergable(Layer other) {
337 return other instanceof GeoImageLayer;
338 }
339
340 @Override
341 public void mergeFrom(Layer from) {
342 GeoImageLayer l = (GeoImageLayer) from;
343
344 ImageEntry selected = null;
345 if (l.currentPhoto >= 0) {
346 selected = l.data.get(l.currentPhoto);
347 }
348
349 data.addAll(l.data);
350 Collections.sort(data);
351
352 // Supress the double photos.
353 if (data.size() > 1) {
354 ImageEntry cur;
355 ImageEntry prev = data.get(data.size() - 1);
356 for (int i = data.size() - 2; i >= 0; i--) {
357 cur = data.get(i);
358 if (cur.file.equals(prev.file)) {
359 data.remove(i);
360 } else {
361 prev = cur;
362 }
363 }
364 }
365
366 if (selected != null) {
367 for (int i = 0; i < data.size() ; i++) {
368 if (data.get(i) == selected) {
369 currentPhoto = i;
370 ImageViewerDialog.showImage(GeoImageLayer.this, data.get(i));
371 break;
372 }
373 }
374 }
375
376 setName(l.getName());
377
378 }
379
380 @Override
381 public void paint(Graphics2D g, MapView mv, Bounds bounds) {
382
383 for (ImageEntry e : data) {
384 if (e.pos != null) {
385 Point p = mv.getPoint(e.pos);
386 if (e.thumbnail != null && e.thumbnail.getWidth(null) > 0 && e.thumbnail.getHeight(null) > 0) {
387 g.drawImage(e.thumbnail,
388 p.x - e.thumbnail.getWidth(null) / 2,
389 p.y - e.thumbnail.getHeight(null) / 2, null);
390 }
391 else {
392 icon.paintIcon(mv, g,
393 p.x - icon.getIconWidth() / 2,
394 p.y - icon.getIconHeight() / 2);
395 }
396 }
397 }
398
399 // Draw the selection on top of the other pictures.
400 if (currentPhoto >= 0 && currentPhoto < data.size()) {
401 ImageEntry e = data.get(currentPhoto);
402
403 if (e.pos != null) {
404 Point p = mv.getPoint(e.pos);
405
406 Rectangle r = new Rectangle(p.x - selectedIcon.getIconWidth() / 2,
407 p.y - selectedIcon.getIconHeight() / 2,
408 selectedIcon.getIconWidth(),
409 selectedIcon.getIconHeight());
410 selectedIcon.paintIcon(mv, g, r.x, r.y);
411 }
412 }
413 }
414
415 @Override
416 public void visitBoundingBox(BoundingXYVisitor v) {
417 for (ImageEntry e : data)
418 v.visit(e.pos);
419 }
420
421 /*
422 * Extract gps from image exif
423 *
424 * If successful, fills in the LatLon and EastNorth attributes of passed in
425 * image;
426 */
427
428 private static void extractExif(ImageEntry e) {
429
430 try {
431 int deg;
432 float min, sec;
433 double lon, lat;
434
435 Metadata metadata = JpegMetadataReader.readMetadata(e.file);
436 Directory dir = metadata.getDirectory(GpsDirectory.class);
437
438 // longitude
439
440 Rational[] components = dir
441 .getRationalArray(GpsDirectory.TAG_GPS_LONGITUDE);
442
443 deg = components[0].intValue();
444 min = components[1].floatValue();
445 sec = components[2].floatValue();
446
447 lon = (deg + (min / 60) + (sec / 3600));
448
449 if (dir.getString(GpsDirectory.TAG_GPS_LONGITUDE_REF).charAt(0) == 'W')
450 lon = -lon;
451
452 // latitude
453
454 components = dir.getRationalArray(GpsDirectory.TAG_GPS_LATITUDE);
455
456 deg = components[0].intValue();
457 min = components[1].floatValue();
458 sec = components[2].floatValue();
459
460 lat = (deg + (min / 60) + (sec / 3600));
461
462 if (dir.getString(GpsDirectory.TAG_GPS_LATITUDE_REF).charAt(0) == 'S')
463 lat = -lat;
464
465 // Store values
466
467 e.setCoor(new LatLon(lat, lon));
468 e.exifCoor = e.pos;
469
470 } catch (Exception p) {
471 e.pos = null;
472 }
473 }
474
475 public void showNextPhoto() {
476 if (data != null && data.size() > 0) {
477 currentPhoto++;
478 if (currentPhoto >= data.size()) {
479 currentPhoto = data.size() - 1;
480 }
481 ImageViewerDialog.showImage(this, data.get(currentPhoto));
482 } else {
483 currentPhoto = -1;
484 }
485 Main.main.map.repaint();
486 }
487
488 public void showPreviousPhoto() {
489 if (data != null && data.size() > 0) {
490 currentPhoto--;
491 if (currentPhoto < 0) {
492 currentPhoto = 0;
493 }
494 ImageViewerDialog.showImage(this, data.get(currentPhoto));
495 } else {
496 currentPhoto = -1;
497 }
498 Main.main.map.repaint();
499 }
500
501 public void checkPreviousNextButtons() {
502 System.err.println("check: " + currentPhoto);
503 ImageViewerDialog.setNextEnabled(currentPhoto < data.size() - 1);
504 ImageViewerDialog.setPreviousEnabled(currentPhoto > 0);
505 }
506
507 public void removeCurrentPhoto() {
508 if (data != null && data.size() > 0 && currentPhoto >= 0 && currentPhoto < data.size()) {
509 data.remove(currentPhoto);
510 if (currentPhoto >= data.size()) {
511 currentPhoto = data.size() - 1;
512 }
513 if (currentPhoto >= 0) {
514 ImageViewerDialog.showImage(this, data.get(currentPhoto));
515 } else {
516 ImageViewerDialog.showImage(this, null);
517 }
518 }
519 Main.main.map.repaint();
520 }
521
522 private MouseAdapter mouseAdapter = null;
523
524 private void hook_up_mouse_events() {
525 mouseAdapter = new MouseAdapter() {
526 @Override public void mousePressed(MouseEvent e) {
527
528 if (e.getButton() != MouseEvent.BUTTON1) {
529 return;
530 }
531 if (isVisible())
532 Main.map.mapView.repaint();
533 }
534
535 @Override public void mouseReleased(MouseEvent ev) {
536
537 if (ev.getButton() != MouseEvent.BUTTON1) {
538 return;
539 }
540 if (!isVisible()) {
541 return;
542 }
543
544 ImageViewerDialog d = ImageViewerDialog.getInstance();
545
546 for (int i = data.size() - 1; i >= 0; --i) {
547 ImageEntry e = data.get(i);
548 if (e.pos == null)
549 continue;
550 Point p = Main.map.mapView.getPoint(e.pos);
551 Rectangle r = new Rectangle(p.x - icon.getIconWidth() / 2,
552 p.y - icon.getIconHeight() / 2,
553 icon.getIconWidth(),
554 icon.getIconHeight());
555 if (r.contains(ev.getPoint())) {
556 currentPhoto = i;
557 ImageViewerDialog.showImage(GeoImageLayer.this, e);
558 Main.main.map.repaint();
559 break;
560 }
561 }
562 Main.map.mapView.repaint();
563 }
564 };
565 Main.map.mapView.addMouseListener(mouseAdapter);
566 Layer.listeners.add(new LayerChangeListener() {
567 public void activeLayerChange(Layer oldLayer, Layer newLayer) {
568 if (newLayer == GeoImageLayer.this && currentPhoto >= 0) {
569 Main.main.map.repaint();
570 ImageViewerDialog.showImage(GeoImageLayer.this, data.get(currentPhoto));
571 }
572 }
573
574 public void layerAdded(Layer newLayer) {
575 }
576
577 public void layerRemoved(Layer oldLayer) {
578 if (oldLayer == GeoImageLayer.this) {
579 Main.map.mapView.removeMouseListener(mouseAdapter);
580 currentPhoto = -1;
581 data.clear();
582 data = null;
583 }
584 }
585 });
586 }
587
588 @Override
589 public void destroy() {
590 if (thumbsloader != null) {
591 thumbsloader.stop = true;
592 }
593 }
594}
Note: See TracBrowser for help on using the repository browser.