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