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