001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.imagery;
003
004import java.io.IOException;
005import java.util.ArrayList;
006import java.util.Arrays;
007import java.util.Collection;
008import java.util.Collections;
009import java.util.HashMap;
010import java.util.HashSet;
011import java.util.List;
012import java.util.Map;
013import java.util.Objects;
014import java.util.Set;
015import java.util.TreeSet;
016
017import org.openstreetmap.josm.Main;
018import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry;
019import org.openstreetmap.josm.io.CachedFile;
020import org.openstreetmap.josm.io.OfflineAccessException;
021import org.openstreetmap.josm.io.OnlineResource;
022import org.openstreetmap.josm.io.imagery.ImageryReader;
023import org.xml.sax.SAXException;
024
025/**
026 * Manages the list of imagery entries that are shown in the imagery menu.
027 */
028public class ImageryLayerInfo {
029
030    public static final ImageryLayerInfo instance = new ImageryLayerInfo();
031    private final List<ImageryInfo> layers = new ArrayList<>();
032    private final Map<String, ImageryInfo> layerIds = new HashMap<>();
033    private final static List<ImageryInfo> defaultLayers = new ArrayList<>();
034    private final static Map<String, ImageryInfo> defaultLayerIds = new HashMap<>();
035
036    private static final String[] DEFAULT_LAYER_SITES = {
037        Main.getJOSMWebsite()+"/maps"
038    };
039
040    /**
041     * Returns the list of imagery layers sites.
042     * @return the list of imagery layers sites
043     * @since 7434
044     */
045    public static Collection<String> getImageryLayersSites() {
046        return Main.pref.getCollection("imagery.layers.sites", Arrays.asList(DEFAULT_LAYER_SITES));
047    }
048
049    private ImageryLayerInfo() {
050    }
051
052    public ImageryLayerInfo(ImageryLayerInfo info) {
053        layers.addAll(info.layers);
054    }
055
056    public void clear() {
057        layers.clear();
058        layerIds.clear();
059    }
060
061    public void load() {
062        clear();
063        List<ImageryPreferenceEntry> entries = Main.pref.getListOfStructs("imagery.entries", null, ImageryPreferenceEntry.class);
064        if (entries != null) {
065            for (ImageryPreferenceEntry prefEntry : entries) {
066                try {
067                    ImageryInfo i = new ImageryInfo(prefEntry);
068                    add(i);
069                } catch (IllegalArgumentException e) {
070                    Main.warn("Unable to load imagery preference entry:"+e);
071                }
072            }
073            Collections.sort(layers);
074        }
075        loadDefaults(false);
076    }
077
078    /**
079     * Loads the available imagery entries.
080     *
081     * The data is downloaded from the JOSM website (or loaded from cache).
082     * Entries marked as "default" are added to the user selection, if not
083     * already present.
084     *
085     * @param clearCache if true, clear the cache and start a fresh download.
086     */
087    public void loadDefaults(boolean clearCache) {
088        defaultLayers.clear();
089        defaultLayerIds.clear();
090        for (String source : getImageryLayersSites()) {
091            boolean online = true;
092            try {
093                OnlineResource.JOSM_WEBSITE.checkOfflineAccess(source, Main.getJOSMWebsite());
094            } catch (OfflineAccessException e) {
095                Main.warn(e.getMessage());
096                online = false;
097            }
098            if (clearCache && online) {
099                CachedFile.cleanup(source);
100            }
101            try {
102                ImageryReader reader = new ImageryReader(source);
103                Collection<ImageryInfo> result = reader.parse();
104                defaultLayers.addAll(result);
105            } catch (IOException ex) {
106                Main.error(ex, false);
107            } catch (SAXException ex) {
108                Main.error(ex);
109            }
110        }
111        while (defaultLayers.remove(null));
112        Collections.sort(defaultLayers);
113        buildIdMap(defaultLayers, defaultLayerIds);
114        updateEntriesFromDefaults();
115        buildIdMap(layers, layerIds);
116    }
117
118    /**
119     * Build the mapping of unique ids to {@link ImageryInfo}s.
120     * @param lst input list
121     * @param idMap output map
122     */
123    private static void buildIdMap(List<ImageryInfo> lst, Map<String, ImageryInfo> idMap) {
124        idMap.clear();
125        Set<String> notUnique = new HashSet<>();
126        for (ImageryInfo i : lst) {
127            if (i.getId() != null) {
128                if (idMap.containsKey(i.getId())) {
129                    notUnique.add(i.getId());
130                    Main.error("Id ''{0}'' is not unique - used by ''{1}'' and ''{2}''!",
131                            i.getId(), i.getName(), idMap.get(i.getId()).getName());
132                    continue;
133                }
134                idMap.put(i.getId(), i);
135            }
136        }
137        for (String i : notUnique) {
138            idMap.remove(i);
139        }
140    }
141
142    /**
143     * Update user entries according to the list of default entries.
144     */
145    public void updateEntriesFromDefaults() {
146        // add new default entries to the user selection
147        boolean changed = false;
148        Collection<String> knownDefaults = Main.pref.getCollection("imagery.layers.default");
149        Collection<String> newKnownDefaults = new TreeSet<>(knownDefaults);
150        for (ImageryInfo def : defaultLayers) {
151            if (def.isDefaultEntry()) {
152                boolean isKnownDefault = false;
153                for (String url : knownDefaults) {
154                    if (isSimilar(url, def.getUrl())) {
155                        isKnownDefault = true;
156                        break;
157                    }
158                }
159                boolean isInUserList = false;
160                if (!isKnownDefault) {
161                    newKnownDefaults.add(def.getUrl());
162                    for (ImageryInfo i : layers) {
163                        if (isSimilar(def, i)) {
164                            isInUserList = true;
165                            break;
166                        }
167                    }
168                }
169                if (!isKnownDefault && !isInUserList) {
170                    add(new ImageryInfo(def));
171                    changed = true;
172                }
173            }
174        }
175        Main.pref.putCollection("imagery.layers.default", newKnownDefaults);
176
177        // Add ids to user entries without id.
178        // Only do this the first time for each id, so the user can have
179        // custom entries that don't get updated automatically
180        Collection<String> addedIds = Main.pref.getCollection("imagery.layers.addedIds");
181        Collection<String> newAddedIds = new TreeSet<>(addedIds);
182        for (ImageryInfo info : layers) {
183            for (ImageryInfo def : defaultLayers) {
184                if (isSimilar(def, info)) {
185                    if (def.getId() != null && !addedIds.contains(def.getId())) {
186                        if (!defaultLayerIds.containsKey(def.getId())) {
187                            // ignore ids used more than once (have been purged from the map)
188                            continue;
189                        }
190                        newAddedIds.add(def.getId());
191                        if (info.getId() == null) {
192                            info.setId(def.getId());
193                            changed = true;
194                        }
195                    }
196                }
197            }
198        }
199        Main.pref.putCollection("imagery.layers.addedIds", newAddedIds);
200
201        // automatically update user entries with same id as a default entry
202        for (int i=0; i<layers.size(); i++) {
203            ImageryInfo info = layers.get(i);
204            if (info.getId() == null) {
205                continue;
206            }
207            ImageryInfo matchingDefault = defaultLayerIds.get(info.getId());
208            if (matchingDefault != null && !matchingDefault.equalsPref(info)) {
209                layers.set(i, matchingDefault);
210                changed = true;
211            }
212        }
213
214        if (changed) {
215            save();
216        }
217    }
218
219    private boolean isSimilar(ImageryInfo iiA, ImageryInfo iiB) {
220        if (iiA.getId() != null && iiB.getId() != null) return iiA.getId().equals(iiB.getId());
221        return isSimilar(iiA.getUrl(), iiB.getUrl());
222    }
223
224    // some additional checks to respect extended URLs in preferences (legacy workaround)
225    private boolean isSimilar(String a, String b) {
226        return Objects.equals(a, b) || (a != null && b != null && !a.isEmpty() && !b.isEmpty() && (a.contains(b) || b.contains(a)));
227    }
228
229    public void add(ImageryInfo info) {
230        layers.add(info);
231    }
232
233    public void remove(ImageryInfo info) {
234        layers.remove(info);
235    }
236
237    public void save() {
238        List<ImageryPreferenceEntry> entries = new ArrayList<>();
239        for (ImageryInfo info : layers) {
240            entries.add(new ImageryPreferenceEntry(info));
241        }
242        Main.pref.putListOfStructs("imagery.entries", entries, ImageryPreferenceEntry.class);
243    }
244
245    public List<ImageryInfo> getLayers() {
246        return Collections.unmodifiableList(layers);
247    }
248
249    public List<ImageryInfo> getDefaultLayers() {
250        return Collections.unmodifiableList(defaultLayers);
251    }
252
253    public static void addLayer(ImageryInfo info) {
254        instance.add(info);
255        instance.save();
256    }
257
258    public static void addLayers(Collection<ImageryInfo> infos) {
259        for (ImageryInfo i : infos) {
260            instance.add(i);
261        }
262        instance.save();
263        Collections.sort(instance.layers);
264    }
265
266    /**
267     * Get unique id for ImageryInfo.
268     *
269     * This takes care, that no id is used twice (due to a user error)
270     * @param info the ImageryInfo to look up
271     * @return null, if there is no id or the id is used twice,
272     * the corresponding id otherwise
273     */
274    public String getUniqueId(ImageryInfo info) {
275        if (info.getId() != null && layerIds.get(info.getId()) == info) {
276            return info.getId();
277        }
278        return null;
279    }
280}