source: josm/trunk/src/org/openstreetmap/josm/gui/draw/MapViewPath.java@ 13652

Last change on this file since 13652 was 12505, checked in by michael2402, 7 years ago

Add a method that allows traversing an offset version of a MapViewPath

  • Property svn:eol-style set to native
File size: 15.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.draw;
3
4import java.awt.BasicStroke;
5import java.awt.Shape;
6import java.awt.Stroke;
7import java.awt.geom.Path2D;
8import java.awt.geom.PathIterator;
9import java.util.ArrayList;
10
11import org.openstreetmap.josm.data.coor.EastNorth;
12import org.openstreetmap.josm.data.coor.ILatLon;
13import org.openstreetmap.josm.data.osm.visitor.paint.OffsetIterator;
14import org.openstreetmap.josm.gui.MapView;
15import org.openstreetmap.josm.gui.MapViewState;
16import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
17import org.openstreetmap.josm.gui.MapViewState.MapViewRectangle;
18
19/**
20 * 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.
21 * <p>
22 * 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
23 * performance if the zoom level is high and the part outside the view is long. See {@link #computeClippedLine(Stroke)}.
24 * @author Michael Zangl
25 * @since 10875
26 */
27public class MapViewPath extends MapPath2D {
28
29 private final MapViewState state;
30
31 /**
32 * Create a new path
33 * @param mv The map view to use for coordinate conversion.
34 */
35 public MapViewPath(MapView mv) {
36 this(mv.getState());
37 }
38
39 /**
40 * Create a new path
41 * @param state The state to use for coordinate conversion.
42 */
43 public MapViewPath(MapViewState state) {
44 this.state = state;
45 }
46
47 /**
48 * Gets the map view state this path is used for.
49 * @return The state.
50 * @since 11748
51 */
52 public MapViewState getMapViewState() {
53 return state;
54 }
55
56 /**
57 * Move the cursor to the given node.
58 * @param n The node
59 * @return this for easy chaining.
60 */
61 public MapViewPath moveTo(ILatLon n) {
62 moveTo(n.getEastNorth(state.getProjecting()));
63 return this;
64 }
65
66 /**
67 * Move the cursor to the given position.
68 * @param eastNorth The position
69 * @return this for easy chaining.
70 */
71 public MapViewPath moveTo(EastNorth eastNorth) {
72 moveTo(state.getPointFor(eastNorth));
73 return this;
74 }
75
76 @Override
77 public MapViewPath moveTo(MapViewPoint p) {
78 super.moveTo(p);
79 return this;
80 }
81
82 /**
83 * Draw a line to the node.
84 * <p>
85 * line clamping to view is done automatically.
86 * @param n The node
87 * @return this for easy chaining.
88 */
89 public MapViewPath lineTo(ILatLon n) {
90 lineTo(n.getEastNorth(state.getProjecting()));
91 return this;
92 }
93
94 /**
95 * Draw a line to the position.
96 * <p>
97 * line clamping to view is done automatically.
98 * @param eastNorth The position
99 * @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}
Note: See TracBrowser for help on using the repository browser.