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