001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.io.importexport;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.File;
007import java.io.IOException;
008import java.util.ArrayList;
009import java.util.Arrays;
010import java.util.Collections;
011import java.util.EnumSet;
012import java.util.HashSet;
013import java.util.List;
014import java.util.Set;
015import java.util.regex.Matcher;
016import java.util.regex.Pattern;
017import java.util.stream.Collectors;
018
019import javax.imageio.ImageIO;
020
021import org.openstreetmap.josm.actions.ExtensionFileFilter;
022import org.openstreetmap.josm.gui.layer.GpxLayer;
023import org.openstreetmap.josm.gui.layer.geoimage.GeoImageLayer;
024import org.openstreetmap.josm.gui.progress.ProgressMonitor;
025import org.openstreetmap.josm.io.CachedFile;
026import org.openstreetmap.josm.io.IllegalDataException;
027
028/**
029 * File importer allowing to import geotagged images
030 * @since 17548
031 */
032public class ImageImporter extends FileImporter {
033
034    /** Check if the filename starts with a borked path ({@link java.io.File#File} drops consecutive {@code /} characters). */
035    private static final Pattern URL_START_BAD = Pattern.compile("^(https?:/)([^/].*)$");
036    /** Check for the beginning of a "good" url */
037    private static final Pattern URL_START_GOOD = Pattern.compile("^https?://.*$");
038
039    private GpxLayer gpx;
040
041    /**
042     * The supported image file types on the current system
043     */
044    public static final List<String> SUPPORTED_FILE_TYPES = Collections
045            .unmodifiableList(Arrays.stream(ImageIO.getReaderFileSuffixes()).sorted().collect(Collectors.toList()));
046
047    /**
048     * The default file filter
049     */
050    public static final ExtensionFileFilter FILE_FILTER = getFileFilters(false);
051
052    /**
053     * An alternate file filter that also includes folders.
054     */
055    public static final ExtensionFileFilter FILE_FILTER_WITH_FOLDERS = getFileFilters(true);
056
057    private static ExtensionFileFilter getFileFilters(boolean folder) {
058        String typeStr = String.join(",", SUPPORTED_FILE_TYPES);
059        String humanStr = tr("Image Files") + " (*." + String.join(", *.", SUPPORTED_FILE_TYPES);
060        if (folder) {
061            humanStr += ", " + tr("folder");
062        }
063        humanStr += ")";
064
065        return new ExtensionFileFilter(typeStr, "jpg", humanStr);
066    }
067
068    /**
069     * Constructs a new {@code ImageImporter}.
070     */
071    public ImageImporter() {
072        this(false);
073    }
074
075    /**
076     * Constructs a new {@code ImageImporter} with folders selection, if wanted.
077     * @param includeFolders If true, includes folders in the file filter
078     */
079    public ImageImporter(boolean includeFolders) {
080        super(includeFolders ? FILE_FILTER_WITH_FOLDERS : FILE_FILTER);
081    }
082
083    /**
084     * Constructs a new {@code ImageImporter} for the given GPX layer. Folders selection is allowed.
085     * @param gpx The GPX layer
086     */
087    public ImageImporter(GpxLayer gpx) {
088        this(true);
089        this.gpx = gpx;
090    }
091
092    @Override
093    public boolean acceptFile(File pathname) {
094        return super.acceptFile(pathname) || pathname.isDirectory();
095    }
096
097    @Override
098    public void importData(List<File> sel, ProgressMonitor progressMonitor) throws IOException, IllegalDataException {
099        progressMonitor.beginTask(tr("Looking for image files"), 1);
100        try {
101            List<File> files = new ArrayList<>();
102            Set<String> visitedDirs = new HashSet<>();
103            addRecursiveFiles(this.options, files, visitedDirs, sel, progressMonitor.createSubTaskMonitor(1, true));
104
105            if (progressMonitor.isCanceled())
106                return;
107
108            if (files.isEmpty())
109                throw new IOException(tr("No image files found."));
110
111            GeoImageLayer.create(files, gpx);
112        } finally {
113            progressMonitor.finishTask();
114        }
115    }
116
117    static void addRecursiveFiles(List<File> files, Set<String> visitedDirs, List<File> sel, ProgressMonitor progressMonitor)
118            throws IOException {
119        addRecursiveFiles(EnumSet.noneOf(Options.class), files, visitedDirs, sel, progressMonitor);
120    }
121
122    static void addRecursiveFiles(Set<Options> options, List<File> files, Set<String> visitedDirs, List<File> sel,
123            ProgressMonitor progressMonitor) throws IOException {
124        if (progressMonitor.isCanceled())
125            return;
126
127        progressMonitor.beginTask(null, sel.size());
128        try {
129            for (File f : sel) {
130                if (f.isDirectory()) {
131                    if (visitedDirs.add(f.getCanonicalPath())) { // Do not loop over symlinks
132                        File[] dirFiles = f.listFiles(); // Can be null for some strange directories (like lost+found)
133                        if (dirFiles != null) {
134                            addRecursiveFiles(options, files, visitedDirs, Arrays.asList(dirFiles),
135                                    progressMonitor.createSubTaskMonitor(1, true));
136                        }
137                    } else {
138                        progressMonitor.worked(1);
139                    }
140                } else {
141                    /* Check if the path is a web path, and if so, ensure that it is "correct" */
142                    final String path = f.getPath();
143                    Matcher matcherBad = URL_START_BAD.matcher(path);
144                    final String realPath;
145                    if (matcherBad.matches()) {
146                        realPath = matcherBad.replaceFirst(matcherBad.group(1) + "/" + matcherBad.group(2));
147                    } else {
148                        realPath = path;
149                    }
150                    if (URL_START_GOOD.matcher(realPath).matches() && FILE_FILTER.accept(f)
151                            && options.contains(Options.ALLOW_WEB_RESOURCES)) {
152                        try (CachedFile cachedFile = new CachedFile(realPath)) {
153                            files.add(cachedFile.getFile());
154                        }
155                    } else if (FILE_FILTER.accept(f)) {
156                        files.add(f);
157                    }
158                    progressMonitor.worked(1);
159                }
160            }
161        } finally {
162            progressMonitor.finishTask();
163        }
164    }
165
166    @Override
167    public boolean isBatchImporter() {
168        return true;
169    }
170
171    /**
172     * Needs to be the last, to avoid problems.
173     */
174    @Override
175    public double getPriority() {
176        return -1000;
177    }
178}