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; 019 020import org.openstreetmap.josm.data.StructUtils; 021import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry; 022import org.openstreetmap.josm.gui.PleaseWaitRunnable; 023import org.openstreetmap.josm.io.CachedFile; 024import org.openstreetmap.josm.io.OfflineAccessException; 025import org.openstreetmap.josm.io.OnlineResource; 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 = true; 162 try { 163 OnlineResource.JOSM_WEBSITE.checkOfflineAccess(source, Config.getUrls().getJOSMWebsite()); 164 } catch (OfflineAccessException e) { 165 Logging.log(Logging.LEVEL_WARN, e); 166 online = false; 167 } 168 if (clearCache && online) { 169 CachedFile.cleanup(source); 170 } 171 try { 172 reader = new ImageryReader(source); 173 reader.setFastFail(fastFail); 174 Collection<ImageryInfo> result = reader.parse(); 175 newLayers.addAll(result); 176 } catch (IOException ex) { 177 loadError = true; 178 Logging.log(Logging.LEVEL_ERROR, ex); 179 } catch (SAXException ex) { 180 loadError = true; 181 Logging.error(ex); 182 } 183 } 184 185 @Override 186 protected void finish() { 187 defaultLayers.clear(); 188 allDefaultLayers.clear(); 189 defaultLayers.addAll(newLayers); 190 for (ImageryInfo layer : newLayers) { 191 allDefaultLayers.add(layer); 192 for (ImageryInfo sublayer : layer.getMirrors()) { 193 allDefaultLayers.add(sublayer); 194 } 195 } 196 defaultLayerIds.clear(); 197 Collections.sort(defaultLayers); 198 Collections.sort(allDefaultLayers); 199 buildIdMap(allDefaultLayers, defaultLayerIds); 200 updateEntriesFromDefaults(!loadError); 201 buildIdMap(layers, layerIds); 202 if (!loadError && !defaultLayerIds.isEmpty()) { 203 dropOldEntries(); 204 } 205 } 206 } 207 208 /** 209 * Build the mapping of unique ids to {@link ImageryInfo}s. 210 * @param lst input list 211 * @param idMap output map 212 */ 213 private static void buildIdMap(List<ImageryInfo> lst, Map<String, ImageryInfo> idMap) { 214 idMap.clear(); 215 Set<String> notUnique = new HashSet<>(); 216 for (ImageryInfo i : lst) { 217 if (i.getId() != null) { 218 if (idMap.containsKey(i.getId())) { 219 notUnique.add(i.getId()); 220 Logging.error("Id ''{0}'' is not unique - used by ''{1}'' and ''{2}''!", 221 i.getId(), i.getName(), idMap.get(i.getId()).getName()); 222 continue; 223 } 224 idMap.put(i.getId(), i); 225 Collection<String> old = i.getOldIds(); 226 if (old != null) { 227 for (String id : old) { 228 if (idMap.containsKey(id)) { 229 Logging.error("Old Id ''{0}'' is not unique - used by ''{1}'' and ''{2}''!", 230 i.getId(), i.getName(), idMap.get(i.getId()).getName()); 231 } else { 232 idMap.put(id, i); 233 } 234 } 235 } 236 } 237 } 238 for (String i : notUnique) { 239 idMap.remove(i); 240 } 241 } 242 243 /** 244 * Update user entries according to the list of default entries. 245 * @param dropold if <code>true</code> old entries should be removed 246 * @since 11706 247 */ 248 public void updateEntriesFromDefaults(boolean dropold) { 249 // add new default entries to the user selection 250 boolean changed = false; 251 Collection<String> knownDefaults = new TreeSet<>(Config.getPref().getList("imagery.layers.default")); 252 Collection<String> newKnownDefaults = new TreeSet<>(); 253 for (ImageryInfo def : defaultLayers) { 254 if (def.isDefaultEntry()) { 255 boolean isKnownDefault = false; 256 for (String entry : knownDefaults) { 257 if (entry.equals(def.getId())) { 258 isKnownDefault = true; 259 newKnownDefaults.add(entry); 260 knownDefaults.remove(entry); 261 break; 262 } else if (isSimilar(entry, def.getUrl())) { 263 isKnownDefault = true; 264 if (def.getId() != null) { 265 newKnownDefaults.add(def.getId()); 266 } 267 knownDefaults.remove(entry); 268 break; 269 } 270 } 271 boolean isInUserList = false; 272 if (!isKnownDefault) { 273 if (def.getId() != null) { 274 newKnownDefaults.add(def.getId()); 275 for (ImageryInfo i : layers) { 276 if (isSimilar(def, i)) { 277 isInUserList = true; 278 break; 279 } 280 } 281 } else { 282 Logging.error("Default imagery ''{0}'' has no id. Skipping.", def.getName()); 283 } 284 } 285 if (!isKnownDefault && !isInUserList) { 286 add(new ImageryInfo(def)); 287 changed = true; 288 } 289 } 290 } 291 if (!dropold && !knownDefaults.isEmpty()) { 292 newKnownDefaults.addAll(knownDefaults); 293 } 294 Config.getPref().putList("imagery.layers.default", new ArrayList<>(newKnownDefaults)); 295 296 // automatically update user entries with same id as a default entry 297 for (int i = 0; i < layers.size(); i++) { 298 ImageryInfo info = layers.get(i); 299 if (info.getId() == null) { 300 continue; 301 } 302 ImageryInfo matchingDefault = defaultLayerIds.get(info.getId()); 303 if (matchingDefault != null && !matchingDefault.equalsPref(info)) { 304 layers.set(i, matchingDefault); 305 Logging.info(tr("Update imagery ''{0}''", info.getName())); 306 changed = true; 307 } 308 } 309 310 if (changed) { 311 save(); 312 } 313 } 314 315 /** 316 * Drop entries with Id which do no longer exist (removed from defaults). 317 * @since 11527 318 */ 319 public void dropOldEntries() { 320 List<String> drop = new ArrayList<>(); 321 322 for (Map.Entry<String, ImageryInfo> info : layerIds.entrySet()) { 323 if (!defaultLayerIds.containsKey(info.getKey())) { 324 remove(info.getValue()); 325 drop.add(info.getKey()); 326 Logging.info(tr("Drop old imagery ''{0}''", info.getValue().getName())); 327 } 328 } 329 330 if (!drop.isEmpty()) { 331 for (String id : drop) { 332 layerIds.remove(id); 333 } 334 save(); 335 } 336 } 337 338 private static boolean isSimilar(ImageryInfo iiA, ImageryInfo iiB) { 339 if (iiA == null || iiA.getImageryType() != iiB.getImageryType()) 340 return false; 341 if (iiA.getId() != null && iiB.getId() != null) 342 return iiA.getId().equals(iiB.getId()); 343 return isSimilar(iiA.getUrl(), iiB.getUrl()); 344 } 345 346 // some additional checks to respect extended URLs in preferences (legacy workaround) 347 private static boolean isSimilar(String a, String b) { 348 return Objects.equals(a, b) || (a != null && b != null && !a.isEmpty() && !b.isEmpty() && (a.contains(b) || b.contains(a))); 349 } 350 351 /** 352 * Add a new imagery entry. 353 * @param info imagery entry to add 354 */ 355 public void add(ImageryInfo info) { 356 layers.add(info); 357 } 358 359 /** 360 * Remove an imagery entry. 361 * @param info imagery entry to remove 362 */ 363 public void remove(ImageryInfo info) { 364 layers.remove(info); 365 } 366 367 /** 368 * Save the list of imagery entries to preferences. 369 */ 370 public void save() { 371 List<ImageryPreferenceEntry> entries = new ArrayList<>(); 372 for (ImageryInfo info : layers) { 373 entries.add(new ImageryPreferenceEntry(info)); 374 } 375 StructUtils.putListOfStructs(Config.getPref(), "imagery.entries", entries, ImageryPreferenceEntry.class); 376 } 377 378 /** 379 * List of usable layers 380 * @return unmodifiable list containing usable layers 381 */ 382 public List<ImageryInfo> getLayers() { 383 return Collections.unmodifiableList(layers); 384 } 385 386 /** 387 * List of available default layers 388 * @return unmodifiable list containing available default layers 389 */ 390 public List<ImageryInfo> getDefaultLayers() { 391 return Collections.unmodifiableList(defaultLayers); 392 } 393 394 /** 395 * List of all available default layers (including mirrors) 396 * @return unmodifiable list containing available default layers 397 * @since 11570 398 */ 399 public List<ImageryInfo> getAllDefaultLayers() { 400 return Collections.unmodifiableList(allDefaultLayers); 401 } 402 403 public static void addLayer(ImageryInfo info) { 404 instance.add(info); 405 instance.save(); 406 } 407 408 public static void addLayers(Collection<ImageryInfo> infos) { 409 for (ImageryInfo i : infos) { 410 instance.add(i); 411 } 412 instance.save(); 413 Collections.sort(instance.layers); 414 } 415 416 /** 417 * Get unique id for ImageryInfo. 418 * 419 * This takes care, that no id is used twice (due to a user error) 420 * @param info the ImageryInfo to look up 421 * @return null, if there is no id or the id is used twice, 422 * the corresponding id otherwise 423 */ 424 public String getUniqueId(ImageryInfo info) { 425 if (info.getId() != null && layerIds.get(info.getId()) == info) { 426 return info.getId(); 427 } 428 return null; 429 } 430 431 /** 432 * Returns imagery layer info for the given id. 433 * @param id imagery layer id. 434 * @return imagery layer info for the given id, or {@code null} 435 * @since 13797 436 */ 437 public ImageryInfo getLayer(String id) { 438 return layerIds.get(id); 439 } 440}