001 /* License: GPL. Copyright 2007 by Immanuel Scholz and others */
002 package org.openstreetmap.josm.data.osm.visitor.paint;
003
004 import java.awt.BasicStroke;
005 import java.awt.Color;
006 import java.awt.Graphics2D;
007 import java.awt.Point;
008 import java.awt.Polygon;
009 import java.awt.Rectangle;
010 import java.awt.RenderingHints;
011 import java.awt.Stroke;
012 import java.awt.geom.GeneralPath;
013 import java.util.Iterator;
014
015 import org.openstreetmap.josm.Main;
016 import org.openstreetmap.josm.data.Bounds;
017 import org.openstreetmap.josm.data.osm.BBox;
018 import org.openstreetmap.josm.data.osm.Changeset;
019 import org.openstreetmap.josm.data.osm.DataSet;
020 import org.openstreetmap.josm.data.osm.Node;
021 import org.openstreetmap.josm.data.osm.OsmPrimitive;
022 import org.openstreetmap.josm.data.osm.Relation;
023 import org.openstreetmap.josm.data.osm.RelationMember;
024 import org.openstreetmap.josm.data.osm.Way;
025 import org.openstreetmap.josm.data.osm.WaySegment;
026 import org.openstreetmap.josm.data.osm.visitor.Visitor;
027 import org.openstreetmap.josm.gui.NavigatableComponent;
028
029 /**
030 * A map renderer that paints a simple scheme of every primitive it visits to a
031 * previous set graphic environment.
032 */
033 public class WireframeMapRenderer extends AbstractMapRenderer implements Visitor {
034
035 /** Color Preference for ways not matching any other group */
036 protected Color dfltWayColor;
037 /** Color Preference for relations */
038 protected Color relationColor;
039 /** Color Preference for untagged ways */
040 protected Color untaggedWayColor;
041 /** Color Preference for tagged nodes */
042 protected Color taggedColor;
043 /** Color Preference for multiply connected nodes */
044 protected Color connectionColor;
045 /** Color Preference for tagged and multiply connected nodes */
046 protected Color taggedConnectionColor;
047 /** Preference: should directional arrows be displayed */
048 protected boolean showDirectionArrow;
049 /** Preference: should arrows for oneways be displayed */
050 protected boolean showOnewayArrow;
051 /** Preference: should only the last arrow of a way be displayed */
052 protected boolean showHeadArrowOnly;
053 /** Preference: should the segement numbers of ways be displayed */
054 protected boolean showOrderNumber;
055 /** Preference: should selected nodes be filled */
056 protected boolean fillSelectedNode;
057 /** Preference: should unselected nodes be filled */
058 protected boolean fillUnselectedNode;
059 /** Preference: should tagged nodes be filled */
060 protected boolean fillTaggedNode;
061 /** Preference: should multiply connected nodes be filled */
062 protected boolean fillConnectionNode;
063 /** Preference: size of selected nodes */
064 protected int selectedNodeSize;
065 /** Preference: size of unselected nodes */
066 protected int unselectedNodeSize;
067 /** Preference: size of multiply connected nodes */
068 protected int connectionNodeSize;
069 /** Preference: size of tagged nodes */
070 protected int taggedNodeSize;
071
072 /** Color cache to draw subsequent segments of same color as one <code>Path</code>. */
073 protected Color currentColor = null;
074 /** Path store to draw subsequent segments of same color as one <code>Path</code>. */
075 protected GeneralPath currentPath = new GeneralPath();
076 /**
077 * <code>DataSet</code> passed to the @{link render} function to overcome the argument
078 * limitations of @{link Visitor} interface. Only valid until end of rendering call.
079 */
080 private DataSet ds;
081
082 /** Helper variable for {@link #drawSgement} */
083 private static final double PHI = Math.toRadians(20);
084 /** Helper variable for {@link #drawSgement} */
085 private static final double cosPHI = Math.cos(PHI);
086 /** Helper variable for {@link #drawSgement} */
087 private static final double sinPHI = Math.sin(PHI);
088
089 /** Helper variable for {@link #visit(Relation) */
090 private Stroke relatedWayStroke = new BasicStroke(
091 4, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL);
092
093 /**
094 * Creates an wireframe render
095 *
096 * @param g the graphics context. Must not be null.
097 * @param nc the map viewport. Must not be null.
098 * @param isInactiveMode if true, the paint visitor shall render OSM objects such that they
099 * look inactive. Example: rendering of data in an inactive layer using light gray as color only.
100 * @throws IllegalArgumentException thrown if {@code g} is null
101 * @throws IllegalArgumentException thrown if {@code nc} is null
102 */
103 public WireframeMapRenderer(Graphics2D g, NavigatableComponent nc, boolean isInactiveMode) {
104 super(g, nc, isInactiveMode);
105 }
106
107 @Override
108 public void getColors() {
109 super.getColors();
110 dfltWayColor = PaintColors.DEFAULT_WAY.get();
111 relationColor = PaintColors.RELATION.get();
112 untaggedWayColor = PaintColors.UNTAGGED_WAY.get();
113 highlightColor = PaintColors.HIGHLIGHT_WIREFRAME.get();
114 taggedColor = PaintColors.TAGGED.get();
115 connectionColor = PaintColors.CONNECTION.get();
116
117 if (taggedColor != nodeColor) {
118 taggedConnectionColor = taggedColor;
119 } else {
120 taggedConnectionColor = connectionColor;
121 }
122 }
123
124 @Override
125 protected void getSettings(boolean virtual) {
126 super.getSettings(virtual);
127 MapPaintSettings settings = MapPaintSettings.INSTANCE;
128 showDirectionArrow = settings.isShowDirectionArrow();
129 showOnewayArrow = settings.isShowOnewayArrow();
130 showHeadArrowOnly = settings.isShowHeadArrowOnly();
131 showOrderNumber = settings.isShowOrderNumber();
132 selectedNodeSize = settings.getSelectedNodeSize();
133 unselectedNodeSize = settings.getUnselectedNodeSize();
134 connectionNodeSize = settings.getConnectionNodeSize();
135 taggedNodeSize = settings.getTaggedNodeSize();
136 fillSelectedNode = settings.isFillSelectedNode();
137 fillUnselectedNode = settings.isFillUnselectedNode();
138 fillConnectionNode = settings.isFillConnectionNode();
139 fillTaggedNode = settings.isFillTaggedNode();
140
141 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
142 Main.pref.getBoolean("mappaint.wireframe.use-antialiasing", false) ?
143 RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
144 }
145
146 /**
147 * Renders the dataset for display.
148 *
149 * @param data <code>DataSet</code> to display
150 * @param virtual <code>true</code> if virtual nodes are used
151 * @param bounds display boundaries
152 */
153 public void render(DataSet data, boolean virtual, Bounds bounds) {
154 BBox bbox = new BBox(bounds);
155 this.ds = data;
156 getSettings(virtual);
157
158 /* draw tagged ways first, then untagged ways. takes
159 time to iterate through list twice, OTOH does not
160 require changing the colour while painting... */
161 for (final OsmPrimitive osm: data.searchRelations(bbox)) {
162 if (osm.isDrawable() && !ds.isSelected(osm) && !osm.isDisabledAndHidden()) {
163 osm.visit(this);
164 }
165 }
166
167 for (final OsmPrimitive osm:data.searchWays(bbox)){
168 if (osm.isDrawable() && !ds.isSelected(osm) && !osm.isDisabledAndHidden() && osm.isTagged()) {
169 osm.visit(this);
170 }
171 }
172 displaySegments();
173
174 for (final OsmPrimitive osm:data.searchWays(bbox)){
175 if (osm.isDrawable() && !ds.isSelected(osm) && !osm.isDisabledAndHidden() && !osm.isTagged()) {
176 osm.visit(this);
177 }
178 }
179 displaySegments();
180 for (final OsmPrimitive osm : data.getSelected()) {
181 if (osm.isDrawable()) {
182 osm.visit(this);
183 }
184 }
185 displaySegments();
186
187 for (final OsmPrimitive osm: data.searchNodes(bbox)) {
188 if (osm.isDrawable() && !ds.isSelected(osm) && !osm.isDisabledAndHidden())
189 {
190 osm.visit(this);
191 }
192 }
193 drawVirtualNodes(data, bbox);
194
195 // draw highlighted way segments over the already drawn ways. Otherwise each
196 // way would have to be checked if it contains a way segment to highlight when
197 // in most of the cases there won't be more than one segment. Since the wireframe
198 // renderer does not feature any transparency there should be no visual difference.
199 for (final WaySegment wseg : data.getHighlightedWaySegments()) {
200 drawSegment(nc.getPoint(wseg.getFirstNode()), nc.getPoint(wseg.getSecondNode()), highlightColor, false);
201 }
202 displaySegments();
203 }
204
205 /**
206 * Helper function to calculate maximum of 4 values.
207 *
208 * @param a First value
209 * @param b Second value
210 * @param c Third value
211 * @param d Fourth value
212 */
213 private static final int max(int a, int b, int c, int d) {
214 return Math.max(Math.max(a, b), Math.max(c, d));
215 }
216
217 /**
218 * Draw a small rectangle.
219 * White if selected (as always) or red otherwise.
220 *
221 * @param n The node to draw.
222 */
223 @Override
224 public void visit(Node n) {
225 if (n.isIncomplete()) return;
226
227 if (n.isHighlighted()) {
228 drawNode(n, highlightColor, selectedNodeSize, fillSelectedNode);
229 } else {
230 Color color;
231
232 if (isInactiveMode || n.isDisabled()) {
233 color = inactiveColor;
234 } else if (ds.isSelected(n)) {
235 color = selectedColor;
236 } else if (n.isConnectionNode()) {
237 if (n.isTagged()) {
238 color = taggedConnectionColor;
239 } else {
240 color = connectionColor;
241 }
242 } else {
243 if (n.isTagged()) {
244 color = taggedColor;
245 } else {
246 color = nodeColor;
247 }
248 }
249
250 final int size = max((ds.isSelected(n) ? selectedNodeSize : 0),
251 (n.isTagged() ? taggedNodeSize : 0),
252 (n.isConnectionNode() ? connectionNodeSize : 0),
253 unselectedNodeSize);
254
255 final boolean fill = (ds.isSelected(n) && fillSelectedNode) ||
256 (n.isTagged() && fillTaggedNode) ||
257 (n.isConnectionNode() && fillConnectionNode) ||
258 fillUnselectedNode;
259
260 drawNode(n, color, size, fill);
261 }
262 }
263
264 /**
265 * Draw a line for all way segments.
266 * @param w The way to draw.
267 */
268 @Override
269 public void visit(Way w) {
270 if (w.isIncomplete() || w.getNodesCount() < 2)
271 return;
272
273 /* show direction arrows, if draw.segment.relevant_directions_only is not set, the way is tagged with a direction key
274 (even if the tag is negated as in oneway=false) or the way is selected */
275
276 boolean showThisDirectionArrow = ds.isSelected(w) || showDirectionArrow;
277 /* head only takes over control if the option is true,
278 the direction should be shown at all and not only because it's selected */
279 boolean showOnlyHeadArrowOnly = showThisDirectionArrow && !ds.isSelected(w) && showHeadArrowOnly;
280 Color wayColor;
281
282 if (isInactiveMode || w.isDisabled()) {
283 wayColor = inactiveColor;
284 } else if(w.isHighlighted()) {
285 wayColor = highlightColor;
286 } else if(ds.isSelected(w)) {
287 wayColor = selectedColor;
288 } else if (!w.isTagged()) {
289 wayColor = untaggedWayColor;
290 } else {
291 wayColor = dfltWayColor;
292 }
293
294 Iterator<Node> it = w.getNodes().iterator();
295 if (it.hasNext()) {
296 Point lastP = nc.getPoint(it.next());
297 for (int orderNumber = 1; it.hasNext(); orderNumber++) {
298 Point p = nc.getPoint(it.next());
299 drawSegment(lastP, p, wayColor,
300 showOnlyHeadArrowOnly ? !it.hasNext() : showThisDirectionArrow);
301 if (showOrderNumber && !isInactiveMode) {
302 drawOrderNumber(lastP, p, orderNumber, g.getColor());
303 }
304 lastP = p;
305 }
306 }
307 }
308
309 /**
310 * Draw objects used in relations.
311 * @param r The relation to draw.
312 */
313 @Override
314 public void visit(Relation r) {
315 if (r.isIncomplete()) return;
316
317 Color col;
318 if (isInactiveMode || r.isDisabled()) {
319 col = inactiveColor;
320 } else if (ds.isSelected(r)) {
321 col = selectedColor;
322 } else {
323 col = relationColor;
324 }
325 g.setColor(col);
326
327 for (RelationMember m : r.getMembers()) {
328 if (m.getMember().isIncomplete() || !m.getMember().isDrawable()) {
329 continue;
330 }
331
332 if (m.isNode()) {
333 Point p = nc.getPoint(m.getNode());
334 if (p.x < 0 || p.y < 0
335 || p.x > nc.getWidth() || p.y > nc.getHeight()) {
336 continue;
337 }
338
339 g.drawOval(p.x-3, p.y-3, 6, 6);
340 } else if (m.isWay()) {
341 GeneralPath path = new GeneralPath();
342
343 boolean first = true;
344 for (Node n : m.getWay().getNodes()) {
345 if (!n.isDrawable()) {
346 continue;
347 }
348 Point p = nc.getPoint(n);
349 if (first) {
350 path.moveTo(p.x, p.y);
351 first = false;
352 } else {
353 path.lineTo(p.x, p.y);
354 }
355 }
356
357 g.draw(relatedWayStroke.createStrokedShape(path));
358 }
359 }
360 }
361
362 /**
363 * Visitor for changesets not used in this class
364 * @param cs The changeset for inspection.
365 */
366 @Override
367 public void visit(Changeset cs) {/* ignore */}
368
369 @Override
370 public void drawNode(Node n, Color color, int size, boolean fill) {
371 if (size > 1) {
372 int radius = size / 2;
373 Point p = nc.getPoint(n);
374 if ((p.x < 0) || (p.y < 0) || (p.x > nc.getWidth())
375 || (p.y > nc.getHeight()))
376 return;
377 g.setColor(color);
378 if (fill) {
379 g.fillRect(p.x - radius, p.y - radius, size, size);
380 g.drawRect(p.x - radius, p.y - radius, size, size);
381 } else {
382 g.drawRect(p.x - radius, p.y - radius, size, size);
383 }
384 }
385 }
386
387 /**
388 * Draw a line with the given color.
389 *
390 * @param path The path to append this segment.
391 * @param p1 First point of the way segment.
392 * @param p2 Second point of the way segment.
393 * @param showDirection <code>true</code> if segment direction should be indicated
394 */
395 protected void drawSegment(GeneralPath path, Point p1, Point p2, boolean showDirection) {
396 Rectangle bounds = g.getClipBounds();
397 bounds.grow(100, 100); // avoid arrow heads at the border
398 LineClip clip = new LineClip(p1, p2, bounds);
399 if (clip.execute()) {
400 p1 = clip.getP1();
401 p2 = clip.getP2();
402 path.moveTo(p1.x, p1.y);
403 path.lineTo(p2.x, p2.y);
404
405 if (showDirection) {
406 final double l = 10. / p1.distance(p2);
407
408 final double sx = l * (p1.x - p2.x);
409 final double sy = l * (p1.y - p2.y);
410
411 path.lineTo (p2.x + (int) Math.round(cosPHI * sx - sinPHI * sy), p2.y + (int) Math.round(sinPHI * sx + cosPHI * sy));
412 path.moveTo (p2.x + (int) Math.round(cosPHI * sx + sinPHI * sy), p2.y + (int) Math.round(- sinPHI * sx + cosPHI * sy));
413 path.lineTo(p2.x, p2.y);
414 }
415 }
416 }
417
418 /**
419 * Draw a line with the given color.
420 *
421 * @param p1 First point of the way segment.
422 * @param p2 Second point of the way segment.
423 * @param col The color to use for drawing line.
424 * @param showDirection <code>true</code> if segment direction should be indicated.
425 */
426 protected void drawSegment(Point p1, Point p2, Color col, boolean showDirection) {
427 if (col != currentColor) {
428 displaySegments(col);
429 }
430 drawSegment(currentPath, p1, p2, showDirection);
431 }
432
433 /**
434 * Checks if a polygon is visible in display.
435 *
436 * @param polygon The polygon to check.
437 * @return <code>true</code> if polygon is visible.
438 */
439 protected boolean isPolygonVisible(Polygon polygon) {
440 Rectangle bounds = polygon.getBounds();
441 if (bounds.width == 0 && bounds.height == 0) return false;
442 if (bounds.x > nc.getWidth()) return false;
443 if (bounds.y > nc.getHeight()) return false;
444 if (bounds.x + bounds.width < 0) return false;
445 if (bounds.y + bounds.height < 0) return false;
446 return true;
447 }
448
449 /**
450 * Finally display all segments in currect path.
451 */
452 protected void displaySegments() {
453 displaySegments(null);
454 }
455
456 /**
457 * Finally display all segments in currect path.
458 *
459 * @param newColor This color is set after the path is drawn.
460 */
461 protected void displaySegments(Color newColor) {
462 if (currentPath != null) {
463 g.setColor(currentColor);
464 g.draw(currentPath);
465 currentPath = new GeneralPath();
466 currentColor = newColor;
467 }
468 }
469 }