// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.mappaint;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.swing.ImageIcon;
import javax.swing.SwingUtilities;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.Tag;
import org.openstreetmap.josm.gui.PleaseWaitRunnable;
import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
import org.openstreetmap.josm.gui.mappaint.styleelement.MapImage;
import org.openstreetmap.josm.gui.mappaint.styleelement.NodeElement;
import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement;
import org.openstreetmap.josm.gui.preferences.SourceEntry;
import org.openstreetmap.josm.gui.preferences.map.MapPaintPreference.MapPaintPrefHelper;
import org.openstreetmap.josm.gui.progress.ProgressMonitor;
import org.openstreetmap.josm.io.CachedFile;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.Utils;
/**
* This class manages the list of available map paint styles and gives access to
* the ElemStyles singleton.
*
* On change, {@link MapPaintSylesUpdateListener#mapPaintStylesUpdated()} is fired
* for all listeners.
*/
public final class MapPaintStyles {
private static final Collection<String> DEPRECATED_IMAGE_NAMES = Arrays.asList(
"presets/misc/deprecated.svg",
"misc/deprecated.png");
private static ElemStyles styles = new ElemStyles();
/**
* Returns the {@link ElemStyles} singleton instance.
*
* The returned object is read only, any manipulation happens via one of
* the other wrapper methods in this class. ({@link #readFromPreferences},
* {@link #moveStyles}, ...)
* @return the {@code ElemStyles} singleton instance
*/
public static ElemStyles getStyles() {
return styles;
}
private MapPaintStyles() {
// Hide default constructor for utils classes
}
/**
* Value holder for a reference to a tag name. A style instruction
* <pre>
* text: a_tag_name;
* </pre>
* results in a tag reference for the tag <tt>a_tag_name</tt> in the
* style cascade.
*/
public static class TagKeyReference {
public final String key;
public TagKeyReference(String key) {
this.key = key;
}
@Override
public String toString() {
return "TagKeyReference{" + "key='" + key + "'}";
}
}
/**
* IconReference is used to remember the associated style source for each icon URL.
* This is necessary because image URLs can be paths relative
* to the source file and we have cascading of properties from different source files.
*/
public static class IconReference {
public final String iconName;
public final StyleSource source;
public IconReference(String iconName, StyleSource source) {
this.iconName = iconName;
this.source = source;
}
@Override
public String toString() {
return "IconReference{" + "iconName='" + iconName + "' source='" + source.getDisplayString() + "'}";
}
/**
* Determines whether this icon represents a deprecated icon
* @return whether this icon represents a deprecated icon
* @since 10927
*/
public boolean isDeprecatedIcon() {
return DEPRECATED_IMAGE_NAMES.contains(iconName);
}
}
/**
* Image provider for icon. Note that this is a provider only. A @link{ImageProvider#get()} call may still fail!
*
* @param ref reference to the requested icon
* @param test if <code>true</code> than the icon is request is tested
* @return image provider for icon (can be <code>null</code> when <code>test</code> is <code>true</code>).
* @see #getIcon(IconReference, int,int)
* @since 8097
*/
public static ImageProvider getIconProvider(IconReference ref, boolean test) {
final String namespace = ref.source.getPrefName();
ImageProvider i = new ImageProvider(ref.iconName)
.setDirs(getIconSourceDirs(ref.source))
.setId("mappaint."+namespace)
.setArchive(ref.source.zipIcons)
.setInArchiveDir(ref.source.getZipEntryDirName())
.setOptional(true);
if (test && i.get() == null) {
String msg = "Mappaint style \""+namespace+"\" ("+ref.source.getDisplayString()+") icon \"" + ref.iconName + "\" not found.";
ref.source.logWarning(msg);
Main.warn(msg);
return null;
}
return i;
}
/**
* Return scaled icon.
*
* @param ref reference to the requested icon
* @param width icon width or -1 for autoscale
* @param height icon height or -1 for autoscale
* @return image icon or <code>null</code>.
* @see #getIconProvider(IconReference, boolean)
*/
public static ImageIcon getIcon(IconReference ref, int width, int height) {
final String namespace = ref.source.getPrefName();
ImageIcon i = getIconProvider(ref, false).setSize(width, height).get();
if (i == null) {
Main.warn("Mappaint style \""+namespace+"\" ("+ref.source.getDisplayString()+") icon \"" + ref.iconName + "\" not found.");
return null;
}
return i;
}
/**
* No icon with the given name was found, show a dummy icon instead
* @param source style source
* @return the icon misc/no_icon.png, in descending priority:
* - relative to source file
* - from user icon paths
* - josm's default icon
* can be null if the defaults are turned off by user
*/
public static ImageIcon getNoIconIcon(StyleSource source) {
return new ImageProvider("presets/misc/no_icon")
.setDirs(getIconSourceDirs(source))
.setId("mappaint."+source.getPrefName())
.setArchive(source.zipIcons)
.setInArchiveDir(source.getZipEntryDirName())
.setOptional(true).get();
}
public static ImageIcon getNodeIcon(Tag tag) {
return getNodeIcon(tag, true);
}
/**
* Returns the node icon that would be displayed for the given tag.
* @param tag The tag to look an icon for
* @param includeDeprecatedIcon if {@code true}, the special deprecated icon will be returned if applicable
* @return {@code null} if no icon found, or if the icon is deprecated and not wanted
*/
public static ImageIcon getNodeIcon(Tag tag, boolean includeDeprecatedIcon) {
if (tag != null) {
DataSet ds = new DataSet();
Node virtualNode = new Node(LatLon.ZERO);
virtualNode.put(tag.getKey(), tag.getValue());
StyleElementList styleList;
MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
try {
// Add primitive to dataset to avoid DataIntegrityProblemException when evaluating selectors
ds.addPrimitive(virtualNode);
styleList = getStyles().generateStyles(virtualNode, 0.5, false).a;
ds.removePrimitive(virtualNode);
} finally {
MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
}
if (styleList != null) {
for (StyleElement style : styleList) {
if (style instanceof NodeElement) {
MapImage mapImage = ((NodeElement) style).mapImage;
if (mapImage != null) {
if (includeDeprecatedIcon || mapImage.name == null || !DEPRECATED_IMAGE_NAMES.contains(mapImage.name)) {
return new ImageIcon(mapImage.getImage(false));
} else {
return null; // Deprecated icon found but not wanted
}
}
}
}
}
}
return null;
}
public static List<String> getIconSourceDirs(StyleSource source) {
List<String> dirs = new LinkedList<>();
File sourceDir = source.getLocalSourceDir();
if (sourceDir != null) {
dirs.add(sourceDir.getPath());
}
Collection<String> prefIconDirs = Main.pref.getCollection("mappaint.icon.sources");
for (String fileset : prefIconDirs) {
String[] a;
if (fileset.indexOf('=') >= 0) {
a = fileset.split("=", 2);
} else {
a = new String[] {"", fileset};
}
/* non-prefixed path is generic path, always take it */
if (a[0].isEmpty() || source.getPrefName().equals(a[0])) {
dirs.add(a[1]);
}
}
if (Main.pref.getBoolean("mappaint.icon.enable-defaults", true)) {
/* don't prefix icon path, as it should be generic */
dirs.add("resource://images/");
}
return dirs;
}
/**
* Reloads all styles from the preferences.
*/
public static void readFromPreferences() {
styles.clear();
Collection<? extends SourceEntry> sourceEntries = MapPaintPrefHelper.INSTANCE.get();
for (SourceEntry entry : sourceEntries) {
styles.add(fromSourceEntry(entry));
}
for (StyleSource source : styles.getStyleSources()) {
loadStyleForFirstTime(source);
}
fireMapPaintSylesUpdated();
}
private static void loadStyleForFirstTime(StyleSource source) {
final long startTime = System.currentTimeMillis();
source.loadStyleSource();
if (Main.pref.getBoolean("mappaint.auto_reload_local_styles", true) && source.isLocal()) {
try {
Main.fileWatcher.registerStyleSource(source);
} catch (IOException | IllegalStateException e) {
Main.error(e);
}
}
if (Main.isDebugEnabled() || !source.isValid()) {
final long elapsedTime = System.currentTimeMillis() - startTime;
String message = "Initializing map style " + source.url + " completed in " + Utils.getDurationString(elapsedTime);
if (!source.isValid()) {
Main.warn(message + " (" + source.getErrors().size() + " errors, " + source.getWarnings().size() + " warnings)");
} else {
Main.debug(message);
}
}
}
private static StyleSource fromSourceEntry(SourceEntry entry) {
if (entry.url == null && entry instanceof MapCSSStyleSource) {
return (MapCSSStyleSource) entry;
}
Set<String> mimes = new HashSet<>(Arrays.asList(MapCSSStyleSource.MAPCSS_STYLE_MIME_TYPES.split(", ")));
try (CachedFile cf = new CachedFile(entry.url).setHttpAccept(Utils.join(", ", mimes))) {
String zipEntryPath = cf.findZipEntryPath("mapcss", "style");
if (zipEntryPath != null) {
entry.isZip = true;
entry.zipEntryPath = zipEntryPath;
}
return new MapCSSStyleSource(entry);
}
}
/**
* reload styles
* preferences are the same, but the file source may have changed
* @param sel the indices of styles to reload
*/
public static void reloadStyles(final int... sel) {
List<StyleSource> toReload = new ArrayList<>();
List<StyleSource> data = styles.getStyleSources();
for (int i : sel) {
toReload.add(data.get(i));
}
Main.worker.submit(new MapPaintStyleLoader(toReload));
}
public static class MapPaintStyleLoader extends PleaseWaitRunnable {
private boolean canceled;
private final Collection<StyleSource> sources;
public MapPaintStyleLoader(Collection<StyleSource> sources) {
super(tr("Reloading style sources"));
this.sources = sources;
}
@Override
protected void cancel() {
canceled = true;
}
@Override
protected void finish() {
SwingUtilities.invokeLater(() -> {
fireMapPaintSylesUpdated();
styles.clearCached();
if (Main.isDisplayingMapView()) {
Main.map.mapView.preferenceChanged(null);
Main.map.mapView.repaint();
}
});
}
@Override
protected void realRun() {
ProgressMonitor monitor = getProgressMonitor();
monitor.setTicksCount(sources.size());
for (StyleSource s : sources) {
if (canceled)
return;
monitor.subTask(tr("loading style ''{0}''...", s.getDisplayString()));
s.loadStyleSource();
monitor.worked(1);
}
}
}
/**
* Move position of entries in the current list of StyleSources
* @param sel The indices of styles to be moved.
* @param delta The number of lines it should move. positive int moves
* down and negative moves up.
*/
public static void moveStyles(int[] sel, int delta) {
if (!canMoveStyles(sel, delta))
return;
int[] selSorted = Utils.copyArray(sel);
Arrays.sort(selSorted);
List<StyleSource> data = new ArrayList<>(styles.getStyleSources());
for (int row: selSorted) {
StyleSource t1 = data.get(row);
StyleSource t2 = data.get(row + delta);
data.set(row, t2);
data.set(row + delta, t1);
}
styles.setStyleSources(data);
MapPaintPrefHelper.INSTANCE.put(data);
fireMapPaintSylesUpdated();
styles.clearCached();
Main.map.mapView.repaint();
}
public static boolean canMoveStyles(int[] sel, int i) {
if (sel.length == 0)
return false;
int[] selSorted = Utils.copyArray(sel);
Arrays.sort(selSorted);
if (i < 0) // Up
return selSorted[0] >= -i;
else if (i > 0) // Down
return selSorted[selSorted.length-1] <= styles.getStyleSources().size() - 1 - i;
else
return true;
}
public static void toggleStyleActive(int... sel) {
List<StyleSource> data = styles.getStyleSources();
for (int p : sel) {
StyleSource s = data.get(p);
s.active = !s.active;
}
MapPaintPrefHelper.INSTANCE.put(data);
if (sel.length == 1) {
fireMapPaintStyleEntryUpdated(sel[0]);
} else {
fireMapPaintSylesUpdated();
}
styles.clearCached();
Main.map.mapView.repaint();
}
/**
* Add a new map paint style.
* @param entry map paint style
* @return loaded style source, or {@code null}
*/
public static StyleSource addStyle(SourceEntry entry) {
StyleSource source = fromSourceEntry(entry);
styles.add(source);
loadStyleForFirstTime(source);
refreshStyles();
return source;
}
/**
* Remove a map paint style.
* @param entry map paint style
* @since 11493
*/
public static void removeStyle(SourceEntry entry) {
StyleSource source = fromSourceEntry(entry);
if (styles.remove(source)) {
refreshStyles();
}
}
private static void refreshStyles() {
MapPaintPrefHelper.INSTANCE.put(styles.getStyleSources());
fireMapPaintSylesUpdated();
styles.clearCached();
if (Main.isDisplayingMapView()) {
Main.map.mapView.repaint();
}
}
/***********************************
* MapPaintSylesUpdateListener & related code
* (get informed when the list of MapPaint StyleSources changes)
*/
public interface MapPaintSylesUpdateListener {
void mapPaintStylesUpdated();
void mapPaintStyleEntryUpdated(int idx);
}
private static final CopyOnWriteArrayList<MapPaintSylesUpdateListener> listeners
= new CopyOnWriteArrayList<>();
public static void addMapPaintSylesUpdateListener(MapPaintSylesUpdateListener listener) {
if (listener != null) {
listeners.addIfAbsent(listener);
}
}
public static void removeMapPaintSylesUpdateListener(MapPaintSylesUpdateListener listener) {
listeners.remove(listener);
}
public static void fireMapPaintSylesUpdated() {
for (MapPaintSylesUpdateListener l : listeners) {
l.mapPaintStylesUpdated();
}
}
public static void fireMapPaintStyleEntryUpdated(int idx) {
for (MapPaintSylesUpdateListener l : listeners) {
l.mapPaintStyleEntryUpdated(idx);
}
}
}