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