001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.imagery;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.IOException;
007import java.util.ArrayList;
008import java.util.Arrays;
009import java.util.Collection;
010import java.util.Collections;
011import java.util.HashMap;
012import java.util.HashSet;
013import java.util.List;
014import java.util.Map;
015import java.util.Objects;
016import java.util.Set;
017import java.util.TreeSet;
018import java.util.concurrent.ExecutorService;
019import java.util.stream.Collectors;
020
021import org.openstreetmap.josm.data.StructUtils;
022import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry;
023import org.openstreetmap.josm.gui.PleaseWaitRunnable;
024import org.openstreetmap.josm.io.CachedFile;
025import org.openstreetmap.josm.io.NetworkManager;
026import org.openstreetmap.josm.io.imagery.ImageryReader;
027import org.openstreetmap.josm.spi.preferences.Config;
028import org.openstreetmap.josm.tools.Logging;
029import org.openstreetmap.josm.tools.Utils;
030import org.xml.sax.SAXException;
031
032/**
033 * Manages the list of imagery entries that are shown in the imagery menu.
034 */
035public class ImageryLayerInfo {
036
037    /** Unique instance */
038    public static final ImageryLayerInfo instance = new ImageryLayerInfo();
039    /** List of all usable layers */
040    private final List<ImageryInfo> layers = new ArrayList<>();
041    /** List of layer ids of all usable layers */
042    private final Map<String, ImageryInfo> layerIds = new HashMap<>();
043    /** List of all available default layers */
044    static final List<ImageryInfo> defaultLayers = new ArrayList<>();
045    /** List of all available default layers (including mirrors) */
046    static final List<ImageryInfo> allDefaultLayers = new ArrayList<>();
047    /** List of all layer ids of available default layers (including mirrors) */
048    static final Map<String, ImageryInfo> defaultLayerIds = new HashMap<>();
049
050    private static final String[] DEFAULT_LAYER_SITES = {
051            Config.getUrls().getJOSMWebsite()+"/maps%<?ids=>"
052    };
053
054    /**
055     * Returns the list of imagery layers sites.
056     * @return the list of imagery layers sites
057     * @since 7434
058     */
059    public static Collection<String> getImageryLayersSites() {
060        return Config.getPref().getList("imagery.layers.sites", Arrays.asList(DEFAULT_LAYER_SITES));
061    }
062
063    private ImageryLayerInfo() {
064    }
065
066    /**
067     * Constructs a new {@code ImageryLayerInfo} from an existing one.
068     * @param info info to copy
069     */
070    public ImageryLayerInfo(ImageryLayerInfo info) {
071        layers.addAll(info.layers);
072    }
073
074    /**
075     * Clear the lists of layers.
076     */
077    public void clear() {
078        layers.clear();
079        layerIds.clear();
080    }
081
082    /**
083     * Loads the custom as well as default imagery entries.
084     * @param fastFail whether opening HTTP connections should fail fast, see {@link ImageryReader#setFastFail(boolean)}
085     */
086    public void load(boolean fastFail) {
087        clear();
088        List<ImageryPreferenceEntry> entries = StructUtils.getListOfStructs(
089                Config.getPref(), "imagery.entries", null, ImageryPreferenceEntry.class);
090        if (entries != null) {
091            for (ImageryPreferenceEntry prefEntry : entries) {
092                try {
093                    ImageryInfo i = new ImageryInfo(prefEntry);
094                    add(i);
095                } catch (IllegalArgumentException e) {
096                    Logging.warn("Unable to load imagery preference entry:"+e);
097                }
098            }
099            Collections.sort(layers);
100        }
101        loadDefaults(false, null, fastFail);
102    }
103
104    /**
105     * Loads the available imagery entries.
106     *
107     * The data is downloaded from the JOSM website (or loaded from cache).
108     * Entries marked as "default" are added to the user selection, if not already present.
109     *
110     * @param clearCache if true, clear the cache and start a fresh download.
111     * @param worker executor service which will perform the loading.
112     * If null, it should be performed using a {@link PleaseWaitRunnable} in the background
113     * @param fastFail whether opening HTTP connections should fail fast, see {@link ImageryReader#setFastFail(boolean)}
114     * @since 12634
115     */
116    public void loadDefaults(boolean clearCache, ExecutorService worker, boolean fastFail) {
117        final DefaultEntryLoader loader = new DefaultEntryLoader(clearCache, fastFail);
118        if (worker == null) {
119            loader.realRun();
120            loader.finish();
121        } else {
122            worker.execute(loader);
123        }
124    }
125
126    /**
127     * Loader/updater of the available imagery entries
128     */
129    class DefaultEntryLoader extends PleaseWaitRunnable {
130
131        private final boolean clearCache;
132        private final boolean fastFail;
133        private final List<ImageryInfo> newLayers = new ArrayList<>();
134        private ImageryReader reader;
135        private boolean canceled;
136        private boolean loadError;
137
138        DefaultEntryLoader(boolean clearCache, boolean fastFail) {
139            super(tr("Update default entries"));
140            this.clearCache = clearCache;
141            this.fastFail = fastFail;
142        }
143
144        @Override
145        protected void cancel() {
146            canceled = true;
147            Utils.close(reader);
148        }
149
150        @Override
151        protected void realRun() {
152            for (String source : getImageryLayersSites()) {
153                if (canceled) {
154                    return;
155                }
156                loadSource(source);
157            }
158        }
159
160        protected void loadSource(String source) {
161            boolean online = !NetworkManager.isOffline(source);
162            if (clearCache && online) {
163                CachedFile.cleanup(source);
164            }
165            try {
166                reader = new ImageryReader(source);
167                reader.setFastFail(fastFail);
168                Collection<ImageryInfo> result = reader.parse();
169                newLayers.addAll(result);
170            } catch (IOException ex) {
171                loadError = true;
172                Logging.log(Logging.LEVEL_ERROR, ex);
173            } catch (SAXException ex) {
174                loadError = true;
175                Logging.error(ex);
176            }
177        }
178
179        @Override
180        protected void finish() {
181            defaultLayers.clear();
182            allDefaultLayers.clear();
183            defaultLayers.addAll(newLayers);
184            for (ImageryInfo layer : newLayers) {
185                allDefaultLayers.add(layer);
186                allDefaultLayers.addAll(layer.getMirrors());
187            }
188            defaultLayerIds.clear();
189            Collections.sort(defaultLayers);
190            Collections.sort(allDefaultLayers);
191            buildIdMap(allDefaultLayers, defaultLayerIds);
192            updateEntriesFromDefaults(!loadError);
193            buildIdMap(layers, layerIds);
194            if (!loadError && !defaultLayerIds.isEmpty()) {
195                dropOldEntries();
196            }
197        }
198    }
199
200    /**
201     * Build the mapping of unique ids to {@link ImageryInfo}s.
202     * @param lst input list
203     * @param idMap output map
204     */
205    private static void buildIdMap(List<ImageryInfo> lst, Map<String, ImageryInfo> idMap) {
206        idMap.clear();
207        Set<String> notUnique = new HashSet<>();
208        for (ImageryInfo i : lst) {
209            if (i.getId() != null) {
210                if (idMap.containsKey(i.getId())) {
211                    notUnique.add(i.getId());
212                    Logging.error("Id ''{0}'' is not unique - used by ''{1}'' and ''{2}''!",
213                            i.getId(), i.getName(), idMap.get(i.getId()).getName());
214                    continue;
215                }
216                idMap.put(i.getId(), i);
217                Collection<String> old = i.getOldIds();
218                if (old != null) {
219                    for (String id : old) {
220                        if (idMap.containsKey(id)) {
221                            Logging.error("Old Id ''{0}'' is not unique - used by ''{1}'' and ''{2}''!",
222                                    i.getId(), i.getName(), idMap.get(i.getId()).getName());
223                        } else {
224                            idMap.put(id, i);
225                        }
226                    }
227                }
228            }
229        }
230        for (String i : notUnique) {
231            idMap.remove(i);
232        }
233    }
234
235    /**
236     * Update user entries according to the list of default entries.
237     * @param dropold if <code>true</code> old entries should be removed
238     * @since 11706
239     */
240    public void updateEntriesFromDefaults(boolean dropold) {
241        // add new default entries to the user selection
242        boolean changed = false;
243        Collection<String> knownDefaults = new TreeSet<>(Config.getPref().getList("imagery.layers.default"));
244        Collection<String> newKnownDefaults = new TreeSet<>();
245        for (ImageryInfo def : defaultLayers) {
246            if (def.isDefaultEntry()) {
247                boolean isKnownDefault = false;
248                for (String entry : knownDefaults) {
249                    if (entry.equals(def.getId())) {
250                        isKnownDefault = true;
251                        newKnownDefaults.add(entry);
252                        knownDefaults.remove(entry);
253                        break;
254                    } else if (isSimilar(entry, def.getUrl())) {
255                        isKnownDefault = true;
256                        if (def.getId() != null) {
257                            newKnownDefaults.add(def.getId());
258                        }
259                        knownDefaults.remove(entry);
260                        break;
261                    }
262                }
263                boolean isInUserList = false;
264                if (!isKnownDefault) {
265                    if (def.getId() != null) {
266                        newKnownDefaults.add(def.getId());
267                        isInUserList = layers.stream().anyMatch(i -> isSimilar(def, i));
268                    } else {
269                        Logging.error("Default imagery ''{0}'' has no id. Skipping.", def.getName());
270                    }
271                }
272                if (!isKnownDefault && !isInUserList) {
273                    add(new ImageryInfo(def));
274                    changed = true;
275                }
276            }
277        }
278        if (!dropold && !knownDefaults.isEmpty()) {
279            newKnownDefaults.addAll(knownDefaults);
280        }
281        Config.getPref().putList("imagery.layers.default", new ArrayList<>(newKnownDefaults));
282
283        // automatically update user entries with same id as a default entry
284        for (int i = 0; i < layers.size(); i++) {
285            ImageryInfo info = layers.get(i);
286            if (info.getId() == null) {
287                continue;
288            }
289            ImageryInfo matchingDefault = defaultLayerIds.get(info.getId());
290            if (matchingDefault != null && !matchingDefault.equalsPref(info)) {
291                layers.set(i, matchingDefault);
292                Logging.info(tr("Update imagery ''{0}''", info.getName()));
293                changed = true;
294            }
295        }
296
297        if (changed) {
298            save();
299        }
300    }
301
302    /**
303     * Drop entries with Id which do no longer exist (removed from defaults).
304     * @since 11527
305     */
306    public void dropOldEntries() {
307        List<String> drop = new ArrayList<>();
308
309        for (Map.Entry<String, ImageryInfo> info : layerIds.entrySet()) {
310            if (!defaultLayerIds.containsKey(info.getKey())) {
311                remove(info.getValue());
312                drop.add(info.getKey());
313                Logging.info(tr("Drop old imagery ''{0}''", info.getValue().getName()));
314            }
315        }
316
317        if (!drop.isEmpty()) {
318            for (String id : drop) {
319                layerIds.remove(id);
320            }
321            save();
322        }
323    }
324
325    private static boolean isSimilar(ImageryInfo iiA, ImageryInfo iiB) {
326        if (iiA == null || iiA.getImageryType() != iiB.getImageryType())
327            return false;
328        if (iiA.getId() != null && iiB.getId() != null)
329            return iiA.getId().equals(iiB.getId());
330        return isSimilar(iiA.getUrl(), iiB.getUrl());
331    }
332
333    // some additional checks to respect extended URLs in preferences (legacy workaround)
334    private static boolean isSimilar(String a, String b) {
335        return Objects.equals(a, b) || (!Utils.isEmpty(a) && !Utils.isEmpty(b) && (a.contains(b) || b.contains(a)));
336    }
337
338    /**
339     * Add a new imagery entry.
340     * @param info imagery entry to add
341     */
342    public void add(ImageryInfo info) {
343        layers.add(info);
344    }
345
346    /**
347     * Remove an imagery entry.
348     * @param info imagery entry to remove
349     */
350    public void remove(ImageryInfo info) {
351        layers.remove(info);
352    }
353
354    /**
355     * Save the list of imagery entries to preferences.
356     */
357    public void save() {
358        List<ImageryPreferenceEntry> entries = layers.stream()
359                .map(ImageryPreferenceEntry::new)
360                .collect(Collectors.toList());
361        StructUtils.putListOfStructs(Config.getPref(), "imagery.entries", entries, ImageryPreferenceEntry.class);
362    }
363
364    /**
365     * List of usable layers
366     * @return unmodifiable list containing usable layers
367     */
368    public List<ImageryInfo> getLayers() {
369        return Collections.unmodifiableList(layers);
370    }
371
372    /**
373     * List of available default layers
374     * @return unmodifiable list containing available default layers
375     */
376    public List<ImageryInfo> getDefaultLayers() {
377        return Collections.unmodifiableList(defaultLayers);
378    }
379
380    /**
381     * List of all available default layers (including mirrors)
382     * @return unmodifiable list containing available default layers
383     * @since 11570
384     */
385    public List<ImageryInfo> getAllDefaultLayers() {
386        return Collections.unmodifiableList(allDefaultLayers);
387    }
388
389    public static void addLayer(ImageryInfo info) {
390        instance.add(info);
391        instance.save();
392    }
393
394    public static void addLayers(Collection<ImageryInfo> infos) {
395        for (ImageryInfo i : infos) {
396            instance.add(i);
397        }
398        instance.save();
399        Collections.sort(instance.layers);
400    }
401
402    /**
403     * Get unique id for ImageryInfo.
404     *
405     * This takes care, that no id is used twice (due to a user error)
406     * @param info the ImageryInfo to look up
407     * @return null, if there is no id or the id is used twice,
408     * the corresponding id otherwise
409     */
410    public String getUniqueId(ImageryInfo info) {
411        if (info.getId() != null && layerIds.get(info.getId()) == info) {
412            return info.getId();
413        }
414        return null;
415    }
416
417    /**
418     * Returns imagery layer info for the given id.
419     * @param id imagery layer id.
420     * @return imagery layer info for the given id, or {@code null}
421     * @since 13797
422     */
423    public ImageryInfo getLayer(String id) {
424        return layerIds.get(id);
425    }
426}