001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.draw; 003 004import java.awt.BasicStroke; 005import java.awt.Shape; 006import java.awt.Stroke; 007import java.awt.geom.PathIterator; 008 009import org.openstreetmap.josm.data.coor.EastNorth; 010import org.openstreetmap.josm.data.osm.Node; 011import org.openstreetmap.josm.gui.MapView; 012import org.openstreetmap.josm.gui.MapViewState; 013import org.openstreetmap.josm.gui.MapViewState.MapViewPoint; 014import org.openstreetmap.josm.gui.MapViewState.MapViewRectangle; 015 016 017/** 018 * This is a version of a java Path2D that allows you to add points to it by simply giving their east/north, lat/lon or node coordinates. 019 * <p> 020 * It is possible to clip the part of the path that is outside the view. This is useful when drawing dashed lines. Those lines use up a lot of 021 * performance if the zoom level is high and the part outside the view is long. See {@link #computeClippedLine(Stroke)}. 022 * @author Michael Zangl 023 * @since 10875 024 */ 025public class MapViewPath extends MapPath2D { 026 027 private final MapViewState state; 028 029 /** 030 * Create a new path 031 * @param mv The map view to use for coordinate conversion. 032 */ 033 public MapViewPath(MapView mv) { 034 this(mv.getState()); 035 } 036 037 /** 038 * Create a new path 039 * @param state The state to use for coordinate conversion. 040 */ 041 public MapViewPath(MapViewState state) { 042 this.state = state; 043 } 044 045 /** 046 * Move the cursor to the given node. 047 * @param n The node 048 * @return this for easy chaining. 049 */ 050 public MapViewPath moveTo(Node n) { 051 moveTo(n.getEastNorth()); 052 return this; 053 } 054 055 /** 056 * Move the cursor to the given position. 057 * @param eastNorth The position 058 * @return this for easy chaining. 059 */ 060 public MapViewPath moveTo(EastNorth eastNorth) { 061 moveTo(state.getPointFor(eastNorth)); 062 return this; 063 } 064 065 @Override 066 public MapViewPath moveTo(MapViewPoint p) { 067 super.moveTo(p); 068 return this; 069 } 070 071 /** 072 * Draw a line to the node. 073 * <p> 074 * line clamping to view is done automatically. 075 * @param n The node 076 * @return this for easy chaining. 077 */ 078 public MapViewPath lineTo(Node n) { 079 lineTo(n.getEastNorth()); 080 return this; 081 } 082 083 /** 084 * Draw a line to the position. 085 * <p> 086 * line clamping to view is done automatically. 087 * @param eastNorth The position 088 * @return this for easy chaining. 089 */ 090 public MapViewPath lineTo(EastNorth eastNorth) { 091 lineTo(state.getPointFor(eastNorth)); 092 return this; 093 } 094 095 @Override 096 public MapViewPath lineTo(MapViewPoint p) { 097 super.lineTo(p); 098 return this; 099 } 100 101 /** 102 * Add the given shape centered around the current node. 103 * @param p1 The point to draw around 104 * @param symbol The symbol type 105 * @param size The size of the symbol in pixel 106 * @return this for easy chaining. 107 */ 108 public MapViewPath shapeAround(Node p1, SymbolShape symbol, double size) { 109 shapeAround(p1.getEastNorth(), symbol, size); 110 return this; 111 } 112 113 /** 114 * Add the given shape centered around the current position. 115 * @param eastNorth The point to draw around 116 * @param symbol The symbol type 117 * @param size The size of the symbol in pixel 118 * @return this for easy chaining. 119 */ 120 public MapViewPath shapeAround(EastNorth eastNorth, SymbolShape symbol, double size) { 121 shapeAround(state.getPointFor(eastNorth), symbol, size); 122 return this; 123 } 124 125 @Override 126 public MapViewPath shapeAround(MapViewPoint p, SymbolShape symbol, double size) { 127 super.shapeAround(p, symbol, size); 128 return this; 129 } 130 131 /** 132 * Append a list of nodes 133 * @param nodes The nodes to append 134 * @param connect <code>true</code> if we should use a lineTo as first command. 135 * @return this for easy chaining. 136 */ 137 public MapViewPath append(Iterable<Node> nodes, boolean connect) { 138 appendWay(nodes, connect, false); 139 return this; 140 } 141 142 /** 143 * Append a list of nodes as closed way. 144 * @param nodes The nodes to append 145 * @param connect <code>true</code> if we should use a lineTo as first command. 146 * @return this for easy chaining. 147 */ 148 public MapViewPath appendClosed(Iterable<Node> nodes, boolean connect) { 149 appendWay(nodes, connect, true); 150 return this; 151 } 152 153 private void appendWay(Iterable<Node> nodes, boolean connect, boolean close) { 154 boolean useMoveTo = !connect; 155 Node first = null; 156 for (Node n : nodes) { 157 if (useMoveTo) { 158 moveTo(n); 159 } else { 160 lineTo(n); 161 } 162 if (close && first == null) { 163 first = n; 164 } 165 useMoveTo = false; 166 } 167 if (first != null) { 168 lineTo(first); 169 } 170 } 171 172 /** 173 * Compute a line that is similar to the current path expect for that parts outside the screen are skipped using moveTo commands. 174 * 175 * The line is computed in a way that dashes stay in their place when moving the view. 176 * 177 * The resulting line is not intended to fill areas. 178 * @param stroke The stroke to compute the line for. 179 * @return The new line shape. 180 * @since 11147 181 */ 182 public Shape computeClippedLine(Stroke stroke) { 183 MapPath2D clamped = new MapPath2D(); 184 if (visitClippedLine(stroke, (inLineOffset, start, end, startIsOldEnd) -> { 185 if (!startIsOldEnd) { 186 clamped.moveTo(start); 187 } 188 clamped.lineTo(end); 189 })) { 190 return clamped; 191 } else { 192 // could not clip the path. 193 return this; 194 } 195 } 196 197 /** 198 * Visits all straight segments of this path. The segments are clamped to the view. 199 * If they are clamped, the start points are aligned with the pattern. 200 * @param stroke The stroke to take the dash information from. 201 * @param consumer The consumer to call for each segment 202 * @return false if visiting the path failed because there e.g. were non-straight segments. 203 * @since 11147 204 */ 205 public boolean visitClippedLine(Stroke stroke, PathSegmentConsumer consumer) { 206 if (stroke instanceof BasicStroke && ((BasicStroke) stroke).getDashArray() != null) { 207 float length = 0; 208 for (float f : ((BasicStroke) stroke).getDashArray()) { 209 length += f; 210 } 211 return visitClippedLine(((BasicStroke) stroke).getDashPhase(), length, consumer); 212 } else { 213 return visitClippedLine(0, 0, consumer); 214 } 215 } 216 217 /** 218 * Visits all straight segments of this path. The segments are clamped to the view. 219 * If they are clamped, the start points are aligned with the pattern. 220 * @param strokeOffset The initial offset of the pattern 221 * @param strokeLength The dash pattern length. 0 to use no pattern. 222 * @param consumer The consumer to call for each segment 223 * @return false if visiting the path failed because there e.g. were non-straight segments. 224 * @since 11147 225 */ 226 public boolean visitClippedLine(double strokeOffset, double strokeLength, PathSegmentConsumer consumer) { 227 return new ClampingPathVisitor(state.getViewClipRectangle(), strokeOffset, strokeLength, consumer) 228 .visit(this); 229 } 230 231 232 /** 233 * This class is used to visit the segments of this path. 234 * @author Michael Zangl 235 * @since 11147 236 */ 237 public interface PathSegmentConsumer { 238 239 /** 240 * Add a line segment between two points 241 * @param inLineOffset The offset of start in the line 242 * @param start The start point 243 * @param end The end point 244 * @param startIsOldEnd If the start point equals the last end point. 245 */ 246 void addLineBetween(double inLineOffset, MapViewPoint start, MapViewPoint end, boolean startIsOldEnd); 247 248 } 249 250 private class ClampingPathVisitor { 251 private final MapViewRectangle clip; 252 private final PathSegmentConsumer consumer; 253 protected double strokeProgress; 254 private final double strokeLength; 255 private MapViewPoint lastMoveTo; 256 257 private MapViewPoint cursor; 258 private boolean cursorIsActive = false; 259 260 /** 261 * Create a new {@link ClampingPathVisitor} 262 * @param clip View clip rectangle 263 * @param strokeOffset Initial stroke offset 264 * @param strokeLength Total length of a stroke sequence 265 * @param consumer The consumer to notify of the path segments. 266 */ 267 ClampingPathVisitor(MapViewRectangle clip, double strokeOffset, double strokeLength, PathSegmentConsumer consumer) { 268 this.clip = clip; 269 this.strokeProgress = Math.min(strokeLength - strokeOffset, 0); 270 this.strokeLength = strokeLength; 271 this.consumer = consumer; 272 } 273 274 /** 275 * Append a path to this one. The path is clipped to the current view. 276 * @param mapViewPath The iterator 277 * @return true if adding the path was successful. 278 */ 279 public boolean visit(MapViewPath mapViewPath) { 280 double[] coords = new double[8]; 281 PathIterator it = mapViewPath.getPathIterator(null); 282 while (!it.isDone()) { 283 int type = it.currentSegment(coords); 284 switch (type) { 285 case PathIterator.SEG_CLOSE: 286 visitClose(); 287 break; 288 case PathIterator.SEG_LINETO: 289 visitLineTo(coords[0], coords[1]); 290 break; 291 case PathIterator.SEG_MOVETO: 292 visitMoveTo(coords[0], coords[1]); 293 break; 294 default: 295 // cannot handle this shape - this should be very rare. We let Java2D do the clipping. 296 return false; 297 } 298 it.next(); 299 } 300 return true; 301 } 302 303 void visitClose() { 304 drawLineTo(lastMoveTo); 305 } 306 307 void visitMoveTo(double x, double y) { 308 MapViewPoint point = state.getForView(x, y); 309 lastMoveTo = point; 310 cursor = point; 311 cursorIsActive = false; 312 } 313 314 void visitLineTo(double x, double y) { 315 drawLineTo(state.getForView(x, y)); 316 } 317 318 private void drawLineTo(MapViewPoint next) { 319 MapViewPoint entry = clip.getLineEntry(cursor, next); 320 if (entry != null) { 321 MapViewPoint exit = clip.getLineEntry(next, cursor); 322 if (!cursorIsActive || !entry.equals(cursor)) { 323 entry = alignStrokeOffset(entry, cursor); 324 } 325 consumer.addLineBetween(strokeProgress + cursor.distanceToInView(entry), entry, exit, cursorIsActive); 326 cursorIsActive = exit.equals(next); 327 } 328 strokeProgress += cursor.distanceToInView(next); 329 330 cursor = next; 331 } 332 333 private MapViewPoint alignStrokeOffset(MapViewPoint entry, MapViewPoint originalStart) { 334 double distanceSq = entry.distanceToInViewSq(originalStart); 335 if (distanceSq < 0.01 || strokeLength <= 0.001) { 336 // don't move if there is nothing to move. 337 return entry; 338 } 339 340 double distance = Math.sqrt(distanceSq); 341 double offset = ((strokeProgress + distance)) % strokeLength; 342 if (offset < 0.01) { 343 return entry; 344 } 345 346 return entry.interpolate(originalStart, offset / distance); 347 } 348 } 349}