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.INode; 021import org.openstreetmap.josm.data.osm.IPrimitive; 022import org.openstreetmap.josm.data.osm.IRelation; 023import org.openstreetmap.josm.data.osm.IRelationMember; 024import org.openstreetmap.josm.data.osm.IWay; 025import org.openstreetmap.josm.data.osm.OsmData; 026import org.openstreetmap.josm.data.osm.WaySegment; 027import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor; 028import org.openstreetmap.josm.gui.MapViewState.MapViewPoint; 029import org.openstreetmap.josm.gui.MapViewState.MapViewRectangle; 030import org.openstreetmap.josm.gui.NavigatableComponent; 031import org.openstreetmap.josm.gui.draw.MapPath2D; 032import org.openstreetmap.josm.spi.preferences.Config; 033import org.openstreetmap.josm.tools.Utils; 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 PrimitiveVisitor { 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: should relation ways be shown with outlines */ 073 protected boolean useRelatedWayStroke; 074 /** Preference: size of selected nodes */ 075 protected int selectedNodeSize; 076 /** Preference: size of unselected nodes */ 077 protected int unselectedNodeSize; 078 /** Preference: size of multiply connected nodes */ 079 protected int connectionNodeSize; 080 /** Preference: size of tagged nodes */ 081 protected int taggedNodeSize; 082 083 /** Color cache to draw subsequent segments of same color as one <code>Path</code>. */ 084 protected Color currentColor; 085 /** Path store to draw subsequent segments of same color as one <code>Path</code>. */ 086 protected MapPath2D currentPath = new MapPath2D(); 087 088 /** Helper variable for {@link #drawSegment} */ 089 private static final ArrowPaintHelper ARROW_PAINT_HELPER = new ArrowPaintHelper(Utils.toRadians(20), 10); 090 091 /** Helper variable for {@link #visit(IRelation)} */ 092 private final Stroke relatedWayStroke = new BasicStroke( 093 4, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL); 094 private MapViewRectangle viewClip; 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 if {@code g} is null 104 * @throws IllegalArgumentException 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.equals(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 showOrderNumberOnSelectedWay = settings.isShowOrderNumberOnSelectedWay(); 136 selectedNodeSize = settings.getSelectedNodeSize(); 137 unselectedNodeSize = settings.getUnselectedNodeSize(); 138 connectionNodeSize = settings.getConnectionNodeSize(); 139 taggedNodeSize = settings.getTaggedNodeSize(); 140 fillSelectedNode = settings.isFillSelectedNode(); 141 fillUnselectedNode = settings.isFillUnselectedNode(); 142 fillConnectionNode = settings.isFillConnectionNode(); 143 fillTaggedNode = settings.isFillTaggedNode(); 144 useRelatedWayStroke = 145 Config.getPref().getBoolean("mappaint.wireframe.show-relation-outlines", true); 146 147 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 148 Config.getPref().getBoolean("mappaint.wireframe.use-antialiasing", false) ? 149 RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF); 150 } 151 152 @Override 153 public void render(OsmData<?, ?, ?, ?> data, boolean virtual, Bounds bounds) { 154 BBox bbox = bounds.toBBox(); 155 Rectangle clip = g.getClipBounds(); 156 clip.grow(50, 50); 157 viewClip = mapState.getViewArea(clip); 158 getSettings(virtual); 159 160 for (final IRelation<?> rel : data.searchRelations(bbox)) { 161 if (rel.isDrawable() && !rel.isSelected() && !rel.isDisabledAndHidden()) { 162 rel.accept(this); 163 } 164 } 165 166 // draw tagged ways first, then untagged ways, then highlighted ways 167 List<IWay<?>> highlightedWays = new ArrayList<>(); 168 List<IWay<?>> untaggedWays = new ArrayList<>(); 169 170 for (final IWay<?> way : data.searchWays(bbox)) { 171 if (way.isDrawable() && !way.isSelected() && !way.isDisabledAndHidden()) { 172 if (way.isHighlighted()) { 173 highlightedWays.add(way); 174 } else if (!way.isTagged()) { 175 untaggedWays.add(way); 176 } else { 177 way.accept(this); 178 } 179 } 180 } 181 displaySegments(); 182 183 // Display highlighted ways after the other ones (fix #8276) 184 List<IWay<?>> specialWays = new ArrayList<>(untaggedWays); 185 specialWays.addAll(highlightedWays); 186 for (final IWay<?> way : specialWays) { 187 way.accept(this); 188 } 189 specialWays.clear(); 190 displaySegments(); 191 192 for (final IPrimitive osm : data.getSelected()) { 193 if (osm.isDrawable()) { 194 osm.accept(this); 195 } 196 } 197 displaySegments(); 198 199 for (final INode osm: data.searchNodes(bbox)) { 200 if (osm.isDrawable() && !osm.isSelected() && !osm.isDisabledAndHidden()) { 201 osm.accept(this); 202 } 203 } 204 drawVirtualNodes(data, bbox); 205 206 // draw highlighted way segments over the already drawn ways. Otherwise each 207 // way would have to be checked if it contains a way segment to highlight when 208 // in most of the cases there won't be more than one segment. Since the wireframe 209 // renderer does not feature any transparency there should be no visual difference. 210 for (final WaySegment wseg : data.getHighlightedWaySegments()) { 211 drawSegment(mapState.getPointFor(wseg.getFirstNode()), mapState.getPointFor(wseg.getSecondNode()), highlightColor, false); 212 } 213 displaySegments(); 214 } 215 216 /** 217 * Helper function to calculate maximum of 4 values. 218 * 219 * @param a First value 220 * @param b Second value 221 * @param c Third value 222 * @param d Fourth value 223 * @return maximumof {@code a}, {@code b}, {@code c}, {@code d} 224 */ 225 private static int max(int a, int b, int c, int d) { 226 return Math.max(Math.max(a, b), Math.max(c, d)); 227 } 228 229 /** 230 * Draw a small rectangle. 231 * White if selected (as always) or red otherwise. 232 * 233 * @param n The node to draw. 234 */ 235 @Override 236 public void visit(INode n) { 237 if (n.isIncomplete()) return; 238 239 if (n.isHighlighted()) { 240 drawNode(n, highlightColor, selectedNodeSize, fillSelectedNode); 241 } else { 242 Color color; 243 244 if (isInactiveMode || n.isDisabled()) { 245 color = inactiveColor; 246 } else if (n.isSelected()) { 247 color = selectedColor; 248 } else if (n.isMemberOfSelected()) { 249 color = relationSelectedColor; 250 } else if (n.isConnectionNode()) { 251 if (isNodeTagged(n)) { 252 color = taggedConnectionColor; 253 } else { 254 color = connectionColor; 255 } 256 } else { 257 if (isNodeTagged(n)) { 258 color = taggedColor; 259 } else { 260 color = nodeColor; 261 } 262 } 263 264 final int size = max(n.isSelected() ? selectedNodeSize : 0, 265 isNodeTagged(n) ? taggedNodeSize : 0, 266 n.isConnectionNode() ? connectionNodeSize : 0, 267 unselectedNodeSize); 268 269 final boolean fill = (n.isSelected() && fillSelectedNode) || 270 (isNodeTagged(n) && fillTaggedNode) || 271 (n.isConnectionNode() && fillConnectionNode) || 272 fillUnselectedNode; 273 274 drawNode(n, color, size, fill); 275 } 276 } 277 278 private static boolean isNodeTagged(INode n) { 279 return n.isTagged() || n.isAnnotated(); 280 } 281 282 /** 283 * Draw a line for all way segments. 284 * @param w The way to draw. 285 */ 286 @Override 287 public void visit(IWay<?> w) { 288 if (w.isIncomplete() || w.getNodesCount() < 2) 289 return; 290 291 /* show direction arrows, if draw.segment.relevant_directions_only is not set, the way is tagged with a direction key 292 (even if the tag is negated as in oneway=false) or the way is selected */ 293 294 boolean showThisDirectionArrow = w.isSelected() || showDirectionArrow; 295 /* head only takes over control if the option is true, 296 the direction should be shown at all and not only because it's selected */ 297 boolean showOnlyHeadArrowOnly = showThisDirectionArrow && showHeadArrowOnly && !w.isSelected(); 298 Color wayColor; 299 300 if (isInactiveMode || w.isDisabled()) { 301 wayColor = inactiveColor; 302 } else if (w.isHighlighted()) { 303 wayColor = highlightColor; 304 } else if (w.isSelected()) { 305 wayColor = selectedColor; 306 } else if (w.isMemberOfSelected()) { 307 wayColor = relationSelectedColor; 308 } else if (!w.isTagged()) { 309 wayColor = untaggedWayColor; 310 } else { 311 wayColor = dfltWayColor; 312 } 313 314 Iterator<? extends INode> it = w.getNodes().iterator(); 315 if (it.hasNext()) { 316 MapViewPoint lastP = mapState.getPointFor(it.next()); 317 int lastPOutside = lastP.getOutsideRectangleFlags(viewClip); 318 for (int orderNumber = 1; it.hasNext(); orderNumber++) { 319 MapViewPoint p = mapState.getPointFor(it.next()); 320 int pOutside = p.getOutsideRectangleFlags(viewClip); 321 if ((pOutside & lastPOutside) == 0) { 322 drawSegment(lastP, p, wayColor, 323 showOnlyHeadArrowOnly ? !it.hasNext() : showThisDirectionArrow); 324 if ((showOrderNumber || (showOrderNumberOnSelectedWay && w.isSelected())) && !isInactiveMode) { 325 drawOrderNumber(lastP, p, orderNumber, g.getColor()); 326 } 327 } 328 lastP = p; 329 lastPOutside = pOutside; 330 } 331 } 332 } 333 334 /** 335 * Draw objects used in relations. 336 * @param r The relation to draw. 337 */ 338 @Override 339 public void visit(IRelation<?> r) { 340 if (r.isIncomplete()) return; 341 342 Color col; 343 if (isInactiveMode || r.isDisabled()) { 344 col = inactiveColor; 345 } else if (r.isSelected()) { 346 col = selectedColor; 347 } else if (r.isMultipolygon() && r.isMemberOfSelected()) { 348 col = relationSelectedColor; 349 } else { 350 col = relationColor; 351 } 352 g.setColor(col); 353 354 for (IRelationMember<?> m : r.getMembers()) { 355 if (m.getMember().isIncomplete() || !m.getMember().isDrawable()) { 356 continue; 357 } 358 359 if (m.isNode()) { 360 MapViewPoint p = mapState.getPointFor((INode) m.getMember()); 361 if (p.isInView()) { 362 g.draw(new Ellipse2D.Double(p.getInViewX()-4, p.getInViewY()-4, 9, 9)); 363 } 364 365 } else if (m.isWay()) { 366 GeneralPath path = new GeneralPath(); 367 368 boolean first = true; 369 for (INode n : ((IWay<?>) m.getMember()).getNodes()) { 370 if (!n.isDrawable()) { 371 continue; 372 } 373 MapViewPoint p = mapState.getPointFor(n); 374 if (first) { 375 path.moveTo(p.getInViewX(), p.getInViewY()); 376 first = false; 377 } else { 378 path.lineTo(p.getInViewX(), p.getInViewY()); 379 } 380 } 381 382 if (useRelatedWayStroke) { 383 g.draw(relatedWayStroke.createStrokedShape(path)); 384 } else { 385 g.draw(path); 386 } 387 } 388 } 389 } 390 391 @Override 392 public void drawNode(INode n, Color color, int size, boolean fill) { 393 if (size > 1) { 394 MapViewPoint p = mapState.getPointFor(n); 395 if (!p.isInView()) 396 return; 397 int radius = size / 2; 398 Double shape = new Rectangle2D.Double(p.getInViewX() - radius, p.getInViewY() - radius, size, size); 399 g.setColor(color); 400 if (fill) { 401 g.fill(shape); 402 } 403 g.draw(shape); 404 } 405 } 406 407 /** 408 * Draw a line with the given color. 409 * 410 * @param path The path to append this segment. 411 * @param mv1 First point of the way segment. 412 * @param mv2 Second point of the way segment. 413 * @param showDirection <code>true</code> if segment direction should be indicated 414 * @since 10827 415 */ 416 protected void drawSegment(MapPath2D path, MapViewPoint mv1, MapViewPoint mv2, boolean showDirection) { 417 path.moveTo(mv1); 418 path.lineTo(mv2); 419 if (showDirection) { 420 ARROW_PAINT_HELPER.paintArrowAt(path, mv2, mv1); 421 } 422 } 423 424 /** 425 * Draw a line with the given color. 426 * 427 * @param p1 First point of the way segment. 428 * @param p2 Second point of the way segment. 429 * @param col The color to use for drawing line. 430 * @param showDirection <code>true</code> if segment direction should be indicated. 431 * @since 10827 432 */ 433 protected void drawSegment(MapViewPoint p1, MapViewPoint p2, Color col, boolean showDirection) { 434 if (!col.equals(currentColor)) { 435 displaySegments(col); 436 } 437 drawSegment(currentPath, p1, p2, showDirection); 438 } 439 440 /** 441 * Finally display all segments in currect path. 442 */ 443 protected void displaySegments() { 444 displaySegments(null); 445 } 446 447 /** 448 * Finally display all segments in currect path. 449 * 450 * @param newColor This color is set after the path is drawn. 451 */ 452 protected void displaySegments(Color newColor) { 453 if (currentPath != null) { 454 g.setColor(currentColor); 455 g.draw(currentPath); 456 currentPath = new MapPath2D(); 457 currentColor = newColor; 458 } 459 } 460}