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 ILatLon first = null; 167 for (ILatLon n : nodes) { 168 if (useMoveTo) { 169 moveTo(n); 170 } else { 171 lineTo(n); 172 } 173 if (close && first == null) { 174 first = n; 175 } 176 useMoveTo = false; 177 } 178 if (first != null) { 179 lineTo(first); 180 } 181 } 182 183 /** 184 * Converts a path in east/north coordinates to view space. 185 * @param path The path 186 * @since 11748 187 */ 188 public void appendFromEastNorth(Path2D.Double path) { 189 new PathVisitor() { 190 @Override 191 public void visitMoveTo(double x, double y) { 192 moveTo(new EastNorth(x, y)); 193 } 194 195 @Override 196 public void visitLineTo(double x, double y) { 197 lineTo(new EastNorth(x, y)); 198 } 199 200 @Override 201 public void visitClose() { 202 closePath(); 203 } 204 }.visit(path); 205 } 206 207 /** 208 * Visits all segments of this path. 209 * @param consumer The consumer to send path segments to 210 * @return the total line length 211 * @since 11748 212 */ 213 public double visitLine(PathSegmentConsumer consumer) { 214 LineVisitor visitor = new LineVisitor(consumer); 215 visitor.visit(this); 216 return visitor.inLineOffset; 217 } 218 219 /** 220 * Compute a line that is similar to the current path expect for that parts outside the screen are skipped using moveTo commands. 221 * 222 * The line is computed in a way that dashes stay in their place when moving the view. 223 * 224 * The resulting line is not intended to fill areas. 225 * @param stroke The stroke to compute the line for. 226 * @return The new line shape. 227 * @since 11147 228 */ 229 public Shape computeClippedLine(Stroke stroke) { 230 MapPath2D clamped = new MapPath2D(); 231 if (visitClippedLine(stroke, (inLineOffset, start, end, startIsOldEnd) -> { 232 if (!startIsOldEnd) { 233 clamped.moveTo(start); 234 } 235 clamped.lineTo(end); 236 })) { 237 return clamped; 238 } else { 239 // could not clip the path. 240 return this; 241 } 242 } 243 244 /** 245 * Visits all straight segments of this path. The segments are clamped to the view. 246 * If they are clamped, the start points are aligned with the pattern. 247 * @param stroke The stroke to take the dash information from. 248 * @param consumer The consumer to call for each segment 249 * @return false if visiting the path failed because there e.g. were non-straight segments. 250 * @since 11147 251 */ 252 public boolean visitClippedLine(Stroke stroke, PathSegmentConsumer consumer) { 253 if (stroke instanceof BasicStroke && ((BasicStroke) stroke).getDashArray() != null) { 254 float length = 0; 255 for (float f : ((BasicStroke) stroke).getDashArray()) { 256 length += f; 257 } 258 return visitClippedLine(length, consumer); 259 } else { 260 return visitClippedLine(0, consumer); 261 } 262 } 263 264 /** 265 * Visits all straight segments of this path. The segments are clamped to the view. 266 * If they are clamped, the start points are aligned with the pattern. 267 * @param strokeLength The dash pattern length. 0 to use no pattern. Only segments of this length will be removed from the line. 268 * @param consumer The consumer to call for each segment 269 * @return false if visiting the path failed because there e.g. were non-straight segments. 270 * @since 11147 271 */ 272 public boolean visitClippedLine(double strokeLength, PathSegmentConsumer consumer) { 273 return new ClampingPathVisitor(state.getViewClipRectangle(), strokeLength, consumer) 274 .visit(this); 275 } 276 277 /** 278 * Gets the length of the way in visual space. 279 * @return The length. 280 * @since 11748 281 */ 282 public double getLength() { 283 return visitLine((inLineOffset, start, end, startIsOldEnd) -> { }); 284 } 285 286 /** 287 * Create a new {@link MapViewPath} that is the same as the current one except that it is offset in the view. 288 * @param viewOffset The offset in view pixels 289 * @return The new path 290 * @since 12505 291 */ 292 public MapViewPath offset(double viewOffset) { 293 OffsetPathVisitor visitor = new OffsetPathVisitor(state, viewOffset); 294 visitor.visit(this); 295 return visitor.getPath(); 296 } 297 298 /** 299 * This class is used to visit the segments of this path. 300 * @author Michael Zangl 301 * @since 11147 302 */ 303 @FunctionalInterface 304 public interface PathSegmentConsumer { 305 306 /** 307 * Add a line segment between two points 308 * @param inLineOffset The offset of start in the line 309 * @param start The start point 310 * @param end The end point 311 * @param startIsOldEnd If the start point equals the last end point. 312 */ 313 void addLineBetween(double inLineOffset, MapViewPoint start, MapViewPoint end, boolean startIsOldEnd); 314 } 315 316 private interface PathVisitor { 317 /** 318 * Append a path to this one. The path is clipped to the current view. 319 * @param path The iterator 320 * @return true if adding the path was successful. 321 */ 322 default boolean visit(Path2D.Double path) { 323 double[] coords = new double[8]; 324 PathIterator it = path.getPathIterator(null); 325 while (!it.isDone()) { 326 int type = it.currentSegment(coords); 327 switch (type) { 328 case PathIterator.SEG_CLOSE: 329 visitClose(); 330 break; 331 case PathIterator.SEG_LINETO: 332 visitLineTo(coords[0], coords[1]); 333 break; 334 case PathIterator.SEG_MOVETO: 335 visitMoveTo(coords[0], coords[1]); 336 break; 337 default: 338 // cannot handle this shape - this should be very rare and not happening in OSM draw code. 339 return false; 340 } 341 it.next(); 342 } 343 return true; 344 } 345 346 void visitClose(); 347 348 void visitMoveTo(double x, double y); 349 350 void visitLineTo(double x, double y); 351 } 352 353 private abstract class AbstractMapPathVisitor implements PathVisitor { 354 private MapViewPoint lastMoveTo; 355 356 @Override 357 public void visitMoveTo(double x, double y) { 358 MapViewPoint move = state.getForView(x, y); 359 lastMoveTo = move; 360 visitMoveTo(move); 361 } 362 363 abstract void visitMoveTo(MapViewPoint p); 364 365 @Override 366 public void visitLineTo(double x, double y) { 367 visitLineTo(state.getForView(x, y)); 368 } 369 370 abstract void visitLineTo(MapViewPoint p); 371 372 @Override 373 public void visitClose() { 374 visitLineTo(lastMoveTo); 375 } 376 } 377 378 private final class LineVisitor extends AbstractMapPathVisitor { 379 private final PathSegmentConsumer consumer; 380 private MapViewPoint last; 381 private double inLineOffset; 382 private boolean startIsOldEnd; 383 384 LineVisitor(PathSegmentConsumer consumer) { 385 this.consumer = consumer; 386 } 387 388 @Override 389 void visitMoveTo(MapViewPoint p) { 390 last = p; 391 startIsOldEnd = false; 392 } 393 394 @Override 395 void visitLineTo(MapViewPoint p) { 396 consumer.addLineBetween(inLineOffset, last, p, startIsOldEnd); 397 inLineOffset += last.distanceToInView(p); 398 last = p; 399 startIsOldEnd = true; 400 } 401 } 402 403 private class ClampingPathVisitor extends AbstractMapPathVisitor { 404 private final MapViewRectangle clip; 405 private final PathSegmentConsumer consumer; 406 protected double strokeProgress; 407 private final double strokeLength; 408 409 private MapViewPoint cursor; 410 private boolean cursorIsActive; 411 412 /** 413 * Create a new {@link ClampingPathVisitor} 414 * @param clip View clip rectangle 415 * @param strokeLength Total length of a stroke sequence 416 * @param consumer The consumer to notify of the path segments. 417 */ 418 ClampingPathVisitor(MapViewRectangle clip, double strokeLength, PathSegmentConsumer consumer) { 419 this.clip = clip; 420 this.strokeLength = strokeLength; 421 this.consumer = consumer; 422 } 423 424 @Override 425 void visitMoveTo(MapViewPoint point) { 426 cursor = point; 427 cursorIsActive = false; 428 } 429 430 @Override 431 void visitLineTo(MapViewPoint next) { 432 MapViewPoint entry = clip.getLineEntry(cursor, next); 433 if (entry != null) { 434 MapViewPoint exit = clip.getLineEntry(next, cursor); 435 if (!cursorIsActive || !entry.equals(cursor)) { 436 entry = alignStrokeOffset(entry, cursor); 437 } 438 consumer.addLineBetween(strokeProgress + cursor.distanceToInView(entry), entry, exit, cursorIsActive); 439 cursorIsActive = exit.equals(next); 440 } 441 strokeProgress += cursor.distanceToInView(next); 442 443 cursor = next; 444 } 445 446 private MapViewPoint alignStrokeOffset(MapViewPoint entry, MapViewPoint originalStart) { 447 double distanceSq = entry.distanceToInViewSq(originalStart); 448 if (distanceSq < 0.01 || strokeLength <= 0.001) { 449 // don't move if there is nothing to move. 450 return entry; 451 } 452 453 double distance = Math.sqrt(distanceSq); 454 double offset = (strokeProgress + distance) % strokeLength; 455 if (offset < 0.01) { 456 return entry; 457 } 458 459 return entry.interpolate(originalStart, offset / distance); 460 } 461 } 462 463 private class OffsetPathVisitor extends AbstractMapPathVisitor { 464 private final MapViewPath collector; 465 private final ArrayList<MapViewPoint> points = new ArrayList<>(); 466 private final double offset; 467 468 OffsetPathVisitor(MapViewState state, double offset) { 469 this.collector = new MapViewPath(state); 470 this.offset = offset; 471 } 472 473 @Override 474 void visitMoveTo(MapViewPoint p) { 475 finishLineSegment(); 476 points.add(p); 477 } 478 479 @Override 480 void visitLineTo(MapViewPoint p) { 481 points.add(p); 482 } 483 484 MapViewPath getPath() { 485 finishLineSegment(); 486 return collector; 487 } 488 489 private void finishLineSegment() { 490 if (points.size() > 2) { 491 OffsetIterator iterator = new OffsetIterator(points, offset); 492 collector.moveTo(iterator.next()); 493 while (iterator.hasNext()) { 494 collector.lineTo(iterator.next()); 495 } 496 points.clear(); 497 } 498 } 499 } 500}