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

Last change on this file since 11811 was 11811, checked in by Don-vip, 7 years ago

sonar - squid:S3052 - Fields should not be initialized to default values

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