source: josm/trunk/src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java@ 9277

Last change on this file since 9277 was 9277, checked in by simon04, 8 years ago

see #12255 - Add ImageEntry#loadThumbnail (based on patch by holgermappt)

  • Property svn:eol-style set to native
File size: 15.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.layer.geoimage;
3
4import java.awt.Image;
5import java.io.File;
6import java.io.IOException;
7import java.text.ParseException;
8import java.util.Calendar;
9import java.util.Collections;
10import java.util.Date;
11import java.util.GregorianCalendar;
12import java.util.TimeZone;
13
14import com.drew.imaging.jpeg.JpegMetadataReader;
15import com.drew.lang.CompoundException;
16import com.drew.metadata.Directory;
17import com.drew.metadata.Metadata;
18import com.drew.metadata.MetadataException;
19import com.drew.metadata.exif.ExifIFD0Directory;
20import com.drew.metadata.exif.GpsDirectory;
21import org.openstreetmap.josm.Main;
22import org.openstreetmap.josm.data.SystemOfMeasurement;
23import org.openstreetmap.josm.data.coor.CachedLatLon;
24import org.openstreetmap.josm.data.coor.LatLon;
25import org.openstreetmap.josm.tools.ExifReader;
26
27/**
28 * Stores info about each image
29 */
30public final class ImageEntry implements Comparable<ImageEntry>, Cloneable {
31 private File file;
32 private Integer exifOrientation;
33 private LatLon exifCoor;
34 private Double exifImgDir;
35 private Date exifTime;
36 /**
37 * Flag isNewGpsData indicates that the GPS data of the image is new or has changed.
38 * GPS data includes the position, speed, elevation, time (e.g. as extracted from the GPS track).
39 * The flag can used to decide for which image file the EXIF GPS data is (re-)written.
40 */
41 private boolean isNewGpsData;
42 /** Temporary source of GPS time if not correlated with GPX track. */
43 private Date exifGpsTime;
44 private Image thumbnail;
45
46 /**
47 * The following values are computed from the correlation with the gpx track
48 * or extracted from the image EXIF data.
49 */
50 private CachedLatLon pos;
51 /** Speed in kilometer per hour */
52 private Double speed;
53 /** Elevation (altitude) in meters */
54 private Double elevation;
55 /** The time after correlation with a gpx track */
56 private Date gpsTime;
57
58 /**
59 * When the correlation dialog is open, we like to show the image position
60 * for the current time offset on the map in real time.
61 * On the other hand, when the user aborts this operation, the old values
62 * should be restored. We have a temporary copy, that overrides
63 * the normal values if it is not null. (This may be not the most elegant
64 * solution for this, but it works.)
65 */
66 ImageEntry tmp;
67
68 /**
69 * Constructs a new {@code ImageEntry}.
70 */
71 public ImageEntry() {}
72
73 /**
74 * Constructs a new {@code ImageEntry}.
75 * @param file Path to image file on disk
76 */
77 public ImageEntry(File file) {
78 setFile(file);
79 }
80
81 /**
82 * Returns the position value. The position value from the temporary copy
83 * is returned if that copy exists.
84 * @return the position value
85 */
86 public CachedLatLon getPos() {
87 if (tmp != null)
88 return tmp.pos;
89 return pos;
90 }
91
92 /**
93 * Returns the speed value. The speed value from the temporary copy is
94 * returned if that copy exists.
95 * @return the speed value
96 */
97 public Double getSpeed() {
98 if (tmp != null)
99 return tmp.speed;
100 return speed;
101 }
102
103 /**
104 * Returns the elevation value. The elevation value from the temporary
105 * copy is returned if that copy exists.
106 * @return the elevation value
107 */
108 public Double getElevation() {
109 if (tmp != null)
110 return tmp.elevation;
111 return elevation;
112 }
113
114 /**
115 * Returns the GPS time value. The GPS time value from the temporary copy
116 * is returned if that copy exists.
117 * @return the GPS time value
118 */
119 public Date getGpsTime() {
120 if (tmp != null)
121 return getDefensiveDate(tmp.gpsTime);
122 return getDefensiveDate(gpsTime);
123 }
124
125 /**
126 * Convenient way to determine if this entry has a GPS time, without the cost of building a defensive copy.
127 * @return {@code true} if this entry has a GPS time
128 * @since 6450
129 */
130 public boolean hasGpsTime() {
131 return (tmp != null && tmp.gpsTime != null) || gpsTime != null;
132 }
133
134 /**
135 * Returns associated file.
136 * @return associated file
137 */
138 public File getFile() {
139 return file;
140 }
141
142 /**
143 * Returns EXIF orientation
144 * @return EXIF orientation
145 */
146 public Integer getExifOrientation() {
147 return exifOrientation;
148 }
149
150 /**
151 * Returns EXIF time
152 * @return EXIF time
153 */
154 public Date getExifTime() {
155 return getDefensiveDate(exifTime);
156 }
157
158 /**
159 * Convenient way to determine if this entry has a EXIF time, without the cost of building a defensive copy.
160 * @return {@code true} if this entry has a EXIF time
161 * @since 6450
162 */
163 public boolean hasExifTime() {
164 return exifTime != null;
165 }
166
167 /**
168 * Returns the EXIF GPS time.
169 * @return the EXIF GPS time
170 * @since 6392
171 */
172 public Date getExifGpsTime() {
173 return getDefensiveDate(exifGpsTime);
174 }
175
176 /**
177 * Convenient way to determine if this entry has a EXIF GPS time, without the cost of building a defensive copy.
178 * @return {@code true} if this entry has a EXIF GPS time
179 * @since 6450
180 */
181 public boolean hasExifGpsTime() {
182 return exifGpsTime != null;
183 }
184
185 private static Date getDefensiveDate(Date date) {
186 if (date == null)
187 return null;
188 return new Date(date.getTime());
189 }
190
191 public LatLon getExifCoor() {
192 return exifCoor;
193 }
194
195 public Double getExifImgDir() {
196 if (tmp != null)
197 return tmp.exifImgDir;
198 return exifImgDir;
199 }
200
201 /**
202 * Determines whether a thumbnail is set
203 * @return {@code true} if a thumbnail is set
204 */
205 public boolean hasThumbnail() {
206 return thumbnail != null;
207 }
208
209 /**
210 * Returns the thumbnail.
211 * @return the thumbnail
212 */
213 public Image getThumbnail() {
214 return thumbnail;
215 }
216
217 /**
218 * Sets the thumbnail.
219 * @param thumbnail thumbnail
220 */
221 public void setThumbnail(Image thumbnail) {
222 this.thumbnail = thumbnail;
223 }
224
225 /**
226 * Loads the thumbnail if it was not loaded yet.
227 * @see ThumbsLoader
228 */
229 public void loadThumbnail() {
230 if (thumbnail == null) {
231 new ThumbsLoader(Collections.singleton(this)).run();
232 }
233 }
234
235 /**
236 * Sets the position.
237 * @param pos cached position
238 */
239 public void setPos(CachedLatLon pos) {
240 this.pos = pos;
241 }
242
243 /**
244 * Sets the position.
245 * @param pos position (will be cached)
246 */
247 public void setPos(LatLon pos) {
248 setPos(pos != null ? new CachedLatLon(pos) : null);
249 }
250
251 /**
252 * Sets the speed.
253 * @param speed speed
254 */
255 public void setSpeed(Double speed) {
256 this.speed = speed;
257 }
258
259 /**
260 * Sets the elevation.
261 * @param elevation elevation
262 */
263 public void setElevation(Double elevation) {
264 this.elevation = elevation;
265 }
266
267 /**
268 * Sets associated file.
269 * @param file associated file
270 */
271 public void setFile(File file) {
272 this.file = file;
273 }
274
275 /**
276 * Sets EXIF orientation.
277 * @param exifOrientation EXIF orientation
278 */
279 public void setExifOrientation(Integer exifOrientation) {
280 this.exifOrientation = exifOrientation;
281 }
282
283 /**
284 * Sets EXIF time.
285 * @param exifTime EXIF time
286 */
287 public void setExifTime(Date exifTime) {
288 this.exifTime = getDefensiveDate(exifTime);
289 }
290
291 /**
292 * Sets the EXIF GPS time.
293 * @param exifGpsTime the EXIF GPS time
294 * @since 6392
295 */
296 public void setExifGpsTime(Date exifGpsTime) {
297 this.exifGpsTime = getDefensiveDate(exifGpsTime);
298 }
299
300 public void setGpsTime(Date gpsTime) {
301 this.gpsTime = getDefensiveDate(gpsTime);
302 }
303
304 public void setExifCoor(LatLon exifCoor) {
305 this.exifCoor = exifCoor;
306 }
307
308 public void setExifImgDir(Double exifDir) {
309 this.exifImgDir = exifDir;
310 }
311
312 @Override
313 public ImageEntry clone() {
314 Object c;
315 try {
316 c = super.clone();
317 } catch (CloneNotSupportedException e) {
318 throw new RuntimeException(e);
319 }
320 return (ImageEntry) c;
321 }
322
323 @Override
324 public int compareTo(ImageEntry image) {
325 if (exifTime != null && image.exifTime != null)
326 return exifTime.compareTo(image.exifTime);
327 else if (exifTime == null && image.exifTime == null)
328 return 0;
329 else if (exifTime == null)
330 return -1;
331 else
332 return 1;
333 }
334
335 /**
336 * Make a fresh copy and save it in the temporary variable. Use
337 * {@link #applyTmp()} or {@link #discardTmp()} if the temporary variable
338 * is not needed anymore.
339 */
340 public void createTmp() {
341 tmp = clone();
342 tmp.tmp = null;
343 }
344
345 /**
346 * Get temporary variable that is used for real time parameter
347 * adjustments. The temporary variable is created if it does not exist
348 * yet. Use {@link #applyTmp()} or {@link #discardTmp()} if the temporary
349 * variable is not needed anymore.
350 * @return temporary variable
351 */
352 public ImageEntry getTmp() {
353 if (tmp == null) {
354 createTmp();
355 }
356 return tmp;
357 }
358
359 /**
360 * Copy the values from the temporary variable to the main instance. The
361 * temporary variable is deleted.
362 * @see #discardTmp()
363 */
364 public void applyTmp() {
365 if (tmp != null) {
366 pos = tmp.pos;
367 speed = tmp.speed;
368 elevation = tmp.elevation;
369 gpsTime = tmp.gpsTime;
370 exifImgDir = tmp.exifImgDir;
371 tmp = null;
372 }
373 }
374
375 /**
376 * Delete the temporary variable. Temporary modifications are lost.
377 * @see #applyTmp()
378 */
379 public void discardTmp() {
380 tmp = null;
381 }
382
383 /**
384 * If it has been tagged i.e. matched to a gpx track or retrieved lat/lon from exif
385 * @return {@code true} if it has been tagged
386 */
387 public boolean isTagged() {
388 return pos != null;
389 }
390
391 /**
392 * String representation. (only partial info)
393 */
394 @Override
395 public String toString() {
396 return file.getName()+": "+
397 "pos = "+pos+" | "+
398 "exifCoor = "+exifCoor+" | "+
399 (tmp == null ? " tmp==null" :
400 " [tmp] pos = "+tmp.pos);
401 }
402
403 /**
404 * Indicates that the image has new GPS data.
405 * That flag is set by new GPS data providers. It is used e.g. by the photo_geotagging plugin
406 * to decide for which image file the EXIF GPS data needs to be (re-)written.
407 * @since 6392
408 */
409 public void flagNewGpsData() {
410 isNewGpsData = true;
411 }
412
413 /**
414 * Remove the flag that indicates new GPS data.
415 * The flag is cleared by a new GPS data consumer.
416 */
417 public void unflagNewGpsData() {
418 isNewGpsData = false;
419 }
420
421 /**
422 * Queries whether the GPS data changed.
423 * @return {@code true} if GPS data changed, {@code false} otherwise
424 * @since 6392
425 */
426 public boolean hasNewGpsData() {
427 return isNewGpsData;
428 }
429
430 /**
431 * Extract GPS metadata from image EXIF. Has no effect if the image file is not set
432 *
433 * If successful, fills in the LatLon, speed, elevation, image direction, and other attributes
434 */
435 public void extractExif() {
436
437 Metadata metadata;
438 Directory dirExif;
439 GpsDirectory dirGps;
440
441 if (file == null) {
442 return;
443 }
444
445 try {
446 metadata = JpegMetadataReader.readMetadata(file);
447 dirExif = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
448 dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
449 } catch (CompoundException | IOException p) {
450 setExifCoor(null);
451 setPos(null);
452 return;
453 }
454
455 try {
456 if (dirExif != null) {
457 int orientation = dirExif.getInt(ExifIFD0Directory.TAG_ORIENTATION);
458 setExifOrientation(orientation);
459 }
460 } catch (MetadataException ex) {
461 Main.debug(ex.getMessage());
462 }
463
464 if (dirGps == null) {
465 setExifCoor(null);
466 setPos(null);
467 return;
468 }
469
470 try {
471 double speed = dirGps.getDouble(GpsDirectory.TAG_SPEED);
472 String speedRef = dirGps.getString(GpsDirectory.TAG_SPEED_REF);
473 if ("M".equalsIgnoreCase(speedRef)) {
474 // miles per hour
475 speed *= SystemOfMeasurement.IMPERIAL.bValue / 1000;
476 } else if ("N".equalsIgnoreCase(speedRef)) {
477 // knots == nautical miles per hour
478 speed *= SystemOfMeasurement.NAUTICAL_MILE.bValue / 1000;
479 }
480 // default is K (km/h)
481 setSpeed(speed);
482 } catch (Exception ex) {
483 Main.debug(ex.getMessage());
484 }
485
486 try {
487 double ele = dirGps.getDouble(GpsDirectory.TAG_ALTITUDE);
488 int d = dirGps.getInt(GpsDirectory.TAG_ALTITUDE_REF);
489 if (d == 1) {
490 ele *= -1;
491 }
492 setElevation(ele);
493 } catch (MetadataException ex) {
494 Main.debug(ex.getMessage());
495 }
496
497 try {
498 LatLon latlon = ExifReader.readLatLon(dirGps);
499 setExifCoor(latlon);
500 setPos(getExifCoor());
501
502 } catch (Exception ex) { // (other exceptions, e.g. #5271)
503 Main.error("Error reading EXIF from file: " + ex);
504 setExifCoor(null);
505 setPos(null);
506 }
507
508 try {
509 Double direction = ExifReader.readDirection(dirGps);
510 if (direction != null) {
511 setExifImgDir(direction);
512 }
513 } catch (Exception ex) { // (CompoundException and other exceptions, e.g. #5271)
514 Main.debug(ex.getMessage());
515 }
516
517 // Changed to silently cope with no time info in exif. One case
518 // of person having time that couldn't be parsed, but valid GPS info
519 try {
520 setExifTime(ExifReader.readTime(file));
521 } catch (ParseException ex) {
522 setExifTime(null);
523 }
524
525 // Time and date. We can have these cases:
526 // 1) GPS_TIME_STAMP not set -> date/time will be null
527 // 2) GPS_DATE_STAMP not set -> use EXIF date or set to default
528 // 3) GPS_TIME_STAMP and GPS_DATE_STAMP are set
529 int[] timeStampComps = dirGps.getIntArray(GpsDirectory.TAG_TIME_STAMP);
530 if (timeStampComps != null) {
531 int gpsHour = timeStampComps[0];
532 int gpsMin = timeStampComps[1];
533 int gpsSec = timeStampComps[2];
534 Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
535
536 // We have the time. Next step is to check if the GPS date stamp is set.
537 // dirGps.getString() always succeeds, but the return value might be null.
538 String dateStampStr = dirGps.getString(GpsDirectory.TAG_DATE_STAMP);
539 if (dateStampStr != null && dateStampStr.matches("^\\d+:\\d+:\\d+$")) {
540 String[] dateStampComps = dateStampStr.split(":");
541 cal.set(Calendar.YEAR, Integer.parseInt(dateStampComps[0]));
542 cal.set(Calendar.MONTH, Integer.parseInt(dateStampComps[1]) - 1);
543 cal.set(Calendar.DAY_OF_MONTH, Integer.parseInt(dateStampComps[2]));
544 } else {
545 // No GPS date stamp in EXIF data. Copy it from EXIF time.
546 // Date is not set if EXIF time is not available.
547 if (hasExifTime()) {
548 // Time not set yet, so we can copy everything, not just date.
549 cal.setTime(getExifTime());
550 }
551 }
552
553 cal.set(Calendar.HOUR_OF_DAY, gpsHour);
554 cal.set(Calendar.MINUTE, gpsMin);
555 cal.set(Calendar.SECOND, gpsSec);
556
557 setExifGpsTime(cal.getTime());
558 }
559 }
560}
Note: See TracBrowser for help on using the repository browser.