001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.mappaint;
003
004import java.util.Map;
005import java.util.Objects;
006import java.util.concurrent.ConcurrentHashMap;
007import java.util.function.Function;
008
009import org.openstreetmap.josm.tools.Pair;
010
011/**
012 * Caches styles for a single primitive.
013 * <p>
014 * This object is immutable.
015 */
016public final class StyleCache {
017
018    // TODO: clean up the intern pool from time to time (after purge or layer removal)
019    private static final Map<StyleCache, StyleCache> internPool = new ConcurrentHashMap<>();
020
021    /**
022     * An empty style cache entry
023     */
024    public static final StyleCache EMPTY_STYLECACHE = new StyleCache().intern();
025
026    private DividedScale<StyleElementList> plainStyle;
027    private DividedScale<StyleElementList> selectedStyle;
028
029    private StyleCache(StyleCache sc) {
030        plainStyle = sc.plainStyle;
031        selectedStyle = sc.selectedStyle;
032    }
033
034    private StyleCache() {
035    }
036
037    /**
038     * Creates a new copy of this style cache with a new entry added.
039     * @param o The style to cache.
040     * @param r The range the style is for.
041     * @param selected The style list we should use (selected/unselected)
042     * @return The new object.
043     */
044    public StyleCache put(StyleElementList o, Range r, boolean selected) {
045        StyleCache s = new StyleCache(this);
046
047        if (selected) {
048            s.selectedStyle = scale(s.selectedStyle).put(o, r);
049        } else {
050            s.plainStyle = scale(s.plainStyle).put(o, r);
051        }
052        return s.intern();
053    }
054
055    private static DividedScale<StyleElementList> scale(DividedScale<StyleElementList> scale) {
056        return scale == null ? new DividedScale<>() : scale;
057    }
058
059    /**
060     * Get the style for a specific style. Returns the range as well.
061     * @param scale The current scale
062     * @param selected true to get the state for a selected element,
063     * @return The style and the range it is valid for.
064     */
065    public Pair<StyleElementList, Range> getWithRange(double scale, boolean selected) {
066        DividedScale<StyleElementList> style = selected ? selectedStyle : plainStyle;
067        return style != null ? style.getWithRange(scale) : Pair.create(null, Range.ZERO_TO_INFINITY);
068    }
069
070    @Override
071    public String toString() {
072        return "StyleCache{PLAIN: " + plainStyle + " SELECTED: " + selectedStyle + "}";
073    }
074
075    @Override
076    public int hashCode() {
077        return 31 * Objects.hashCode(plainStyle) + Objects.hashCode(selectedStyle);
078    }
079
080    @Override
081    public boolean equals(Object obj) {
082        if (obj == null) return false;
083        if (getClass() != obj.getClass()) return false;
084        final StyleCache other = (StyleCache) obj;
085        return Objects.equals(plainStyle, other.plainStyle) && Objects.equals(selectedStyle, other.selectedStyle);
086    }
087
088    /**
089     * Like String.intern() (reduce memory consumption).
090     * StyleCache must not be changed after it has been added to the intern pool.
091     * @return style cache
092     */
093    private StyleCache intern() {
094        return internPool.computeIfAbsent(this, Function.identity());
095    }
096
097    /**
098     * Clears the style cache. This should only be used for testing.
099     * It may be removed some day and replaced by a WeakReference implementation that automatically forgets old entries.
100     */
101    static void clearStyleCachePool() {
102        internPool.clear();
103    }
104
105    /**
106     * Get the size of the intern pool. Only for tests!
107     * @return size of the intern pool
108     */
109    public static int getInternPoolSize() {
110        return internPool.size();
111    }
112}