source: josm/trunk/src/org/openstreetmap/josm/data/imagery/ImageryLayerInfo.java@ 13536

Last change on this file since 13536 was 13536, checked in by stoecker, 6 years ago

add possibility to change map ids (see #14655), add overlay flag for imagery

  • Property svn:eol-style set to native
File size: 15.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.imagery;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.IOException;
7import java.util.ArrayList;
8import java.util.Arrays;
9import java.util.Collection;
10import java.util.Collections;
11import java.util.HashMap;
12import java.util.HashSet;
13import java.util.List;
14import java.util.Map;
15import java.util.Objects;
16import java.util.Set;
17import java.util.TreeSet;
18import java.util.concurrent.ExecutorService;
19
20import org.openstreetmap.josm.Main;
21import org.openstreetmap.josm.data.StructUtils;
22import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry;
23import org.openstreetmap.josm.gui.PleaseWaitRunnable;
24import org.openstreetmap.josm.io.CachedFile;
25import org.openstreetmap.josm.io.OfflineAccessException;
26import org.openstreetmap.josm.io.OnlineResource;
27import org.openstreetmap.josm.io.imagery.ImageryReader;
28import org.openstreetmap.josm.spi.preferences.Config;
29import org.openstreetmap.josm.tools.Logging;
30import org.openstreetmap.josm.tools.Utils;
31import org.xml.sax.SAXException;
32
33/**
34 * Manages the list of imagery entries that are shown in the imagery menu.
35 */
36public class ImageryLayerInfo {
37
38 /** Unique instance */
39 public static final ImageryLayerInfo instance = new ImageryLayerInfo();
40 /** List of all usable layers */
41 private final List<ImageryInfo> layers = new ArrayList<>();
42 /** List of layer ids of all usable layers */
43 private final Map<String, ImageryInfo> layerIds = new HashMap<>();
44 /** List of all available default layers */
45 private static final List<ImageryInfo> defaultLayers = new ArrayList<>();
46 /** List of all available default layers (including mirrors) */
47 private static final List<ImageryInfo> allDefaultLayers = new ArrayList<>();
48 /** List of all layer ids of available default layers (including mirrors) */
49 private static final Map<String, ImageryInfo> defaultLayerIds = new HashMap<>();
50
51 private static final String[] DEFAULT_LAYER_SITES = {
52 Main.getJOSMWebsite()+"/maps%<?ids=>"
53 };
54
55 /**
56 * Returns the list of imagery layers sites.
57 * @return the list of imagery layers sites
58 * @since 7434
59 */
60 public static Collection<String> getImageryLayersSites() {
61 return Config.getPref().getList("imagery.layers.sites", Arrays.asList(DEFAULT_LAYER_SITES));
62 }
63
64 private ImageryLayerInfo() {
65 }
66
67 /**
68 * Constructs a new {@code ImageryLayerInfo} from an existing one.
69 * @param info info to copy
70 */
71 public ImageryLayerInfo(ImageryLayerInfo info) {
72 layers.addAll(info.layers);
73 }
74
75 /**
76 * Clear the lists of layers.
77 */
78 public void clear() {
79 layers.clear();
80 layerIds.clear();
81 }
82
83 /**
84 * Loads the custom as well as default imagery entries.
85 * @param fastFail whether opening HTTP connections should fail fast, see {@link ImageryReader#setFastFail(boolean)}
86 */
87 public void load(boolean fastFail) {
88 clear();
89 List<ImageryPreferenceEntry> entries = StructUtils.getListOfStructs(
90 Config.getPref(), "imagery.entries", null, ImageryPreferenceEntry.class);
91 if (entries != null) {
92 for (ImageryPreferenceEntry prefEntry : entries) {
93 try {
94 ImageryInfo i = new ImageryInfo(prefEntry);
95 add(i);
96 } catch (IllegalArgumentException e) {
97 Logging.warn("Unable to load imagery preference entry:"+e);
98 }
99 }
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}
Note: See TracBrowser for help on using the repository browser.