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 static final List<ImageryInfo> defaultLayers = new ArrayList<>();
034    private static final 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            // Do nothing
113        }
114        Collections.sort(defaultLayers);
115        buildIdMap(defaultLayers, defaultLayerIds);
116        updateEntriesFromDefaults();
117        buildIdMap(layers, layerIds);
118    }
119
120    /**
121     * Build the mapping of unique ids to {@link ImageryInfo}s.
122     * @param lst input list
123     * @param idMap output map
124     */
125    private static void buildIdMap(List<ImageryInfo> lst, Map<String, ImageryInfo> idMap) {
126        idMap.clear();
127        Set<String> notUnique = new HashSet<>();
128        for (ImageryInfo i : lst) {
129            if (i.getId() != null) {
130                if (idMap.containsKey(i.getId())) {
131                    notUnique.add(i.getId());
132                    Main.error("Id ''{0}'' is not unique - used by ''{1}'' and ''{2}''!",
133                            i.getId(), i.getName(), idMap.get(i.getId()).getName());
134                    continue;
135                }
136                idMap.put(i.getId(), i);
137            }
138        }
139        for (String i : notUnique) {
140            idMap.remove(i);
141        }
142    }
143
144    /**
145     * Update user entries according to the list of default entries.
146     */
147    public void updateEntriesFromDefaults() {
148        // add new default entries to the user selection
149        boolean changed = false;
150        Collection<String> knownDefaults = Main.pref.getCollection("imagery.layers.default");
151        Collection<String> newKnownDefaults = new TreeSet<>(knownDefaults);
152        for (ImageryInfo def : defaultLayers) {
153            // temporary migration code, so all user preferences will get updated with new settings from JOSM site (can be removed ~Dez. 2015)
154            if (def.getNoTileHeaders() != null || def.getTileSize() > 0 || def.getMetadataHeaders() != null) {
155                for (ImageryInfo i: layers) {
156                    if (isSimilar(def,  i)) {
157                        if (def.getNoTileHeaders() != null) {
158                            i.setNoTileHeaders(def.getNoTileHeaders());
159                        }
160                        if (def.getTileSize() > 0) {
161                            i.setTileSize(def.getTileSize());
162                        }
163                        if (def.getMetadataHeaders() != null && def.getMetadataHeaders().size() > 0) {
164                            i.setMetadataHeaders(def.getMetadataHeaders());
165                        }
166                        changed = true;
167                    }
168                }
169            }
170
171            if (def.isDefaultEntry()) {
172                boolean isKnownDefault = false;
173                for (String url : knownDefaults) {
174                    if (isSimilar(url, def.getUrl())) {
175                        isKnownDefault = true;
176                        break;
177                    }
178                }
179                boolean isInUserList = false;
180                if (!isKnownDefault) {
181                    newKnownDefaults.add(def.getUrl());
182                    for (ImageryInfo i : layers) {
183                        if (isSimilar(def, i)) {
184                            isInUserList = true;
185                            break;
186                        }
187                    }
188                }
189                if (!isKnownDefault && !isInUserList) {
190                    add(new ImageryInfo(def));
191                    changed = true;
192                }
193            }
194        }
195        Main.pref.putCollection("imagery.layers.default", newKnownDefaults);
196
197        // Add ids to user entries without id.
198        // Only do this the first time for each id, so the user can have
199        // custom entries that don't get updated automatically
200        Collection<String> addedIds = Main.pref.getCollection("imagery.layers.addedIds");
201        Collection<String> newAddedIds = new TreeSet<>(addedIds);
202        for (ImageryInfo info : layers) {
203            for (ImageryInfo def : defaultLayers) {
204                if (isSimilar(def, info)) {
205                    if (def.getId() != null && !addedIds.contains(def.getId())) {
206                        if (!defaultLayerIds.containsKey(def.getId())) {
207                            // ignore ids used more than once (have been purged from the map)
208                            continue;
209                        }
210                        newAddedIds.add(def.getId());
211                        if (info.getId() == null) {
212                            info.setId(def.getId());
213                            changed = true;
214                        }
215                    }
216                }
217            }
218        }
219        Main.pref.putCollection("imagery.layers.addedIds", newAddedIds);
220
221        // automatically update user entries with same id as a default entry
222        for (int i = 0; i < layers.size(); i++) {
223            ImageryInfo info = layers.get(i);
224            if (info.getId() == null) {
225                continue;
226            }
227            ImageryInfo matchingDefault = defaultLayerIds.get(info.getId());
228            if (matchingDefault != null && !matchingDefault.equalsPref(info)) {
229                layers.set(i, matchingDefault);
230                changed = true;
231            }
232        }
233
234        if (changed) {
235            save();
236        }
237    }
238
239    private boolean isSimilar(ImageryInfo iiA, ImageryInfo iiB) {
240        if (iiA == null)
241            return false;
242        if (!iiA.getImageryType().equals(iiB.getImageryType()))
243            return false;
244        if (iiA.getId() != null && iiB.getId() != null) return iiA.getId().equals(iiB.getId());
245        return isSimilar(iiA.getUrl(), iiB.getUrl());
246    }
247
248    // some additional checks to respect extended URLs in preferences (legacy workaround)
249    private static boolean isSimilar(String a, String b) {
250        return Objects.equals(a, b) || (a != null && b != null && !a.isEmpty() && !b.isEmpty() && (a.contains(b) || b.contains(a)));
251    }
252
253    public void add(ImageryInfo info) {
254        layers.add(info);
255    }
256
257    public void remove(ImageryInfo info) {
258        layers.remove(info);
259    }
260
261    public void save() {
262        List<ImageryPreferenceEntry> entries = new ArrayList<>();
263        for (ImageryInfo info : layers) {
264            entries.add(new ImageryPreferenceEntry(info));
265        }
266        Main.pref.putListOfStructs("imagery.entries", entries, ImageryPreferenceEntry.class);
267    }
268
269    public List<ImageryInfo> getLayers() {
270        return Collections.unmodifiableList(layers);
271    }
272
273    public List<ImageryInfo> getDefaultLayers() {
274        return Collections.unmodifiableList(defaultLayers);
275    }
276
277    public static void addLayer(ImageryInfo info) {
278        instance.add(info);
279        instance.save();
280    }
281
282    public static void addLayers(Collection<ImageryInfo> infos) {
283        for (ImageryInfo i : infos) {
284            instance.add(i);
285        }
286        instance.save();
287        Collections.sort(instance.layers);
288    }
289
290    /**
291     * Get unique id for ImageryInfo.
292     *
293     * This takes care, that no id is used twice (due to a user error)
294     * @param info the ImageryInfo to look up
295     * @return null, if there is no id or the id is used twice,
296     * the corresponding id otherwise
297     */
298    public String getUniqueId(ImageryInfo info) {
299        if (info.getId() != null && layerIds.get(info.getId()) == info) {
300            return info.getId();
301        }
302        return null;
303    }
304}