// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.data.imagery;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry;
import org.openstreetmap.josm.gui.PleaseWaitRunnable;
import org.openstreetmap.josm.io.CachedFile;
import org.openstreetmap.josm.io.OfflineAccessException;
import org.openstreetmap.josm.io.OnlineResource;
import org.openstreetmap.josm.io.imagery.ImageryReader;
import org.openstreetmap.josm.tools.Utils;
import org.xml.sax.SAXException;
/**
* Manages the list of imagery entries that are shown in the imagery menu.
*/
public class ImageryLayerInfo {
public static final ImageryLayerInfo instance = new ImageryLayerInfo();
/** List of all usable layers */
private final List<ImageryInfo> layers = new ArrayList<>();
/** List of layer ids of all usable layers */
private final Map<String, ImageryInfo> layerIds = new HashMap<>();
/** List of all available default layers */
private static final List<ImageryInfo> defaultLayers = new ArrayList<>();
/** List of all available default layers (including mirrors) */
private static final List<ImageryInfo> allDefaultLayers = new ArrayList<>();
/** List of all layer ids of available default layers (including mirrors) */
private static final Map<String, ImageryInfo> defaultLayerIds = new HashMap<>();
private static final String[] DEFAULT_LAYER_SITES = {
Main.getJOSMWebsite()+"/maps"
};
/**
* Returns the list of imagery layers sites.
* @return the list of imagery layers sites
* @since 7434
*/
public static Collection<String> getImageryLayersSites() {
return Main.pref.getCollection("imagery.layers.sites", Arrays.asList(DEFAULT_LAYER_SITES));
}
private ImageryLayerInfo() {
}
public ImageryLayerInfo(ImageryLayerInfo info) {
layers.addAll(info.layers);
}
public void clear() {
layers.clear();
layerIds.clear();
}
/**
* Loads the custom as well as default imagery entries.
* @param fastFail whether opening HTTP connections should fail fast, see {@link ImageryReader#setFastFail(boolean)}
*/
public void load(boolean fastFail) {
clear();
List<ImageryPreferenceEntry> entries = Main.pref.getListOfStructs("imagery.entries", null, ImageryPreferenceEntry.class);
if (entries != null) {
for (ImageryPreferenceEntry prefEntry : entries) {
try {
ImageryInfo i = new ImageryInfo(prefEntry);
add(i);
} catch (IllegalArgumentException e) {
Main.warn("Unable to load imagery preference entry:"+e);
}
}
Collections.sort(layers);
}
loadDefaults(false, true, fastFail);
}
/**
* Loads the available imagery entries.
*
* The data is downloaded from the JOSM website (or loaded from cache).
* Entries marked as "default" are added to the user selection, if not
* already present.
*
* @param clearCache if true, clear the cache and start a fresh download.
* @param quiet whether not the loading should be performed using a {@link PleaseWaitRunnable} in the background
* @param fastFail whether opening HTTP connections should fail fast, see {@link ImageryReader#setFastFail(boolean)}
*/
public void loadDefaults(boolean clearCache, boolean quiet, boolean fastFail) {
final DefaultEntryLoader loader = new DefaultEntryLoader(clearCache, fastFail);
if (quiet) {
loader.realRun();
loader.finish();
} else {
Main.worker.execute(new DefaultEntryLoader(clearCache, fastFail));
}
}
/**
* Loader/updater of the available imagery entries
*/
class DefaultEntryLoader extends PleaseWaitRunnable {
private final boolean clearCache;
private final boolean fastFail;
private final List<ImageryInfo> newLayers = new ArrayList<>();
private ImageryReader reader;
private boolean canceled;
private boolean loadError;
DefaultEntryLoader(boolean clearCache, boolean fastFail) {
super(tr("Update default entries"));
this.clearCache = clearCache;
this.fastFail = fastFail;
}
@Override
protected void cancel() {
canceled = true;
Utils.close(reader);
}
@Override
protected void realRun() {
for (String source : getImageryLayersSites()) {
if (canceled) {
return;
}
loadSource(source);
}
}
protected void loadSource(String source) {
boolean online = true;
try {
OnlineResource.JOSM_WEBSITE.checkOfflineAccess(source, Main.getJOSMWebsite());
} catch (OfflineAccessException e) {
Main.warn(e, false);
online = false;
}
if (clearCache && online) {
CachedFile.cleanup(source);
}
try {
reader = new ImageryReader(source);
reader.setFastFail(fastFail);
Collection<ImageryInfo> result = reader.parse();
newLayers.addAll(result);
} catch (IOException ex) {
loadError = true;
Main.error(ex, false);
} catch (SAXException ex) {
loadError = true;
Main.error(ex);
}
}
@Override
protected void finish() {
defaultLayers.clear();
allDefaultLayers.clear();
defaultLayers.addAll(newLayers);
for (ImageryInfo layer : newLayers) {
allDefaultLayers.add(layer);
for (ImageryInfo sublayer : layer.getMirrors()) {
allDefaultLayers.add(sublayer);
}
}
defaultLayerIds.clear();
Collections.sort(defaultLayers);
Collections.sort(allDefaultLayers);
buildIdMap(allDefaultLayers, defaultLayerIds);
updateEntriesFromDefaults(!loadError);
buildIdMap(layers, layerIds);
if (!loadError && !defaultLayerIds.isEmpty()) {
dropOldEntries();
}
}
}
/**
* Build the mapping of unique ids to {@link ImageryInfo}s.
* @param lst input list
* @param idMap output map
*/
private static void buildIdMap(List<ImageryInfo> lst, Map<String, ImageryInfo> idMap) {
idMap.clear();
Set<String> notUnique = new HashSet<>();
for (ImageryInfo i : lst) {
if (i.getId() != null) {
if (idMap.containsKey(i.getId())) {
notUnique.add(i.getId());
Main.error("Id ''{0}'' is not unique - used by ''{1}'' and ''{2}''!",
i.getId(), i.getName(), idMap.get(i.getId()).getName());
continue;
}
idMap.put(i.getId(), i);
}
}
for (String i : notUnique) {
idMap.remove(i);
}
}
/**
* Update user entries according to the list of default entries.
* @param dropold if <code>true</code> old entries should be removed
* @since 11706
*/
public void updateEntriesFromDefaults(boolean dropold) {
// add new default entries to the user selection
boolean changed = false;
Collection<String> knownDefaults = new TreeSet<>(Main.pref.getCollection("imagery.layers.default"));
Collection<String> newKnownDefaults = new TreeSet<>();
for (ImageryInfo def : defaultLayers) {
if (def.isDefaultEntry()) {
boolean isKnownDefault = false;
for (String entry : knownDefaults) {
if (entry.equals(def.getId())) {
isKnownDefault = true;
newKnownDefaults.add(entry);
knownDefaults.remove(entry);
break;
} else if (isSimilar(entry, def.getUrl())) {
isKnownDefault = true;
if (def.getId() != null) {
newKnownDefaults.add(def.getId());
}
knownDefaults.remove(entry);
break;
}
}
boolean isInUserList = false;
if (!isKnownDefault) {
if (def.getId() != null) {
newKnownDefaults.add(def.getId());
for (ImageryInfo i : layers) {
if (isSimilar(def, i)) {
isInUserList = true;
break;
}
}
} else {
Main.error("Default imagery ''{0}'' has no id. Skipping.", def.getName());
}
}
if (!isKnownDefault && !isInUserList) {
add(new ImageryInfo(def));
changed = true;
}
}
}
if (!dropold && !knownDefaults.isEmpty()) {
newKnownDefaults.addAll(knownDefaults);
}
Main.pref.putCollection("imagery.layers.default", newKnownDefaults);
// automatically update user entries with same id as a default entry
for (int i = 0; i < layers.size(); i++) {
ImageryInfo info = layers.get(i);
if (info.getId() == null) {
continue;
}
ImageryInfo matchingDefault = defaultLayerIds.get(info.getId());
if (matchingDefault != null && !matchingDefault.equalsPref(info)) {
layers.set(i, matchingDefault);
Main.info(tr("Update imagery ''{0}''", info.getName()));
changed = true;
}
}
if (changed) {
save();
}
}
/**
* Drop entries with Id which do no longer exist (removed from defaults).
* @since 11527
*/
public void dropOldEntries() {
List<String> drop = new ArrayList<>();
for (Map.Entry<String, ImageryInfo> info : layerIds.entrySet()) {
if (!defaultLayerIds.containsKey(info.getKey())) {
remove(info.getValue());
drop.add(info.getKey());
Main.info(tr("Drop old imagery ''{0}''", info.getValue().getName()));
}
}
if (!drop.isEmpty()) {
for (String id : drop) {
layerIds.remove(id);
}
save();
}
}
private static boolean isSimilar(ImageryInfo iiA, ImageryInfo iiB) {
if (iiA == null)
return false;
if (!iiA.getImageryType().equals(iiB.getImageryType()))
return false;
if (iiA.getId() != null && iiB.getId() != null) return iiA.getId().equals(iiB.getId());
return isSimilar(iiA.getUrl(), iiB.getUrl());
}
// some additional checks to respect extended URLs in preferences (legacy workaround)
private static boolean isSimilar(String a, String b) {
return Objects.equals(a, b) || (a != null && b != null && !a.isEmpty() && !b.isEmpty() && (a.contains(b) || b.contains(a)));
}
public void add(ImageryInfo info) {
layers.add(info);
}
public void remove(ImageryInfo info) {
layers.remove(info);
}
public void save() {
List<ImageryPreferenceEntry> entries = new ArrayList<>();
for (ImageryInfo info : layers) {
entries.add(new ImageryPreferenceEntry(info));
}
Main.pref.putListOfStructs("imagery.entries", entries, ImageryPreferenceEntry.class);
}
/**
* List of usable layers
* @return unmodifiable list containing usable layers
*/
public List<ImageryInfo> getLayers() {
return Collections.unmodifiableList(layers);
}
/**
* List of available default layers
* @return unmodifiable list containing available default layers
*/
public List<ImageryInfo> getDefaultLayers() {
return Collections.unmodifiableList(defaultLayers);
}
/**
* List of all available default layers (including mirrors)
* @return unmodifiable list containing available default layers
* @since 11570
*/
public List<ImageryInfo> getAllDefaultLayers() {
return Collections.unmodifiableList(allDefaultLayers);
}
public static void addLayer(ImageryInfo info) {
instance.add(info);
instance.save();
}
public static void addLayers(Collection<ImageryInfo> infos) {
for (ImageryInfo i : infos) {
instance.add(i);
}
instance.save();
Collections.sort(instance.layers);
}
/**
* Get unique id for ImageryInfo.
*
* This takes care, that no id is used twice (due to a user error)
* @param info the ImageryInfo to look up
* @return null, if there is no id or the id is used twice,
* the corresponding id otherwise
*/
public String getUniqueId(ImageryInfo info) {
if (info.getId() != null && layerIds.get(info.getId()) == info) {
return info.getId();
}
return null;
}
}