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

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

Fix #14926: Preserve image phase when clamping a line.

  • Property svn:eol-style set to native
File size: 14.1 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;
9
10import org.openstreetmap.josm.data.coor.EastNorth;
11import org.openstreetmap.josm.data.coor.ILatLon;
12import org.openstreetmap.josm.gui.MapView;
13import org.openstreetmap.josm.gui.MapViewState;
14import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
15import org.openstreetmap.josm.gui.MapViewState.MapViewRectangle;
16
17/**
18 * 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.
19 * <p>
20 * 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
21 * performance if the zoom level is high and the part outside the view is long. See {@link #computeClippedLine(Stroke)}.
22 * @author Michael Zangl
23 * @since 10875
24 */
25public class MapViewPath extends MapPath2D {
26
27 private final MapViewState state;
28
29 /**
30 * Create a new path
31 * @param mv The map view to use for coordinate conversion.
32 */
33 public MapViewPath(MapView mv) {
34 this(mv.getState());
35 }
36
37 /**
38 * Create a new path
39 * @param state The state to use for coordinate conversion.
40 */
41 public MapViewPath(MapViewState state) {
42 this.state = state;
43 }
44
45 /**
46 * Gets the map view state this path is used for.
47 * @return The state.
48 * @since 11748
49 */
50 public MapViewState getMapViewState() {
51 return state;
52 }
53
54 /**
55 * Move the cursor to the given node.
56 * @param n The node
57 * @return this for easy chaining.
58 */
59 public MapViewPath moveTo(ILatLon n) {
60 moveTo(n.getEastNorth(state.getProjecting()));
61 return this;
62 }
63
64 /**
65 * Move the cursor to the given position.
66 * @param eastNorth The position
67 * @return this for easy chaining.
68 */
69 public MapViewPath moveTo(EastNorth eastNorth) {
70 moveTo(state.getPointFor(eastNorth));
71 return this;
72 }
73
74 @Override
75 public MapViewPath moveTo(MapViewPoint p) {
76 super.moveTo(p);
77 return this;
78 }
79
80 /**
81 * Draw a line to the node.
82 * <p>
83 * line clamping to view is done automatically.
84 * @param n The node
85 * @return this for easy chaining.
86 */
87 public MapViewPath lineTo(ILatLon n) {
88 lineTo(n.getEastNorth(state.getProjecting()));
89 return this;
90 }
91
92 /**
93 * Draw a line to the position.
94 * <p>
95 * line clamping to view is done automatically.
96 * @param eastNorth The position
97 * @return this for easy chaining.
98 */
99 public MapViewPath lineTo(EastNorth eastNorth) {
100 lineTo(state.getPointFor(eastNorth));
101 return this;
102 }
103
104 @Override
105 public MapViewPath lineTo(MapViewPoint p) {
106 super.lineTo(p);
107 return this;
108 }
109
110 /**
111 * Add the given shape centered around the current node.
112 * @param p1 The point to draw around
113 * @param symbol The symbol type
114 * @param size The size of the symbol in pixel
115 * @return this for easy chaining.
116 */
117 public MapViewPath shapeAround(ILatLon p1, SymbolShape symbol, double size) {
118 shapeAround(p1.getEastNorth(state.getProjecting()), symbol, size);
119 return this;
120 }
121
122 /**
123 * Add the given shape centered around the current position.
124 * @param eastNorth The point to draw around
125 * @param symbol The symbol type
126 * @param size The size of the symbol in pixel
127 * @return this for easy chaining.
128 */
129 public MapViewPath shapeAround(EastNorth eastNorth, SymbolShape symbol, double size) {
130 shapeAround(state.getPointFor(eastNorth), symbol, size);
131 return this;
132 }
133
134 @Override
135 public MapViewPath shapeAround(MapViewPoint p, SymbolShape symbol, double size) {
136 super.shapeAround(p, symbol, size);
137 return this;
138 }
139
140 /**
141 * Append a list of nodes
142 * @param nodes The nodes to append
143 * @param connect <code>true</code> if we should use a lineTo as first command.
144 * @return this for easy chaining.
145 */
146 public MapViewPath append(Iterable<? extends ILatLon> nodes, boolean connect) {
147 appendWay(nodes, connect, false);
148 return this;
149 }
150
151 /**
152 * Append a list of nodes as closed way.
153 * @param nodes The nodes to append
154 * @param connect <code>true</code> if we should use a lineTo as first command.
155 * @return this for easy chaining.
156 */
157 public MapViewPath appendClosed(Iterable<? extends ILatLon> nodes, boolean connect) {
158 appendWay(nodes, connect, true);
159 return this;
160 }
161
162 private void appendWay(Iterable<? extends ILatLon> nodes, boolean connect, boolean close) {
163 boolean useMoveTo = !connect;
164 ILatLon first = null;
165 for (ILatLon n : nodes) {
166 if (useMoveTo) {
167 moveTo(n);
168 } else {
169 lineTo(n);
170 }
171 if (close && first == null) {
172 first = n;
173 }
174 useMoveTo = false;
175 }
176 if (first != null) {
177 lineTo(first);
178 }
179 }
180
181 /**
182 * Converts a path in east/north coordinates to view space.
183 * @param path The path
184 * @since 11748
185 */
186 public void appendFromEastNorth(Path2D.Double path) {
187 new PathVisitor() {
188 @Override
189 public void visitMoveTo(double x, double y) {
190 moveTo(new EastNorth(x, y));
191 }
192
193 @Override
194 public void visitLineTo(double x, double y) {
195 lineTo(new EastNorth(x, y));
196 }
197
198 @Override
199 public void visitClose() {
200 closePath();
201 }
202 }.visit(path);
203 }
204
205 /**
206 * Visits all segments of this path.
207 * @param consumer The consumer to send path segments to
208 * @return the total line length
209 * @since 11748
210 */
211 public double visitLine(PathSegmentConsumer consumer) {
212 LineVisitor visitor = new LineVisitor(consumer);
213 visitor.visit(this);
214 return visitor.inLineOffset;
215 }
216
217 /**
218 * Compute a line that is similar to the current path expect for that parts outside the screen are skipped using moveTo commands.
219 *
220 * The line is computed in a way that dashes stay in their place when moving the view.
221 *
222 * The resulting line is not intended to fill areas.
223 * @param stroke The stroke to compute the line for.
224 * @return The new line shape.
225 * @since 11147
226 */
227 public Shape computeClippedLine(Stroke stroke) {
228 MapPath2D clamped = new MapPath2D();
229 if (visitClippedLine(stroke, (inLineOffset, start, end, startIsOldEnd) -> {
230 if (!startIsOldEnd) {
231 clamped.moveTo(start);
232 }
233 clamped.lineTo(end);
234 })) {
235 return clamped;
236 } else {
237 // could not clip the path.
238 return this;
239 }
240 }
241
242 /**
243 * Visits all straight segments of this path. The segments are clamped to the view.
244 * If they are clamped, the start points are aligned with the pattern.
245 * @param stroke The stroke to take the dash information from.
246 * @param consumer The consumer to call for each segment
247 * @return false if visiting the path failed because there e.g. were non-straight segments.
248 * @since 11147
249 */
250 public boolean visitClippedLine(Stroke stroke, PathSegmentConsumer consumer) {
251 if (stroke instanceof BasicStroke && ((BasicStroke) stroke).getDashArray() != null) {
252 float length = 0;
253 for (float f : ((BasicStroke) stroke).getDashArray()) {
254 length += f;
255 }
256 return visitClippedLine(length, consumer);
257 } else {
258 return visitClippedLine(0, consumer);
259 }
260 }
261
262 /**
263 * Visits all straight segments of this path. The segments are clamped to the view.
264 * If they are clamped, the start points are aligned with the pattern.
265 * @param strokeLength The dash pattern length. 0 to use no pattern. Only segments of this length will be removed from the line.
266 * @param consumer The consumer to call for each segment
267 * @return false if visiting the path failed because there e.g. were non-straight segments.
268 * @since 11147
269 */
270 public boolean visitClippedLine(double strokeLength, PathSegmentConsumer consumer) {
271 return new ClampingPathVisitor(state.getViewClipRectangle(), strokeLength, consumer)
272 .visit(this);
273 }
274
275 /**
276 * Gets the length of the way in visual space.
277 * @return The length.
278 * @since 11748
279 */
280 public double getLength() {
281 return visitLine((inLineOffset, start, end, startIsOldEnd) -> { });
282 }
283
284 /**
285 * This class is used to visit the segments of this path.
286 * @author Michael Zangl
287 * @since 11147
288 */
289 @FunctionalInterface
290 public interface PathSegmentConsumer {
291
292 /**
293 * Add a line segment between two points
294 * @param inLineOffset The offset of start in the line
295 * @param start The start point
296 * @param end The end point
297 * @param startIsOldEnd If the start point equals the last end point.
298 */
299 void addLineBetween(double inLineOffset, MapViewPoint start, MapViewPoint end, boolean startIsOldEnd);
300 }
301
302 private interface PathVisitor {
303 /**
304 * Append a path to this one. The path is clipped to the current view.
305 * @param path The iterator
306 * @return true if adding the path was successful.
307 */
308 default boolean visit(Path2D.Double path) {
309 double[] coords = new double[8];
310 PathIterator it = path.getPathIterator(null);
311 while (!it.isDone()) {
312 int type = it.currentSegment(coords);
313 switch (type) {
314 case PathIterator.SEG_CLOSE:
315 visitClose();
316 break;
317 case PathIterator.SEG_LINETO:
318 visitLineTo(coords[0], coords[1]);
319 break;
320 case PathIterator.SEG_MOVETO:
321 visitMoveTo(coords[0], coords[1]);
322 break;
323 default:
324 // cannot handle this shape - this should be very rare and not happening in OSM draw code.
325 return false;
326 }
327 it.next();
328 }
329 return true;
330 }
331
332 void visitClose();
333
334 void visitMoveTo(double x, double y);
335
336 void visitLineTo(double x, double y);
337 }
338
339 private abstract class AbstractMapPathVisitor implements PathVisitor {
340 private MapViewPoint lastMoveTo;
341
342 @Override
343 public void visitMoveTo(double x, double y) {
344 MapViewPoint move = state.getForView(x, y);
345 lastMoveTo = move;
346 visitMoveTo(move);
347 }
348
349 abstract void visitMoveTo(MapViewPoint p);
350
351 @Override
352 public void visitLineTo(double x, double y) {
353 visitLineTo(state.getForView(x, y));
354 }
355
356 abstract void visitLineTo(MapViewPoint p);
357
358 @Override
359 public void visitClose() {
360 visitLineTo(lastMoveTo);
361 }
362 }
363
364 private final class LineVisitor extends AbstractMapPathVisitor {
365 private final PathSegmentConsumer consumer;
366 private MapViewPoint last;
367 private double inLineOffset;
368 private boolean startIsOldEnd;
369
370 LineVisitor(PathSegmentConsumer consumer) {
371 this.consumer = consumer;
372 }
373
374 @Override
375 void visitMoveTo(MapViewPoint p) {
376 last = p;
377 startIsOldEnd = false;
378 }
379
380 @Override
381 void visitLineTo(MapViewPoint p) {
382 consumer.addLineBetween(inLineOffset, last, p, startIsOldEnd);
383 inLineOffset += last.distanceToInView(p);
384 last = p;
385 startIsOldEnd = true;
386 }
387 }
388
389 private class ClampingPathVisitor extends AbstractMapPathVisitor {
390 private final MapViewRectangle clip;
391 private final PathSegmentConsumer consumer;
392 protected double strokeProgress;
393 private final double strokeLength;
394
395 private MapViewPoint cursor;
396 private boolean cursorIsActive;
397
398 /**
399 * Create a new {@link ClampingPathVisitor}
400 * @param clip View clip rectangle
401 * @param strokeLength Total length of a stroke sequence
402 * @param consumer The consumer to notify of the path segments.
403 */
404 ClampingPathVisitor(MapViewRectangle clip, double strokeLength, PathSegmentConsumer consumer) {
405 this.clip = clip;
406 this.strokeLength = strokeLength;
407 this.consumer = consumer;
408 }
409
410 @Override
411 void visitMoveTo(MapViewPoint point) {
412 cursor = point;
413 cursorIsActive = false;
414 }
415
416 @Override
417 void visitLineTo(MapViewPoint next) {
418 MapViewPoint entry = clip.getLineEntry(cursor, next);
419 if (entry != null) {
420 MapViewPoint exit = clip.getLineEntry(next, cursor);
421 if (!cursorIsActive || !entry.equals(cursor)) {
422 entry = alignStrokeOffset(entry, cursor);
423 }
424 consumer.addLineBetween(strokeProgress + cursor.distanceToInView(entry), entry, exit, cursorIsActive);
425 cursorIsActive = exit.equals(next);
426 }
427 strokeProgress += cursor.distanceToInView(next);
428
429 cursor = next;
430 }
431
432 private MapViewPoint alignStrokeOffset(MapViewPoint entry, MapViewPoint originalStart) {
433 double distanceSq = entry.distanceToInViewSq(originalStart);
434 if (distanceSq < 0.01 || strokeLength <= 0.001) {
435 // don't move if there is nothing to move.
436 return entry;
437 }
438
439 double distance = Math.sqrt(distanceSq);
440 double offset = (strokeProgress + distance) % strokeLength;
441 if (offset < 0.01) {
442 return entry;
443 }
444
445 return entry.interpolate(originalStart, offset / distance);
446 }
447 }
448
449}
Note: See TracBrowser for help on using the repository browser.