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.Path2D; 008import java.awt.geom.PathIterator; 009import java.util.ArrayList; 010 011import org.openstreetmap.josm.data.coor.EastNorth; 012import org.openstreetmap.josm.data.coor.ILatLon; 013import org.openstreetmap.josm.data.osm.visitor.paint.OffsetIterator; 014import org.openstreetmap.josm.gui.MapView; 015import org.openstreetmap.josm.gui.MapViewState; 016import org.openstreetmap.josm.gui.MapViewState.MapViewPoint; 017import org.openstreetmap.josm.gui.MapViewState.MapViewRectangle; 018 019/** 020 * 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. 021 * <p> 022 * 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 023 * performance if the zoom level is high and the part outside the view is long. See {@link #computeClippedLine(Stroke)}. 024 * @author Michael Zangl 025 * @since 10875 026 */ 027public class MapViewPath extends MapPath2D { 028 029 private final MapViewState state; 030 031 /** 032 * Create a new path 033 * @param mv The map view to use for coordinate conversion. 034 */ 035 public MapViewPath(MapView mv) { 036 this(mv.getState()); 037 } 038 039 /** 040 * Create a new path 041 * @param state The state to use for coordinate conversion. 042 */ 043 public MapViewPath(MapViewState state) { 044 this.state = state; 045 } 046 047 /** 048 * Gets the map view state this path is used for. 049 * @return The state. 050 * @since 11748 051 */ 052 public MapViewState getMapViewState() { 053 return state; 054 } 055 056 /** 057 * Move the cursor to the given node. 058 * @param n The node 059 * @return this for easy chaining. 060 */ 061 public MapViewPath moveTo(ILatLon n) { 062 moveTo(n.getEastNorth(state.getProjecting())); 063 return this; 064 } 065 066 /** 067 * Move the cursor to the given position. 068 * @param eastNorth The position 069 * @return this for easy chaining. 070 */ 071 public MapViewPath moveTo(EastNorth eastNorth) { 072 moveTo(state.getPointFor(eastNorth)); 073 return this; 074 } 075 076 @Override 077 public MapViewPath moveTo(MapViewPoint p) { 078 super.moveTo(p); 079 return this; 080 } 081 082 /** 083 * Draw a line to the node. 084 * <p> 085 * line clamping to view is done automatically. 086 * @param n The node 087 * @return this for easy chaining. 088 */ 089 public MapViewPath lineTo(ILatLon n) { 090 lineTo(n.getEastNorth(state.getProjecting())); 091 return this; 092 } 093 094 /** 095 * Draw a line to the position. 096 * <p> 097 * line clamping to view is done automatically. 098 * @param eastNorth The position 099 * @return this for easy chaining. 100 */ 101 public MapViewPath lineTo(EastNorth eastNorth) { 102 lineTo(state.getPointFor(eastNorth)); 103 return this; 104 } 105 106 @Override 107 public MapViewPath lineTo(MapViewPoint p) { 108 super.lineTo(p); 109 return this; 110 } 111 112 /** 113 * Add the given shape centered around the current node. 114 * @param p1 The point to draw around 115 * @param symbol The symbol type 116 * @param size The size of the symbol in pixel 117 * @return this for easy chaining. 118 */ 119 public MapViewPath shapeAround(ILatLon p1, SymbolShape symbol, double size) { 120 shapeAround(p1.getEastNorth(state.getProjecting()), symbol, size); 121 return this; 122 } 123 124 /** 125 * Add the given shape centered around the current position. 126 * @param eastNorth The point to draw around 127 * @param symbol The symbol type 128 * @param size The size of the symbol in pixel 129 * @return this for easy chaining. 130 */ 131 public MapViewPath shapeAround(EastNorth eastNorth, SymbolShape symbol, double size) { 132 shapeAround(state.getPointFor(eastNorth), symbol, size); 133 return this; 134 } 135 136 @Override 137 public MapViewPath shapeAround(MapViewPoint p, SymbolShape symbol, double size) { 138 super.shapeAround(p, symbol, size); 139 return this; 140 } 141 142 /** 143 * Append a list of nodes 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 append(Iterable<? extends ILatLon> nodes, boolean connect) { 149 appendWay(nodes, connect, false); 150 return this; 151 } 152 153 /** 154 * Append a list of nodes as closed way. 155 * @param nodes The nodes to append 156 * @param connect <code>true</code> if we should use a lineTo as first command. 157 * @return this for easy chaining. 158 */ 159 public MapViewPath appendClosed(Iterable<? extends ILatLon> nodes, boolean connect) { 160 appendWay(nodes, connect, true); 161 return this; 162 } 163 164 private void appendWay(Iterable<? extends ILatLon> nodes, boolean connect, boolean close) { 165 boolean useMoveTo = !connect; 166 for (ILatLon n : nodes) { 167 if (useMoveTo) { 168 moveTo(n); 169 } else { 170 lineTo(n); 171 } 172 useMoveTo = false; 173 } 174 if (close) { 175 closePath(); 176 } 177 } 178 179 /** 180 * Converts a path in east/north coordinates to view space. 181 * @param path The path 182 * @since 11748 183 */ 184 public void appendFromEastNorth(Path2D.Double path) { 185 new PathVisitor() { 186 @Override 187 public void visitMoveTo(double x, double y) { 188 moveTo(new EastNorth(x, y)); 189 } 190 191 @Override 192 public void visitLineTo(double x, double y) { 193 lineTo(new EastNorth(x, y)); 194 } 195 196 @Override 197 public void visitClose() { 198 closePath(); 199 } 200 }.visit(path); 201 } 202 203 /** 204 * Visits all segments of this path. 205 * @param consumer The consumer to send path segments to 206 * @return the total line length 207 * @since 11748 208 */ 209 public double visitLine(PathSegmentConsumer consumer) { 210 LineVisitor visitor = new LineVisitor(consumer); 211 visitor.visit(this); 212 return visitor.inLineOffset; 213 } 214 215 /** 216 * Compute a line that is similar to the current path expect for that parts outside the screen are skipped using moveTo commands. 217 * 218 * The line is computed in a way that dashes stay in their place when moving the view. 219 * 220 * The resulting line is not intended to fill areas. 221 * @param stroke The stroke to compute the line for. 222 * @return The new line shape. 223 * @since 11147 224 */ 225 public Shape computeClippedLine(Stroke stroke) { 226 MapPath2D clamped = new MapPath2D(); 227 if (visitClippedLine(stroke, (inLineOffset, start, end, startIsOldEnd) -> { 228 if (!startIsOldEnd) { 229 clamped.moveTo(start); 230 } 231 clamped.lineTo(end); 232 })) { 233 return clamped; 234 } else { 235 // could not clip the path. 236 return this; 237 } 238 } 239 240 /** 241 * Visits all straight segments of this path. The segments are clamped to the view. 242 * If they are clamped, the start points are aligned with the pattern. 243 * @param stroke The stroke to take the dash information from. 244 * @param consumer The consumer to call for each segment 245 * @return false if visiting the path failed because there e.g. were non-straight segments. 246 * @since 11147 247 */ 248 public boolean visitClippedLine(Stroke stroke, PathSegmentConsumer consumer) { 249 if (stroke instanceof BasicStroke && ((BasicStroke) stroke).getDashArray() != null) { 250 float length = 0; 251 for (float f : ((BasicStroke) stroke).getDashArray()) { 252 length += f; 253 } 254 return visitClippedLine(length, consumer); 255 } else { 256 return visitClippedLine(0, consumer); 257 } 258 } 259 260 /** 261 * Visits all straight segments of this path. The segments are clamped to the view. 262 * If they are clamped, the start points are aligned with the pattern. 263 * @param strokeLength The dash pattern length. 0 to use no pattern. Only segments of this length will be removed from the line. 264 * @param consumer The consumer to call for each segment 265 * @return false if visiting the path failed because there e.g. were non-straight segments. 266 * @since 11147 267 */ 268 public boolean visitClippedLine(double strokeLength, PathSegmentConsumer consumer) { 269 return new ClampingPathVisitor(state.getViewClipRectangle(), strokeLength, consumer) 270 .visit(this); 271 } 272 273 /** 274 * Gets the length of the way in visual space. 275 * @return The length. 276 * @since 11748 277 */ 278 public double getLength() { 279 return visitLine((inLineOffset, start, end, startIsOldEnd) -> { }); 280 } 281 282 /** 283 * Create a new {@link MapViewPath} that is the same as the current one except that it is offset in the view. 284 * @param viewOffset The offset in view pixels 285 * @return The new path 286 * @since 12505 287 */ 288 public MapViewPath offset(double viewOffset) { 289 OffsetPathVisitor visitor = new OffsetPathVisitor(state, viewOffset); 290 visitor.visit(this); 291 return visitor.getPath(); 292 } 293 294 /** 295 * This class is used to visit the segments of this path. 296 * @author Michael Zangl 297 * @since 11147 298 */ 299 @FunctionalInterface 300 public interface PathSegmentConsumer { 301 302 /** 303 * Add a line segment between two points 304 * @param inLineOffset The offset of start in the line 305 * @param start The start point 306 * @param end The end point 307 * @param startIsOldEnd If the start point equals the last end point. 308 */ 309 void addLineBetween(double inLineOffset, MapViewPoint start, MapViewPoint end, boolean startIsOldEnd); 310 } 311 312 private interface PathVisitor { 313 /** 314 * Append a path to this one. The path is clipped to the current view. 315 * @param path The iterator 316 * @return true if adding the path was successful. 317 */ 318 default boolean visit(Path2D.Double path) { 319 double[] coords = new double[8]; 320 PathIterator it = path.getPathIterator(null); 321 while (!it.isDone()) { 322 int type = it.currentSegment(coords); 323 switch (type) { 324 case PathIterator.SEG_CLOSE: 325 visitClose(); 326 break; 327 case PathIterator.SEG_LINETO: 328 visitLineTo(coords[0], coords[1]); 329 break; 330 case PathIterator.SEG_MOVETO: 331 visitMoveTo(coords[0], coords[1]); 332 break; 333 default: 334 // cannot handle this shape - this should be very rare and not happening in OSM draw code. 335 return false; 336 } 337 it.next(); 338 } 339 return true; 340 } 341 342 void visitClose(); 343 344 void visitMoveTo(double x, double y); 345 346 void visitLineTo(double x, double y); 347 } 348 349 private abstract class AbstractMapPathVisitor implements PathVisitor { 350 private MapViewPoint lastMoveTo; 351 352 @Override 353 public void visitMoveTo(double x, double y) { 354 MapViewPoint move = state.getForView(x, y); 355 lastMoveTo = move; 356 visitMoveTo(move); 357 } 358 359 abstract void visitMoveTo(MapViewPoint p); 360 361 @Override 362 public void visitLineTo(double x, double y) { 363 visitLineTo(state.getForView(x, y)); 364 } 365 366 abstract void visitLineTo(MapViewPoint p); 367 368 @Override 369 public void visitClose() { 370 visitLineTo(lastMoveTo); 371 } 372 } 373 374 private final class LineVisitor extends AbstractMapPathVisitor { 375 private final PathSegmentConsumer consumer; 376 private MapViewPoint last; 377 private double inLineOffset; 378 private boolean startIsOldEnd; 379 380 LineVisitor(PathSegmentConsumer consumer) { 381 this.consumer = consumer; 382 } 383 384 @Override 385 void visitMoveTo(MapViewPoint p) { 386 last = p; 387 startIsOldEnd = false; 388 } 389 390 @Override 391 void visitLineTo(MapViewPoint p) { 392 consumer.addLineBetween(inLineOffset, last, p, startIsOldEnd); 393 inLineOffset += last.distanceToInView(p); 394 last = p; 395 startIsOldEnd = true; 396 } 397 } 398 399 private class ClampingPathVisitor extends AbstractMapPathVisitor { 400 private final MapViewRectangle clip; 401 private final PathSegmentConsumer consumer; 402 protected double strokeProgress; 403 private final double strokeLength; 404 405 private MapViewPoint cursor; 406 private boolean cursorIsActive; 407 408 /** 409 * Create a new {@link ClampingPathVisitor} 410 * @param clip View clip rectangle 411 * @param strokeLength Total length of a stroke sequence 412 * @param consumer The consumer to notify of the path segments. 413 */ 414 ClampingPathVisitor(MapViewRectangle clip, double strokeLength, PathSegmentConsumer consumer) { 415 this.clip = clip; 416 this.strokeLength = strokeLength; 417 this.consumer = consumer; 418 } 419 420 @Override 421 void visitMoveTo(MapViewPoint point) { 422 cursor = point; 423 cursorIsActive = false; 424 } 425 426 @Override 427 void visitLineTo(MapViewPoint next) { 428 MapViewPoint entry = clip.getLineEntry(cursor, next); 429 if (entry != null) { 430 MapViewPoint exit = clip.getLineEntry(next, cursor); 431 if (!cursorIsActive || !entry.equals(cursor)) { 432 entry = alignStrokeOffset(entry, cursor); 433 } 434 consumer.addLineBetween(strokeProgress + cursor.distanceToInView(entry), entry, exit, cursorIsActive); 435 cursorIsActive = exit.equals(next); 436 } 437 strokeProgress += cursor.distanceToInView(next); 438 439 cursor = next; 440 } 441 442 private MapViewPoint alignStrokeOffset(MapViewPoint entry, MapViewPoint originalStart) { 443 double distanceSq = entry.distanceToInViewSq(originalStart); 444 if (distanceSq < 0.01 || strokeLength <= 0.001) { 445 // don't move if there is nothing to move. 446 return entry; 447 } 448 449 double distance = Math.sqrt(distanceSq); 450 double offset = (strokeProgress + distance) % strokeLength; 451 if (offset < 0.01) { 452 return entry; 453 } 454 455 return entry.interpolate(originalStart, offset / distance); 456 } 457 } 458 459 private class OffsetPathVisitor extends AbstractMapPathVisitor { 460 private final MapViewPath collector; 461 private final ArrayList<MapViewPoint> points = new ArrayList<>(); 462 private final double offset; 463 464 OffsetPathVisitor(MapViewState state, double offset) { 465 this.collector = new MapViewPath(state); 466 this.offset = offset; 467 } 468 469 @Override 470 void visitMoveTo(MapViewPoint p) { 471 finishLineSegment(); 472 points.add(p); 473 } 474 475 @Override 476 void visitLineTo(MapViewPoint p) { 477 points.add(p); 478 } 479 480 MapViewPath getPath() { 481 finishLineSegment(); 482 return collector; 483 } 484 485 private void finishLineSegment() { 486 if (points.size() > 2) { 487 OffsetIterator iterator = new OffsetIterator(points, offset); 488 collector.moveTo(iterator.next()); 489 while (iterator.hasNext()) { 490 collector.lineTo(iterator.next()); 491 } 492 points.clear(); 493 } 494 } 495 } 496}