001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm.visitor.paint;
003
004import java.util.Iterator;
005import java.util.List;
006import java.util.NoSuchElementException;
007import java.util.stream.Collectors;
008
009import org.openstreetmap.josm.data.osm.Node;
010import org.openstreetmap.josm.gui.MapViewState;
011import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
012import org.openstreetmap.josm.tools.Utils;
013
014/**
015 * Iterates over a list of Way Nodes and returns screen coordinates that
016 * represent a line that is shifted by a certain offset perpendicular
017 * to the way direction.
018 *
019 * There is no intention, to handle consecutive duplicate Nodes in a
020 * perfect way, but it should not throw an exception.
021 * @since 11696 made public
022 */
023public class OffsetIterator implements Iterator<MapViewPoint> {
024    private final MapViewState mapState;
025    private final List<MapViewPoint> nodes;
026    private final double offset;
027    private int idx;
028
029    private MapViewPoint prev;
030    /* 'prev0' is a point that has distance 'offset' from 'prev' and the
031     * line from 'prev' to 'prev0' is perpendicular to the way segment from
032     * 'prev' to the current point.
033     */
034    private double xPrev0;
035    private double yPrev0;
036
037    /**
038     * Creates a new offset iterator
039     * @param nodes The nodes of the original line
040     * @param offset The offset of the line.
041     */
042    public OffsetIterator(List<MapViewPoint> nodes, double offset) {
043        if (nodes.size() < 2) {
044            throw new IllegalArgumentException("There must be at least 2 nodes.");
045        }
046        this.mapState = nodes.get(0).getMapViewState();
047        this.nodes = nodes;
048        this.offset = offset;
049    }
050
051    /**
052     * Creates a new offset iterator
053     * @param mapState The map view state this iterator is for.
054     * @param nodes The nodes of the original line
055     * @param offset The offset of the line.
056     */
057    public OffsetIterator(MapViewState mapState, List<Node> nodes, double offset) {
058        this.mapState = mapState;
059        this.nodes = nodes.stream().filter(Node::isLatLonKnown).map(mapState::getPointFor).collect(Collectors.toList());
060        this.offset = offset;
061    }
062
063    @Override
064    public boolean hasNext() {
065        return idx < nodes.size();
066    }
067
068    @Override
069    public MapViewPoint next() {
070        if (!hasNext())
071            throw new NoSuchElementException();
072
073        MapViewPoint current = getForIndex(idx);
074
075        if (Math.abs(offset) < 0.1d) {
076            idx++;
077            return current;
078        }
079
080        double xCurrent = current.getInViewX();
081        double yCurrent = current.getInViewY();
082        if (idx == nodes.size() - 1) {
083            ++idx;
084            if (prev != null) {
085                return mapState.getForView(xPrev0 + xCurrent - prev.getInViewX(),
086                                           yPrev0 + yCurrent - prev.getInViewY());
087            } else {
088                return current;
089            }
090        }
091
092        MapViewPoint next = getForIndex(idx + 1);
093        double dxNext = next.getInViewX() - xCurrent;
094        double dyNext = next.getInViewY() - yCurrent;
095        double lenNext = Math.sqrt(dxNext*dxNext + dyNext*dyNext);
096
097        if (lenNext < 1e-11) {
098            lenNext = 1; // value does not matter, because dy_next and dx_next is 0
099        }
100
101        // calculate the position of the translated current point
102        double om = offset / lenNext;
103        double xCurrent0 = xCurrent + om * dyNext;
104        double yCurrent0 = yCurrent - om * dxNext;
105
106        if (idx == 0) {
107            ++idx;
108            prev = current;
109            xPrev0 = xCurrent0;
110            yPrev0 = yCurrent0;
111            return mapState.getForView(xCurrent0, yCurrent0);
112        } else {
113            double dxPrev = xCurrent - prev.getInViewX();
114            double dyPrev = yCurrent - prev.getInViewY();
115            // determine intersection of the lines parallel to the two segments
116            double det = dxNext*dyPrev - dxPrev*dyNext;
117            double m = dxNext*(yCurrent0 - yPrev0) - dyNext*(xCurrent0 - xPrev0);
118
119            if (Utils.equalsEpsilon(det, 0) || Math.signum(det) != Math.signum(m)) {
120                ++idx;
121                prev = current;
122                xPrev0 = xCurrent0;
123                yPrev0 = yCurrent0;
124                return mapState.getForView(xCurrent0, yCurrent0);
125            }
126
127            double f = m / det;
128            if (f < 0) {
129                ++idx;
130                prev = current;
131                xPrev0 = xCurrent0;
132                yPrev0 = yCurrent0;
133                return mapState.getForView(xCurrent0, yCurrent0);
134            }
135            // the position of the intersection or intermittent point
136            double cx = xPrev0 + f * dxPrev;
137            double cy = yPrev0 + f * dyPrev;
138
139            if (f > 1) {
140                // check if the intersection point is too far away, this will happen for sharp angles
141                double dxI = cx - xCurrent;
142                double dyI = cy - yCurrent;
143                double lenISq = dxI * dxI + dyI * dyI;
144
145                if (lenISq > Math.abs(2 * offset * offset)) {
146                    // intersection point is too far away, calculate intermittent points for capping
147                    double dxPrev0 = xCurrent0 - xPrev0;
148                    double dyPrev0 = yCurrent0 - yPrev0;
149                    double lenPrev0 = Math.sqrt(dxPrev0 * dxPrev0 + dyPrev0 * dyPrev0);
150                    f = 1 + Math.abs(offset / lenPrev0);
151                    double cxCap = xPrev0 + f * dxPrev;
152                    double cyCap = yPrev0 + f * dyPrev;
153                    xPrev0 = cxCap;
154                    yPrev0 = cyCap;
155                    // calculate a virtual prev point which lies on a line that goes through current and
156                    // is perpendicular to the line that goes through current and the intersection
157                    // so that the next capping point is calculated with it.
158                    double lenI = Math.sqrt(lenISq);
159                    double xv = xCurrent + dyI / lenI;
160                    double yv = yCurrent - dxI / lenI;
161
162                    prev = mapState.getForView(xv, yv);
163                    return mapState.getForView(cxCap, cyCap);
164                }
165            }
166            ++idx;
167            prev = current;
168            xPrev0 = xCurrent0;
169            yPrev0 = yCurrent0;
170            return mapState.getForView(cx, cy);
171        }
172    }
173
174    private MapViewPoint getForIndex(int i) {
175        return nodes.get(i);
176    }
177
178    @Override
179    public void remove() {
180        throw new UnsupportedOperationException();
181    }
182}