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; 016import org.openstreetmap.josm.Main; 017import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry; 018import org.openstreetmap.josm.io.CachedFile; 019import org.openstreetmap.josm.io.imagery.ImageryReader; 020import org.xml.sax.SAXException; 021 022/** 023 * Manages the list of imagery entries that are shown in the imagery menu. 024 */ 025public class ImageryLayerInfo { 026 027 public static final ImageryLayerInfo instance = new ImageryLayerInfo(); 028 private final List<ImageryInfo> layers = new ArrayList<>(); 029 private final Map<String, ImageryInfo> layerIds = new HashMap<>(); 030 private final static List<ImageryInfo> defaultLayers = new ArrayList<>(); 031 private final static Map<String, ImageryInfo> defaultLayerIds = new HashMap<>(); 032 033 private static final String[] DEFAULT_LAYER_SITES = { 034 Main.getJOSMWebsite()+"/maps" 035 }; 036 037 private ImageryLayerInfo() { 038 } 039 040 public ImageryLayerInfo(ImageryLayerInfo info) { 041 layers.addAll(info.layers); 042 } 043 044 public void clear() { 045 layers.clear(); 046 layerIds.clear(); 047 } 048 049 public void load() { 050 clear(); 051 List<ImageryPreferenceEntry> entries = Main.pref.getListOfStructs("imagery.entries", null, ImageryPreferenceEntry.class); 052 if (entries != null) { 053 for (ImageryPreferenceEntry prefEntry : entries) { 054 try { 055 ImageryInfo i = new ImageryInfo(prefEntry); 056 add(i); 057 } catch (IllegalArgumentException e) { 058 Main.warn("Unable to load imagery preference entry:"+e); 059 } 060 } 061 Collections.sort(layers); 062 } 063 loadDefaults(false); 064 } 065 066 /** 067 * Loads the available imagery entries. 068 * 069 * The data is downloaded from the JOSM website (or loaded from cache). 070 * Entries marked as "default" are added to the user selection, if not 071 * already present. 072 * 073 * @param clearCache if true, clear the cache and start a fresh download. 074 */ 075 public void loadDefaults(boolean clearCache) { 076 defaultLayers.clear(); 077 defaultLayerIds.clear(); 078 for (String source : Main.pref.getCollection("imagery.layers.sites", Arrays.asList(DEFAULT_LAYER_SITES))) { 079 if (clearCache) { 080 CachedFile.cleanup(source); 081 } 082 try { 083 ImageryReader reader = new ImageryReader(source); 084 Collection<ImageryInfo> result = reader.parse(); 085 defaultLayers.addAll(result); 086 } catch (IOException ex) { 087 Main.error(ex, false); 088 } catch (SAXException ex) { 089 Main.error(ex); 090 } 091 } 092 while (defaultLayers.remove(null)); 093 Collections.sort(defaultLayers); 094 buildIdMap(defaultLayers, defaultLayerIds); 095 updateEntriesFromDefaults(); 096 buildIdMap(layers, layerIds); 097 } 098 099 /** 100 * Build the mapping of unique ids to {@link ImageryInfo}s. 101 * @param lst input list 102 * @param idMap output map 103 */ 104 private static void buildIdMap(List<ImageryInfo> lst, Map<String, ImageryInfo> idMap) { 105 idMap.clear(); 106 Set<String> notUnique = new HashSet<>(); 107 for (ImageryInfo i : lst) { 108 if (i.getId() != null) { 109 if (idMap.containsKey(i.getId())) { 110 notUnique.add(i.getId()); 111 Main.error("Id ''{0}'' is not unique - used by ''{1}'' and ''{2}''!", 112 i.getId(), i.getName(), idMap.get(i.getId()).getName()); 113 continue; 114 } 115 idMap.put(i.getId(), i); 116 } 117 } 118 for (String i : notUnique) { 119 idMap.remove(i); 120 } 121 } 122 123 /** 124 * Update user entries according to the list of default entries. 125 */ 126 public void updateEntriesFromDefaults() { 127 // add new default entries to the user selection 128 boolean changed = false; 129 Collection<String> knownDefaults = Main.pref.getCollection("imagery.layers.default"); 130 Collection<String> newKnownDefaults = new TreeSet<>(knownDefaults); 131 for (ImageryInfo def : defaultLayers) { 132 if (def.isDefaultEntry()) { 133 boolean isKnownDefault = false; 134 for (String url : knownDefaults) { 135 if (isSimilar(url, def.getUrl())) { 136 isKnownDefault = true; 137 break; 138 } 139 } 140 boolean isInUserList = false; 141 if (!isKnownDefault) { 142 newKnownDefaults.add(def.getUrl()); 143 for (ImageryInfo i : layers) { 144 if (isSimilar(def, i)) { 145 isInUserList = true; 146 break; 147 } 148 } 149 } 150 if (!isKnownDefault && !isInUserList) { 151 add(new ImageryInfo(def)); 152 changed = true; 153 } 154 } 155 } 156 Main.pref.putCollection("imagery.layers.default", newKnownDefaults); 157 158 // Add ids to user entries without id. 159 // Only do this the first time for each id, so the user can have 160 // custom entries that don't get updated automatically 161 Collection<String> addedIds = Main.pref.getCollection("imagery.layers.addedIds"); 162 Collection<String> newAddedIds = new TreeSet<>(addedIds); 163 for (ImageryInfo info : layers) { 164 for (ImageryInfo def : defaultLayers) { 165 if (isSimilar(def, info)) { 166 if (def.getId() != null && !addedIds.contains(def.getId())) { 167 if (!defaultLayerIds.containsKey(def.getId())) { 168 // ignore ids used more than once (have been purged from the map) 169 continue; 170 } 171 newAddedIds.add(def.getId()); 172 if (info.getId() == null) { 173 info.setId(def.getId()); 174 changed = true; 175 } 176 } 177 } 178 } 179 } 180 Main.pref.putCollection("imagery.layers.addedIds", newAddedIds); 181 182 // automatically update user entries with same id as a default entry 183 for (int i=0; i<layers.size(); i++) { 184 ImageryInfo info = layers.get(i); 185 if (info.getId() == null) { 186 continue; 187 } 188 ImageryInfo matchingDefault = defaultLayerIds.get(info.getId()); 189 if (matchingDefault != null && !matchingDefault.equalsPref(info)) { 190 layers.set(i, matchingDefault); 191 changed = true; 192 } 193 } 194 195 if (changed) { 196 save(); 197 } 198 } 199 200 private boolean isSimilar(ImageryInfo iiA, ImageryInfo iiB) { 201 if (iiA.getId() != null && iiB.getId() != null) return iiA.getId().equals(iiB.getId()); 202 return isSimilar(iiA.getUrl(), iiB.getUrl()); 203 } 204 205 // some additional checks to respect extended URLs in preferences (legacy workaround) 206 private boolean isSimilar(String a, String b) { 207 return Objects.equals(a, b) || (a != null && b != null && !a.isEmpty() && !b.isEmpty() && (a.contains(b) || b.contains(a))); 208 } 209 210 public void add(ImageryInfo info) { 211 layers.add(info); 212 } 213 214 public void remove(ImageryInfo info) { 215 layers.remove(info); 216 } 217 218 public void save() { 219 List<ImageryPreferenceEntry> entries = new ArrayList<>(); 220 for (ImageryInfo info : layers) { 221 entries.add(new ImageryPreferenceEntry(info)); 222 } 223 Main.pref.putListOfStructs("imagery.entries", entries, ImageryPreferenceEntry.class); 224 } 225 226 public List<ImageryInfo> getLayers() { 227 return Collections.unmodifiableList(layers); 228 } 229 230 public List<ImageryInfo> getDefaultLayers() { 231 return Collections.unmodifiableList(defaultLayers); 232 } 233 234 public static void addLayer(ImageryInfo info) { 235 instance.add(info); 236 instance.save(); 237 } 238 239 public static void addLayers(Collection<ImageryInfo> infos) { 240 for (ImageryInfo i : infos) { 241 instance.add(i); 242 } 243 instance.save(); 244 Collections.sort(instance.layers); 245 } 246 247 /** 248 * Get unique id for ImageryInfo. 249 * 250 * This takes care, that no id is used twice (due to a user error) 251 * @param info the ImageryInfo to look up 252 * @return null, if there is no id or the id is used twice, 253 * the corresponding id otherwise 254 */ 255 public String getUniqueId(ImageryInfo info) { 256 if (info.getId() != null && layerIds.get(info.getId()) == info) { 257 return info.getId(); 258 } 259 return null; 260 } 261}