// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.mappaint; import java.util.ArrayList; import java.util.List; import java.util.Objects; import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement; import org.openstreetmap.josm.tools.Pair; /** * Splits the range of possible scale values (0 < scale < +Infinity) into * multiple subranges, for each scale range it keeps a data object of a certain * type T (can be null). * * Used for caching style information for different zoom levels. * * Immutable class, equals & hashCode is required (the same for * {@link StyleElementList}, {@link StyleElement} and its subclasses). * * @param <T> the type of the data objects */ public class DividedScale<T> { /** * This exception type is for debugging #8997 and can later be replaced by AssertionError */ public static class RangeViolatedError extends RuntimeException { /** * Constructs a new {@code RangeViolatedError} * @param message error message */ public RangeViolatedError(String message) { super(message); } } /* list of boundaries for the scale ranges */ private final List<Double> bd; /* data objects for each scale range */ private final List<T> data; protected DividedScale() { bd = new ArrayList<>(); bd.add(0.0); bd.add(Double.POSITIVE_INFINITY); data = new ArrayList<>(); data.add(null); } protected DividedScale(DividedScale<T> s) { bd = new ArrayList<>(s.bd); data = new ArrayList<>(s.data); } /** * Looks up the data object for a certain scale value. * * @param scale scale * @return the data object at the given scale, can be null */ public T get(double scale) { if (scale <= 0) throw new IllegalArgumentException("scale must be <= 0 but is "+scale); for (int i = 0; i < data.size(); ++i) { if (bd.get(i) < scale && scale <= bd.get(i+1)) { return data.get(i); } } throw new AssertionError(); } /** * Looks up the data object for a certain scale value and additionally returns * the scale range where the object is valid. * * @param scale scale * @return pair containing data object and range */ public Pair<T, Range> getWithRange(double scale) { if (scale <= 0) throw new IllegalArgumentException("scale must be <= 0 but is "+scale); for (int i = 0; i < data.size(); ++i) { if (bd.get(i) < scale && scale <= bd.get(i+1)) { return new Pair<>(data.get(i), new Range(bd.get(i), bd.get(i+1))); } } throw new AssertionError(); } /** * Add data object which is valid for the given range. * * This is only possible, if there is no data for the given range yet. * * @param o data object * @param r the valid range * @return a new, updated, <code>DividedScale</code> object */ public DividedScale<T> put(T o, Range r) { DividedScale<T> s = new DividedScale<>(this); s.putImpl(o, r.getLower(), r.getUpper()); s.consistencyTest(); return s; } /** * Implementation of the <code>put</code> operation. * * ASCII-art explanation: * * data[i] * --|-------|---------|-- * bd[i-1] bd[i] bd[i+1] * * (--------] * lower upper * @param o data object * @param lower lower bound * @param upper upper bound */ private void putImpl(T o, double lower, double upper) { int i = 0; while (bd.get(i) < lower) { ++i; } if (bd.get(i) == lower) { if (upper > bd.get(i+1)) throw new RangeViolatedError("the new range must be within a single subrange (1)"); if (data.get(i) != null) throw new RangeViolatedError("the new range must be within a subrange that has no data"); if (bd.get(i+1) == upper) { // --|-------|--------|-- // i-1 i i+1 // (--------] data.set(i, o); } else { // --|-------|--------|-- // i-1 i i+1 // (-----] bd.add(i+1, upper); data.add(i, o); } } else { if (bd.get(i) < upper) throw new RangeViolatedError("the new range must be within a single subrange (2)"); if (data.get(i-1) != null) throw new AssertionError(); // --|-------|--------|-- // i-1 i i+1 // (--] or // (----] bd.add(i, lower); data.add(i, o); // --|--|----|--------|-- // i-1 i i+1 i+2 // (--] if (bd.get(i+1) > upper) { bd.add(i+1, upper); data.add(i+1, null); } } } /** * Runs a consistency test. * @throws AssertionError When an invariant is broken. */ public void consistencyTest() { if (bd.size() < 2) throw new AssertionError(bd); if (data.isEmpty()) throw new AssertionError(data); if (bd.size() != data.size() + 1) throw new AssertionError(); if (bd.get(0) != 0) throw new AssertionError(); if (bd.get(bd.size() - 1) != Double.POSITIVE_INFINITY) throw new AssertionError(); for (int i = 0; i < data.size() - 1; ++i) { if (bd.get(i) >= bd.get(i + 1)) throw new AssertionError(); } } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; DividedScale<?> that = (DividedScale<?>) obj; return Objects.equals(bd, that.bd) && Objects.equals(data, that.data); } @Override public int hashCode() { return Objects.hash(bd, data); } @Override public String toString() { return "DS{" + bd + ' ' + data + '}'; } }