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}