se.trixon.toolbox.photokml.Operation.java Source code

Java tutorial

Introduction

Here is the source code for se.trixon.toolbox.photokml.Operation.java

Source

/*
 * Copyright 2015 Patrik Karlsson.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package se.trixon.toolbox.photokml;

import se.trixon.almond.Scaler;
import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.lang.GeoLocation;
import com.drew.metadata.Metadata;
import com.drew.metadata.exif.ExifSubIFDDirectory;
import com.drew.metadata.exif.GpsDescriptor;
import com.drew.metadata.exif.GpsDirectory;
import de.micromata.opengis.kml.v_2_2_0.Folder;
import de.micromata.opengis.kml.v_2_2_0.Kml;
import java.awt.Dimension;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.attribute.BasicFileAttributes;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.api.progress.ProgressHandleFactory;
import org.openide.awt.StatusDisplayer;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import se.trixon.almond.GraphicsHelper;
import se.trixon.almond.dialogs.Message;
import se.trixon.almond.dictionary.Dict;
import se.trixon.toolbox.core.Toolbox;

/**
 *
 * @author Patrik Karlsson <patrik@trixon.se>
 */
public class Operation {

    private final ResourceBundle mBundle;
    private final File mDestinationFile;
    private final List<Exception> mExceptions = new ArrayList<>();
    private final List<File> mFiles = new ArrayList<>();
    private boolean mInterrupted = false;
    private final OperationListener mListener;
    private final Options mOptions = Options.INSTANCE;
    private ProgressHandle mProgressHandle;
    private final RequestProcessor mRequestProcessor = new RequestProcessor("interruptible tasks", 1, true);
    private final File[] mSourceFiles;
    private final PathMatcher mSourcePattern;
    private final boolean mValidSourceString;

    public Operation(OperationListener operationListener, File destination) {
        mListener = operationListener;
        mDestinationFile = destination;
        mBundle = NbBundle.getBundle(PhotoKmlTopComponent.class);

        mValidSourceString = mOptions.getSourcePaths().length() > 0;
        String[] sourcePathsAsStrings = mOptions.getSourcePaths().split(SystemUtils.PATH_SEPARATOR);
        mSourceFiles = new File[sourcePathsAsStrings.length];

        for (int i = 0; i < sourcePathsAsStrings.length; i++) {
            if (SystemUtils.IS_OS_WINDOWS) {
                sourcePathsAsStrings[i] = StringUtils.trim(sourcePathsAsStrings[i]);
            }

            mSourceFiles[i] = new File(sourcePathsAsStrings[i]);
        }

        mSourcePattern = FileSystems.getDefault().getPathMatcher("glob:" + mOptions.getSourcePattern());
    }

    public void start() {
        if (!mValidSourceString) {
            return;
        }

        mListener.onOperationStarted();
        OperationRunnable runnable = new OperationRunnable();
        final RequestProcessor.Task task = mRequestProcessor.create(runnable);
        mProgressHandle = ProgressHandleFactory.createHandle(mBundle.getString("Tool-Name"), task);
        task.addTaskListener((org.openide.util.Task task1) -> {
            mProgressHandle.finish();

            if (mInterrupted) {
                mListener.onOperationInterrupted();
            }
        });

        mProgressHandle.start();
        task.schedule(0);
    }

    private class OperationRunnable implements Runnable {

        private final ArrayList<File> mFilesWithErrors = new ArrayList<>();
        private final Pattern mFolderByRegexPattern = Pattern.compile(mOptions.getFoldersRegex());
        private final Map<String, Folder> mFolders = new HashMap<>();

        private Kml mKml = new Kml();
        private int mNumOfExif;
        private int mNumOfFiles;
        private int mNumOfGps;
        private final Folder mRootFolder;
        private final long mStartTime = System.currentTimeMillis();

        public OperationRunnable() {
            mRootFolder = mKml.createAndSetFolder().withName(mOptions.getFoldersRootName());
            mRootFolder.setDescription(mOptions.getFoldersRootDescription());
        }

