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; 018import java.util.concurrent.ExecutorService; 019import java.util.stream.Collectors; 020 021import org.openstreetmap.josm.data.StructUtils; 022import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry; 023import org.openstreetmap.josm.gui.PleaseWaitRunnable; 024import org.openstreetmap.josm.io.CachedFile; 025import org.openstreetmap.josm.io.NetworkManager; 026import org.openstreetmap.josm.io.imagery.ImageryReader; 027import org.openstreetmap.josm.spi.preferences.Config; 028import org.openstreetmap.josm.tools.Logging; 029import org.openstreetmap.josm.tools.Utils; 030import org.xml.sax.SAXException; 031 032/** 033 * Manages the list of imagery entries that are shown in the imagery menu. 034 */ 035public class ImageryLayerInfo { 036 037 /** Unique instance */ 038 public static final ImageryLayerInfo instance = new ImageryLayerInfo(); 039 /** List of all usable layers */ 040 private final List<ImageryInfo> layers = new ArrayList<>(); 041 /** List of layer ids of all usable layers */ 042 private final Map<String, ImageryInfo> layerIds = new HashMap<>(); 043 /** List of all available default layers */ 044 static final List<ImageryInfo> defaultLayers = new ArrayList<>(); 045 /** List of all available default layers (including mirrors) */ 046 static final List<ImageryInfo> allDefaultLayers = new ArrayList<>(); 047 /** List of all layer ids of available default layers (including mirrors) */ 048 static final Map<String, ImageryInfo> defaultLayerIds = new HashMap<>(); 049 050 private static final String[] DEFAULT_LAYER_SITES = { 051 Config.getUrls().getJOSMWebsite()+"/maps%<?ids=>" 052 }; 053 054 /** 055 * Returns the list of imagery layers sites. 056 * @return the list of imagery layers sites 057 * @since 7434 058 */ 059 public static Collection<String> getImageryLayersSites() { 060 return Config.getPref().getList("imagery.layers.sites", Arrays.asList(DEFAULT_LAYER_SITES)); 061 } 062 063 private ImageryLayerInfo() { 064 } 065 066 /** 067 * Constructs a new {@code ImageryLayerInfo} from an existing one. 068 * @param info info to copy 069 */ 070 public ImageryLayerInfo(ImageryLayerInfo info) { 071 layers.addAll(info.layers); 072 } 073 074 /** 075 * Clear the lists of layers. 076 */ 077 public void clear() { 078 layers.clear(); 079 layerIds.clear(); 080 } 081 082 /** 083 * Loads the custom as well as default imagery entries. 084 * @param fastFail whether opening HTTP connections should fail fast, see {@link ImageryReader#setFastFail(boolean)} 085 */ 086 public void load(boolean fastFail) { 087 clear(); 088 List<ImageryPreferenceEntry> entries = StructUtils.getListOfStructs( 089 Config.getPref(), "imagery.entries", null, ImageryPreferenceEntry.class); 090 if (entries != null) { 091 for (ImageryPreferenceEntry prefEntry : entries) { 092 try { 093 ImageryInfo i = new ImageryInfo(prefEntry); 094 add(i); 095 } catch (IllegalArgumentException e) { 096 Logging.warn("Unable to load imagery preference entry:"+e); 097 } 098 } 099 Collections.sort(layers); 100 } 101 loadDefaults(false, null, fastFail); 102 } 103 104 /** 105 * Loads the available imagery entries. 106 * 107 * The data is downloaded from the JOSM website (or loaded from cache). 108 * Entries marked as "default" are added to the user selection, if not already present. 109 * 110 * @param clearCache if true, clear the cache and start a fresh download. 111 * @param worker executor service which will perform the loading. 112 * If null, it should be performed using a {@link PleaseWaitRunnable} in the background 113 * @param fastFail whether opening HTTP connections should fail fast, see {@link ImageryReader#setFastFail(boolean)} 114 * @since 12634 115 */ 116 public void loadDefaults(boolean clearCache, ExecutorService worker, boolean fastFail) { 117 final DefaultEntryLoader loader = new DefaultEntryLoader(clearCache, fastFail); 118 if (worker == null) { 119 loader.realRun(); 120 loader.finish(); 121 } else { 122 worker.execute(loader); 123 } 124 } 125 126 /** 127 * Loader/updater of the available imagery entries 128 */ 129 class DefaultEntryLoader extends PleaseWaitRunnable { 130 131 private final boolean clearCache; 132 private final boolean fastFail; 133 private final List<ImageryInfo> newLayers = new ArrayList<>(); 134 private ImageryReader reader; 135 private boolean canceled; 136 private boolean loadError; 137 138 DefaultEntryLoader(boolean clearCache, boolean fastFail) { 139 super(tr("Update default entries")); 140 this.clearCache = clearCache; 141 this.fastFail = fastFail; 142 } 143 144 @Override 145 protected void cancel() { 146 canceled = true; 147 Utils.close(reader); 148 } 149 150 @Override 151 protected void realRun() { 152 for (String source : getImageryLayersSites()) { 153 if (canceled) { 154 return; 155 } 156 loadSource(source); 157 } 158 } 159 160 protected void loadSource(String source) { 161 boolean online = !NetworkManager.isOffline(source); 162 if (clearCache && online) { 163 CachedFile.cleanup(source); 164 } 165 try { 166 reader = new ImageryReader(source); 167 reader.setFastFail(fastFail); 168 Collection<ImageryInfo> result = reader.parse(); 169 newLayers.addAll(result); 170 } catch (IOException ex) { 171 loadError = true; 172 Logging.log(Logging.LEVEL_ERROR, ex); 173 } catch (SAXException ex) { 174 loadError = true; 175 Logging.error(ex); 176 } 177 } 178 179 @Override 180 protected void finish() { 181 defaultLayers.clear(); 182 allDefaultLayers.clear(); 183 defaultLayers.addAll(newLayers); 184 for (ImageryInfo layer : newLayers) { 185 allDefaultLayers.add(layer); 186 allDefaultLayers.addAll(layer.getMirrors()); 187 } 188 defaultLayerIds.clear(); 189 Collections.sort(defaultLayers); 190 Collections.sort(allDefaultLayers); 191 buildIdMap(allDefaultLayers, defaultLayerIds); 192 updateEntriesFromDefaults(!loadError); 193 buildIdMap(layers, layerIds); 194 if (!loadError && !defaultLayerIds.isEmpty()) { 195 dropOldEntries(); 196 } 197 } 198 } 199 200 /** 201 * Build the mapping of unique ids to {@link ImageryInfo}s. 202 * @param lst input list 203 * @param idMap output map 204 */ 205 private static void buildIdMap(List<ImageryInfo> lst, Map<String, ImageryInfo> idMap) { 206 idMap.clear(); 207 Set<String> notUnique = new HashSet<>(); 208 for (ImageryInfo i : lst) { 209 if (i.getId() != null) { 210 if (idMap.containsKey(i.getId())) { 211 notUnique.add(i.getId()); 212 Logging.error("Id ''{0}'' is not unique - used by ''{1}'' and ''{2}''!", 213 i.getId(), i.getName(), idMap.get(i.getId()).getName()); 214 continue; 215 } 216 idMap.put(i.getId(), i); 217 Collection<String> old = i.getOldIds(); 218 if (old != null) { 219 for (String id : old) { 220 if (idMap.containsKey(id)) { 221 Logging.error("Old Id ''{0}'' is not unique - used by ''{1}'' and ''{2}''!", 222 i.getId(), i.getName(), idMap.get(i.getId()).getName()); 223 } else { 224 idMap.put(id, i); 225 } 226 } 227 } 228 } 229 } 230 for (String i : notUnique) { 231 idMap.remove(i); 232 } 233 } 234 235 /** 236 * Update user entries according to the list of default entries. 237 * @param dropold if <code>true</code> old entries should be removed 238 * @since 11706 239 */ 240 public void updateEntriesFromDefaults(boolean dropold) { 241 // add new default entries to the user selection 242 boolean changed = false; 243 Collection<String> knownDefaults = new TreeSet<>(Config.getPref().getList("imagery.layers.default")); 244 Collection<String> newKnownDefaults = new TreeSet<>(); 245 for (ImageryInfo def : defaultLayers) { 246 if (def.isDefaultEntry()) { 247 boolean isKnownDefault = false; 248 for (String entry : knownDefaults) { 249 if (entry.equals(def.getId())) { 250 isKnownDefault = true; 251 newKnownDefaults.add(entry); 252 knownDefaults.remove(entry); 253 break; 254 } else if (isSimilar(entry, def.getUrl())) { 255 isKnownDefault = true; 256 if (def.getId() != null) { 257 newKnownDefaults.add(def.getId()); 258 } 259 knownDefaults.remove(entry); 260 break; 261 } 262 } 263 boolean isInUserList = false; 264 if (!isKnownDefault) { 265 if (def.getId() != null) { 266 newKnownDefaults.add(def.getId()); 267 isInUserList = layers.stream().anyMatch(i -> isSimilar(def, i)); 268 } else { 269 Logging.error("Default imagery ''{0}'' has no id. Skipping.", def.getName()); 270 } 271 } 272 if (!isKnownDefault && !isInUserList) { 273 add(new ImageryInfo(def)); 274 changed = true; 275 } 276 } 277 } 278 if (!dropold && !knownDefaults.isEmpty()) { 279 newKnownDefaults.addAll(knownDefaults); 280 } 281 Config.getPref().putList("imagery.layers.default", new ArrayList<>(newKnownDefaults)); 282 283 // automatically update user entries with same id as a default entry 284 for (int i = 0; i < layers.size(); i++) { 285 ImageryInfo info = layers.get(i); 286 if (info.getId() == null) { 287 continue; 288 } 289 ImageryInfo matchingDefault = defaultLayerIds.get(info.getId()); 290 if (matchingDefault != null && !matchingDefault.equalsPref(info)) { 291 layers.set(i, matchingDefault); 292 Logging.info(tr("Update imagery ''{0}''", info.getName())); 293 changed = true; 294 } 295 } 296 297 if (changed) { 298 save(); 299 } 300 } 301 302 /** 303 * Drop entries with Id which do no longer exist (removed from defaults). 304 * @since 11527 305 */ 306 public void dropOldEntries() { 307 List<String> drop = new ArrayList<>(); 308 309 for (Map.Entry<String, ImageryInfo> info : layerIds.entrySet()) { 310 if (!defaultLayerIds.containsKey(info.getKey())) { 311 remove(info.getValue()); 312 drop.add(info.getKey()); 313 Logging.info(tr("Drop old imagery ''{0}''", info.getValue().getName())); 314 } 315 } 316 317 if (!drop.isEmpty()) { 318 for (String id : drop) { 319 layerIds.remove(id); 320 } 321 save(); 322 } 323 } 324 325 private static boolean isSimilar(ImageryInfo iiA, ImageryInfo iiB) { 326 if (iiA == null || iiA.getImageryType() != iiB.getImageryType()) 327 return false; 328 if (iiA.getId() != null && iiB.getId() != null) 329 return iiA.getId().equals(iiB.getId()); 330 return isSimilar(iiA.getUrl(), iiB.getUrl()); 331 } 332 333 // some additional checks to respect extended URLs in preferences (legacy workaround) 334 private static boolean isSimilar(String a, String b) { 335 return Objects.equals(a, b) || (!Utils.isEmpty(a) && !Utils.isEmpty(b) && (a.contains(b) || b.contains(a))); 336 } 337 338 /** 339 * Add a new imagery entry. 340 * @param info imagery entry to add 341 */ 342 public void add(ImageryInfo info) { 343 layers.add(info); 344 } 345 346 /** 347 * Remove an imagery entry. 348 * @param info imagery entry to remove 349 */ 350 public void remove(ImageryInfo info) { 351 layers.remove(info); 352 } 353 354 /** 355 * Save the list of imagery entries to preferences. 356 */ 357 public void save() { 358 List<ImageryPreferenceEntry> entries = layers.stream() 359 .map(ImageryPreferenceEntry::new) 360 .collect(Collectors.toList()); 361 StructUtils.putListOfStructs(Config.getPref(), "imagery.entries", entries, ImageryPreferenceEntry.class); 362 } 363 364 /** 365 * List of usable layers 366 * @return unmodifiable list containing usable layers 367 */ 368 public List<ImageryInfo> getLayers() { 369 return Collections.unmodifiableList(layers); 370 } 371 372 /** 373 * List of available default layers 374 * @return unmodifiable list containing available default layers 375 */ 376 public List<ImageryInfo> getDefaultLayers() { 377 return Collections.unmodifiableList(defaultLayers); 378 } 379 380 /** 381 * List of all available default layers (including mirrors) 382 * @return unmodifiable list containing available default layers 383 * @since 11570 384 */ 385 public List<ImageryInfo> getAllDefaultLayers() { 386 return Collections.unmodifiableList(allDefaultLayers); 387 } 388 389 public static void addLayer(ImageryInfo info) { 390 instance.add(info); 391 instance.save(); 392 } 393 394 public static void addLayers(Collection<ImageryInfo> infos) { 395 for (ImageryInfo i : infos) { 396 instance.add(i); 397 } 398 instance.save(); 399 Collections.sort(instance.layers); 400 } 401 402 /** 403 * Get unique id for ImageryInfo. 404 * 405 * This takes care, that no id is used twice (due to a user error) 406 * @param info the ImageryInfo to look up 407 * @return null, if there is no id or the id is used twice, 408 * the corresponding id otherwise 409 */ 410 public String getUniqueId(ImageryInfo info) { 411 if (info.getId() != null && layerIds.get(info.getId()) == info) { 412 return info.getId(); 413 } 414 return null; 415 } 416 417 /** 418 * Returns imagery layer info for the given id. 419 * @param id imagery layer id. 420 * @return imagery layer info for the given id, or {@code null} 421 * @since 13797 422 */ 423 public ImageryInfo getLayer(String id) { 424 return layerIds.get(id); 425 } 426}