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 }