001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.layer.geoimage; 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.Collection; 011import java.util.HashSet; 012import java.util.LinkedHashSet; 013import java.util.List; 014import java.util.Map; 015import java.util.Set; 016import java.util.TreeMap; 017import java.util.stream.Collectors; 018 019import javax.swing.JOptionPane; 020 021import org.openstreetmap.josm.data.preferences.BooleanProperty; 022import org.openstreetmap.josm.gui.MainApplication; 023import org.openstreetmap.josm.gui.PleaseWaitRunnable; 024import org.openstreetmap.josm.gui.io.importexport.ImageImporter; 025import org.openstreetmap.josm.gui.layer.GpxLayer; 026import org.openstreetmap.josm.tools.Logging; 027import org.openstreetmap.josm.tools.Utils; 028 029/** 030 * Loads a set of images, while displaying a dialog that indicates what the plugin is currently doing. 031 * In facts, this object is instantiated with a list of files. These files may be JPEG files or 032 * directories. In case of directories, they are scanned to find all the images they contain. 033 * Then all the images that have be found are loaded as ImageEntry instances. 034 * 035 * @since 18035 (extracted from GeoImageLayer) 036 */ 037final class ImagesLoader extends PleaseWaitRunnable { 038 039 private boolean canceled; 040 private final List<GeoImageLayer> layers = new ArrayList<>(); 041 private final Collection<File> selection; 042 private final Set<String> loadedDirectories = new HashSet<>(); 043 private final Set<String> errorMessages; 044 private final GpxLayer gpxLayer; 045 046 private static final BooleanProperty PROP_ONE_LAYER_PER_FOLDER = new BooleanProperty("geoimage.one-layer-per-folder", false); 047 048 /** 049 * Constructs a new {@code ImagesLoader}. 050 * @param selection image files to load 051 * @param gpxLayer associated GPX layer 052 */ 053 ImagesLoader(Collection<File> selection, GpxLayer gpxLayer) { 054 super(tr("Extracting GPS locations from EXIF")); 055 this.selection = selection; 056 this.gpxLayer = gpxLayer; 057 errorMessages = new LinkedHashSet<>(); 058 } 059 060 private void rememberError(String message) { 061 this.errorMessages.add(message); 062 } 063 064 @Override 065 protected void realRun() throws IOException { 066 progressMonitor.subTask(tr("Starting directory scan")); 067 Collection<File> files = new ArrayList<>(); 068 try { 069 addRecursiveFiles(files, selection); 070 } catch (IllegalStateException e) { 071 Logging.debug(e); 072 rememberError(e.getMessage()); 073 } 074 075 if (canceled) 076 return; 077 progressMonitor.subTask(tr("Read photos...")); 078 progressMonitor.setTicksCount(files.size()); 079 080 // read the image files 081 Map<String, List<ImageEntry>> entries = new TreeMap<>(); 082 083 for (File f : files) { 084 085 if (canceled) { 086 break; 087 } 088 089 progressMonitor.subTask(tr("Reading {0}...", f.getName())); 090 progressMonitor.worked(1); 091 092 ImageEntry e = new ImageEntry(f); 093 e.extractExif(); 094 File parentFile = f.getParentFile(); 095 entries.computeIfAbsent(parentFile != null ? parentFile.getName() : "", x -> new ArrayList<>()).add(e); 096 } 097 if (Boolean.TRUE.equals(PROP_ONE_LAYER_PER_FOLDER.get())) { 098 entries.entrySet().stream().map(e -> new GeoImageLayer(e.getValue(), gpxLayer, e.getKey())).forEach(layers::add); 099 } else { 100 layers.add(new GeoImageLayer(entries.values().stream().flatMap(List<ImageEntry>::stream).collect(Collectors.toList()), gpxLayer)); 101 } 102 files.clear(); 103 } 104 105 private void addRecursiveFiles(Collection<File> files, Collection<File> sel) { 106 boolean nullFile = false; 107 108 for (File f : sel) { 109 110 if (canceled) { 111 break; 112 } 113 114 if (f == null) { 115 nullFile = true; 116 117 } else if (f.isDirectory()) { 118 String canonical = null; 119 try { 120 canonical = f.getCanonicalPath(); 121 } catch (IOException e) { 122 Logging.error(e); 123 rememberError(tr("Unable to get canonical path for directory {0}\n", 124 f.getAbsolutePath())); 125 } 126 127 if (canonical == null || loadedDirectories.contains(canonical)) { 128 continue; 129 } else { 130 loadedDirectories.add(canonical); 131 } 132 133 File[] children = f.listFiles(ImageImporter.FILE_FILTER_WITH_FOLDERS); 134 if (children != null) { 135 progressMonitor.subTask(tr("Scanning directory {0}", f.getPath())); 136 addRecursiveFiles(files, Arrays.asList(children)); 137 } else { 138 rememberError(tr("Error while getting files from directory {0}\n", f.getPath())); 139 } 140 141 } else { 142 files.add(f); 143 } 144 } 145 146 if (nullFile) { 147 throw new IllegalStateException(tr("One of the selected files was null")); 148 } 149 } 150 151 private String formatErrorMessages() { 152 StringBuilder sb = new StringBuilder(); 153 sb.append("<html>"); 154 if (errorMessages.size() == 1) { 155 sb.append(Utils.escapeReservedCharactersHTML(errorMessages.iterator().next())); 156 } else { 157 sb.append(Utils.joinAsHtmlUnorderedList(errorMessages)); 158 } 159 sb.append("</html>"); 160 return sb.toString(); 161 } 162 163 @Override 164 protected void finish() { 165 if (!errorMessages.isEmpty()) { 166 JOptionPane.showMessageDialog( 167 MainApplication.getMainFrame(), 168 formatErrorMessages(), 169 tr("Error"), 170 JOptionPane.ERROR_MESSAGE 171 ); 172 } 173 for (GeoImageLayer layer : layers) { 174 MainApplication.getLayerManager().addLayer(layer); 175 176 if (!canceled && !layer.getImageData().getImages().isEmpty()) { 177 boolean noGeotagFound = true; 178 for (ImageEntry e : layer.getImageData().getImages()) { 179 if (e.getPos() != null) { 180 noGeotagFound = false; 181 } 182 } 183 if (noGeotagFound) { 184 new CorrelateGpxWithImages(layer).actionPerformed(null); 185 } 186 } 187 } 188 } 189 190 @Override 191 protected void cancel() { 192 canceled = true; 193 } 194}