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}