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.INode; 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<? extends INode> nodes, double offset) { 058 this.mapState = mapState; 059 this.nodes = nodes.stream().filter(INode::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}