001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.mappaint;
003
004import java.util.ArrayList;
005import java.util.Arrays;
006import java.util.Collection;
007import java.util.Iterator;
008import java.util.List;
009import java.util.Objects;
010
011import org.openstreetmap.josm.data.osm.Storage;
012import org.openstreetmap.josm.tools.Pair;
013
014/**
015 * Caches styles for a single primitive.
016 * Splits the range of possible scale values (0 < scale < +Infinity) into multiple
017 * subranges, for each scale range it keeps a list of styles.
018 * Immutable class, equals & hashCode is required (the same for StyleList, ElemStyle
019 * and its subclasses).
020 */
021public final class StyleCache {
022    /* list of boundaries for the scale ranges */
023    private final List<Double> bd;
024    /* styles for each scale range */
025    private final List<StyleList> data;
026
027    private static final Storage<StyleCache> internPool = new Storage<>(); // TODO: clean up the intern pool from time to time (after purge or layer removal)
028
029    public static final StyleCache EMPTY_STYLECACHE = (new StyleCache()).intern();
030
031    private StyleCache() {
032        bd = new ArrayList<>();
033        bd.add(0.0);
034        bd.add(Double.POSITIVE_INFINITY);
035        data = new ArrayList<>();
036        data.add(null);
037    }
038
039    private StyleCache(StyleCache s) {
040        bd = new ArrayList<>(s.bd);
041        data = new ArrayList<>(s.data);
042    }
043
044    /**
045     * List of Styles, immutable
046     */
047    public static class StyleList implements Iterable<ElemStyle> {
048        private List<ElemStyle> lst;
049
050        /**
051         * Constructs a new {@code StyleList}.
052         */
053        public StyleList() {
054            lst = new ArrayList<>();
055        }
056
057        public StyleList(ElemStyle... init) {
058            lst = new ArrayList<>(Arrays.asList(init));
059        }
060
061        public StyleList(Collection<ElemStyle> sl) {
062            lst = new ArrayList<>(sl);
063        }
064
065        public StyleList(StyleList sl, ElemStyle s) {
066            lst = new ArrayList<>(sl.lst);
067            lst.add(s);
068        }
069
070        @Override
071        public Iterator<ElemStyle> iterator() {
072            return lst.iterator();
073        }
074
075        public boolean isEmpty() {
076            return lst.isEmpty();
077        }
078
079        public int size() {
080            return lst.size();
081        }
082
083        @Override
084        public String toString() {
085            return lst.toString();
086        }
087
088        @Override
089        public boolean equals(Object obj) {
090            if (obj == null || getClass() != obj.getClass())
091                return false;
092            final StyleList other = (StyleList) obj;
093            return Objects.equals(lst, other.lst);
094        }
095
096        @Override
097        public int hashCode() {
098            return lst.hashCode();
099        }
100    }
101
102    /**
103     * looks up styles for a certain scale value
104     */
105    public StyleList get(double scale) {
106        if (scale <= 0)
107            throw new IllegalArgumentException();
108        for (int i=0; i<data.size(); ++i) {
109            if (bd.get(i) < scale && scale <= bd.get(i+1)) {
110                return data.get(i);
111            }
112        }
113        throw new AssertionError();
114    }
115
116    /**
117     * looks up styles for a certain scale value and additionally returns
118     * the scale range for the returned styles
119     */
120    public Pair<StyleList, Range> getWithRange(double scale) {
121        if (scale <= 0)
122            throw new IllegalArgumentException();
123        for (int i=0; i<data.size(); ++i) {
124            if (bd.get(i) < scale && scale <= bd.get(i+1)) {
125                return new Pair<>(data.get(i), new Range(bd.get(i), bd.get(i+1)));
126            }
127        }
128        throw new AssertionError();
129    }
130
131    public StyleCache put(StyleList sl, Range r) {
132        return put(sl, r.getLower(), r.getUpper());
133    }
134
135    /**
136     * add a new styles to the cache. this is only possible, if
137     * for this scale range, there is nothing in the cache yet.
138     */
139    public StyleCache put(StyleList sl, double lower, double upper) {
140        StyleCache s = new StyleCache(this);
141        s.putImpl(sl, lower, upper);
142        s.consistencyTest();
143        return s.intern();
144    }
145
146    // this exception type is for debugging #8997 and can later be replaced
147    // by AssertionError
148    public static class RangeViolatedError extends Error {
149    }
150
151    /**
152     * ASCII-art explanation:
153     *
154     *              data[i]
155     *  --|-------|---------|--
156     * bd[i-1]  bd[i]    bd[i+1]
157     *
158     *         (--------]
159     *       lower     upper
160     */
161    private void putImpl(StyleList sl, double lower, double upper) {
162        int i=0;
163        while (bd.get(i) < lower) {
164            ++i;
165        }
166        if (bd.get(i) == lower) {
167            if (upper > bd.get(i+1))
168                throw new RangeViolatedError();
169            if (data.get(i) != null)
170                throw new AssertionError("the new range must be within a subrange that has no data");
171
172            //  --|-------|--------|--
173            //   i-1      i       i+1
174            //            (--------]
175            if (bd.get(i+1) == upper) {
176                data.set(i, sl);
177            }
178            //  --|-------|--------|--
179            //   i-1      i       i+1
180            //            (-----]
181            else {
182                bd.add(i+1, upper);
183                data.add(i, sl);
184            }
185            return;
186        } else {
187            if (bd.get(i) < upper)
188                throw new AssertionError("the new range must be within a single subrange");
189            if (data.get(i-1) != null)
190                throw new AssertionError();
191
192            //  --|-------|--------|--
193            //   i-1      i       i+1
194            //       (--]   or
195            //       (----]
196            bd.add(i, lower);
197            data.add(i, sl);
198
199            //  --|--|----|--------|--
200            //   i-1 i   i+1      i+2
201            //       (--]
202            if (bd.get(i+1) > upper) {
203                bd.add(i+1, upper);
204                data.add(i+1, null);
205            }
206            return;
207        }
208    }
209
210    public void consistencyTest() {
211        if (bd.size() < 2) throw new AssertionError();
212        if (data.size() < 1) throw new AssertionError();
213        if (bd.size() != data.size() + 1) throw new AssertionError();
214        if (bd.get(0) != 0) throw new AssertionError();
215        if (bd.get(bd.size() - 1) != Double.POSITIVE_INFINITY) throw new AssertionError();
216        for (int i=0; i<data.size() - 1; ++i) {
217            if (bd.get(i) >= bd.get(i + 1)) throw new AssertionError();
218        }
219    }
220
221    /**
222     * Like String.intern() (reduce memory consumption).
223     * StyleCache must not be changed after it has
224     * been added to the intern pool.
225     */
226    public StyleCache intern() {
227        return internPool.putUnique(this);
228    }
229
230    @Override
231    public boolean equals(Object obj) {
232        if (obj == null || getClass() != obj.getClass())
233            return false;
234        final StyleCache other = (StyleCache) obj;
235        return bd.equals(other.bd) && data.equals(other.data);
236    }
237
238    @Override
239    public int hashCode() {
240        int hash = 7;
241        hash = 23 * hash + bd.hashCode();
242        hash = 23 * hash + data.hashCode();
243        return hash;
244    }
245
246    @Override
247    public String toString() {
248        return "SC{" + bd + ' ' + data + '}';
249    }
250}