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 static final List<ImageryInfo> defaultLayers = new ArrayList<>(); 034 private static final 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 // Do nothing 113 } 114 Collections.sort(defaultLayers); 115 buildIdMap(defaultLayers, defaultLayerIds); 116 updateEntriesFromDefaults(); 117 buildIdMap(layers, layerIds); 118 } 119 120 /** 121 * Build the mapping of unique ids to {@link ImageryInfo}s. 122 * @param lst input list 123 * @param idMap output map 124 */ 125 private static void buildIdMap(List<ImageryInfo> lst, Map<String, ImageryInfo> idMap) { 126 idMap.clear(); 127 Set<String> notUnique = new HashSet<>(); 128 for (ImageryInfo i : lst) { 129 if (i.getId() != null) { 130 if (idMap.containsKey(i.getId())) { 131 notUnique.add(i.getId()); 132 Main.error("Id ''{0}'' is not unique - used by ''{1}'' and ''{2}''!", 133 i.getId(), i.getName(), idMap.get(i.getId()).getName()); 134 continue; 135 } 136 idMap.put(i.getId(), i); 137 } 138 } 139 for (String i : notUnique) { 140 idMap.remove(i); 141 } 142 } 143 144 /** 145 * Update user entries according to the list of default entries. 146 */ 147 public void updateEntriesFromDefaults() { 148 // add new default entries to the user selection 149 boolean changed = false; 150 Collection<String> knownDefaults = Main.pref.getCollection("imagery.layers.default"); 151 Collection<String> newKnownDefaults = new TreeSet<>(knownDefaults); 152 for (ImageryInfo def : defaultLayers) { 153 // temporary migration code, so all user preferences will get updated with new settings from JOSM site (can be removed ~Dez. 2015) 154 if (def.getNoTileHeaders() != null || def.getTileSize() > 0 || def.getMetadataHeaders() != null) { 155 for (ImageryInfo i: layers) { 156 if (isSimilar(def, i)) { 157 if (def.getNoTileHeaders() != null) { 158 i.setNoTileHeaders(def.getNoTileHeaders()); 159 } 160 if (def.getTileSize() > 0) { 161 i.setTileSize(def.getTileSize()); 162 } 163 if (def.getMetadataHeaders() != null && def.getMetadataHeaders().size() > 0) { 164 i.setMetadataHeaders(def.getMetadataHeaders()); 165 } 166 changed = true; 167 } 168 } 169 } 170 171 if (def.isDefaultEntry()) { 172 boolean isKnownDefault = false; 173 for (String url : knownDefaults) { 174 if (isSimilar(url, def.getUrl())) { 175 isKnownDefault = true; 176 break; 177 } 178 } 179 boolean isInUserList = false; 180 if (!isKnownDefault) { 181 newKnownDefaults.add(def.getUrl()); 182 for (ImageryInfo i : layers) { 183 if (isSimilar(def, i)) { 184 isInUserList = true; 185 break; 186 } 187 } 188 } 189 if (!isKnownDefault && !isInUserList) { 190 add(new ImageryInfo(def)); 191 changed = true; 192 } 193 } 194 } 195 Main.pref.putCollection("imagery.layers.default", newKnownDefaults); 196 197 // Add ids to user entries without id. 198 // Only do this the first time for each id, so the user can have 199 // custom entries that don't get updated automatically 200 Collection<String> addedIds = Main.pref.getCollection("imagery.layers.addedIds"); 201 Collection<String> newAddedIds = new TreeSet<>(addedIds); 202 for (ImageryInfo info : layers) { 203 for (ImageryInfo def : defaultLayers) { 204 if (isSimilar(def, info)) { 205 if (def.getId() != null && !addedIds.contains(def.getId())) { 206 if (!defaultLayerIds.containsKey(def.getId())) { 207 // ignore ids used more than once (have been purged from the map) 208 continue; 209 } 210 newAddedIds.add(def.getId()); 211 if (info.getId() == null) { 212 info.setId(def.getId()); 213 changed = true; 214 } 215 } 216 } 217 } 218 } 219 Main.pref.putCollection("imagery.layers.addedIds", newAddedIds); 220 221 // automatically update user entries with same id as a default entry 222 for (int i = 0; i < layers.size(); i++) { 223 ImageryInfo info = layers.get(i); 224 if (info.getId() == null) { 225 continue; 226 } 227 ImageryInfo matchingDefault = defaultLayerIds.get(info.getId()); 228 if (matchingDefault != null && !matchingDefault.equalsPref(info)) { 229 layers.set(i, matchingDefault); 230 changed = true; 231 } 232 } 233 234 if (changed) { 235 save(); 236 } 237 } 238 239 private boolean isSimilar(ImageryInfo iiA, ImageryInfo iiB) { 240 if (iiA == null) 241 return false; 242 if (!iiA.getImageryType().equals(iiB.getImageryType())) 243 return false; 244 if (iiA.getId() != null && iiB.getId() != null) return iiA.getId().equals(iiB.getId()); 245 return isSimilar(iiA.getUrl(), iiB.getUrl()); 246 } 247 248 // some additional checks to respect extended URLs in preferences (legacy workaround) 249 private static boolean isSimilar(String a, String b) { 250 return Objects.equals(a, b) || (a != null && b != null && !a.isEmpty() && !b.isEmpty() && (a.contains(b) || b.contains(a))); 251 } 252 253 public void add(ImageryInfo info) { 254 layers.add(info); 255 } 256 257 public void remove(ImageryInfo info) { 258 layers.remove(info); 259 } 260 261 public void save() { 262 List<ImageryPreferenceEntry> entries = new ArrayList<>(); 263 for (ImageryInfo info : layers) { 264 entries.add(new ImageryPreferenceEntry(info)); 265 } 266 Main.pref.putListOfStructs("imagery.entries", entries, ImageryPreferenceEntry.class); 267 } 268 269 public List<ImageryInfo> getLayers() { 270 return Collections.unmodifiableList(layers); 271 } 272 273 public List<ImageryInfo> getDefaultLayers() { 274 return Collections.unmodifiableList(defaultLayers); 275 } 276 277 public static void addLayer(ImageryInfo info) { 278 instance.add(info); 279 instance.save(); 280 } 281 282 public static void addLayers(Collection<ImageryInfo> infos) { 283 for (ImageryInfo i : infos) { 284 instance.add(i); 285 } 286 instance.save(); 287 Collections.sort(instance.layers); 288 } 289 290 /** 291 * Get unique id for ImageryInfo. 292 * 293 * This takes care, that no id is used twice (due to a user error) 294 * @param info the ImageryInfo to look up 295 * @return null, if there is no id or the id is used twice, 296 * the corresponding id otherwise 297 */ 298 public String getUniqueId(ImageryInfo info) { 299 if (info.getId() != null && layerIds.get(info.getId()) == info) { 300 return info.getId(); 301 } 302 return null; 303 } 304}