Java tutorial
/******************************************************************************* * Copyright (C) 2005, 2016 Wolfgang Schramm and Contributors * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation version 2 of the License. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110, USA *******************************************************************************/ package net.tourbook.photo; import java.awt.Point; import java.io.File; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import net.tourbook.common.UI; import net.tourbook.common.map.CommonMapProvider; import net.tourbook.common.map.GeoPosition; import net.tourbook.common.time.TimeTools; import net.tourbook.common.util.StatusUtil; import net.tourbook.common.util.Util; import org.apache.commons.imaging.Imaging; import org.apache.commons.imaging.ImagingConstants; import org.apache.commons.imaging.common.IImageMetadata; import org.apache.commons.imaging.common.IImageMetadata.IImageMetadataItem; import org.apache.commons.imaging.common.ImageMetadata.Item; import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata; import org.apache.commons.imaging.formats.jpeg.JpegPhotoshopMetadata; import org.apache.commons.imaging.formats.jpeg.iptc.IptcTypes; import org.apache.commons.imaging.formats.tiff.TiffField; import org.apache.commons.imaging.formats.tiff.TiffImageMetadata; import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants; import org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants; import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShortOrLong; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.osgi.util.NLS; public class Photo { private String _uniqueId; public static final int MAP_IMAGE_DEFAULT_WIDTH_HEIGHT = 80; private static final org.eclipse.swt.graphics.Point MAP_IMAGE_DEFAULT_SIZE = new org.eclipse.swt.graphics.Point( MAP_IMAGE_DEFAULT_WIDTH_HEIGHT, MAP_IMAGE_DEFAULT_WIDTH_HEIGHT); /** * Photo image file */ public File imageFile; public String imagePathName; public String imageFileName; public String imageFileExt; /** * File path name is the unique key for a photo. */ public String imageFilePathName; /** * Last modified date/time of the image file which is provided by the file system with the * system time zone. */ private LocalDateTime _imageFileLastModified; /** * Exif date/time, it has no time zone but UTC with 0 time offset is used */ private LocalDateTime _exifDateTime; /** * Exif time in milliseconds, when not available, the last modified time of the image file is * used. */ public long imageExifTime; /** * Time in ms (or {@link Long#MIN_VALUE} when not set) when photo was taken + time adjustments, * e.g. wrong time zone, wrong time is set in the camera. */ public long adjustedTimeTour = Long.MIN_VALUE; public long adjustedTimeLink = Long.MIN_VALUE; public long imageFileSize; /** * Camera which is used to take this photo, is <code>null</code> when not yet set. */ public Camera camera; /** * Is <code>true</code> when photo exif data are loaded. */ public boolean isExifLoaded; /** * Is <code>true</code> when this photo contains geo coordinates. */ public boolean isLinkPhotoWithGps; public boolean isTourPhotoWithGps; /** * Is <code>true</code> when geo coordinates origin is in the photo EXIF data. */ public boolean isGeoFromExif; /** * Is <code>true</code> when a photo is saved in a tour. * <p> * This allows to set rating stars which requires that they can be saved in a tour. */ public boolean isSavedInTour; /** * Key is tourId */ private final HashMap<Long, TourPhotoReference> _tourPhotoRef = new HashMap<Long, TourPhotoReference>(); /** * When sql loading state is {@link PhotoSqlLoadingState#NOT_LOADED}, the photo is created from * the file system and {@link #_tourPhotoRef} needs to be retrieved from the sql db. */ private AtomicReference<PhotoSqlLoadingState> _photoSqlLoadingState = new AtomicReference<PhotoSqlLoadingState>( PhotoSqlLoadingState.NOT_LOADED); /** * Rating stars are very complicated when a photo is saved in multiple tours. Currently * (8.1.2013) ratings stars can be set only for ALL tours. */ public int ratingStars; private PhotoImageMetadata _photoImageMetadata; /** * <pre> * Orientation * * The image orientation viewed in terms of rows and columns. * Type = SHORT * Default = 1 * * 1 = The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side. * 2 = The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side. * 3 = The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side. * 4 = The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side. * 5 = The 0th row is the visual left-hand side of the image, and the 0th column is the visual top. * 6 = The 0th row is the visual right-hand side of the image, and the 0th column is the visual top. * 7 = The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom. * 8 = The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom. * Other = reserved * </pre> */ private int _orientation = 1; private int _photoImageWidth = Integer.MIN_VALUE; private int _photoImageHeight = Integer.MIN_VALUE; private int _thumbImageWidth = Integer.MIN_VALUE; private int _thumbImageHeight = Integer.MIN_VALUE; /** * When <code>true</code>, EXIF geo is returned when available, otherwise tour geo is returned * when available. When requested geo is not available, the other is returned. */ // private static boolean _isGetExifGeo = false; /** * */ private static IPhotoServiceProvider _photoServiceProvider; /** * Double.MIN_VALUE cannot be used, it cannot be saved in the database. 0 is the value when the * value is not set !!! */ private double _exifLatitude = 0; private double _exifLongitude = 0; private double _tourLatitude = 0; private double _tourLongitude = 0; private double _linkLatitude = 0; private double _linkLongitude = 0; private String _gpsAreaInfo; private double _imageDirection = Double.MIN_VALUE; private double _altitude = Double.MIN_VALUE; private static DateTimeFormatter _dtParser; static { setupTimeZone(); } /** * caches the world positions for the photo lat/long values for each zoom level * <p> * key: projection id + zoom level */ private final HashMap<Integer, Point> _tourWorldPosition = new HashMap<Integer, Point>(); private final HashMap<Integer, Point> _linkWorldPosition = new HashMap<Integer, Point>(); /** * Contains image keys for each image quality which can be used to get images from an image * cache */ private String _imageKeyThumb; private String _imageKeyHQ; private String _imageKeyOriginal; /** * This array keeps track of the loading state for the photo images and for different qualities */ private PhotoLoadingState _photoLoadingStateThumb; private PhotoLoadingState _photoLoadingStateHQ; private PhotoLoadingState _photoLoadingStateOriginal; /** * Is <code>true</code> when loading the image causes an error. */ private boolean _isLoadingError; /** * Is <code>true</code> when the image file is available in the file system. */ private boolean _isImageFileAvailable; /** * Exif thumb state * <p> * * <pre> * 1 exif thumb image is available * 0 exif thumb image is not available * -1 exif thumb has not yet been retrieved * </pre> */ private int _exifThumbImageState = -1; /** * Image size which is painted in the map */ private org.eclipse.swt.graphics.Point _mapImageSize = MAP_IMAGE_DEFAULT_SIZE; /** * This is the image size which the user has selected to paint a photo image. */ private static int PAINTED_MAP_IMAGE_WIDTH = MAP_IMAGE_DEFAULT_WIDTH_HEIGHT; private int _paintedMapImageWidth; /** */ public Photo(final File photoImageFile) { setupPhoto(photoImageFile, new Path(photoImageFile.getPath())); } public Photo(final String imageFilePathName) { this(new File(imageFilePathName)); } public static String getImageKeyHQ(final String imageFilePathName) { return Util.computeMD5(imageFilePathName + "_HQ");//$NON-NLS-1$ } public static String getImageKeyThumb(final String imageFilePathName) { return Util.computeMD5(imageFilePathName + "_Thumb");//$NON-NLS-1$ } public static IPhotoServiceProvider getPhotoServiceProvider() { final IPhotoServiceProvider photoServiceProvider = _photoServiceProvider; return photoServiceProvider; } public static void setPaintedMapImageWidth(final int paintedMapImageWidth) { PAINTED_MAP_IMAGE_WIDTH = paintedMapImageWidth; } public static void setPhotoServiceProvider(final IPhotoServiceProvider photoServiceProvider) { _photoServiceProvider = photoServiceProvider; } public static void setupTimeZone() { _dtParser = DateTimeFormatter// .ofPattern("yyyy:MM:dd HH:mm:ss") //$NON-NLS-1$ .withZone(TimeTools.getDefaultTimeZone()); } public void addTour(final Long tourId, final long photoId) { if (_tourPhotoRef.containsKey(tourId) == false) { _tourPhotoRef.put(tourId, new TourPhotoReference(tourId, photoId)); } } /** * Creates metadata from image metadata * * @param imageMetadata * Can be <code>null</code> when not available * @return */ private PhotoImageMetadata createPhotoMetadata(final IImageMetadata imageMetadata) { final PhotoImageMetadata photoMetadata = new PhotoImageMetadata(); /* * read meta data for this photo */ if (imageMetadata instanceof TiffImageMetadata) { photoMetadata.isExifFromImage = true; final TiffImageMetadata tiffMetadata = (TiffImageMetadata) imageMetadata; photoMetadata.exifDateTime = getTiffValueDate(tiffMetadata); photoMetadata.orientation = 1; photoMetadata.imageWidth = getTiffValueInt(tiffMetadata, TiffTagConstants.TIFF_TAG_IMAGE_WIDTH, Integer.MIN_VALUE); photoMetadata.imageHeight = getTiffValueInt(tiffMetadata, TiffTagConstants.TIFF_TAG_IMAGE_LENGTH, Integer.MIN_VALUE); photoMetadata.model = getTiffValueString(tiffMetadata, TiffTagConstants.TIFF_TAG_MODEL); } else if (imageMetadata instanceof JpegImageMetadata) { photoMetadata.isExifFromImage = true; final JpegImageMetadata jpegMetadata = (JpegImageMetadata) imageMetadata; photoMetadata.exifDateTime = getExifValueDate(jpegMetadata); photoMetadata.orientation = getExifValueInt(jpegMetadata, TiffTagConstants.TIFF_TAG_ORIENTATION, 1); photoMetadata.imageWidth = getExifValueInt(jpegMetadata, ExifTagConstants.EXIF_TAG_EXIF_IMAGE_WIDTH, Integer.MIN_VALUE); photoMetadata.imageHeight = getExifValueInt(jpegMetadata, ExifTagConstants.EXIF_TAG_EXIF_IMAGE_LENGTH, Integer.MIN_VALUE); photoMetadata.imageDirection = getExifValueDouble(jpegMetadata, GpsTagConstants.GPS_TAG_GPS_IMG_DIRECTION); photoMetadata.altitude = getExifValueDouble(jpegMetadata, GpsTagConstants.GPS_TAG_GPS_ALTITUDE); photoMetadata.model = getExifValueString(jpegMetadata, TiffTagConstants.TIFF_TAG_MODEL); /* * GPS */ final TiffImageMetadata exifMetadata = jpegMetadata.getExif(); if (exifMetadata != null) { try { final TiffImageMetadata.GPSInfo gpsInfo = exifMetadata.getGPS(); if (gpsInfo != null) { photoMetadata.latitude = gpsInfo.getLatitudeAsDegreesNorth(); photoMetadata.longitude = gpsInfo.getLongitudeAsDegreesEast(); } } catch (final Exception e) { // ignore } } photoMetadata.gpsAreaInfo = getExifValueGpsArea(jpegMetadata); /* * photoshop metadata */ final JpegPhotoshopMetadata pshopMetadata = jpegMetadata.getPhotoshop(); if (pshopMetadata != null) { final List<? extends IImageMetadataItem> pshopItems = pshopMetadata.getItems(); for (final IImageMetadataItem pshopItem : pshopItems) { if (pshopItem instanceof Item) { final Item item = (Item) pshopItem; final String keyword = item.getKeyword(); if (keyword.equals(IptcTypes.OBJECT_NAME.name)) { photoMetadata.objectName = item.getText(); } else if (keyword.equals(IptcTypes.CAPTION_ABSTRACT.name)) { photoMetadata.captionAbstract = item.getText(); } } } } } // set file date time photoMetadata.fileDateTime = _imageFileLastModified; //// this will log all available meta data // System.out.println(UI.timeStampNano()); // System.out.println(UI.timeStampNano() + " " + imageFileName); // System.out.println(UI.timeStampNano()); // System.out.println(imageMetadata.toString()); // System.out.println(UI.timeStampNano()); // System.out.println(photoMetadata); // System.out.println(UI.timeStampNano()); // // TODO remove SYSTEM.OUT.PRINTLN return photoMetadata; } public String dumpLoadingState() { final StringBuilder sb = new StringBuilder(); sb.append("Thumb:" + _photoLoadingStateThumb); //$NON-NLS-1$ sb.append("\tHQ:" + _photoLoadingStateHQ); //$NON-NLS-1$ sb.append("\tOriginal:" + _photoLoadingStateOriginal); //$NON-NLS-1$ return sb.toString(); } public void dumpTourReferences() { for (final TourPhotoReference ref : _tourPhotoRef.values()) { System.out.println(UI.timeStampNano() + " \t\tphotoId=" + ref.photoId); //$NON-NLS-1$ // TODO remove SYSTEM.OUT.PRINTLN } } @Override public boolean equals(final Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!(obj instanceof Photo)) { return false; } final Photo other = (Photo) obj; if (_uniqueId == null) { if (other._uniqueId != null) { return false; } } else if (!_uniqueId.equals(other._uniqueId)) { return false; } return true; } public double getAltitude() { return _altitude; } /** * @return Returns photo image height which is available, in this sequence: * <p> * <li>Original image height<br> * <li>Thumb image height<br> * <li>{@link Integer#MIN_VALUE} when image height is not yet set */ public int getAvailableImageHeight() { if (_photoImageHeight != Integer.MIN_VALUE) { return _photoImageHeight; } if (_thumbImageHeight != Integer.MIN_VALUE) { return _thumbImageHeight; } return Integer.MIN_VALUE; } /** * @return Returns photo image width which is available, in this sequence: * <p> * <li>Original image width<br> * <li>Thumb image width<br> * <li>{@link Integer#MIN_VALUE} when image width is not yet set */ public int getAvailableImageWidth() { if (_photoImageWidth != Integer.MIN_VALUE) { return _photoImageWidth; } if (_thumbImageWidth != Integer.MIN_VALUE) { return _thumbImageWidth; } return Integer.MIN_VALUE; } /** * @return Returns image size as visible text which is displayed in the UI. */ public String getDimensionText() { final StringBuilder sbDimenstion = new StringBuilder(); if (isImageSizeAvailable()) { final boolean isThumbSize = isThumbImageSize(); if (isThumbSize) { sbDimenstion.append(UI.SYMBOL_BRACKET_LEFT); } sbDimenstion.append(getAvailableImageWidth()); sbDimenstion.append(UI.DIMENSION); sbDimenstion.append(getAvailableImageHeight()); if (isThumbSize) { sbDimenstion.append(UI.SYMBOL_BRACKET_RIGHT); } } return sbDimenstion.toString(); } public LocalDateTime getExifDateTime() { return _exifDateTime; } /** * @return Returns EXIF thumb image stage * * <pre> * 1 exif thumb image is available * 0 exif thumb image is not available * -1 exif thumb has not yet been loaded * </pre> */ public int getExifThumbImageState() { return _exifThumbImageState; } /** * Date/Time * * @param jpegMetadata * @param file * @return */ private LocalDateTime getExifValueDate(final JpegImageMetadata jpegMetadata) { // /* // * !!! time is not correct, maybe it is the time when the GPS signal was // * received !!! // */ // printTagValue(jpegMetadata, TiffConstants.GPS_TAG_GPS_TIME_STAMP); try { final TiffField exifDate = jpegMetadata.findEXIFValueWithExactMatch(// ExifTagConstants.EXIF_TAG_DATE_TIME_ORIGINAL); if (exifDate != null) { return LocalDateTime.parse(exifDate.getStringValue(), _dtParser); } final TiffField tiffDate = jpegMetadata .findEXIFValueWithExactMatch(TiffTagConstants.TIFF_TAG_DATE_TIME); if (tiffDate != null) { return LocalDateTime.parse(tiffDate.getStringValue(), _dtParser); } } catch (final Exception e) { // ignore } return null; } /** * Image direction * * @param tagInfo */ private double getExifValueDouble(final JpegImageMetadata jpegMetadata, final TagInfo tagInfo) { try { final TiffField field = jpegMetadata.findEXIFValueWithExactMatch(tagInfo); if (field != null) { return field.getDoubleValue(); } } catch (final Exception e) { // ignore } return Double.MIN_VALUE; } /** * GPS area info */ private String getExifValueGpsArea(final JpegImageMetadata jpegMetadata) { try { final TiffField field = jpegMetadata .findEXIFValueWithExactMatch(GpsTagConstants.GPS_TAG_GPS_AREA_INFORMATION); if (field != null) { final Object fieldValue = field.getValue(); if (fieldValue != null) { /** * source: Exif 2.2 specification * * <pre> * * Table 6 Character Codes and their Designation * * Character Code Code Designation (8 Bytes) References * ASCII 41.H, 53.H, 43.H, 49.H, 49.H, 00.H, 00.H, 00.H ITU-T T.50 IA5 * JIS A.H, 49.H, 53.H, 00.H, 00.H, 00.H, 00.H, 00.H JIS X208-1990 * Unicode 55.H, 4E.H, 49.H, 43.H, 4F.H, 44.H, 45.H, 00.H Unicode Standard * Undefined 00.H, 00.H, 00.H, 00.H, 00.H, 00.H, 00.H, 00.H Undefined * * </pre> */ final byte[] byteArrayValue = field.getByteArrayValue(); final int fieldLength = byteArrayValue.length; if (fieldLength > 0) { /** * <pre> * * skipping 1 + 6 characters: * * 1 character code * 2...7 have no idea why these bytes are set to none valid characters * * </pre> */ final byte[] valueBytes = Arrays.copyOfRange(byteArrayValue, 7, fieldLength); String valueString = null; final byte firstByte = byteArrayValue[0]; if (firstByte == 0x55) { valueString = new String(valueBytes, UI.UTF_16); } else { valueString = new String(valueBytes); } return valueString; } } } } catch (final Exception e) { // ignore } return null; } private int getExifValueInt(final JpegImageMetadata jpegMetadata, final TagInfo tiffTag, final int defaultValue) { try { final TiffField field = jpegMetadata.findEXIFValueWithExactMatch(tiffTag); if (field != null) { return field.getIntValue(); } } catch (final Exception e) { // ignore } return defaultValue; } private String getExifValueString(final JpegImageMetadata jpegMetadata, final TagInfo tagInfo) { try { final TiffField field = jpegMetadata.findEXIFValueWithExactMatch(tagInfo); if (field != null) { return field.getStringValue(); } } catch (final Exception e) { // ignore } return null; } public String getGpsAreaInfo() { return _gpsAreaInfo; } /** * @return Returns the cardinal direction (Himmelsrichtung) in degrees or * {@link Double#MIN_VALUE} when not set. */ public double getImageDirection() { return _imageDirection; } public LocalDateTime getImageFileDateTime() { return _imageFileLastModified; } /** * @return Returns an image key which can be used to get images from an image cache. This key is * a MD5 hash from the full image file path and the image quality. */ public String getImageKey(final ImageQuality imageQuality) { if (imageQuality == ImageQuality.HQ) { return _imageKeyHQ; } else if (imageQuality == ImageQuality.ORIGINAL) { return _imageKeyOriginal; } else { return _imageKeyThumb; } } /** * Updates metadata from image file. * * @return Returns photo image metadata, metadata is loaded from the image file when it's not * yet loaded. */ public PhotoImageMetadata getImageMetaData() { if (_photoImageMetadata == null) { getImageMetaData(false); } return _photoImageMetadata; } /** * Updated metadata from the image file * * @param isReadThumbnail * @return Returns image metadata <b>with</b> image thumbnail <b>only</b> when * <code>isReadThumbnail</code> is <code>true</code>, otherwise it checks if metadata * are already loaded. */ public IImageMetadata getImageMetaData(final Boolean isReadThumbnail) { if (_photoImageMetadata != null && isReadThumbnail == false) { // meta data are available but the exif thumnail is not requested return null; } if (PhotoLoadManager.isImageLoadingError(imageFilePathName)) { // image could not be loaded previously return null; } IImageMetadata imageFileMetadata = null; try { /* * read metadata WITH thumbnail image info, this is the default when the pamameter is * ommitted */ final HashMap<String, Object> params = new HashMap<String, Object>(); params.put(ImagingConstants.PARAM_KEY_READ_THUMBNAILS, isReadThumbnail); // final long start = System.currentTimeMillis(); imageFileMetadata = Imaging.getMetadata(imageFile, params); // System.out.println(UI.timeStamp() // + Thread.currentThread().getName() // + "read exif\t" // + ((System.currentTimeMillis() - start) + " ms") // + ("\tWithThumb: " + isReadThumbnail) // + ("\t" + imageFilePathName) // // // ); // // TODO remove SYSTEM.OUT.PRINTLN // // System.out.println(imageFileMetadata); // // TODO remove SYSTEM.OUT.PRINTLN } catch (final Exception e) { StatusUtil.log(NLS.bind(// "Could not read metadata from image \"{0}\"", //$NON-NLS-1$ imageFile)); PhotoLoadManager.putPhotoInLoadingErrorMap(imageFilePathName); } finally { final PhotoImageMetadata photoImageMetadata = createPhotoMetadata(imageFileMetadata); updateImageMetadata(photoImageMetadata); } return imageFileMetadata; } /** * @return Returns image meta data or <code>null</code> when not loaded or not available. */ public PhotoImageMetadata getImageMetaDataRaw() { return _photoImageMetadata; } public double getLinkLatitude() { return _linkLatitude != 0 // ? _linkLatitude : _exifLatitude; } public double getLinkLongitude() { return _linkLongitude != 0 // ? _linkLongitude : _exifLongitude; } /** * @return Returns the loading state for the given photo quality */ public PhotoLoadingState getLoadingState(final ImageQuality imageQuality) { if (imageQuality == ImageQuality.HQ) { return _photoLoadingStateHQ; } else if (imageQuality == ImageQuality.ORIGINAL) { return _photoLoadingStateOriginal; } else { return _photoLoadingStateThumb; } } /** * @return Returns size when image is painted on the map or <code>null</code>, when not yet set. */ public org.eclipse.swt.graphics.Point getMapImageSize() { if (PAINTED_MAP_IMAGE_WIDTH != _paintedMapImageWidth) { setMapImageSize(); _paintedMapImageWidth = PAINTED_MAP_IMAGE_WIDTH; } return _mapImageSize; } /** * <pre> * Orientation * * The image orientation viewed in terms of rows and columns. * Type = SHORT * Default = 1 * * 1 = The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side. * 2 = The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side. * 3 = The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side. * 4 = The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side. * 5 = The 0th row is the visual left-hand side of the image, and the 0th column is the visual top. * 6 = The 0th row is the visual right-hand side of the image, and the 0th column is the visual top. * 7 = The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom. * 8 = The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom. * Other = reserved * </pre> * * @return */ public int getOrientation() { return _orientation; } /** * @return Return date/time for the image. First EXIF date is returned, when not available, * image file date/time is returned. */ public LocalDateTime getOriginalDateTime() { return _exifDateTime != null ? _exifDateTime : _imageFileLastModified; } public int getPhotoImageHeight() { return _photoImageHeight; } /** * @return Returns photo image width or {@link Integer#MIN_VALUE} when width is not set. */ public int getPhotoImageWidth() { return _photoImageWidth; } public AtomicReference<PhotoSqlLoadingState> getSqlLoadingState() { return _photoSqlLoadingState; } private LocalDateTime getTiffValueDate(final TiffImageMetadata tiffMetadata) { try { final TiffField exifDate = tiffMetadata.findField(ExifTagConstants.EXIF_TAG_DATE_TIME_ORIGINAL, true); if (exifDate != null) { return LocalDateTime.parse(exifDate.getStringValue(), _dtParser); } final TiffField date = tiffMetadata.findField(TiffTagConstants.TIFF_TAG_DATE_TIME, true); if (date != null) { return LocalDateTime.parse(date.getStringValue(), _dtParser); } } catch (final Exception e) { // ignore } return null; } private int getTiffValueInt(final TiffImageMetadata tiffMetadata, final TagInfoShortOrLong tiffTag, final int defaultValue) { try { final TiffField field = tiffMetadata.findField(tiffTag, true); if (field != null) { return field.getIntValue(); } } catch (final Exception e) { // ignore } return defaultValue; } private String getTiffValueString(final TiffImageMetadata tiffMetadata, final TagInfo tagInfo) { try { final TiffField field = tiffMetadata.findField(tagInfo, true); if (field != null) { return field.getStringValue(); } } catch (final Exception e) { // ignore } return null; } /** * @return Returns latitude. * <p> * <b> Double.MIN_VALUE cannot be used, it cannot be saved in the database. * <p> * Returns 0 when the value is not set !!! </b> */ public double getTourLatitude() { return _tourLatitude != 0 // ? _tourLatitude : _exifLatitude; } public double getTourLongitude() { return _tourLongitude != 0 // ? _tourLongitude : _exifLongitude; } public HashMap<Long, TourPhotoReference> getTourPhotoReferences() { return _tourPhotoRef; } public String getUniqueId() { return imageFilePathName; } /** * @param mapProvider * @param projectionId * @param zoomLevel * @param isLinkPhotoDisplayed * @return Returns the world position for this photo or <code>null</code> when geo position is * not set. */ public Point getWorldPosition(final CommonMapProvider mapProvider, final String projectionId, final int zoomLevel, final boolean isLinkPhotoDisplayed) { final double latitude = isLinkPhotoDisplayed // ? getLinkLatitude() : getTourLatitude(); if (latitude == 0) { return null; } final Integer hashKey = projectionId.hashCode() + zoomLevel; final Point worldPosition = isLinkPhotoDisplayed // ? _linkWorldPosition.get(hashKey) : _tourWorldPosition.get(hashKey); if (worldPosition == null) { // convert lat/long into world pixels which depends on the map projection final GeoPosition photoGeoPosition = new GeoPosition(latitude, isLinkPhotoDisplayed ? getLinkLongitude() : getTourLongitude()); final Point geoToPixel = mapProvider.geoToPixel(photoGeoPosition, zoomLevel); if (isLinkPhotoDisplayed) { _linkWorldPosition.put(hashKey, geoToPixel); } else { _tourWorldPosition.put(hashKey, geoToPixel); } return geoToPixel; } return worldPosition; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((_uniqueId == null) ? 0 : _uniqueId.hashCode()); return result; } public boolean isImageFileAvailable() { return _isImageFileAvailable; } /** * @return Return <code>true</code> when image size (thumb or original) is available. */ public boolean isImageSizeAvailable() { if (_photoImageWidth != Integer.MIN_VALUE || _thumbImageWidth != Integer.MIN_VALUE) { return true; } return false; } public boolean isLoadingError() { return _isLoadingError; } /** * @return Returns <code>true</code> when the original image size is not available but the thumb * image size. */ private boolean isThumbImageSize() { if (_photoImageWidth != Integer.MIN_VALUE) { return false; } return _thumbImageWidth != Integer.MIN_VALUE; } public void removeTour(final Long tourId) { _tourPhotoRef.remove(tourId); } public void replaceImageFile(final IPath newImageFilePathName) { // force loading of metadata _photoImageMetadata = null; setupPhoto(newImageFilePathName.toFile(), newImageFilePathName); // PhotoLoadManager.putPhotoInLoadingErrorMap(imageFilePathName); } public void resetLinkGeoPositions() { _linkLatitude = 0; _linkLongitude = 0; isLinkPhotoWithGps = isGeoFromExif; } public void resetLinkWorldPosition() { _linkWorldPosition.clear(); } public void resetTourExifState() { // photo is not saved any more in a tour _tourLatitude = 0; _tourLongitude = 0; isTourPhotoWithGps = isGeoFromExif; } public void setAltitude(final double altitude) { _altitude = altitude; } public void setGpsAreaInfo(final String gpsAreaInfo) { _gpsAreaInfo = gpsAreaInfo; } public void setLinkGeoPosition(final double linkLatitude, final double linkLongitude) { _linkLatitude = linkLatitude; _linkLongitude = linkLongitude; isLinkPhotoWithGps = true; } public void setLoadingState(final PhotoLoadingState photoLoadingState, final ImageQuality imageQuality) { if (imageQuality == ImageQuality.HQ) { _photoLoadingStateHQ = photoLoadingState; } else if (imageQuality == ImageQuality.ORIGINAL) { _photoLoadingStateOriginal = photoLoadingState; } else { _photoLoadingStateThumb = photoLoadingState; } // set overall loading error if (photoLoadingState == PhotoLoadingState.IMAGE_IS_INVALID) { _isLoadingError = true; } // // System.out // .println("set state\t" + imageQuality + "\t" + photoLoadingState + "\t" + imageFileName); // // TODO remove SYSTEM.OUT.PRINTLN } private void setMapImageSize() { final int imageCanvasWidth = PAINTED_MAP_IMAGE_WIDTH; final int imageCanvasHeight = imageCanvasWidth; final int imageWidth = _photoImageWidth != Integer.MIN_VALUE ? _photoImageWidth : _thumbImageWidth; final int imageHeight = _photoImageHeight != Integer.MIN_VALUE ? _photoImageHeight : _thumbImageHeight; _mapImageSize = RendererHelper.getBestSize(this, // imageWidth, imageHeight, imageCanvasWidth, imageCanvasHeight); } public void setPhotoDimension(final int width, final int height) { _photoImageWidth = width; _photoImageHeight = height; setMapImageSize(); } public void setStateExifThumb(final int exifThumbState) { _exifThumbImageState = exifThumbState; } public void setThumbDimension(final int width, final int height) { _thumbImageWidth = width; _thumbImageHeight = height; setMapImageSize(); } public void setThumbSaveError() { PhotoLoadManager.putPhotoInThumbSaveErrorMap(imageFilePathName); } public void setTourGeoPosition(final double latitude, final double longitude) { _tourLatitude = latitude; _tourLongitude = longitude; isTourPhotoWithGps = true; } private void setupPhoto(final File photoImageFile, final IPath photoImagePath) { final String photoImageFilePathName = photoImageFile.getAbsolutePath(); final long lastModified = photoImageFile.lastModified(); imageFile = photoImageFile; imageFileName = photoImageFile.getName(); imageFilePathName = photoImageFilePathName; imagePathName = photoImagePath.removeLastSegments(1).toOSString(); imageFileExt = photoImagePath.getFileExtension(); imageFileSize = photoImageFile.length(); _imageFileLastModified = LocalDateTime.ofInstant(// Instant.ofEpochMilli(lastModified), // ZoneOffset.UTC ZoneId.systemDefault() // ); // initially sort by file date until exif data are loaded imageExifTime = lastModified; _uniqueId = photoImageFilePathName; /* * initialize image keys and loading states */ _imageKeyThumb = getImageKeyThumb(photoImageFilePathName); _imageKeyHQ = getImageKeyHQ(photoImageFilePathName); _imageKeyOriginal = Util.computeMD5(photoImageFilePathName + "_Original");//$NON-NLS-1$ _isImageFileAvailable = photoImageFile.exists(); if (_isImageFileAvailable) { _photoLoadingStateThumb = PhotoLoadingState.UNDEFINED; _photoLoadingStateHQ = PhotoLoadingState.UNDEFINED; _photoLoadingStateOriginal = PhotoLoadingState.UNDEFINED; _isLoadingError = false; } else { _photoLoadingStateThumb = PhotoLoadingState.IMAGE_IS_INVALID; _photoLoadingStateHQ = PhotoLoadingState.IMAGE_IS_INVALID; _photoLoadingStateOriginal = PhotoLoadingState.IMAGE_IS_INVALID; _isLoadingError = true; } } @Override public String toString() { // final String rotateDegree = _orientation == 8 ? "270" // // : _orientation == 3 ? "180" // // : _orientation == 6 ? "90" : "0"; return "" //$NON-NLS-1$ // +"Photo " // + (imageFileName) + ("\t_exifDateTime " + _exifDateTime) //$NON-NLS-1$ // + (_exifDateTime == null ? "-no date-" : "\t" + _exifDateTime) // + ("\trotate:" + rotateDegree) // + (_imageWidth == Integer.MIN_VALUE ? "-no size-" : "\t" + _imageWidth + "x" + _imageHeight) // + ("\tEXIF GPS: " + _exifLatitude + " - " + _exifLongitude) //$NON-NLS-1$ //$NON-NLS-2$ // + ("\tLink GPS: " + _linkLatitude + " - " + _linkLongitude) //$NON-NLS-1$ //$NON-NLS-2$ // + ("\tTour GPS: " + _tourLatitude + " - " + _tourLongitude) //$NON-NLS-1$ //$NON-NLS-2$ // ; } public void updateImageMetadata(final PhotoImageMetadata photoImageMetadata) { _photoImageMetadata = photoImageMetadata; if (photoImageMetadata.isExifFromImage) { /* * Set these data only when they are contained in the image file, this ensures that e.g. * width/height is not overwritten with default values. */ _exifDateTime = photoImageMetadata.exifDateTime; _photoImageWidth = photoImageMetadata.imageWidth; _photoImageHeight = photoImageMetadata.imageHeight; _orientation = photoImageMetadata.orientation; _imageDirection = photoImageMetadata.imageDirection; _altitude = photoImageMetadata.altitude; _exifLatitude = photoImageMetadata.latitude; _exifLongitude = photoImageMetadata.longitude; _gpsAreaInfo = photoImageMetadata.gpsAreaInfo; } // rotate image, swap with and height if (_photoImageWidth != Integer.MIN_VALUE && _photoImageHeight != Integer.MIN_VALUE) { if (_orientation > 1) { // see here http://www.impulseadventure.com/photo/exif-orientation.html if (_orientation == 6 || _orientation == 8) { // camera is rotated to the left or right by 90 degree final int imageWidth = _photoImageWidth; _photoImageWidth = _photoImageHeight; _photoImageHeight = imageWidth; } } } setMapImageSize(); isExifLoaded = true; /* * set state if gps data are available, this state is used for filtering the photos and to * indicate that exif data are loaded */ final boolean isExifGPS = _exifLatitude != 0; final boolean isTourGPS = _tourLatitude != 0; isGeoFromExif = isExifGPS; isTourPhotoWithGps = isTourGPS || isExifGPS; // sort by exif date when available if (_exifDateTime != null) { final long exifUTCMills = TimeTools.toEpochMilli(_exifDateTime); imageExifTime = exifUTCMills; } } }