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