        @Override
        public void run() {
            mInterrupted = !generateFileList();
            String status;

            if (!mInterrupted && !mFiles.isEmpty()) {
                mProgressHandle.switchToDeterminate(mFiles.size());
                mListener.onOperationProcessingStarted();
                status = String.format("\n%s\n", Dict.PROCESSING.getString());
                mListener.onOperationLog(status);

                int progress = 0;
                for (File sourceFile : mFiles) {
                    Toolbox.setStatusText(sourceFile.getAbsolutePath(), StatusDisplayer.IMPORTANCE_ERROR_HIGHLIGHT);
                    mProgressHandle.progress(progress++);
                    try {
                        process(sourceFile);
                    } catch (ImageProcessingException | IOException ex) {
                        mFilesWithErrors.add(sourceFile);
                    }

                    if (Thread.interrupted()) {
                        mInterrupted = true;
                        break;
                    }
                }
            }

            if (mInterrupted) {
                status = Dict.TASK_ABORTED.getString();
                mListener.onOperationLog("\n" + status);
            } else {
                for (Exception exception : mExceptions) {
                    mListener.onOperationLog(String.format("#%s", exception.getLocalizedMessage()));
                }

                if (!mFilesWithErrors.isEmpty()) {
                    mListener.onOperationLog("\n" + mBundle.getString("ignoredFiles") + "\n");

                    for (File fileWithError : mFilesWithErrors) {
                        logSuccess(false, fileWithError);
                    }
                }

                if (!mFiles.isEmpty()) {
                    saveToFile();
                }
            }
            //            if (mFiles.isEmpty()) {
            //                mListener.onOperationFailed();
            //            }

            Toolbox.setStatusText("", StatusDisplayer.IMPORTANCE_ERROR_HIGHLIGHT);
        }

        private void addPhoto(StringBuilder sb, File sourceFile, ExifSubIFDDirectory exifDirectory) {

            Dimension originalDimension = null;

            try {
                originalDimension = GraphicsHelper.getImgageDimension(sourceFile);
            } catch (IOException ex) {
                Message.error(Dict.ERROR.getString(), ex.getLocalizedMessage());
            }

            if (originalDimension == null) {
                originalDimension = new Dimension(200, 200);
            }

            Scaler scaler = new Scaler(new Dimension(originalDimension));

            if (mOptions.isPhotoBalloonMaxWidth()) {
                scaler.setWidth(mOptions.getPhotoBalloonMaxWidthValue());
            }

            if (mOptions.isPhotoBalloonMaxHeight()) {
                scaler.setHeight(mOptions.getPhotoBalloonMaxHeightValue());
            }

            Dimension newDimension = scaler.getDimension();
            String imageTagFormat = "<p><img src='%s' width='%d' height='%d'></p>";
            String imageTag = String.format(imageTagFormat, getImagePath(sourceFile), newDimension.width,
                    newDimension.height);
            sb.append(imageTag);
        }

        private boolean generateFileList() {
            mListener.onOperationLog(String.format("* %s", Dict.GENERATING_FILELIST.getString()));
            boolean invalidPath = false;

            if (mFiles != null) {
                for (File file : mFiles) {
                    String path = file.getAbsolutePath();

                    if (path.contains(SystemUtils.PATH_SEPARATOR)) {
                        invalidPath = true;
                        String message = String.format(Dict.INVALID_PATH.getString(), file.getAbsolutePath());
                        mListener.onOperationLog(message);
                    }
                }

                if (invalidPath) {
                    String message = String.format(Dict.ERROR_PATH_SEPARATOR.getString(),
                            SystemUtils.PATH_SEPARATOR);
                    Message.error(Dict.IO_ERROR_TITLE.getString(), message);
                }
            }

            for (File file : mSourceFiles) {
                if (file.isDirectory()) {
                    FileVisitor fileVisitor = new FileVisitor(mSourcePattern, mFiles);
                    try {
                        EnumSet<FileVisitOption> fileVisitOptions;
                        if (mOptions.isSourceFollowLinks()) {
                            fileVisitOptions = EnumSet.of(FileVisitOption.FOLLOW_LINKS);
                        } else {
                            fileVisitOptions = EnumSet.noneOf(FileVisitOption.class);
                        }

                        if (mOptions.isSourceRecursive()) {
                            Files.walkFileTree(file.toPath(), fileVisitOptions, Integer.MAX_VALUE, fileVisitor);
                        } else {
                            Files.walkFileTree(file.toPath(), fileVisitOptions, 1, fileVisitor);
                        }

                        if (fileVisitor.isInterrupted()) {
                            return false;
                        }
                    } catch (IOException ex) {
                        Exceptions.printStackTrace(ex);
                    }
                } else if (file.isFile() && mSourcePattern.matches(file.toPath().getFileName())) {
                    mFiles.add(file);
                }
            }

            if (mFiles.isEmpty()) {
                mListener.onOperationLog("* " + Dict.FILELIST_EMPTY.getString());
                mListener.onOperationFailed(Dict.FILELIST_EMPTY.getString());
            } else {
                Collections.sort(mFiles);
            }

            return true;
        }

