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