package de.eisfeldj.augendiagnosefx.util.imagefile; import java.io.File; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import de.eisfeldj.augendiagnosefx.util.DateUtil; import de.eisfeldj.augendiagnosefx.util.Logger; import de.eisfeldj.augendiagnosefx.util.ResourceConstants; import de.eisfeldj.augendiagnosefx.util.ResourceUtil; import de.eisfeldj.augendiagnosefx.util.imagefile.ImageUtil.Resolution; import javafx.scene.image.Image; /** * Utility class to handle an eye photo, in particular regarding personName policies. */ public class EyePhoto { /** * The date format used for the file name. */ private static final String DATE_FORMAT = "yyyy-MM-dd"; /** * The maximum size of the image cache. */ private static final int MAX_IMAGE_CACHE = 4; /** * Indicator if the file has already a formatted name. */ private boolean mFormattedName = false; /** * The path of the file. */ private String mPath; /** * The filename. */ private String mFilename; /** * The name of the person. */ private String mPersonName; /** * The date of the image. */ private Date mDate; /** * The information of right/left eye. */ private RightLeft mRightLeft; /** * The file suffix. */ private String mSuffix; /** * A cache of the bitmap (to avoid too frequent generation). */ private Image mCachedImage; /** * A cache of the thumbnail. */ private Image mCachedThumbnail; /** * The list of eye photos having a cached image. */ private static final List<WeakReference<EyePhoto>> CACHED_EYE_PHOTOS = new ArrayList<>(); /** * A map from path to EyePhoto objects - for reuse. * * <p>Note: WeakHashMap cannot be used, as the garbage collection should be dependent on values, not on keys. */ private static final HashMap<String, WeakReference<EyePhoto>> EYE_PHOTO_MAP = new HashMap<>(); /** * Create the EyePhoto, giving a file resource. * * @param file * the file. */ public EyePhoto(final File file) { setPath(file.getParent()); setFilename(file.getName()); // Auto-correct file name if safely possible if (mFilename != null && !mFilename.equals(getFilename()) && !getFile().exists()) { boolean success = new File(getPath(), mFilename).renameTo(new File(getPath(), getFilename())); if (!success) { Logger.warning("Failed to rename file" + mFilename + " to " + getFilename()); } } } /** * Get an EyePhoto, giving a file resource (returning an existing instance if available). * * @param file The file. * @return The EyePhoto. */ public static EyePhoto fromFile(final File file) { synchronized (EYE_PHOTO_MAP) { EyePhoto eyePhoto = null; WeakReference<EyePhoto> eyePhotoReference = EYE_PHOTO_MAP.get(file.getAbsolutePath()); if (eyePhotoReference != null) { eyePhoto = eyePhotoReference.get(); } if (eyePhoto == null) { eyePhoto = new EyePhoto(file); EYE_PHOTO_MAP.put(file.getAbsolutePath(), new WeakReference<>(eyePhoto)); } return eyePhoto; } } /** * Retrieve the filename (excluding path). * * @return the filename. */ public final String getFilename() { if (mFormattedName) { return getPersonName() + " " + getDateString(DATE_FORMAT) + " " + getRightLeft().toShortString() + "." + getSuffix(); } else { return mFilename; } } /** * Retrieve the file path. * * @return the file path. */ public final String getAbsolutePath() { return getFile().getAbsolutePath(); } /** * Set the filename (extracting from it the person personName, the date and the left/right property). * * @param filename * the filename */ private void setFilename(final String filename) { this.mFilename = filename; int suffixPosition = filename.lastIndexOf('.'); int rightLeftPosition = filename.lastIndexOf(' ', suffixPosition); int datePosition = filename.lastIndexOf(' ', rightLeftPosition - 1); if (datePosition > 0) { setPersonName(filename.substring(0, datePosition)); mFormattedName = setDateString(filename.substring(datePosition + 1, rightLeftPosition), DATE_FORMAT); setRightLeft(RightLeft.fromString(filename.substring(rightLeftPosition + 1, suffixPosition))); setSuffix(filename.substring(suffixPosition + 1)); } else { if (suffixPosition > 0) { setSuffix(filename.substring(suffixPosition + 1)); } mFormattedName = false; } } /** * Retrieve the file path. * * @return the file path. */ public final String getPath() { return mPath; } private void setPath(final String path) { this.mPath = path; } /** * Retrieve the right/left information. * * @return the right/left information. */ public final RightLeft getRightLeft() { return mRightLeft; } private void setRightLeft(final RightLeft rightLeft) { this.mRightLeft = rightLeft; } /** * Retrieve the person name (use getFilename for the file name). * * @return the person name. */ public final String getPersonName() { return mPersonName; } /** * Set the person name (trimmed). * * @param name * the person name */ private void setPersonName(final String name) { if (name == null) { this.mPersonName = null; } else { this.mPersonName = name.trim(); } } /** * Retrieve the date as a string. * * @param format * the date format. * @return the date string. */ public final String getDateString(final String format) { return DateUtil.format(getDate(), format); } /** * Set the date from a String. * * @param dateString * the date string * @param format * the date format * @return true if successful. */ private boolean setDateString(final String dateString, final String format) { try { setDate(DateUtil.parse(dateString, format)); return true; } catch (Exception e) { return false; } } /** * Retrieve the date. * * @return the date. */ public final Date getDate() { return mDate; } private void setDate(final Date date) { this.mDate = date; } /** * Retrieve the file suffix. * * @return the suffix. */ public final String getSuffix() { return mSuffix; } private void setSuffix(final String suffix) { this.mSuffix = suffix.toLowerCase(Locale.getDefault()); } /** * Check if the file name is formatted as eye photo. * * @return true if the file name is formatted as eye photo. */ public final boolean isFormatted() { return mFormattedName; } /** * Retrieve the photo as File. * * @return the file */ public final File getFile() { return new File(getPath(), getFilename()); } /** * Check if the file exists. * * @return true if the file exists. */ public final boolean exists() { return getFile().exists(); } /** * Delete the eye photo from the file system. * * @return true if the deletion was successful. */ public final boolean delete() { return getFile().delete(); } /** * Move the eye photo to a target path and target personName (given via EyePhoto object). * * @param target * the file information of the target file. * @return true if the renaming was successful. */ public final boolean moveTo(final EyePhoto target) { if (target.getFile().exists()) { // do not allow overwriting return false; } return getFile().renameTo(target.getFile()); } /** * Move the eye photo to a target folder. * * @param folderName * the target folder * @return true if the move was successful. */ public final boolean moveToFolder(final String folderName) { File folder = new File(folderName); if (!folder.exists() || !folder.isDirectory()) { // target folder does not exist return false; } File targetFile = new File(folder, getFilename()); if (targetFile.exists()) { // do not overwrite return false; } return getFile().renameTo(targetFile); } /** * Copy the eye photo to a target path and target personName (given via EyePhoto object). * * @param target * the file information of the target file. * @return true if the copying was successful. */ public final boolean copyTo(final EyePhoto target) { if (target.getFile().exists()) { // do not allow overwriting return false; } return FileUtil.copyFile(getFile(), target.getFile()); } /** * Return an Image of this photo. * * @param resolution * Indicator of the resolution in which the image should be returned. * @return the Image */ public final Image getImage(final Resolution resolution) { switch (resolution) { case THUMB: if (mCachedThumbnail == null) { mCachedThumbnail = ImageUtil.getImage(getFile(), Resolution.THUMB); } return mCachedThumbnail; case NORMAL: Image result = mCachedImage; if (result == null) { result = ImageUtil.getImage(getFile(), Resolution.NORMAL); synchronized (CACHED_EYE_PHOTOS) { mCachedImage = result; CACHED_EYE_PHOTOS.add(new WeakReference<>(this)); // Ensure that not too many images are cached if (CACHED_EYE_PHOTOS.size() > MAX_IMAGE_CACHE) { EyePhoto firstInList = CACHED_EYE_PHOTOS.get(0).get(); if (firstInList != null) { firstInList.mCachedImage = null; } CACHED_EYE_PHOTOS.remove(0); } } } else { synchronized (CACHED_EYE_PHOTOS) { int index = -1; for (int i = 0; i < CACHED_EYE_PHOTOS.size(); i++) { if (CACHED_EYE_PHOTOS.get(i).get() == this) { index = i; break; } } if (index >= 0) { CACHED_EYE_PHOTOS.remove(index); CACHED_EYE_PHOTOS.add(new WeakReference<>(this)); } } } return result; case FULL: // Full size image is not cached. return ImageUtil.getImage(getFile(), Resolution.FULL); default: return null; } } /** * Change the personName renaming the file (keeping the path). * * @param targetName * the target name * @return true if the renaming was successful. */ public final boolean changePersonName(final String targetName) { EyePhoto target = cloneFromPath(); target.setPersonName(targetName); boolean success = moveTo(target); if (success) { // update metadata JpegMetadata metadata = target.getImageMetadata(); if (metadata == null) { metadata = new JpegMetadata(); target.updateMetadataWithDefaults(metadata); } if (metadata.getPerson() == null || metadata.getPerson().length() == 0 || metadata.getPerson().equals(getPersonName())) { metadata.setPerson(targetName); } target.storeImageMetadata(metadata); } return success; } /** * Change the date renaming the file (keeping the path). * * @param newDate * the target date. * @return true if the change was successful. */ public final boolean changeDate(final Date newDate) { EyePhoto target = cloneFromPath(); target.setDate(newDate); boolean success = moveTo(target); if (success) { // update metadata JpegMetadata metadata = target.getImageMetadata(); if (metadata == null) { metadata = new JpegMetadata(); target.updateMetadataWithDefaults(metadata); } metadata.setOrganizeDate(newDate); target.storeImageMetadata(metadata); } return success; } /** * Retrieve a clone of this object from the absolute path. * * @return a clone (recreation) of this object having the same absolute path. */ public final EyePhoto cloneFromPath() { return new EyePhoto(new File(getAbsolutePath())); } /** * Get the metadata stored in the file. * * @return the metadata. */ public final JpegMetadata getImageMetadata() { return JpegSynchronizationUtil.getJpegMetadata(getAbsolutePath()); } /** * Store the metadata in the file. * * @param metadata * the metadata to be stored. */ public final void storeImageMetadata(final JpegMetadata metadata) { JpegSynchronizationUtil.storeJpegMetadata(getAbsolutePath(), metadata); } /** * Update metadata object with default metadata, based on the file name. * * @param metadata * the metadata object to be enhanced by the default information. */ public final void updateMetadataWithDefaults(final JpegMetadata metadata) { metadata.setPerson(getPersonName()); metadata.setOrganizeDate(getDate()); metadata.setRightLeft(getRightLeft()); metadata.setTitle(getPersonName() + " - " + getRightLeft().getTitleSuffix()); } /** * Store person, date and rightLeft in the metadata. */ public final void storeDefaultMetadata() { JpegMetadata metadata = getImageMetadata(); if (metadata == null) { metadata = new JpegMetadata(); } updateMetadataWithDefaults(metadata); storeImageMetadata(metadata); } /** * Compare two images for equality (by path). * * @param other * the other image to be compared to * @return true if the bitmaps have the same path. */ @Override public final boolean equals(final Object other) { if (!(other instanceof EyePhoto)) { return false; } EyePhoto otherPhoto = (EyePhoto) other; return otherPhoto.getAbsolutePath().equals(getAbsolutePath()); } /** * Ensure that hashCode() matches equals(). * * @return the hashCode. */ @Override public final int hashCode() { return getAbsolutePath().hashCode(); } /** * Enumeration for left eye vs. right eye. */ public enum RightLeft { /** * Enumeration values for right eye and left eye. */ RIGHT, LEFT; /** * Convert into a short string, to be used for filenames. * * @return the short string (dependent on language!) */ public final String toShortString() { switch (this) { case LEFT: return ResourceUtil.getString(ResourceConstants.FILE_INFIX_LEFT); case RIGHT: return ResourceUtil.getString(ResourceConstants.FILE_INFIX_RIGHT); default: return ""; } } @Override public String toString() { switch (this) { case LEFT: return "LEFT"; case RIGHT: return "RIGHT"; default: return ""; } } /** * The suffix to be used for the title of the image. * * @return the title suffix. */ public final String getTitleSuffix() { switch (this) { case LEFT: return ResourceUtil.getString(ResourceConstants.SUFFIX_TITLE_LEFT); case RIGHT: return ResourceUtil.getString(ResourceConstants.SUFFIX_TITLE_RIGHT); default: return null; } } /** * Convert a String into a RightLeft enum (by first letter). * * @param rightLeftString * The String to be converted. * @return the converted RightString. */ public static final RightLeft fromString(final String rightLeftString) { if (rightLeftString != null && rightLeftString.matches("[rRdD].*")) { return RIGHT; } else { return LEFT; } } } }