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