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