// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.data.osm.visitor.paint; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import org.openstreetmap.josm.data.osm.Node; import org.openstreetmap.josm.gui.MapViewState; import org.openstreetmap.josm.gui.MapViewState.MapViewPoint; import org.openstreetmap.josm.tools.Utils; /** * Iterates over a list of Way Nodes and returns screen coordinates that * represent a line that is shifted by a certain offset perpendicular * to the way direction. * * There is no intention, to handle consecutive duplicate Nodes in a * perfect way, but it should not throw an exception. * @since 11696 made public */ public class OffsetIterator implements Iterator<MapViewPoint> { private final MapViewState mapState; private final List<Node> nodes; private final double offset; private int idx; private MapViewPoint prev; /* 'prev0' is a point that has distance 'offset' from 'prev' and the * line from 'prev' to 'prev0' is perpendicular to the way segment from * 'prev' to the current point. */ private double xPrev0; private double yPrev0; /** * Creates a new offset iterator * @param mapState The map view state this iterator is for. * @param nodes The nodes of the original line * @param offset The offset of the line. */ public OffsetIterator(MapViewState mapState, List<Node> nodes, double offset) { this.mapState = mapState; this.nodes = nodes; this.offset = offset; idx = 0; } @Override public boolean hasNext() { return idx < nodes.size(); } @Override public MapViewPoint next() { if (!hasNext()) throw new NoSuchElementException(); MapViewPoint current = getForIndex(idx); if (Math.abs(offset) < 0.1d) { idx++; return current; } double xCurrent = current.getInViewX(); double yCurrent = current.getInViewY(); if (idx == nodes.size() - 1) { ++idx; if (prev != null) { return mapState.getForView(xPrev0 + xCurrent - prev.getInViewX(), yPrev0 + yCurrent - prev.getInViewY()); } else { return current; } } MapViewPoint next = getForIndex(idx + 1); double dxNext = next.getInViewX() - xCurrent; double dyNext = next.getInViewY() - yCurrent; double lenNext = Math.sqrt(dxNext*dxNext + dyNext*dyNext); if (lenNext < 1e-11) { lenNext = 1; // value does not matter, because dy_next and dx_next is 0 } // calculate the position of the translated current point double om = offset / lenNext; double xCurrent0 = xCurrent + om * dyNext; double yCurrent0 = yCurrent - om * dxNext; if (idx == 0) { ++idx; prev = current; xPrev0 = xCurrent0; yPrev0 = yCurrent0; return mapState.getForView(xCurrent0, yCurrent0); } else { double dxPrev = xCurrent - prev.getInViewX(); double dyPrev = yCurrent - prev.getInViewY(); // determine intersection of the lines parallel to the two segments double det = dxNext*dyPrev - dxPrev*dyNext; double m = dxNext*(yCurrent0 - yPrev0) - dyNext*(xCurrent0 - xPrev0); if (Utils.equalsEpsilon(det, 0) || Math.signum(det) != Math.signum(m)) { ++idx; prev = current; xPrev0 = xCurrent0; yPrev0 = yCurrent0; return mapState.getForView(xCurrent0, yCurrent0); } double f = m / det; if (f < 0) { ++idx; prev = current; xPrev0 = xCurrent0; yPrev0 = yCurrent0; return mapState.getForView(xCurrent0, yCurrent0); } // the position of the intersection or intermittent point double cx = xPrev0 + f * dxPrev; double cy = yPrev0 + f * dyPrev; if (f > 1) { // check if the intersection point is too far away, this will happen for sharp angles double dxI = cx - xCurrent; double dyI = cy - yCurrent; double lenISq = dxI * dxI + dyI * dyI; if (lenISq > Math.abs(2 * offset * offset)) { // intersection point is too far away, calculate intermittent points for capping double dxPrev0 = xCurrent0 - xPrev0; double dyPrev0 = yCurrent0 - yPrev0; double lenPrev0 = Math.sqrt(dxPrev0 * dxPrev0 + dyPrev0 * dyPrev0); f = 1 + Math.abs(offset / lenPrev0); double cxCap = xPrev0 + f * dxPrev; double cyCap = yPrev0 + f * dyPrev; xPrev0 = cxCap; yPrev0 = cyCap; // calculate a virtual prev point which lies on a line that goes through current and // is perpendicular to the line that goes through current and the intersection // so that the next capping point is calculated with it. double lenI = Math.sqrt(lenISq); double xv = xCurrent + dyI / lenI; double yv = yCurrent - dxI / lenI; prev = mapState.getForView(xv, yv); return mapState.getForView(cxCap, cyCap); } } ++idx; prev = current; xPrev0 = xCurrent0; yPrev0 = yCurrent0; return mapState.getForView(cx, cy); } } private MapViewPoint getForIndex(int i) { return mapState.getPointFor(nodes.get(i)); } @Override public void remove() { throw new UnsupportedOperationException(); } }