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

Last change on this file since 9329 was 9329, checked in by Don-vip, 8 years ago

fix #12255 - fix EXIF time parsing regression

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