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.awt.geom.Point2D; 014 import java.util.Collection; 015 import java.util.Iterator; 016 017 import org.openstreetmap.josm.Main; 018 import org.openstreetmap.josm.data.Bounds; 019 import org.openstreetmap.josm.data.osm.BBox; 020 import org.openstreetmap.josm.data.osm.Changeset; 021 import org.openstreetmap.josm.data.osm.DataSet; 022 import org.openstreetmap.josm.data.osm.Node; 023 import org.openstreetmap.josm.data.osm.OsmPrimitive; 024 import org.openstreetmap.josm.data.osm.Relation; 025 import org.openstreetmap.josm.data.osm.RelationMember; 026 import org.openstreetmap.josm.data.osm.Way; 027 import org.openstreetmap.josm.data.osm.WaySegment; 028 import org.openstreetmap.josm.data.osm.visitor.Visitor; 029 import org.openstreetmap.josm.gui.NavigatableComponent; 030 031 /** 032 * A map renderer that paints a simple scheme of every primitive it visits to a 033 * previous set graphic environment. 034 */ 035 public class WireframeMapRenderer extends AbstractMapRenderer implements Visitor { 036 037 /** Color Preference for inactive objects */ 038 protected Color inactiveColor; 039 /** Color Preference for selected objects */ 040 protected Color selectedColor; 041 /** Color Preference for nodes */ 042 protected Color nodeColor; 043 /** Color Preference for ways not matching any other group */ 044 protected Color dfltWayColor; 045 /** Color Preference for relations */ 046 protected Color relationColor; 047 /** Color Preference for untagged ways */ 048 protected Color untaggedWayColor; 049 /** Color Preference for background */ 050 protected Color backgroundColor; 051 /** Color Preference for hightlighted objects */ 052 protected Color highlightColor; 053 /** Color Preference for tagged nodes */ 054 protected Color taggedColor; 055 /** Color Preference for multiply connected nodes */ 056 protected Color connectionColor; 057 /** Color Preference for tagged and multiply connected nodes */ 058 protected Color taggedConnectionColor; 059 /** Preference: should directional arrows be displayed */ 060 protected boolean showDirectionArrow; 061 /** Preference: should arrows for oneways be displayed */ 062 protected boolean showOnewayArrow; 063 /** Preference: should only the last arrow of a way be displayed */ 064 protected boolean showHeadArrowOnly; 065 /** Preference: should the segement numbers of ways be displayed */ 066 protected boolean showOrderNumber; 067 /** Preference: should selected nodes be filled */ 068 protected boolean fillSelectedNode; 069 /** Preference: should unselected nodes be filled */ 070 protected boolean fillUnselectedNode; 071 /** Preference: should tagged nodes be filled */ 072 protected boolean fillTaggedNode; 073 /** Preference: should multiply connected nodes be filled */ 074 protected boolean fillConnectionNode; 075 /** Preference: size of selected nodes */ 076 protected int selectedNodeSize; 077 /** Preference: size of unselected nodes */ 078 protected int unselectedNodeSize; 079 /** Preference: size of multiply connected nodes */ 080 protected int connectionNodeSize; 081 /** Preference: size of tagged nodes */ 082 protected int taggedNodeSize; 083 /** Preference: size of virtual nodes (0 displayes display) */ 084 protected int virtualNodeSize; 085 /** Preference: minimum space (displayed way length) to display virtual nodes */ 086 protected int virtualNodeSpace; 087 /** Preference: minimum space (displayed way length) to display segment numbers */ 088 protected int segmentNumberSpace; 089 090 /** Color cache to draw subsequent segments of same color as one <code>Path</code>. */ 091 protected Color currentColor = null; 092 /** Path store to draw subsequent segments of same color as one <code>Path</code>. */ 093 protected GeneralPath currentPath = new GeneralPath(); 094 /** 095 * <code>DataSet</code> passed to the @{link render} function to overcome the argument 096 * limitations of @{link Visitor} interface. Only valid until end of rendering call. 097 */ 098 private DataSet ds; 099 100 /** Helper variable for {@link #drawSgement} */ 101 private static final double PHI = Math.toRadians(20); 102 /** Helper variable for {@link #drawSgement} */ 103 private static final double cosPHI = Math.cos(PHI); 104 /** Helper variable for {@link #drawSgement} */ 105 private static final double sinPHI = Math.sin(PHI); 106 107 /** Helper variable for {@link #visit(Relation) */ 108 private Stroke relatedWayStroke = new BasicStroke( 109 4, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL); 110 111 /** 112 * Creates an wireframe render 113 * 114 * @param g the graphics context. Must not be null. 115 * @param nc the map viewport. Must not be null. 116 * @param isInactiveMode if true, the paint visitor shall render OSM objects such that they 117 * look inactive. Example: rendering of data in an inactive layer using light gray as color only. 118 * @throws IllegalArgumentException thrown if {@code g} is null 119 * @throws IllegalArgumentException thrown if {@code nc} is null 120 */ 121 public WireframeMapRenderer(Graphics2D g, NavigatableComponent nc, boolean isInactiveMode) { 122 super(g, nc, isInactiveMode); 123 } 124 125 /** 126 * Reads the color definitions from preferences. This function is <code>public</code>, so that 127 * color names in preferences can be displayed even without calling the wireframe display before. 128 */ 129 public void getColors() 130 { 131 inactiveColor = PaintColors.INACTIVE.get(); 132 selectedColor = PaintColors.SELECTED.get(); 133 nodeColor = PaintColors.NODE.get(); 134 dfltWayColor = PaintColors.DEFAULT_WAY.get(); 135 relationColor = PaintColors.RELATION.get(); 136 untaggedWayColor = PaintColors.UNTAGGED_WAY.get(); 137 backgroundColor = PaintColors.BACKGROUND.get(); 138 highlightColor = PaintColors.HIGHLIGHT_WIREFRAME.get(); 139 taggedColor = PaintColors.TAGGED.get(); 140 connectionColor = PaintColors.CONNECTION.get(); 141 142 if (taggedColor != nodeColor) { 143 taggedConnectionColor = taggedColor; 144 } else { 145 taggedConnectionColor = connectionColor; 146 } 147 } 148 149 /** 150 * Reads all the settings from preferences. Calls the @{link #getColors} 151 * function. 152 * 153 * @param virtual <code>true</code> if virtual nodes are used 154 */ 155 protected void getSettings(boolean virtual) { 156 MapPaintSettings settings = MapPaintSettings.INSTANCE; 157 showDirectionArrow = settings.isShowDirectionArrow(); 158 showOnewayArrow = settings.isShowOnewayArrow(); 159 showHeadArrowOnly = settings.isShowHeadArrowOnly(); 160 showOrderNumber = settings.isShowOrderNumber(); 161 selectedNodeSize = settings.getSelectedNodeSize(); 162 unselectedNodeSize = settings.getUnselectedNodeSize(); 163 connectionNodeSize = settings.getConnectionNodeSize(); 164 taggedNodeSize = settings.getTaggedNodeSize(); 165 fillSelectedNode = settings.isFillSelectedNode(); 166 fillUnselectedNode = settings.isFillUnselectedNode(); 167 fillConnectionNode = settings.isFillConnectionNode(); 168 fillTaggedNode = settings.isFillTaggedNode(); 169 virtualNodeSize = virtual ? Main.pref.getInteger("mappaint.node.virtual-size", 8) / 2 : 0; 170 virtualNodeSpace = Main.pref.getInteger("mappaint.node.virtual-space", 70); 171 segmentNumberSpace = Main.pref.getInteger("mappaint.segmentnumber.space", 40); 172 getColors(); 173 174 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 175 Main.pref.getBoolean("mappaint.wireframe.use-antialiasing", false) ? 176 RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF); 177 } 178 179 /** 180 * Renders the dataset for display. 181 * 182 * @param data <code>DataSet</code> to display 183 * @param virtual <code>true</code> if virtual nodes are used 184 * @param bounds display boundaries 185 */ 186 public void render(DataSet data, boolean virtual, Bounds bounds) { 187 BBox bbox = new BBox(bounds); 188 this.ds = data; 189 getSettings(virtual); 190 191 /* draw tagged ways first, then untagged ways. takes 192 time to iterate through list twice, OTOH does not 193 require changing the colour while painting... */ 194 for (final OsmPrimitive osm: data.searchRelations(bbox)) { 195 if (osm.isDrawable() && !ds.isSelected(osm) && !osm.isDisabledAndHidden()) { 196 osm.visit(this); 197 } 198 } 199 200 for (final OsmPrimitive osm:data.searchWays(bbox)){ 201 if (osm.isDrawable() && !ds.isSelected(osm) && !osm.isDisabledAndHidden() && osm.isTagged()) { 202 osm.visit(this); 203 } 204 } 205 displaySegments(); 206 207 for (final OsmPrimitive osm:data.searchWays(bbox)){ 208 if (osm.isDrawable() && !ds.isSelected(osm) && !osm.isDisabledAndHidden() && !osm.isTagged()) { 209 osm.visit(this); 210 } 211 } 212 displaySegments(); 213 for (final OsmPrimitive osm : data.getSelected()) { 214 if (osm.isDrawable()) { 215 osm.visit(this); 216 } 217 } 218 displaySegments(); 219 220 for (final OsmPrimitive osm: data.searchNodes(bbox)) { 221 if (osm.isDrawable() && !ds.isSelected(osm) && !osm.isDisabledAndHidden()) 222 { 223 osm.visit(this); 224 } 225 } 226 drawVirtualNodes(data.searchWays(bbox), data.getHighlightedVirtualNodes()); 227 228 // draw highlighted way segments over the already drawn ways. Otherwise each 229 // way would have to be checked if it contains a way segment to highlight when 230 // in most of the cases there won't be more than one segment. Since the wireframe 231 // renderer does not feature any transparency there should be no visual difference. 232 for(final WaySegment wseg : data.getHighlightedWaySegments()) { 233 drawSegment(nc.getPoint(wseg.getFirstNode()), nc.getPoint(wseg.getSecondNode()), highlightColor, false); 234 } 235 displaySegments(); 236 } 237 238 /** 239 * Helper function to calculate maximum of 4 values. 240 * 241 * @param a First value 242 * @param b Second value 243 * @param c Third value 244 * @param d Fourth value 245 */ 246 private static final int max(int a, int b, int c, int d) { 247 return Math.max(Math.max(a, b), Math.max(c, d)); 248 } 249 250 /** 251 * Draw a small rectangle. 252 * White if selected (as always) or red otherwise. 253 * 254 * @param n The node to draw. 255 */ 256 @Override 257 public void visit(Node n) { 258 if (n.isIncomplete()) return; 259 260 if (n.isHighlighted()) { 261 drawNode(n, highlightColor, selectedNodeSize, fillSelectedNode); 262 } else { 263 Color color; 264 265 if (isInactiveMode || n.isDisabled()) { 266 color = inactiveColor; 267 } else if (ds.isSelected(n)) { 268 color = selectedColor; 269 } else if (n.isConnectionNode()) { 270 if (n.isTagged()) { 271 color = taggedConnectionColor; 272 } else { 273 color = connectionColor; 274 } 275 } else { 276 if (n.isTagged()) { 277 color = taggedColor; 278 } else { 279 color = nodeColor; 280 } 281 } 282 283 final int size = max((ds.isSelected(n) ? selectedNodeSize : 0), 284 (n.isTagged() ? taggedNodeSize : 0), 285 (n.isConnectionNode() ? connectionNodeSize : 0), 286 unselectedNodeSize); 287 288 final boolean fill = (ds.isSelected(n) && fillSelectedNode) || 289 (n.isTagged() && fillTaggedNode) || 290 (n.isConnectionNode() && fillConnectionNode) || 291 fillUnselectedNode; 292 293 drawNode(n, color, size, fill); 294 } 295 } 296 297 /** 298 * Checks if a way segemnt is large enough for additional information display. 299 * 300 * @param p1 First point of the way segment. 301 * @param p2 Second point of the way segment. 302 * @param space The free space to check against. 303 * @return <code>true</code> if segment is larger than required space 304 */ 305 public static boolean isLargeSegment(Point2D p1, Point2D p2, int space) 306 { 307 double xd = Math.abs(p1.getX()-p2.getX()); 308 double yd = Math.abs(p1.getY()-p2.getY()); 309 return (xd+yd > space); 310 } 311 312 /** 313 * Draws virtual nodes. 314 * 315 * @param ways The ways to draw nodes for. 316 * @param highlightVirtualNodes Way segements, where nodesshould be highlighted. 317 */ 318 public void drawVirtualNodes(Collection<Way> ways, Collection<WaySegment> highlightVirtualNodes) { 319 if (virtualNodeSize == 0) 320 return; 321 // print normal virtual nodes 322 GeneralPath path = new GeneralPath(); 323 for (Way osm : ways) { 324 if (osm.isUsable() && !osm.isDisabledAndHidden() && !osm.isDisabled()) { 325 visitVirtual(path, osm); 326 } 327 } 328 g.setColor(nodeColor); 329 g.draw(path); 330 // print highlighted virtual nodes. Since only the color changes, simply 331 // drawing them over the existing ones works fine (at least in their current 332 // simple style) 333 path = new GeneralPath(); 334 for (WaySegment wseg: highlightVirtualNodes){ 335 if (wseg.way.isUsable() && !wseg.way.isDisabled()) { 336 visitVirtual(path, wseg.toWay()); 337 } 338 } 339 g.setColor(highlightColor); 340 g.draw(path); 341 } 342 343 /** 344 * Creates path for drawing virtual nodes for one way. 345 * 346 * @param path The path to append drawing to. 347 * @param w The ways to draw node for. 348 */ 349 public void visitVirtual(GeneralPath path, Way w) { 350 Iterator<Node> it = w.getNodes().iterator(); 351 if (it.hasNext()) { 352 Point lastP = nc.getPoint(it.next()); 353 while(it.hasNext()) 354 { 355 Point p = nc.getPoint(it.next()); 356 if(isSegmentVisible(lastP, p) && isLargeSegment(lastP, p, virtualNodeSpace)) 357 { 358 int x = (p.x+lastP.x)/2; 359 int y = (p.y+lastP.y)/2; 360 path.moveTo(x-virtualNodeSize, y); 361 path.lineTo(x+virtualNodeSize, y); 362 path.moveTo(x, y-virtualNodeSize); 363 path.lineTo(x, y+virtualNodeSize); 364 } 365 lastP = p; 366 } 367 } 368 } 369 370 /** 371 * Draw a line for all way segments. 372 * @param w The way to draw. 373 */ 374 @Override 375 public void visit(Way w) { 376 if (w.isIncomplete() || w.getNodesCount() < 2) 377 return; 378 379 /* show direction arrows, if draw.segment.relevant_directions_only is not set, the way is tagged with a direction key 380 (even if the tag is negated as in oneway=false) or the way is selected */ 381 382 boolean showThisDirectionArrow = ds.isSelected(w) || showDirectionArrow; 383 /* head only takes over control if the option is true, 384 the direction should be shown at all and not only because it's selected */ 385 boolean showOnlyHeadArrowOnly = showThisDirectionArrow && !ds.isSelected(w) && showHeadArrowOnly; 386 Color wayColor; 387 388 if (isInactiveMode || w.isDisabled()) { 389 wayColor = inactiveColor; 390 } else if(w.isHighlighted()) { 391 wayColor = highlightColor; 392 } else if(ds.isSelected(w)) { 393 wayColor = selectedColor; 394 } else if (!w.isTagged()) { 395 wayColor = untaggedWayColor; 396 } else { 397 wayColor = dfltWayColor; 398 } 399 400 Iterator<Node> it = w.getNodes().iterator(); 401 if (it.hasNext()) { 402 Point lastP = nc.getPoint(it.next()); 403 for (int orderNumber = 1; it.hasNext(); orderNumber++) { 404 Point p = nc.getPoint(it.next()); 405 drawSegment(lastP, p, wayColor, 406 showOnlyHeadArrowOnly ? !it.hasNext() : showThisDirectionArrow); 407 if (showOrderNumber && !isInactiveMode) { 408 drawOrderNumber(lastP, p, orderNumber); 409 } 410 lastP = p; 411 } 412 } 413 } 414 415 /** 416 * Draw objects used in relations. 417 * @param r The relation to draw. 418 */ 419 @Override 420 public void visit(Relation r) { 421 if (r.isIncomplete()) return; 422 423 Color col; 424 if (isInactiveMode || r.isDisabled()) { 425 col = inactiveColor; 426 } else if (ds.isSelected(r)) { 427 col = selectedColor; 428 } else { 429 col = relationColor; 430 } 431 g.setColor(col); 432 433 for (RelationMember m : r.getMembers()) { 434 if (m.getMember().isIncomplete() || !m.getMember().isDrawable()) { 435 continue; 436 } 437 438 if (m.isNode()) { 439 Point p = nc.getPoint(m.getNode()); 440 if (p.x < 0 || p.y < 0 441 || p.x > nc.getWidth() || p.y > nc.getHeight()) { 442 continue; 443 } 444 445 g.drawOval(p.x-3, p.y-3, 6, 6); 446 } else if (m.isWay()) { 447 GeneralPath path = new GeneralPath(); 448 449 boolean first = true; 450 for (Node n : m.getWay().getNodes()) { 451 if (!n.isDrawable()) { 452 continue; 453 } 454 Point p = nc.getPoint(n); 455 if (first) { 456 path.moveTo(p.x, p.y); 457 first = false; 458 } else { 459 path.lineTo(p.x, p.y); 460 } 461 } 462 463 g.draw(relatedWayStroke.createStrokedShape(path)); 464 } 465 } 466 } 467 468 /** 469 * Visitor for changesets not used in this class 470 * @param cs The changeset for inspection. 471 */ 472 @Override 473 public void visit(Changeset cs) {/* ignore */} 474 475 /** 476 * Draw an number of the order of the two consecutive nodes within the 477 * parents way 478 * 479 * @param p1 First point of the way segment. 480 * @param p2 Second point of the way segment. 481 * @param orderNumber The number of the segment in the way. 482 */ 483 protected void drawOrderNumber(Point p1, Point p2, int orderNumber) { 484 if (isSegmentVisible(p1, p2) && isLargeSegment(p1, p2, segmentNumberSpace)) { 485 String on = Integer.toString(orderNumber); 486 int strlen = on.length(); 487 int x = (p1.x+p2.x)/2 - 4*strlen; 488 int y = (p1.y+p2.y)/2 + 4; 489 490 if(virtualNodeSize != 0 && isLargeSegment(p1, p2, virtualNodeSpace)) 491 { 492 y = (p1.y+p2.y)/2 - virtualNodeSize - 3; 493 } 494 495 displaySegments(); /* draw nodes on top! */ 496 Color c = g.getColor(); 497 g.setColor(backgroundColor); 498 g.fillRect(x-1, y-12, 8*strlen+1, 14); 499 g.setColor(c); 500 g.drawString(on, x, y); 501 } 502 } 503 504 /** 505 * Draw the node as small rectangle with the given color. 506 * 507 * @param n The node to draw. 508 * @param color The color of the node. 509 */ 510 public void drawNode(Node n, Color color, int size, boolean fill) { 511 if (size > 1) { 512 int radius = size / 2; 513 Point p = nc.getPoint(n); 514 if ((p.x < 0) || (p.y < 0) || (p.x > nc.getWidth()) 515 || (p.y > nc.getHeight())) 516 return; 517 g.setColor(color); 518 if (fill) { 519 g.fillRect(p.x - radius, p.y - radius, size, size); 520 g.drawRect(p.x - radius, p.y - radius, size, size); 521 } else { 522 g.drawRect(p.x - radius, p.y - radius, size, size); 523 } 524 } 525 } 526 527 /** 528 * Draw a line with the given color. 529 * 530 * @param path The path to append this segment. 531 * @param p1 First point of the way segment. 532 * @param p2 Second point of the way segment. 533 * @param showDirection <code>true</code> if segment direction should be indicated 534 */ 535 protected void drawSegment(GeneralPath path, Point p1, Point p2, boolean showDirection) { 536 Rectangle bounds = g.getClipBounds(); 537 bounds.grow(100, 100); // avoid arrow heads at the border 538 LineClip clip = new LineClip(p1, p2, bounds); 539 if (clip.execute()) { 540 p1 = clip.getP1(); 541 p2 = clip.getP2(); 542 path.moveTo(p1.x, p1.y); 543 path.lineTo(p2.x, p2.y); 544 545 if (showDirection) { 546 final double l = 10. / p1.distance(p2); 547 548 final double sx = l * (p1.x - p2.x); 549 final double sy = l * (p1.y - p2.y); 550 551 path.lineTo (p2.x + (int) Math.round(cosPHI * sx - sinPHI * sy), p2.y + (int) Math.round(sinPHI * sx + cosPHI * sy)); 552 path.moveTo (p2.x + (int) Math.round(cosPHI * sx + sinPHI * sy), p2.y + (int) Math.round(- sinPHI * sx + cosPHI * sy)); 553 path.lineTo(p2.x, p2.y); 554 } 555 } 556 } 557 558 /** 559 * Draw a line with the given color. 560 * 561 * @param p1 First point of the way segment. 562 * @param p2 Second point of the way segment. 563 * @param col The color to use for drawing line. 564 * @param showDirection <code>true</code> if segment direction should be indicated. 565 */ 566 protected void drawSegment(Point p1, Point p2, Color col, boolean showDirection) { 567 if (col != currentColor) { 568 displaySegments(col); 569 } 570 drawSegment(currentPath, p1, p2, showDirection); 571 } 572 573 /** 574 * Checks if segment is visible in display. 575 * 576 * @param p1 First point of the way segment. 577 * @param p2 Second point of the way segment. 578 * @return <code>true</code> if segment is visible. 579 */ 580 protected boolean isSegmentVisible(Point p1, Point p2) { 581 if ((p1.x < 0) && (p2.x < 0)) return false; 582 if ((p1.y < 0) && (p2.y < 0)) return false; 583 if ((p1.x > nc.getWidth()) && (p2.x > nc.getWidth())) return false; 584 if ((p1.y > nc.getHeight()) && (p2.y > nc.getHeight())) return false; 585 return true; 586 } 587 588 /** 589 * Checks if a polygon is visible in display. 590 * 591 * @param polygon The polygon to check. 592 * @return <code>true</code> if polygon is visible. 593 */ 594 protected boolean isPolygonVisible(Polygon polygon) { 595 Rectangle bounds = polygon.getBounds(); 596 if (bounds.width == 0 && bounds.height == 0) return false; 597 if (bounds.x > nc.getWidth()) return false; 598 if (bounds.y > nc.getHeight()) return false; 599 if (bounds.x + bounds.width < 0) return false; 600 if (bounds.y + bounds.height < 0) return false; 601 return true; 602 } 603 604 /** 605 * Finally display all segments in currect path. 606 */ 607 protected void displaySegments() { 608 displaySegments(null); 609 } 610 611 /** 612 * Finally display all segments in currect path. 613 * 614 * @param newColor This color is set after the path is drawn. 615 */ 616 protected void displaySegments(Color newColor) { 617 if (currentPath != null) { 618 g.setColor(currentColor); 619 g.draw(currentPath); 620 currentPath = new GeneralPath(); 621 currentColor = newColor; 622 } 623 } 624 }