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}