        private Folder getFolder(File file, Date date) {
            String key;
            Folder folder;
            if (mOptions.isFoldersSubFolders()) {
                if (mOptions.getFoldersBy() == 0) {
                    key = file.getParentFile().getName();
                } else if (mOptions.getFoldersBy() == 1) {
                    try {
                        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(mOptions.getFoldersDatePattern());
                        key = simpleDateFormat.format(date);
                    } catch (Exception e) {
                        key = "NULL";
                    }
                } else {
                    key = mOptions.getFoldersRegexDefault();
                    Matcher matcher = mFolderByRegexPattern.matcher(file.getParent());
                    if (matcher.find()) {
                        key = matcher.group();
                    }
                }

                folder = getFolder(key);
            } else {
                folder = mRootFolder;
            }

            return folder;
        }

        private Folder getFolder(String key) {
            Folder folder = mFolders.get(key);

            if (folder == null) {
                folder = new Folder().withName(key);
                mFolders.put(key, folder);
            }

            return folder;
        }

        private Date getImageDate(File file, ExifSubIFDDirectory exifDirectory) {
            Date date;

            if (exifDirectory.containsTag(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL)) {
                date = exifDirectory.getDate(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL);
            } else {
                long millis = 0;
                try {
                    BasicFileAttributes attr = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
                    millis = attr.creationTime().toMillis();
                } catch (IOException ex) {
                    millis = file.lastModified();
                } finally {
                    date = new Date(millis);
                }
            }

            return date;
        }

        private String getImagePath(File file) {
            String imageSrc;

            if (mOptions.isPhotoBaseUrl()) {
                imageSrc = mOptions.getPhotoBaseUrlValue() + file.getName();
            } else {
                Path relativePath = mDestinationFile.toPath().relativize(file.toPath());

                //            try {
                //                relativeSourcePath = URLEncoder.encode(relativePath.toString(), "UTF-8");
                //            } catch (UnsupportedEncodingException ex) {
                //                Exceptions.printStackTrace(ex);
                //            }
                imageSrc = StringUtils.replace(relativePath.toString(), "..", ".", 1);

                if (SystemUtils.IS_OS_WINDOWS) {
                    imageSrc = imageSrc.replace("\\", "/");
                }
            }

            if (mOptions.isPhotoForceLowerCaseExtension()) {
                String ext = FilenameUtils.getExtension(imageSrc);
                if (!StringUtils.isBlank(ext) && !StringUtils.isAllLowerCase(ext)) {
                    String noExt = FilenameUtils.removeExtension(imageSrc);
                    imageSrc = String.format("%s.%s", noExt, ext.toLowerCase());
                }
            }

            return imageSrc;
        }

        private void logSuccess(boolean b, File sourceFile) {
            String prefix = b ? "+" : "-";
            mListener.onOperationLog(String.format(" %s %s", prefix, sourceFile.getAbsolutePath()));
        }

