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