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

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

see #15574:

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