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; 018 019import org.openstreetmap.josm.Main; 020import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry; 021import org.openstreetmap.josm.gui.PleaseWaitRunnable; 022import org.openstreetmap.josm.io.CachedFile; 023import org.openstreetmap.josm.io.OfflineAccessException; 024import org.openstreetmap.josm.io.OnlineResource; 025import org.openstreetmap.josm.io.imagery.ImageryReader; 026import org.openstreetmap.josm.tools.Utils; 027import org.xml.sax.SAXException; 028 029/** 030 * Manages the list of imagery entries that are shown in the imagery menu. 031 */ 032public class ImageryLayerInfo { 033 034 public static final ImageryLayerInfo instance = new ImageryLayerInfo(); 035 private final List<ImageryInfo> layers = new ArrayList<>(); 036 private final Map<String, ImageryInfo> layerIds = new HashMap<>(); 037 private static final List<ImageryInfo> defaultLayers = new ArrayList<>(); 038 private static final Map<String, ImageryInfo> defaultLayerIds = new HashMap<>(); 039 040 private static final String[] DEFAULT_LAYER_SITES = { 041 Main.getJOSMWebsite()+"/maps" 042 }; 043 044 /** 045 * Returns the list of imagery layers sites. 046 * @return the list of imagery layers sites 047 * @since 7434 048 */ 049 public static Collection<String> getImageryLayersSites() { 050 return Main.pref.getCollection("imagery.layers.sites", Arrays.asList(DEFAULT_LAYER_SITES)); 051 } 052 053 private ImageryLayerInfo() { 054 } 055 056 public ImageryLayerInfo(ImageryLayerInfo info) { 057 layers.addAll(info.layers); 058 } 059 060 public void clear() { 061 layers.clear(); 062 layerIds.clear(); 063 } 064 065 /** 066 * Loads the custom as well as default imagery entries. 067 * @param fastFail whether opening HTTP connections should fail fast, see {@link ImageryReader#setFastFail(boolean)} 068 */ 069 public void load(boolean fastFail) { 070 clear(); 071 List<ImageryPreferenceEntry> entries = Main.pref.getListOfStructs("imagery.entries", null, ImageryPreferenceEntry.class); 072 if (entries != null) { 073 for (ImageryPreferenceEntry prefEntry : entries) { 074 try { 075 ImageryInfo i = new ImageryInfo(prefEntry); 076 add(i); 077 } catch (IllegalArgumentException e) { 078 Main.warn("Unable to load imagery preference entry:"+e); 079 } 080 } 081 Collections.sort(layers); 082 } 083 loadDefaults(false, true, fastFail); 084 } 085 086 /** 087 * Loads the available imagery entries. 088 * 089 * The data is downloaded from the JOSM website (or loaded from cache). 090 * Entries marked as "default" are added to the user selection, if not 091 * already present. 092 * 093 * @param clearCache if true, clear the cache and start a fresh download. 094 * @param quiet whether not the loading should be performed using a {@link PleaseWaitRunnable} in the background 095 * @param fastFail whether opening HTTP connections should fail fast, see {@link ImageryReader#setFastFail(boolean)} 096 */ 097 public void loadDefaults(boolean clearCache, boolean quiet, boolean fastFail) { 098 final DefaultEntryLoader loader = new DefaultEntryLoader(clearCache, fastFail); 099 if (quiet) { 100 loader.realRun(); 101 loader.finish(); 102 } else { 103 Main.worker.execute(new DefaultEntryLoader(clearCache, fastFail)); 104 } 105 } 106 107 /** 108 * Loader/updater of the available imagery entries 109 */ 110 class DefaultEntryLoader extends PleaseWaitRunnable { 111 112 private final boolean clearCache; 113 private final boolean fastFail; 114 private final List<ImageryInfo> newLayers = new ArrayList<>(); 115 private ImageryReader reader; 116 private boolean canceled; 117 118 DefaultEntryLoader(boolean clearCache, boolean fastFail) { 119 super(tr("Update default entries")); 120 this.clearCache = clearCache; 121 this.fastFail = fastFail; 122 } 123 124 @Override 125 protected void cancel() { 126 canceled = true; 127 Utils.close(reader); 128 } 129 130 @Override 131 protected void realRun() { 132 for (String source : getImageryLayersSites()) { 133 if (canceled) { 134 return; 135 } 136 loadSource(source); 137 } 138 } 139 140 protected void loadSource(String source) { 141 boolean online = true; 142 try { 143 OnlineResource.JOSM_WEBSITE.checkOfflineAccess(source, Main.getJOSMWebsite()); 144 } catch (OfflineAccessException e) { 145 Main.warn(e.getMessage()); 146 online = false; 147 } 148 if (clearCache && online) { 149 CachedFile.cleanup(source); 150 } 151 try { 152 reader = new ImageryReader(source); 153 reader.setFastFail(fastFail); 154 Collection<ImageryInfo> result = reader.parse(); 155 newLayers.addAll(result); 156 } catch (IOException ex) { 157 Main.error(ex, false); 158 } catch (SAXException ex) { 159 Main.error(ex); 160 } 161 } 162 163 @Override 164 protected void finish() { 165 defaultLayers.clear(); 166 defaultLayers.addAll(newLayers); 167 defaultLayerIds.clear(); 168 Collections.sort(defaultLayers); 169 buildIdMap(defaultLayers, defaultLayerIds); 170 updateEntriesFromDefaults(); 171 buildIdMap(layers, layerIds); 172 } 173 } 174 175 /** 176 * Build the mapping of unique ids to {@link ImageryInfo}s. 177 * @param lst input list 178 * @param idMap output map 179 */ 180 private static void buildIdMap(List<ImageryInfo> lst, Map<String, ImageryInfo> idMap) { 181 idMap.clear(); 182 Set<String> notUnique = new HashSet<>(); 183 for (ImageryInfo i : lst) { 184 if (i.getId() != null) { 185 if (idMap.containsKey(i.getId())) { 186 notUnique.add(i.getId()); 187 Main.error("Id ''{0}'' is not unique - used by ''{1}'' and ''{2}''!", 188 i.getId(), i.getName(), idMap.get(i.getId()).getName()); 189 continue; 190 } 191 idMap.put(i.getId(), i); 192 } 193 } 194 for (String i : notUnique) { 195 idMap.remove(i); 196 } 197 } 198 199 /** 200 * Update user entries according to the list of default entries. 201 */ 202 public void updateEntriesFromDefaults() { 203 // add new default entries to the user selection 204 boolean changed = false; 205 Collection<String> knownDefaults = Main.pref.getCollection("imagery.layers.default"); 206 Collection<String> newKnownDefaults = new TreeSet<>(knownDefaults); 207 for (ImageryInfo def : defaultLayers) { 208 if (def.isDefaultEntry()) { 209 boolean isKnownDefault = false; 210 for (String url : knownDefaults) { 211 if (isSimilar(url, def.getUrl())) { 212 isKnownDefault = true; 213 break; 214 } 215 } 216 boolean isInUserList = false; 217 if (!isKnownDefault) { 218 newKnownDefaults.add(def.getUrl()); 219 for (ImageryInfo i : layers) { 220 if (isSimilar(def, i)) { 221 isInUserList = true; 222 break; 223 } 224 } 225 } 226 if (!isKnownDefault && !isInUserList) { 227 add(new ImageryInfo(def)); 228 changed = true; 229 } 230 } 231 } 232 Main.pref.putCollection("imagery.layers.default", newKnownDefaults); 233 234 // Add ids to user entries without id. 235 // Only do this the first time for each id, so the user can have 236 // custom entries that don't get updated automatically 237 Collection<String> addedIds = Main.pref.getCollection("imagery.layers.addedIds"); 238 Collection<String> newAddedIds = new TreeSet<>(addedIds); 239 for (ImageryInfo info : layers) { 240 for (ImageryInfo def : defaultLayers) { 241 if (isSimilar(def, info)) { 242 if (def.getId() != null && !addedIds.contains(def.getId())) { 243 if (!defaultLayerIds.containsKey(def.getId())) { 244 // ignore ids used more than once (have been purged from the map) 245 continue; 246 } 247 newAddedIds.add(def.getId()); 248 if (info.getId() == null) { 249 info.setId(def.getId()); 250 changed = true; 251 } 252 } 253 } 254 } 255 } 256 Main.pref.putCollection("imagery.layers.addedIds", newAddedIds); 257 258 // automatically update user entries with same id as a default entry 259 for (int i = 0; i < layers.size(); i++) { 260 ImageryInfo info = layers.get(i); 261 if (info.getId() == null) { 262 continue; 263 } 264 ImageryInfo matchingDefault = defaultLayerIds.get(info.getId()); 265 if (matchingDefault != null && !matchingDefault.equalsPref(info)) { 266 layers.set(i, matchingDefault); 267 changed = true; 268 } 269 } 270 271 if (changed) { 272 save(); 273 } 274 } 275 276 private boolean isSimilar(ImageryInfo iiA, ImageryInfo iiB) { 277 if (iiA == null) 278 return false; 279 if (!iiA.getImageryType().equals(iiB.getImageryType())) 280 return false; 281 if (iiA.getId() != null && iiB.getId() != null) return iiA.getId().equals(iiB.getId()); 282 return isSimilar(iiA.getUrl(), iiB.getUrl()); 283 } 284 285 // some additional checks to respect extended URLs in preferences (legacy workaround) 286 private static boolean isSimilar(String a, String b) { 287 return Objects.equals(a, b) || (a != null && b != null && !a.isEmpty() && !b.isEmpty() && (a.contains(b) || b.contains(a))); 288 } 289 290 public void add(ImageryInfo info) { 291 layers.add(info); 292 } 293 294 public void remove(ImageryInfo info) { 295 layers.remove(info); 296 } 297 298 public void save() { 299 List<ImageryPreferenceEntry> entries = new ArrayList<>(); 300 for (ImageryInfo info : layers) { 301 entries.add(new ImageryPreferenceEntry(info)); 302 } 303 Main.pref.putListOfStructs("imagery.entries", entries, ImageryPreferenceEntry.class); 304 } 305 306 public List<ImageryInfo> getLayers() { 307 return Collections.unmodifiableList(layers); 308 } 309 310 public List<ImageryInfo> getDefaultLayers() { 311 return Collections.unmodifiableList(defaultLayers); 312 } 313 314 public static void addLayer(ImageryInfo info) { 315 instance.add(info); 316 instance.save(); 317 } 318 319 public static void addLayers(Collection<ImageryInfo> infos) { 320 for (ImageryInfo i : infos) { 321 instance.add(i); 322 } 323 instance.save(); 324 Collections.sort(instance.layers); 325 } 326 327 /** 328 * Get unique id for ImageryInfo. 329 * 330 * This takes care, that no id is used twice (due to a user error) 331 * @param info the ImageryInfo to look up 332 * @return null, if there is no id or the id is used twice, 333 * the corresponding id otherwise 334 */ 335 public String getUniqueId(ImageryInfo info) { 336 if (info.getId() != null && layerIds.get(info.getId()) == info) { 337 return info.getId(); 338 } 339 return null; 340 } 341}