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

Last change on this file since 11144 was 11144, checked in by michael2402, 8 years ago

Fix #13793: Clip paths in a way that let's the dashes stay in place.

  • Property svn:eol-style set to native
File size: 9.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.PathIterator;
8
9import org.openstreetmap.josm.data.coor.EastNorth;
10import org.openstreetmap.josm.data.osm.Node;
11import org.openstreetmap.josm.gui.MapView;
12import org.openstreetmap.josm.gui.MapViewState;
13import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
14import org.openstreetmap.josm.gui.MapViewState.MapViewRectangle;
15
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 * Move the cursor to the given node.
47 * @param n The node
48 * @return this for easy chaining.
49 */
50 public MapViewPath moveTo(Node n) {
51 moveTo(n.getEastNorth());
52 return this;
53 }
54
55 /**
56 * Move the cursor to the given position.
57 * @param eastNorth The position
58 * @return this for easy chaining.
59 */
60 public MapViewPath moveTo(EastNorth eastNorth) {
61 moveTo(state.getPointFor(eastNorth));
62 return this;
63 }
64
65 @Override
66 public MapViewPath moveTo(MapViewPoint p) {
67 super.moveTo(p);
68 return this;
69 }
70
71 /**
72 * Draw a line to the node.
73 * <p>
74 * line clamping to view is done automatically.
75 * @param n The node
76 * @return this for easy chaining.
77 */
78 public MapViewPath lineTo(Node n) {
79 lineTo(n.getEastNorth());
80 return this;
81 }
82
83 /**
84 * Draw a line to the position.
85 * <p>
86 * line clamping to view is done automatically.
87 * @param eastNorth The position
88 * @return this for easy chaining.
89 */
90 public MapViewPath lineTo(EastNorth eastNorth) {
91 lineTo(state.getPointFor(eastNorth));
92 return this;
93 }
94
95 @Override
96 public MapViewPath lineTo(MapViewPoint p) {
97 super.lineTo(p);
98 return this;
99 }
100
101 /**
102 * Add the given shape centered around the current node.
103 * @param p1 The point to draw around
104 * @param symbol The symbol type
105 * @param size The size of the symbol in pixel
106 * @return this for easy chaining.
107 */
108 public MapViewPath shapeAround(Node p1, SymbolShape symbol, double size) {
109 shapeAround(p1.getEastNorth(), symbol, size);
110 return this;
111 }
112
113 /**
114 * Add the given shape centered around the current position.
115 * @param eastNorth The point to draw around
116 * @param symbol The symbol type
117 * @param size The size of the symbol in pixel
118 * @return this for easy chaining.
119 */
120 public MapViewPath shapeAround(EastNorth eastNorth, SymbolShape symbol, double size) {
121 shapeAround(state.getPointFor(eastNorth), symbol, size);
122 return this;
123 }
124
125 @Override
126 public MapViewPath shapeAround(MapViewPoint p, SymbolShape symbol, double size) {
127 super.shapeAround(p, symbol, size);
128 return this;
129 }
130
131 /**
132 * Append a list of nodes
133 * @param nodes The nodes to append
134 * @param connect <code>true</code> if we should use a lineTo as first command.
135 * @return this for easy chaining.
136 */
137 public MapViewPath append(Iterable<Node> nodes, boolean connect) {
138 appendWay(nodes, connect, false);
139 return this;
140 }
141
142 /**
143 * Append a list of nodes as closed way.
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 appendClosed(Iterable<Node> nodes, boolean connect) {
149 appendWay(nodes, connect, true);
150 return this;
151 }
152
153 private void appendWay(Iterable<Node> nodes, boolean connect, boolean close) {
154 boolean useMoveTo = !connect;
155 Node first = null;
156 for (Node n : nodes) {
157 if (useMoveTo) {
158 moveTo(n);
159 } else {
160 lineTo(n);
161 }
162 if (close && first == null) {
163 first = n;
164 }
165 useMoveTo = false;
166 }
167 if (first != null) {
168 lineTo(first);
169 }
170 }
171
172 /**
173 * Compute a line that is similar to the current path expect for that parts outside the screen are skipped using moveTo commands.
174 *
175 * The line is computed in a way that dashes stay in their place when moving the view.
176 *
177 * The resulting line is not intended to fill areas.
178 * @param stroke The stroke to compute the line for.
179 * @return The new line shape.
180 */
181 public Shape computeClippedLine(Stroke stroke) {
182 if (stroke instanceof BasicStroke && ((BasicStroke) stroke).getDashArray() != null) {
183 float length = 0;
184 for (float f : ((BasicStroke) stroke).getDashArray()) {
185 length += f;
186 }
187 return computeClippedLine(((BasicStroke) stroke).getDashPhase(), length);
188 } else {
189 return computeClippedLine(0, 0);
190 }
191 }
192
193 private Shape computeClippedLine(double strokeOffset, double strokeLength) {
194 ClampingPathVisitor path = new ClampingPathVisitor(state.getViewClipRectangle(), strokeOffset, strokeLength);
195 if (path.visit(getPathIterator(null))) {
196 return path;
197 } else {
198 // could not clip the path.
199 return this;
200 }
201 }
202
203 private class ClampingPathVisitor extends MapPath2D {
204 private final MapViewRectangle clip;
205 private double strokeOffset;
206 private final double strokeLength;
207 private MapViewPoint lastMoveTo;
208
209 private MapViewPoint cursor;
210 private boolean cursorIsActive = false;
211
212 ClampingPathVisitor(MapViewRectangle clip, double strokeOffset, double strokeLength) {
213 this.clip = clip;
214 this.strokeOffset = strokeOffset;
215 this.strokeLength = strokeLength;
216 }
217
218 /**
219 * Append a path to this one. The path is clipped to the current view.
220 * @param it The iterator
221 * @return true if adding the path was successful.
222 */
223 public boolean visit(PathIterator it) {
224 double[] coords = new double[8];
225 while (!it.isDone()) {
226 int type = it.currentSegment(coords);
227 switch (type) {
228 case PathIterator.SEG_CLOSE:
229 visitClose();
230 break;
231 case PathIterator.SEG_LINETO:
232 visitLineTo(coords[0], coords[1]);
233 break;
234 case PathIterator.SEG_MOVETO:
235 visitMoveTo(coords[0], coords[1]);
236 break;
237 default:
238 // cannot handle this shape - this should be very rare. We let Java2D do the clipping.
239 return false;
240 }
241 it.next();
242 }
243 return true;
244 }
245
246 void visitClose() {
247 drawLineTo(lastMoveTo);
248 }
249
250 void visitMoveTo(double x, double y) {
251 MapViewPoint point = state.getForView(x, y);
252 lastMoveTo = point;
253 cursor = point;
254 }
255
256 void visitLineTo(double x, double y) {
257 drawLineTo(state.getForView(x, y));
258 }
259
260 private void drawLineTo(MapViewPoint next) {
261 MapViewPoint entry = clip.getLineEntry(cursor, next);
262 if (entry != null) {
263 MapViewPoint exit = clip.getLineEntry(next, cursor);
264 if (!cursorIsActive || !entry.equals(cursor)) {
265 entry = alignStrokeOffset(entry, cursor);
266 moveTo(entry);
267 }
268 lineTo(exit);
269 cursorIsActive = exit.equals(next);
270 }
271 strokeOffset += cursor.distanceToInView(next);
272
273 cursor = next;
274 }
275
276 private MapViewPoint alignStrokeOffset(MapViewPoint entry, MapViewPoint originalStart) {
277 double distanceSq = entry.distanceToInViewSq(originalStart);
278 if (distanceSq < 0.01 || strokeLength <= 0.001) {
279 // don't move if there is nothing to move.
280 return entry;
281 }
282
283 double distance = Math.sqrt(distanceSq);
284 double offset = ((strokeOffset + distance)) % strokeLength;
285 if (offset < 0.01) {
286 return entry;
287 }
288
289 return entry.interpolate(originalStart, offset / distance);
290 }
291
292 }
293}
Note: See TracBrowser for help on using the repository browser.