source: josm/trunk/src/org/openstreetmap/josm/data/gpx/GpxImageEntry.java@ 17880

Last change on this file since 17880 was 17880, checked in by simon04, 4 years ago

see #8472 - Show geocoded images from Wikimedia Commons as GeoImageLayer

The icon wikimedia_commons.svg is taken from https://commons.wikimedia.org/wiki/File:Commons-logo.svg ("This image of simple geometry is ineligible for copyright and therefore in the public domain, because it consists entirely of information that is common property and contains no original authorship.")

  • Property svn:eol-style set to native
File size: 23.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.gpx;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.File;
7import java.io.IOException;
8import java.time.Instant;
9import java.util.Date;
10import java.util.List;
11import java.util.Locale;
12import java.util.Objects;
13import java.util.function.Consumer;
14
15import org.openstreetmap.josm.data.IQuadBucketType;
16import org.openstreetmap.josm.data.coor.CachedLatLon;
17import org.openstreetmap.josm.data.coor.LatLon;
18import org.openstreetmap.josm.data.osm.BBox;
19import org.openstreetmap.josm.tools.ExifReader;
20import org.openstreetmap.josm.tools.JosmRuntimeException;
21import org.openstreetmap.josm.tools.Logging;
22
23import com.drew.imaging.jpeg.JpegMetadataReader;
24import com.drew.imaging.jpeg.JpegProcessingException;
25import com.drew.imaging.png.PngMetadataReader;
26import com.drew.imaging.png.PngProcessingException;
27import com.drew.imaging.tiff.TiffMetadataReader;
28import com.drew.imaging.tiff.TiffProcessingException;
29import com.drew.metadata.Directory;
30import com.drew.metadata.Metadata;
31import com.drew.metadata.MetadataException;
32import com.drew.metadata.exif.ExifIFD0Directory;
33import com.drew.metadata.exif.GpsDirectory;
34import com.drew.metadata.iptc.IptcDirectory;
35import com.drew.metadata.jpeg.JpegDirectory;
36
37/**
38 * Stores info about each image
39 * @since 14205 (extracted from gui.layer.geoimage.ImageEntry)
40 */
41public class GpxImageEntry implements Comparable<GpxImageEntry>, IQuadBucketType {
42 private File file;
43 private Integer exifOrientation;
44 private LatLon exifCoor;
45 private Double exifImgDir;
46 private Instant exifTime;
47 /**
48 * Flag isNewGpsData indicates that the GPS data of the image is new or has changed.
49 * GPS data includes the position, speed, elevation, time (e.g. as extracted from the GPS track).
50 * The flag can used to decide for which image file the EXIF GPS data is (re-)written.
51 */
52 private boolean isNewGpsData;
53 /** Temporary source of GPS time if not correlated with GPX track. */
54 private Instant exifGpsTime;
55
56 private String iptcCaption;
57 private String iptcHeadline;
58 private List<String> iptcKeywords;
59 private String iptcObjectName;
60
61 /**
62 * The following values are computed from the correlation with the gpx track
63 * or extracted from the image EXIF data.
64 */
65 private CachedLatLon pos;
66 /** Speed in kilometer per hour */
67 private Double speed;
68 /** Elevation (altitude) in meters */
69 private Double elevation;
70 /** The time after correlation with a gpx track */
71 private Instant gpsTime;
72
73 private int width;
74 private int height;
75
76 /**
77 * When the correlation dialog is open, we like to show the image position
78 * for the current time offset on the map in real time.
79 * On the other hand, when the user aborts this operation, the old values
80 * should be restored. We have a temporary copy, that overrides
81 * the normal values if it is not null. (This may be not the most elegant
82 * solution for this, but it works.)
83 */
84 private GpxImageEntry tmp;
85
86 /**
87 * Constructs a new {@code GpxImageEntry}.
88 */
89 public GpxImageEntry() {}
90
91 /**
92 * Constructs a new {@code GpxImageEntry} from an existing instance.
93 * @param other existing instance
94 * @since 14624
95 */
96 public GpxImageEntry(GpxImageEntry other) {
97 file = other.file;
98 exifOrientation = other.exifOrientation;
99 exifCoor = other.exifCoor;
100 exifImgDir = other.exifImgDir;
101 exifTime = other.exifTime;
102 isNewGpsData = other.isNewGpsData;
103 exifGpsTime = other.exifGpsTime;
104 pos = other.pos;
105 speed = other.speed;
106 elevation = other.elevation;
107 gpsTime = other.gpsTime;
108 width = other.width;
109 height = other.height;
110 tmp = other.tmp;
111 }
112
113 /**
114 * Constructs a new {@code GpxImageEntry}.
115 * @param file Path to image file on disk
116 */
117 public GpxImageEntry(File file) {
118 setFile(file);
119 }
120
121 /**
122 * Returns width of the image this GpxImageEntry represents.
123 * @return width of the image this GpxImageEntry represents
124 * @since 13220
125 */
126 public int getWidth() {
127 return width;
128 }
129
130 /**
131 * Returns height of the image this GpxImageEntry represents.
132 * @return height of the image this GpxImageEntry represents
133 * @since 13220
134 */
135 public int getHeight() {
136 return height;
137 }
138
139 /**
140 * Returns the position value. The position value from the temporary copy
141 * is returned if that copy exists.
142 * @return the position value
143 */
144 public CachedLatLon getPos() {
145 if (tmp != null)
146 return tmp.pos;
147 return pos;
148 }
149
150 /**
151 * Returns the speed value. The speed value from the temporary copy is
152 * returned if that copy exists.
153 * @return the speed value
154 */
155 public Double getSpeed() {
156 if (tmp != null)
157 return tmp.speed;
158 return speed;
159 }
160
161 /**
162 * Returns the elevation value. The elevation value from the temporary
163 * copy is returned if that copy exists.
164 * @return the elevation value
165 */
166 public Double getElevation() {
167 if (tmp != null)
168 return tmp.elevation;
169 return elevation;
170 }
171
172 /**
173 * Returns the GPS time value. The GPS time value from the temporary copy
174 * is returned if that copy exists.
175 * @return the GPS time value
176 * @deprecated Use {@link #getGpsInstant}
177 */
178 @Deprecated
179 public Date getGpsTime() {
180 if (tmp != null)
181 return getDefensiveDate(tmp.gpsTime);
182 return getDefensiveDate(gpsTime);
183 }
184
185 /**
186 * Returns the GPS time value. The GPS time value from the temporary copy
187 * is returned if that copy exists.
188 * @return the GPS time value
189 */
190 public Instant getGpsInstant() {
191 return tmp != null ? tmp.gpsTime : gpsTime;
192 }
193
194 /**
195 * Convenient way to determine if this entry has a GPS time, without the cost of building a defensive copy.
196 * @return {@code true} if this entry has a GPS time
197 * @since 6450
198 */
199 public boolean hasGpsTime() {
200 return (tmp != null && tmp.gpsTime != null) || gpsTime != null;
201 }
202
203 /**
204 * Returns associated file.
205 * @return associated file
206 */
207 public File getFile() {
208 return file;
209 }
210
211 /**
212 * Returns a display name for this entry
213 * @return a display name for this entry
214 */
215 public String getDisplayName() {
216 return file == null ? "" : file.getName();
217 }
218
219 /**
220 * Returns EXIF orientation
221 * @return EXIF orientation
222 */
223 public Integer getExifOrientation() {
224 return exifOrientation != null ? exifOrientation : 1;
225 }
226
227 /**
228 * Returns EXIF time
229 * @return EXIF time
230 * @deprecated Use {@link #getExifInstant}
231 */
232 @Deprecated
233 public Date getExifTime() {
234 return getDefensiveDate(exifTime);
235 }
236
237 /**
238 * Returns EXIF time
239 * @return EXIF time
240 */
241 public Instant getExifInstant() {
242 return exifTime;
243 }
244
245 /**
246 * Convenient way to determine if this entry has a EXIF time, without the cost of building a defensive copy.
247 * @return {@code true} if this entry has a EXIF time
248 * @since 6450
249 */
250 public boolean hasExifTime() {
251 return exifTime != null;
252 }
253
254 /**
255 * Returns the EXIF GPS time.
256 * @return the EXIF GPS time
257 * @since 6392
258 * @deprecated Use {@link #getExifGpsInstant}
259 */
260 @Deprecated
261 public Date getExifGpsTime() {
262 return getDefensiveDate(exifGpsTime);
263 }
264
265 /**
266 * Returns the EXIF GPS time.
267 * @return the EXIF GPS time
268 */
269 public Instant getExifGpsInstant() {
270 return exifGpsTime;
271 }
272
273 /**
274 * Convenient way to determine if this entry has a EXIF GPS time, without the cost of building a defensive copy.
275 * @return {@code true} if this entry has a EXIF GPS time
276 * @since 6450
277 */
278 public boolean hasExifGpsTime() {
279 return exifGpsTime != null;
280 }
281
282 private static Date getDefensiveDate(Instant date) {
283 if (date == null)
284 return null;
285 return Date.from(date);
286 }
287
288 public LatLon getExifCoor() {
289 return exifCoor;
290 }
291
292 public Double getExifImgDir() {
293 if (tmp != null)
294 return tmp.exifImgDir;
295 return exifImgDir;
296 }
297
298 /**
299 * Sets the width of this GpxImageEntry.
300 * @param width set the width of this GpxImageEntry
301 * @since 13220
302 */
303 public void setWidth(int width) {
304 this.width = width;
305 }
306
307 /**
308 * Sets the height of this GpxImageEntry.
309 * @param height set the height of this GpxImageEntry
310 * @since 13220
311 */
312 public void setHeight(int height) {
313 this.height = height;
314 }
315
316 /**
317 * Sets the position.
318 * @param pos cached position
319 */
320 public void setPos(CachedLatLon pos) {
321 this.pos = pos;
322 }
323
324 /**
325 * Sets the position.
326 * @param pos position (will be cached)
327 */
328 public void setPos(LatLon pos) {
329 setPos(pos != null ? new CachedLatLon(pos) : null);
330 }
331
332 /**
333 * Sets the speed.
334 * @param speed speed
335 */
336 public void setSpeed(Double speed) {
337 this.speed = speed;
338 }
339
340 /**
341 * Sets the elevation.
342 * @param elevation elevation
343 */
344 public void setElevation(Double elevation) {
345 this.elevation = elevation;
346 }
347
348 /**
349 * Sets associated file.
350 * @param file associated file
351 */
352 public void setFile(File file) {
353 this.file = file;
354 }
355
356 /**
357 * Sets EXIF orientation.
358 * @param exifOrientation EXIF orientation
359 */
360 public void setExifOrientation(Integer exifOrientation) {
361 this.exifOrientation = exifOrientation;
362 }
363
364 /**
365 * Sets EXIF time.
366 * @param exifTime EXIF time
367 * @deprecated Use {@link #setExifTime(Instant)}
368 */
369 @Deprecated
370 public void setExifTime(Date exifTime) {
371 this.exifTime = exifTime == null ? null : exifTime.toInstant();
372 }
373
374 /**
375 * Sets the EXIF GPS time.
376 * @param exifGpsTime the EXIF GPS time
377 * @since 6392
378 * @deprecated Use {@link #setExifGpsTime(Instant)}
379 */
380 @Deprecated
381 public void setExifGpsTime(Date exifGpsTime) {
382 this.exifGpsTime = exifGpsTime == null ? null : exifGpsTime.toInstant();
383 }
384
385 /**
386 * Sets the GPS time.
387 * @param gpsTime the GPS time
388 * @deprecated Use {@link #setGpsTime(Instant)}
389 */
390 @Deprecated
391 public void setGpsTime(Date gpsTime) {
392 this.gpsTime = gpsTime == null ? null : gpsTime.toInstant();
393 }
394
395 /**
396 * Sets EXIF time.
397 * @param exifTime EXIF time
398 */
399 public void setExifTime(Instant exifTime) {
400 this.exifTime = exifTime;
401 }
402
403 /**
404 * Sets the EXIF GPS time.
405 * @param exifGpsTime the EXIF GPS time
406 */
407 public void setExifGpsTime(Instant exifGpsTime) {
408 this.exifGpsTime = exifGpsTime;
409 }
410
411 /**
412 * Sets the GPS time.
413 * @param gpsTime the GPS time
414 */
415 public void setGpsTime(Instant gpsTime) {
416 this.gpsTime = gpsTime;
417 }
418
419 public void setExifCoor(LatLon exifCoor) {
420 this.exifCoor = exifCoor;
421 }
422
423 public void setExifImgDir(Double exifDir) {
424 this.exifImgDir = exifDir;
425 }
426
427 /**
428 * Sets the IPTC caption.
429 * @param iptcCaption the IPTC caption
430 * @since 15219
431 */
432 public void setIptcCaption(String iptcCaption) {
433 this.iptcCaption = iptcCaption;
434 }
435
436 /**
437 * Sets the IPTC headline.
438 * @param iptcHeadline the IPTC headline
439 * @since 15219
440 */
441 public void setIptcHeadline(String iptcHeadline) {
442 this.iptcHeadline = iptcHeadline;
443 }
444
445 /**
446 * Sets the IPTC keywords.
447 * @param iptcKeywords the IPTC keywords
448 * @since 15219
449 */
450 public void setIptcKeywords(List<String> iptcKeywords) {
451 this.iptcKeywords = iptcKeywords;
452 }
453
454 /**
455 * Sets the IPTC object name.
456 * @param iptcObjectName the IPTC object name
457 * @since 15219
458 */
459 public void setIptcObjectName(String iptcObjectName) {
460 this.iptcObjectName = iptcObjectName;
461 }
462
463 /**
464 * Returns the IPTC caption.
465 * @return the IPTC caption
466 * @since 15219
467 */
468 public String getIptcCaption() {
469 return iptcCaption;
470 }
471
472 /**
473 * Returns the IPTC headline.
474 * @return the IPTC headline
475 * @since 15219
476 */
477 public String getIptcHeadline() {
478 return iptcHeadline;
479 }
480
481 /**
482 * Returns the IPTC keywords.
483 * @return the IPTC keywords
484 * @since 15219
485 */
486 public List<String> getIptcKeywords() {
487 return iptcKeywords;
488 }
489
490 /**
491 * Returns the IPTC object name.
492 * @return the IPTC object name
493 * @since 15219
494 */
495 public String getIptcObjectName() {
496 return iptcObjectName;
497 }
498
499 @Override
500 public int compareTo(GpxImageEntry image) {
501 if (exifTime != null && image.exifTime != null)
502 return exifTime.compareTo(image.exifTime);
503 else if (exifTime == null && image.exifTime == null)
504 return 0;
505 else if (exifTime == null)
506 return -1;
507 else
508 return 1;
509 }
510
511 @Override
512 public int hashCode() {
513 return Objects.hash(height, width, isNewGpsData,
514 elevation, exifCoor, exifGpsTime, exifImgDir, exifOrientation, exifTime,
515 iptcCaption, iptcHeadline, iptcKeywords, iptcObjectName,
516 file, gpsTime, pos, speed, tmp);
517 }
518
519 @Override
520 public boolean equals(Object obj) {
521 if (this == obj)
522 return true;
523 if (obj == null || getClass() != obj.getClass())
524 return false;
525 GpxImageEntry other = (GpxImageEntry) obj;
526 return height == other.height
527 && width == other.width
528 && isNewGpsData == other.isNewGpsData
529 && Objects.equals(elevation, other.elevation)
530 && Objects.equals(exifCoor, other.exifCoor)
531 && Objects.equals(exifGpsTime, other.exifGpsTime)
532 && Objects.equals(exifImgDir, other.exifImgDir)
533 && Objects.equals(exifOrientation, other.exifOrientation)
534 && Objects.equals(exifTime, other.exifTime)
535 && Objects.equals(iptcCaption, other.iptcCaption)
536 && Objects.equals(iptcHeadline, other.iptcHeadline)
537 && Objects.equals(iptcKeywords, other.iptcKeywords)
538 && Objects.equals(iptcObjectName, other.iptcObjectName)
539 && Objects.equals(file, other.file)
540 && Objects.equals(gpsTime, other.gpsTime)
541 && Objects.equals(pos, other.pos)
542 && Objects.equals(speed, other.speed)
543 && Objects.equals(tmp, other.tmp);
544 }
545
546 /**
547 * Make a fresh copy and save it in the temporary variable. Use
548 * {@link #applyTmp()} or {@link #discardTmp()} if the temporary variable
549 * is not needed anymore.
550 */
551 public void createTmp() {
552 tmp = new GpxImageEntry(this);
553 tmp.tmp = null;
554 }
555
556 /**
557 * Get temporary variable that is used for real time parameter
558 * adjustments. The temporary variable is created if it does not exist
559 * yet. Use {@link #applyTmp()} or {@link #discardTmp()} if the temporary
560 * variable is not needed anymore.
561 * @return temporary variable
562 */
563 public GpxImageEntry getTmp() {
564 if (tmp == null) {
565 createTmp();
566 }
567 return tmp;
568 }
569
570 /**
571 * Copy the values from the temporary variable to the main instance. The
572 * temporary variable is deleted.
573 * @see #discardTmp()
574 */
575 public void applyTmp() {
576 if (tmp != null) {
577 pos = tmp.pos;
578 speed = tmp.speed;
579 elevation = tmp.elevation;
580 gpsTime = tmp.gpsTime;
581 exifImgDir = tmp.exifImgDir;
582 isNewGpsData = isNewGpsData || tmp.isNewGpsData;
583 tmp = null;
584 }
585 tmpUpdated();
586 }
587
588 /**
589 * Delete the temporary variable. Temporary modifications are lost.
590 * @see #applyTmp()
591 */
592 public void discardTmp() {
593 tmp = null;
594 tmpUpdated();
595 }
596
597 /**
598 * If it has been tagged i.e. matched to a gpx track or retrieved lat/lon from exif
599 * @return {@code true} if it has been tagged
600 */
601 public boolean isTagged() {
602 return pos != null;
603 }
604
605 /**
606 * String representation. (only partial info)
607 */
608 @Override
609 public String toString() {
610 return file.getName()+": "+
611 "pos = "+pos+" | "+
612 "exifCoor = "+exifCoor+" | "+
613 (tmp == null ? " tmp==null" :
614 " [tmp] pos = "+tmp.pos);
615 }
616
617 /**
618 * Indicates that the image has new GPS data.
619 * That flag is set by new GPS data providers. It is used e.g. by the photo_geotagging plugin
620 * to decide for which image file the EXIF GPS data needs to be (re-)written.
621 * @since 6392
622 */
623 public void flagNewGpsData() {
624 isNewGpsData = true;
625 }
626
627 /**
628 * Indicate that the temporary copy has been updated. Mostly used to prevent UI issues.
629 * By default, this is a no-op. Override when needed in subclasses.
630 * @since 17579
631 */
632 protected void tmpUpdated() {
633 // No-op by default
634 }
635
636 @Override
637 public BBox getBBox() {
638 // new BBox(LatLon) is null safe.
639 // Use `getPos` instead of `getExifCoor` since the image may be correlated against a GPX track
640 return new BBox(this.getPos());
641 }
642
643 /**
644 * Remove the flag that indicates new GPS data.
645 * The flag is cleared by a new GPS data consumer.
646 */
647 public void unflagNewGpsData() {
648 isNewGpsData = false;
649 }
650
651 /**
652 * Queries whether the GPS data changed. The flag value from the temporary
653 * copy is returned if that copy exists.
654 * @return {@code true} if GPS data changed, {@code false} otherwise
655 * @since 6392
656 */
657 public boolean hasNewGpsData() {
658 if (tmp != null)
659 return tmp.isNewGpsData;
660 return isNewGpsData;
661 }
662
663 /**
664 * Extract GPS metadata from image EXIF. Has no effect if the image file is not set
665 *
666 * If successful, fills in the LatLon, speed, elevation, image direction, and other attributes
667 * @since 9270
668 */
669 public void extractExif() {
670
671 Metadata metadata;
672
673 if (file == null) {
674 return;
675 }
676
677 String fn = file.getName();
678
679 try {
680 // try to parse metadata according to extension
681 String ext = fn.substring(fn.lastIndexOf('.') + 1).toLowerCase(Locale.US);
682 switch (ext) {
683 case "jpg":
684 case "jpeg":
685 metadata = JpegMetadataReader.readMetadata(file);
686 break;
687 case "tif":
688 case "tiff":
689 metadata = TiffMetadataReader.readMetadata(file);
690 break;
691 case "png":
692 metadata = PngMetadataReader.readMetadata(file);
693 break;
694 default:
695 throw new NoMetadataReaderWarning(ext);
696 }
697 } catch (JpegProcessingException | TiffProcessingException | PngProcessingException | IOException
698 | NoMetadataReaderWarning topException) {
699 //try other formats (e.g. JPEG file with .png extension)
700 try {
701 metadata = JpegMetadataReader.readMetadata(file);
702 } catch (JpegProcessingException | IOException ex1) {
703 try {
704 metadata = TiffMetadataReader.readMetadata(file);
705 } catch (TiffProcessingException | IOException ex2) {
706 try {
707 metadata = PngMetadataReader.readMetadata(file);
708 } catch (PngProcessingException | IOException ex3) {
709 Logging.warn(topException);
710 Logging.info(tr("Can''t parse metadata for file \"{0}\". Using last modified date as timestamp.", fn));
711 setExifTime(Instant.ofEpochMilli(file.lastModified()));
712 setExifCoor(null);
713 setPos(null);
714 return;
715 }
716 }
717 }
718 }
719
720 // Changed to silently cope with no time info in exif. One case
721 // of person having time that couldn't be parsed, but valid GPS info
722 Instant time = null;
723 try {
724 time = ExifReader.readInstant(metadata);
725 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException ex) {
726 Logging.warn(ex);
727 }
728
729 if (time == null) {
730 Logging.info(tr("No EXIF time in file \"{0}\". Using last modified date as timestamp.", fn));
731 time = Instant.ofEpochMilli(file.lastModified()); //use lastModified time if no EXIF time present
732 }
733 setExifTime(time);
734
735 final Directory dir = metadata.getFirstDirectoryOfType(JpegDirectory.class);
736 final Directory dirExif = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
737 final GpsDirectory dirGps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
738
739 try {
740 if (dirExif != null) {
741 setExifOrientation(dirExif.getInt(ExifIFD0Directory.TAG_ORIENTATION));
742 }
743 } catch (MetadataException ex) {
744 Logging.debug(ex);
745 }
746
747 try {
748 if (dir != null) {
749 // there are cases where these do not match width and height stored in dirExif
750 setWidth(dir.getInt(JpegDirectory.TAG_IMAGE_WIDTH));
751 setHeight(dir.getInt(JpegDirectory.TAG_IMAGE_HEIGHT));
752 }
753 } catch (MetadataException ex) {
754 Logging.debug(ex);
755 }
756
757 if (dirGps == null || dirGps.getTagCount() <= 1) {
758 setExifCoor(null);
759 setPos(null);
760 return;
761 }
762
763 ifNotNull(ExifReader.readSpeed(dirGps), this::setSpeed);
764 ifNotNull(ExifReader.readElevation(dirGps), this::setElevation);
765
766 try {
767 setExifCoor(ExifReader.readLatLon(dirGps));
768 setPos(getExifCoor());
769 } catch (MetadataException | IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271)
770 Logging.error("Error reading EXIF from file: " + ex);
771 setExifCoor(null);
772 setPos(null);
773 }
774
775 try {
776 ifNotNull(ExifReader.readDirection(dirGps), this::setExifImgDir);
777 } catch (IndexOutOfBoundsException ex) { // (other exceptions, e.g. #5271)
778 Logging.debug(ex);
779 }
780
781 ifNotNull(dirGps.getGpsDate(), d -> setExifGpsTime(d.toInstant()));
782
783 IptcDirectory dirIptc = metadata.getFirstDirectoryOfType(IptcDirectory.class);
784 if (dirIptc != null) {
785 ifNotNull(ExifReader.readCaption(dirIptc), this::setIptcCaption);
786 ifNotNull(ExifReader.readHeadline(dirIptc), this::setIptcHeadline);
787 ifNotNull(ExifReader.readKeywords(dirIptc), this::setIptcKeywords);
788 ifNotNull(ExifReader.readObjectName(dirIptc), this::setIptcObjectName);
789 }
790 }
791
792 private static class NoMetadataReaderWarning extends Exception {
793 NoMetadataReaderWarning(String ext) {
794 super("No metadata reader for format *." + ext);
795 }
796 }
797
798 private static <T> void ifNotNull(T value, Consumer<T> setter) {
799 if (value != null) {
800 setter.accept(value);
801 }
802 }
803}
Note: See TracBrowser for help on using the repository browser.