        private void process(File sourceFile) throws ImageProcessingException, IOException {
            Metadata metadata = ImageMetadataReader.readMetadata(sourceFile);

            ExifSubIFDDirectory exifDirectory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
            //            ExifSubIFDDescriptor exifSubIFDDescriptor = new ExifSubIFDDescriptor(exifDirectory);

            GpsDirectory gpsDirectory = metadata.getFirstDirectoryOfType(GpsDirectory.class);
            GpsDescriptor gpsDescriptor = new GpsDescriptor(gpsDirectory);

            if (exifDirectory != null) {
                mNumOfExif++;
                if (gpsDirectory != null || mOptions.isPlacemarkIncludeNullCoordinate()) {
                    mNumOfGps++;
                }
            } else {
                throw new ImageProcessingException(
                        String.format(mBundle.getString("exifError"), sourceFile.getAbsolutePath()));
            }

            Date exifDate = getImageDate(sourceFile, exifDirectory);
            String name = FilenameUtils.getBaseName(sourceFile.getAbsolutePath());

            if (mOptions.getPlacemarkNameBy() == 1) {
                try {
                    SimpleDateFormat simpleDateFormat = new SimpleDateFormat(mOptions.getPlacemarkDatePattern());
                    name = simpleDateFormat.format(exifDate);
                } catch (IllegalArgumentException ex) {
                    // nvm - use filename
                } catch (NullPointerException ex) {
                    System.err.println(" ! Invalid date in " + sourceFile.getAbsolutePath());
                }
            } else if (mOptions.getPlacemarkNameBy() == 2) {
                name = "";
            }

            StringBuilder descriptionBuilder = new StringBuilder();
            //            descriptionBuilder.append("<![CDATA[");
            if (mOptions.isDescriptionPhoto()) {
                addPhoto(descriptionBuilder, sourceFile, exifDirectory);
            }

            if (mOptions.isDescriptionFilename()) {
                descriptionBuilder.append(sourceFile.getName()).append("<br />");
            }

            if (mOptions.isDescriptionDate()) {
                descriptionBuilder.append(Toolbox.getDefaultDateFormat().format(exifDate)).append("<br />");
            }

            GeoLocation geoLocation = new GeoLocation(mOptions.getPlacemarkLat(), mOptions.getPlacemarkLon());
            if (gpsDirectory != null) {
                geoLocation = gpsDirectory.getGeoLocation();
                if (geoLocation == null) {
                    throw new ImageProcessingException(sourceFile.getAbsolutePath());
                }

                if (geoLocation.getLatitude() == 0 && geoLocation.getLongitude() == 0) {
                    geoLocation = new GeoLocation(mOptions.getPlacemarkLat(), mOptions.getPlacemarkLon());
                    gpsDirectory = null;
                }

                if (exifDate != null) {
                    if (mOptions.isDescriptionCoordinate()) {
                        descriptionBuilder.append(gpsDescriptor.getDegreesMinutesSecondsDescription())
                                .append("<br />");
                    }

                    if (mOptions.isDescriptionAltitude()) {
                        descriptionBuilder.append(gpsDescriptor.getGpsAltitudeDescription()).append("<br />");
                    }

                    if (mOptions.isDescriptionBearing()) {
                        String bearing = gpsDescriptor.getGpsDirectionDescription(GpsDirectory.TAG_DEST_BEARING);
                        if (bearing != null) {
                            descriptionBuilder.append(bearing).append("<br />");
                        }
                    }
                }
            }

            if (mOptions.isDescriptionCustom()) {
                descriptionBuilder.append(mOptions.getDescriptionCustomText()).append("<br />");
            }

            //            descriptionBuilder.append("]]>");
            boolean shouldAppendToKml = gpsDirectory != null || mOptions.isPlacemarkIncludeNullCoordinate();

            if (shouldAppendToKml) {
                double format = 1000000;
                int latInt = (int) (geoLocation.getLatitude() * format);
                int lonInt = (int) (geoLocation.getLongitude() * format);

                getFolder(sourceFile, exifDate).createAndAddPlacemark().withName(name).withOpen(Boolean.TRUE)
                        .withDescription(descriptionBuilder.toString()).createAndSetPoint()
                        .addToCoordinates(lonInt / format, latInt / format);
                logSuccess(shouldAppendToKml, sourceFile);
            } else {
                mFilesWithErrors.add(sourceFile);
            }
        }

        private void saveToFile() {
            List keys = new ArrayList(mFolders.keySet());
            Collections.sort(keys);

            keys.stream().forEach((key) -> {
                mRootFolder.getFeature().add(mFolders.get((String) key));
            });

            mListener.onOperationLog(
                    "\n" + String.format(Dict.SAVING.getString(), mDestinationFile.getAbsolutePath()));

            try {
                mKml.marshal(mDestinationFile);
                mKml = null;

                long millis = System.currentTimeMillis() - mStartTime;
                long min = TimeUnit.MILLISECONDS.toMinutes(millis);
                long sec = TimeUnit.MILLISECONDS.toSeconds(millis)
                        - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(millis));

                String status = String.format(mBundle.getString("operationSummary"), mNumOfGps, min,
                        Dict.TIME_MIN.getString(), sec, Dict.TIME_SEC.getString());
                if (!mOptions.isPhotoBaseUrl()) {
                    status = status + "\n\n" + mBundle.getString("operationNote");
                }

                mListener.onOperationLog("\n" + status);
                mListener.onOperationFinished(status);
            } catch (FileNotFoundException ex) {
                mListener.onOperationLog(ex.getLocalizedMessage());
            }
        }
    }
}