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;
019
020import org.openstreetmap.josm.data.StructUtils;
021import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry;
022import org.openstreetmap.josm.gui.PleaseWaitRunnable;
023import org.openstreetmap.josm.io.CachedFile;
024import org.openstreetmap.josm.io.OfflineAccessException;
025import org.openstreetmap.josm.io.OnlineResource;
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 = true;
162            try {
163                OnlineResource.JOSM_WEBSITE.checkOfflineAccess(source, Config.getUrls().getJOSMWebsite());
164            } catch (OfflineAccessException e) {
165                Logging.log(Logging.LEVEL_WARN, e);
166                online = false;
167            }
168            if (clearCache && online) {
169                CachedFile.cleanup(source);
170            }
171            try {
172                reader = new ImageryReader(source);
173                reader.setFastFail(fastFail);
174                Collection<ImageryInfo> result = reader.parse();
175                newLayers.addAll(result);
176            } catch (IOException ex) {
177                loadError = true;
178                Logging.log(Logging.LEVEL_ERROR, ex);
179            } catch (SAXException ex) {
180                loadError = true;
181                Logging.error(ex);
182            }
183        }
184
185        @Override
186        protected void finish() {
187            defaultLayers.clear();
188            allDefaultLayers.clear();
189            defaultLayers.addAll(newLayers);
190            for (ImageryInfo layer : newLayers) {
191                allDefaultLayers.add(layer);
192                for (ImageryInfo sublayer : layer.getMirrors()) {
193                    allDefaultLayers.add(sublayer);
194                }
195            }
196            defaultLayerIds.clear();
197            Collections.sort(defaultLayers);
198            Collections.sort(allDefaultLayers);
199            buildIdMap(allDefaultLayers, defaultLayerIds);
200            updateEntriesFromDefaults(!loadError);
201            buildIdMap(layers, layerIds);
202            if (!loadError && !defaultLayerIds.isEmpty()) {
203                dropOldEntries();
204            }
205        }
206    }
207
208    /**
209     * Build the mapping of unique ids to {@link ImageryInfo}s.
210     * @param lst input list
211     * @param idMap output map
212     */
213    private static void buildIdMap(List<ImageryInfo> lst, Map<String, ImageryInfo> idMap) {
214        idMap.clear();
215        Set<String> notUnique = new HashSet<>();
216        for (ImageryInfo i : lst) {
217            if (i.getId() != null) {
218                if (idMap.containsKey(i.getId())) {
219                    notUnique.add(i.getId());
220                    Logging.error("Id ''{0}'' is not unique - used by ''{1}'' and ''{2}''!",
221                            i.getId(), i.getName(), idMap.get(i.getId()).getName());
222                    continue;
223                }
224                idMap.put(i.getId(), i);
225                Collection<String> old = i.getOldIds();
226                if (old != null) {
227                    for (String id : old) {
228                        if (idMap.containsKey(id)) {
229                            Logging.error("Old Id ''{0}'' is not unique - used by ''{1}'' and ''{2}''!",
230                                    i.getId(), i.getName(), idMap.get(i.getId()).getName());
231                        } else {
232                            idMap.put(id, i);
233                        }
234                    }
235                }
236            }
237        }
238        for (String i : notUnique) {
239            idMap.remove(i);
240        }
241    }
242
243    /**
244     * Update user entries according to the list of default entries.
245     * @param dropold if <code>true</code> old entries should be removed
246     * @since 11706
247     */
248    public void updateEntriesFromDefaults(boolean dropold) {
249        // add new default entries to the user selection
250        boolean changed = false;
251        Collection<String> knownDefaults = new TreeSet<>(Config.getPref().getList("imagery.layers.default"));
252        Collection<String> newKnownDefaults = new TreeSet<>();
253        for (ImageryInfo def : defaultLayers) {
254            if (def.isDefaultEntry()) {
255                boolean isKnownDefault = false;
256                for (String entry : knownDefaults) {
257                    if (entry.equals(def.getId())) {
258                        isKnownDefault = true;
259                        newKnownDefaults.add(entry);
260                        knownDefaults.remove(entry);
261                        break;
262                    } else if (isSimilar(entry, def.getUrl())) {
263                        isKnownDefault = true;
264                        if (def.getId() != null) {
265                            newKnownDefaults.add(def.getId());
266                        }
267                        knownDefaults.remove(entry);
268                        break;
269                    }
270                }
271                boolean isInUserList = false;
272                if (!isKnownDefault) {
273                    if (def.getId() != null) {
274                        newKnownDefaults.add(def.getId());
275                        for (ImageryInfo i : layers) {
276                            if (isSimilar(def, i)) {
277                                isInUserList = true;
278                                break;
279                            }
280                        }
281                    } else {
282                        Logging.error("Default imagery ''{0}'' has no id. Skipping.", def.getName());
283                    }
284                }
285                if (!isKnownDefault && !isInUserList) {
286                    add(new ImageryInfo(def));
287                    changed = true;
288                }
289            }
290        }
291        if (!dropold && !knownDefaults.isEmpty()) {
292            newKnownDefaults.addAll(knownDefaults);
293        }
294        Config.getPref().putList("imagery.layers.default", new ArrayList<>(newKnownDefaults));
295
296        // automatically update user entries with same id as a default entry
297        for (int i = 0; i < layers.size(); i++) {
298            ImageryInfo info = layers.get(i);
299            if (info.getId() == null) {
300                continue;
301            }
302            ImageryInfo matchingDefault = defaultLayerIds.get(info.getId());
303            if (matchingDefault != null && !matchingDefault.equalsPref(info)) {
304                layers.set(i, matchingDefault);
305                Logging.info(tr("Update imagery ''{0}''", info.getName()));
306                changed = true;
307            }
308        }
309
310        if (changed) {
311            save();
312        }
313    }
314
315    /**
316     * Drop entries with Id which do no longer exist (removed from defaults).
317     * @since 11527
318     */
319    public void dropOldEntries() {
320        List<String> drop = new ArrayList<>();
321
322        for (Map.Entry<String, ImageryInfo> info : layerIds.entrySet()) {
323            if (!defaultLayerIds.containsKey(info.getKey())) {
324                remove(info.getValue());
325                drop.add(info.getKey());
326                Logging.info(tr("Drop old imagery ''{0}''", info.getValue().getName()));
327            }
328        }
329
330        if (!drop.isEmpty()) {
331            for (String id : drop) {
332                layerIds.remove(id);
333            }
334            save();
335        }
336    }
337
338    private static boolean isSimilar(ImageryInfo iiA, ImageryInfo iiB) {
339        if (iiA == null || iiA.getImageryType() != iiB.getImageryType())
340            return false;
341        if (iiA.getId() != null && iiB.getId() != null)
342            return iiA.getId().equals(iiB.getId());
343        return isSimilar(iiA.getUrl(), iiB.getUrl());
344    }
345
346    // some additional checks to respect extended URLs in preferences (legacy workaround)
347    private static boolean isSimilar(String a, String b) {
348        return Objects.equals(a, b) || (a != null && b != null && !a.isEmpty() && !b.isEmpty() && (a.contains(b) || b.contains(a)));
349    }
350
351    /**
352     * Add a new imagery entry.
353     * @param info imagery entry to add
354     */
355    public void add(ImageryInfo info) {
356        layers.add(info);
357    }
358
359    /**
360     * Remove an imagery entry.
361     * @param info imagery entry to remove
362     */
363    public void remove(ImageryInfo info) {
364        layers.remove(info);
365    }
366
367    /**
368     * Save the list of imagery entries to preferences.
369     */
370    public void save() {
371        List<ImageryPreferenceEntry> entries = new ArrayList<>();
372        for (ImageryInfo info : layers) {
373            entries.add(new ImageryPreferenceEntry(info));
374        }
375        StructUtils.putListOfStructs(Config.getPref(), "imagery.entries", entries, ImageryPreferenceEntry.class);
376    }
377
378    /**
379     * List of usable layers
380     * @return unmodifiable list containing usable layers
381     */
382    public List<ImageryInfo> getLayers() {
383        return Collections.unmodifiableList(layers);
384    }
385
386    /**
387     * List of available default layers
388     * @return unmodifiable list containing available default layers
389     */
390    public List<ImageryInfo> getDefaultLayers() {
391        return Collections.unmodifiableList(defaultLayers);
392    }
393
394    /**
395     * List of all available default layers (including mirrors)
396     * @return unmodifiable list containing available default layers
397     * @since 11570
398     */
399    public List<ImageryInfo> getAllDefaultLayers() {
400        return Collections.unmodifiableList(allDefaultLayers);
401    }
402
403    public static void addLayer(ImageryInfo info) {
404        instance.add(info);
405        instance.save();
406    }
407
408    public static void addLayers(Collection<ImageryInfo> infos) {
409        for (ImageryInfo i : infos) {
410            instance.add(i);
411        }
412        instance.save();
413        Collections.sort(instance.layers);
414    }
415
416    /**
417     * Get unique id for ImageryInfo.
418     *
419     * This takes care, that no id is used twice (due to a user error)
420     * @param info the ImageryInfo to look up
421     * @return null, if there is no id or the id is used twice,
422     * the corresponding id otherwise
423     */
424    public String getUniqueId(ImageryInfo info) {
425        if (info.getId() != null && layerIds.get(info.getId()) == info) {
426            return info.getId();
427        }
428        return null;
429    }
430
431    /**
432     * Returns imagery layer info for the given id.
433     * @param id imagery layer id.
434     * @return imagery layer info for the given id, or {@code null}
435     * @since 13797
436     */
437    public ImageryInfo getLayer(String id) {
438        return layerIds.get(id);
439    }
440}