001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.osm.visitor.paint; 003 004import java.awt.AlphaComposite; 005import java.awt.BasicStroke; 006import java.awt.Color; 007import java.awt.Component; 008import java.awt.Dimension; 009import java.awt.Font; 010import java.awt.FontMetrics; 011import java.awt.Graphics2D; 012import java.awt.Image; 013import java.awt.Point; 014import java.awt.Rectangle; 015import java.awt.RenderingHints; 016import java.awt.Shape; 017import java.awt.TexturePaint; 018import java.awt.font.FontRenderContext; 019import java.awt.font.GlyphVector; 020import java.awt.font.LineMetrics; 021import java.awt.font.TextLayout; 022import java.awt.geom.AffineTransform; 023import java.awt.geom.Path2D; 024import java.awt.geom.Point2D; 025import java.awt.geom.Rectangle2D; 026import java.awt.geom.RoundRectangle2D; 027import java.awt.image.BufferedImage; 028import java.util.ArrayList; 029import java.util.Collection; 030import java.util.Collections; 031import java.util.Comparator; 032import java.util.HashMap; 033import java.util.Iterator; 034import java.util.List; 035import java.util.Map; 036import java.util.NoSuchElementException; 037import java.util.Optional; 038import java.util.concurrent.ForkJoinPool; 039import java.util.concurrent.ForkJoinTask; 040import java.util.concurrent.RecursiveTask; 041import java.util.function.Supplier; 042import java.util.stream.Collectors; 043 044import javax.swing.AbstractButton; 045import javax.swing.FocusManager; 046 047import org.openstreetmap.josm.Main; 048import org.openstreetmap.josm.data.Bounds; 049import org.openstreetmap.josm.data.coor.EastNorth; 050import org.openstreetmap.josm.data.osm.BBox; 051import org.openstreetmap.josm.data.osm.Changeset; 052import org.openstreetmap.josm.data.osm.DataSet; 053import org.openstreetmap.josm.data.osm.Node; 054import org.openstreetmap.josm.data.osm.OsmPrimitive; 055import org.openstreetmap.josm.data.osm.OsmUtils; 056import org.openstreetmap.josm.data.osm.Relation; 057import org.openstreetmap.josm.data.osm.RelationMember; 058import org.openstreetmap.josm.data.osm.Way; 059import org.openstreetmap.josm.data.osm.WaySegment; 060import org.openstreetmap.josm.data.osm.visitor.Visitor; 061import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon; 062import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData; 063import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache; 064import org.openstreetmap.josm.gui.MapViewState.MapViewPoint; 065import org.openstreetmap.josm.gui.NavigatableComponent; 066import org.openstreetmap.josm.gui.draw.MapViewPath; 067import org.openstreetmap.josm.gui.mappaint.ElemStyles; 068import org.openstreetmap.josm.gui.mappaint.MapPaintStyles; 069import org.openstreetmap.josm.gui.mappaint.StyleElementList; 070import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource; 071import org.openstreetmap.josm.gui.mappaint.styleelement.AreaElement; 072import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement; 073import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement.HorizontalTextAlignment; 074import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement.VerticalTextAlignment; 075import org.openstreetmap.josm.gui.mappaint.styleelement.MapImage; 076import org.openstreetmap.josm.gui.mappaint.styleelement.NodeElement; 077import org.openstreetmap.josm.gui.mappaint.styleelement.RepeatImageElement.LineImageAlignment; 078import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement; 079import org.openstreetmap.josm.gui.mappaint.styleelement.Symbol; 080import org.openstreetmap.josm.gui.mappaint.styleelement.TextLabel; 081import org.openstreetmap.josm.tools.CompositeList; 082import org.openstreetmap.josm.tools.Geometry; 083import org.openstreetmap.josm.tools.Geometry.AreaAndPerimeter; 084import org.openstreetmap.josm.tools.ImageProvider; 085import org.openstreetmap.josm.tools.Utils; 086import org.openstreetmap.josm.tools.bugreport.BugReport; 087 088/** 089 * A map renderer which renders a map according to style rules in a set of style sheets. 090 * @since 486 091 */ 092public class StyledMapRenderer extends AbstractMapRenderer { 093 094 private static final ForkJoinPool THREAD_POOL = 095 Utils.newForkJoinPool("mappaint.StyledMapRenderer.style_creation.numberOfThreads", "styled-map-renderer-%d", Thread.NORM_PRIORITY); 096 097 /** 098 * Iterates over a list of Way Nodes and returns screen coordinates that 099 * represent a line that is shifted by a certain offset perpendicular 100 * to the way direction. 101 * 102 * There is no intention, to handle consecutive duplicate Nodes in a 103 * perfect way, but it should not throw an exception. 104 */ 105 private class OffsetIterator implements Iterator<MapViewPoint> { 106 107 private final List<Node> nodes; 108 private final double offset; 109 private int idx; 110 111 private MapViewPoint prev; 112 /* 'prev0' is a point that has distance 'offset' from 'prev' and the 113 * line from 'prev' to 'prev0' is perpendicular to the way segment from 114 * 'prev' to the current point. 115 */ 116 private double xPrev0; 117 private double yPrev0; 118 119 OffsetIterator(List<Node> nodes, double offset) { 120 this.nodes = nodes; 121 this.offset = offset; 122 idx = 0; 123 } 124 125 @Override 126 public boolean hasNext() { 127 return idx < nodes.size(); 128 } 129 130 @Override 131 public MapViewPoint next() { 132 if (!hasNext()) 133 throw new NoSuchElementException(); 134 135 MapViewPoint current = getForIndex(idx); 136 137 if (Math.abs(offset) < 0.1d) { 138 idx++; 139 return current; 140 } 141 142 double xCurrent = current.getInViewX(); 143 double yCurrent = current.getInViewY(); 144 if (idx == nodes.size() - 1) { 145 ++idx; 146 if (prev != null) { 147 return mapState.getForView(xPrev0 + xCurrent - prev.getInViewX(), 148 yPrev0 + yCurrent - prev.getInViewY()); 149 } else { 150 return current; 151 } 152 } 153 154 MapViewPoint next = getForIndex(idx + 1); 155 double dxNext = next.getInViewX() - xCurrent; 156 double dyNext = next.getInViewY() - yCurrent; 157 double lenNext = Math.sqrt(dxNext*dxNext + dyNext*dyNext); 158 159 if (lenNext < 1e-11) { 160 lenNext = 1; // value does not matter, because dy_next and dx_next is 0 161 } 162 163 // calculate the position of the translated current point 164 double om = offset / lenNext; 165 double xCurrent0 = xCurrent + om * dyNext; 166 double yCurrent0 = yCurrent - om * dxNext; 167 168 if (idx == 0) { 169 ++idx; 170 prev = current; 171 xPrev0 = xCurrent0; 172 yPrev0 = yCurrent0; 173 return mapState.getForView(xCurrent0, yCurrent0); 174 } else { 175 double dxPrev = xCurrent - prev.getInViewX(); 176 double dyPrev = yCurrent - prev.getInViewY(); 177 // determine intersection of the lines parallel to the two segments 178 double det = dxNext*dyPrev - dxPrev*dyNext; 179 double m = dxNext*(yCurrent0 - yPrev0) - dyNext*(xCurrent0 - xPrev0); 180 181 if (Utils.equalsEpsilon(det, 0) || Math.signum(det) != Math.signum(m)) { 182 ++idx; 183 prev = current; 184 xPrev0 = xCurrent0; 185 yPrev0 = yCurrent0; 186 return mapState.getForView(xCurrent0, yCurrent0); 187 } 188 189 double f = m / det; 190 if (f < 0) { 191 ++idx; 192 prev = current; 193 xPrev0 = xCurrent0; 194 yPrev0 = yCurrent0; 195 return mapState.getForView(xCurrent0, yCurrent0); 196 } 197 // the position of the intersection or intermittent point 198 double cx = xPrev0 + f * dxPrev; 199 double cy = yPrev0 + f * dyPrev; 200 201 if (f > 1) { 202 // check if the intersection point is too far away, this will happen for sharp angles 203 double dxI = cx - xCurrent; 204 double dyI = cy - yCurrent; 205 double lenISq = dxI * dxI + dyI * dyI; 206 207 if (lenISq > Math.abs(2 * offset * offset)) { 208 // intersection point is too far away, calculate intermittent points for capping 209 double dxPrev0 = xCurrent0 - xPrev0; 210 double dyPrev0 = yCurrent0 - yPrev0; 211 double lenPrev0 = Math.sqrt(dxPrev0 * dxPrev0 + dyPrev0 * dyPrev0); 212 f = 1 + Math.abs(offset / lenPrev0); 213 double cxCap = xPrev0 + f * dxPrev; 214 double cyCap = yPrev0 + f * dyPrev; 215 xPrev0 = cxCap; 216 yPrev0 = cyCap; 217 // calculate a virtual prev point which lies on a line that goes through current and 218 // is perpendicular to the line that goes through current and the intersection 219 // so that the next capping point is calculated with it. 220 double lenI = Math.sqrt(lenISq); 221 double xv = xCurrent + dyI / lenI; 222 double yv = yCurrent - dxI / lenI; 223 224 prev = mapState.getForView(xv, yv); 225 return mapState.getForView(cxCap, cyCap); 226 } 227 } 228 ++idx; 229 prev = current; 230 xPrev0 = xCurrent0; 231 yPrev0 = yCurrent0; 232 return mapState.getForView(cx, cy); 233 } 234 } 235 236 private MapViewPoint getForIndex(int i) { 237 return mapState.getPointFor(nodes.get(i)); 238 } 239 240 @Override 241 public void remove() { 242 throw new UnsupportedOperationException(); 243 } 244 } 245 246 /** 247 * This stores a style and a primitive that should be painted with that style. 248 */ 249 public static class StyleRecord implements Comparable<StyleRecord> { 250 private final StyleElement style; 251 private final OsmPrimitive osm; 252 private final int flags; 253 254 StyleRecord(StyleElement style, OsmPrimitive osm, int flags) { 255 this.style = style; 256 this.osm = osm; 257 this.flags = flags; 258 } 259 260 @Override 261 public int compareTo(StyleRecord other) { 262 if ((this.flags & FLAG_DISABLED) != 0 && (other.flags & FLAG_DISABLED) == 0) 263 return -1; 264 if ((this.flags & FLAG_DISABLED) == 0 && (other.flags & FLAG_DISABLED) != 0) 265 return 1; 266 267 int d0 = Float.compare(this.style.majorZIndex, other.style.majorZIndex); 268 if (d0 != 0) 269 return d0; 270 271 // selected on top of member of selected on top of unselected 272 // FLAG_DISABLED bit is the same at this point 273 if (this.flags > other.flags) 274 return 1; 275 if (this.flags < other.flags) 276 return -1; 277 278 int dz = Float.compare(this.style.zIndex, other.style.zIndex); 279 if (dz != 0) 280 return dz; 281 282 // simple node on top of icons and shapes 283 if (NodeElement.SIMPLE_NODE_ELEMSTYLE.equals(this.style) && !NodeElement.SIMPLE_NODE_ELEMSTYLE.equals(other.style)) 284 return 1; 285 if (!NodeElement.SIMPLE_NODE_ELEMSTYLE.equals(this.style) && NodeElement.SIMPLE_NODE_ELEMSTYLE.equals(other.style)) 286 return -1; 287 288 // newer primitives to the front 289 long id = this.osm.getUniqueId() - other.osm.getUniqueId(); 290 if (id > 0) 291 return 1; 292 if (id < 0) 293 return -1; 294 295 return Float.compare(this.style.objectZIndex, other.style.objectZIndex); 296 } 297 298 /** 299 * Get the style for this style element. 300 * @return The style 301 */ 302 public StyleElement getStyle() { 303 return style; 304 } 305 306 /** 307 * Paints the primitive with the style. 308 * @param paintSettings The settings to use. 309 * @param painter The painter to paint the style. 310 */ 311 public void paintPrimitive(MapPaintSettings paintSettings, StyledMapRenderer painter) { 312 style.paintPrimitive( 313 osm, 314 paintSettings, 315 painter, 316 (flags & FLAG_SELECTED) != 0, 317 (flags & FLAG_OUTERMEMBER_OF_SELECTED) != 0, 318 (flags & FLAG_MEMBER_OF_SELECTED) != 0 319 ); 320 } 321 322 @Override 323 public String toString() { 324 return "StyleRecord [style=" + style + ", osm=" + osm + ", flags=" + flags + "]"; 325 } 326 } 327 328 private static Map<Font, Boolean> IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG = new HashMap<>(); 329 330 /** 331 * Check, if this System has the GlyphVector double translation bug. 332 * 333 * With this bug, <code>gv.setGlyphTransform(i, trfm)</code> has a different 334 * effect than on most other systems, namely the translation components 335 * ("m02" & "m12", {@link AffineTransform}) appear to be twice as large, as 336 * they actually are. The rotation is unaffected (scale & shear not tested 337 * so far). 338 * 339 * This bug has only been observed on Mac OS X, see #7841. 340 * 341 * After switch to Java 7, this test is a false positive on Mac OS X (see #10446), 342 * i.e. it returns true, but the real rendering code does not require any special 343 * handling. 344 * It hasn't been further investigated why the test reports a wrong result in 345 * this case, but the method has been changed to simply return false by default. 346 * (This can be changed with a setting in the advanced preferences.) 347 * 348 * @param font The font to check. 349 * @return false by default, but depends on the value of the advanced 350 * preference glyph-bug=false|true|auto, where auto is the automatic detection 351 * method which apparently no longer gives a useful result for Java 7. 352 */ 353 public static boolean isGlyphVectorDoubleTranslationBug(Font font) { 354 Boolean cached = IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG.get(font); 355 if (cached != null) 356 return cached; 357 String overridePref = Main.pref.get("glyph-bug", "auto"); 358 if ("auto".equals(overridePref)) { 359 FontRenderContext frc = new FontRenderContext(null, false, false); 360 GlyphVector gv = font.createGlyphVector(frc, "x"); 361 gv.setGlyphTransform(0, AffineTransform.getTranslateInstance(1000, 1000)); 362 Shape shape = gv.getGlyphOutline(0); 363 if (Main.isTraceEnabled()) { 364 Main.trace("#10446: shape: "+shape.getBounds()); 365 } 366 // x is about 1000 on normal stystems and about 2000 when the bug occurs 367 int x = shape.getBounds().x; 368 boolean isBug = x > 1500; 369 IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG.put(font, isBug); 370 return isBug; 371 } else { 372 boolean override = Boolean.parseBoolean(overridePref); 373 IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG.put(font, override); 374 return override; 375 } 376 } 377 378 private double circum; 379 private double scale; 380 381 private MapPaintSettings paintSettings; 382 383 private Color highlightColorTransparent; 384 385 /** 386 * Flags used to store the primitive state along with the style. This is the normal style. 387 * <p> 388 * Not used in any public interfaces. 389 */ 390 private static final int FLAG_NORMAL = 0; 391 /** 392 * A primitive with {@link OsmPrimitive#isDisabled()} 393 */ 394 private static final int FLAG_DISABLED = 1; 395 /** 396 * A primitive with {@link OsmPrimitive#isMemberOfSelected()} 397 */ 398 private static final int FLAG_MEMBER_OF_SELECTED = 2; 399 /** 400 * A primitive with {@link OsmPrimitive#isSelected()} 401 */ 402 private static final int FLAG_SELECTED = 4; 403 /** 404 * A primitive with {@link OsmPrimitive#isOuterMemberOfSelected()} 405 */ 406 private static final int FLAG_OUTERMEMBER_OF_SELECTED = 8; 407 408 private static final double PHI = Math.toRadians(20); 409 private static final double cosPHI = Math.cos(PHI); 410 private static final double sinPHI = Math.sin(PHI); 411 412 private Collection<WaySegment> highlightWaySegments; 413 414 // highlight customization fields 415 private int highlightLineWidth; 416 private int highlightPointRadius; 417 private int widerHighlight; 418 private int highlightStep; 419 420 //flag that activate wider highlight mode 421 private boolean useWiderHighlight; 422 423 private boolean useStrokes; 424 private boolean showNames; 425 private boolean showIcons; 426 private boolean isOutlineOnly; 427 428 private Font orderFont; 429 430 private boolean leftHandTraffic; 431 private Object antialiasing; 432 433 private Supplier<RenderBenchmarkCollector> benchmarkFactory = RenderBenchmarkCollector.defaultBenchmarkSupplier(); 434 435 /** 436 * Constructs a new {@code StyledMapRenderer}. 437 * 438 * @param g the graphics context. Must not be null. 439 * @param nc the map viewport. Must not be null. 440 * @param isInactiveMode if true, the paint visitor shall render OSM objects such that they 441 * look inactive. Example: rendering of data in an inactive layer using light gray as color only. 442 * @throws IllegalArgumentException if {@code g} is null 443 * @throws IllegalArgumentException if {@code nc} is null 444 */ 445 public StyledMapRenderer(Graphics2D g, NavigatableComponent nc, boolean isInactiveMode) { 446 super(g, nc, isInactiveMode); 447 448 if (nc != null) { 449 Component focusOwner = FocusManager.getCurrentManager().getFocusOwner(); 450 useWiderHighlight = !(focusOwner instanceof AbstractButton || focusOwner == nc); 451 } 452 } 453 454 private void displaySegments(MapViewPath path, Path2D orientationArrows, Path2D onewayArrows, Path2D onewayArrowsCasing, 455 Color color, BasicStroke line, BasicStroke dashes, Color dashedColor) { 456 g.setColor(isInactiveMode ? inactiveColor : color); 457 if (useStrokes) { 458 g.setStroke(line); 459 } 460 g.draw(path.computeClippedLine(g.getStroke())); 461 462 if (!isInactiveMode && useStrokes && dashes != null) { 463 g.setColor(dashedColor); 464 g.setStroke(dashes); 465 g.draw(path.computeClippedLine(dashes)); 466 } 467 468 if (orientationArrows != null) { 469 g.setColor(isInactiveMode ? inactiveColor : color); 470 g.setStroke(new BasicStroke(line.getLineWidth(), line.getEndCap(), BasicStroke.JOIN_MITER, line.getMiterLimit())); 471 g.draw(orientationArrows); 472 } 473 474 if (onewayArrows != null) { 475 g.setStroke(new BasicStroke(1, line.getEndCap(), BasicStroke.JOIN_MITER, line.getMiterLimit())); 476 g.fill(onewayArrowsCasing); 477 g.setColor(isInactiveMode ? inactiveColor : backgroundColor); 478 g.fill(onewayArrows); 479 } 480 481 if (useStrokes) { 482 g.setStroke(new BasicStroke()); 483 } 484 } 485 486 /** 487 * Displays text at specified position including its halo, if applicable. 488 * 489 * @param gv Text's glyphs to display. If {@code null}, use text from {@code s} instead. 490 * @param s text to display if {@code gv} is {@code null} 491 * @param x X position 492 * @param y Y position 493 * @param disabled {@code true} if element is disabled (filtered out) 494 * @param text text style to use 495 */ 496 private void displayText(GlyphVector gv, String s, int x, int y, boolean disabled, TextLabel text) { 497 if (gv == null && s.isEmpty()) return; 498 if (isInactiveMode || disabled) { 499 g.setColor(inactiveColor); 500 if (gv != null) { 501 g.drawGlyphVector(gv, x, y); 502 } else { 503 g.setFont(text.font); 504 g.drawString(s, x, y); 505 } 506 } else if (text.haloRadius != null) { 507 g.setStroke(new BasicStroke(2*text.haloRadius, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND)); 508 g.setColor(text.haloColor); 509 Shape textOutline; 510 if (gv == null) { 511 FontRenderContext frc = g.getFontRenderContext(); 512 TextLayout tl = new TextLayout(s, text.font, frc); 513 textOutline = tl.getOutline(AffineTransform.getTranslateInstance(x, y)); 514 } else { 515 textOutline = gv.getOutline(x, y); 516 } 517 g.draw(textOutline); 518 g.setStroke(new BasicStroke()); 519 g.setColor(text.color); 520 g.fill(textOutline); 521 } else { 522 g.setColor(text.color); 523 if (gv != null) { 524 g.drawGlyphVector(gv, x, y); 525 } else { 526 g.setFont(text.font); 527 g.drawString(s, x, y); 528 } 529 } 530 } 531 532 /** 533 * Worker function for drawing areas. 534 * 535 * @param osm the primitive 536 * @param path the path object for the area that should be drawn; in case 537 * of multipolygons, this can path can be a complex shape with one outer 538 * polygon and one or more inner polygons 539 * @param color The color to fill the area with. 540 * @param fillImage The image to fill the area with. Overrides color. 541 * @param extent if not null, area will be filled partially; specifies, how 542 * far to fill from the boundary towards the center of the area; 543 * if null, area will be filled completely 544 * @param pfClip clipping area for partial fill (only needed for unclosed 545 * polygons) 546 * @param disabled If this should be drawn with a special disabled style. 547 * @param text The text to write on the area. 548 */ 549 protected void drawArea(OsmPrimitive osm, Path2D.Double path, Color color, 550 MapImage fillImage, Float extent, Path2D.Double pfClip, boolean disabled, TextLabel text) { 551 552 Shape area = path.createTransformedShape(mapState.getAffineTransform()); 553 554 if (!isOutlineOnly) { 555 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); 556 if (fillImage == null) { 557 if (isInactiveMode) { 558 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.33f)); 559 } 560 g.setColor(color); 561 if (extent == null) { 562 g.fill(area); 563 } else { 564 Shape oldClip = g.getClip(); 565 Shape clip = area; 566 if (pfClip != null) { 567 clip = pfClip.createTransformedShape(mapState.getAffineTransform()); 568 } 569 g.clip(clip); 570 g.setStroke(new BasicStroke(2 * extent, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 4)); 571 g.draw(area); 572 g.setClip(oldClip); 573 } 574 } else { 575 TexturePaint texture = new TexturePaint(fillImage.getImage(disabled), 576 new Rectangle(0, 0, fillImage.getWidth(), fillImage.getHeight())); 577 g.setPaint(texture); 578 Float alpha = fillImage.getAlphaFloat(); 579 if (!Utils.equalsEpsilon(alpha, 1f)) { 580 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)); 581 } 582 if (extent == null) { 583 g.fill(area); 584 } else { 585 Shape oldClip = g.getClip(); 586 BasicStroke stroke = new BasicStroke(2 * extent, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER); 587 g.clip(stroke.createStrokedShape(area)); 588 Shape fill = area; 589 if (pfClip != null) { 590 fill = pfClip.createTransformedShape(mapState.getAffineTransform()); 591 } 592 g.fill(fill); 593 g.setClip(oldClip); 594 } 595 g.setPaintMode(); 596 } 597 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing); 598 } 599 600 drawAreaText(osm, text, area); 601 } 602 603 private void drawAreaText(OsmPrimitive osm, TextLabel text, Shape area) { 604 if (text != null && isShowNames()) { 605 // abort if we can't compose the label to be rendered 606 if (text.labelCompositionStrategy == null) return; 607 String name = text.labelCompositionStrategy.compose(osm); 608 if (name == null) return; 609 610 Rectangle pb = area.getBounds(); 611 FontMetrics fontMetrics = g.getFontMetrics(orderFont); // if slow, use cache 612 Rectangle2D nb = fontMetrics.getStringBounds(name, g); // if slow, approximate by strlen()*maxcharbounds(font) 613 614 // Using the Centroid is Nicer for buildings like: +--------+ 615 // but this needs to be fast. As most houses are | 42 | 616 // boxes anyway, the center of the bounding box +---++---+ 617 // will have to do. ++ 618 // Centroids are not optimal either, just imagine a U-shaped house. 619 620 // quick check to see if label box is smaller than primitive box 621 if (pb.width >= nb.getWidth() && pb.height >= nb.getHeight()) { 622 623 final double w = pb.width - nb.getWidth(); 624 final double h = pb.height - nb.getHeight(); 625 626 final int x2 = pb.x + (int) (w/2.0); 627 final int y2 = pb.y + (int) (h/2.0); 628 629 final int nbw = (int) nb.getWidth(); 630 final int nbh = (int) nb.getHeight(); 631 632 Rectangle centeredNBounds = new Rectangle(x2, y2, nbw, nbh); 633 634 // slower check to see if label is displayed inside primitive shape 635 boolean labelOK = area.contains(centeredNBounds); 636 if (!labelOK) { 637 // if center position (C) is not inside osm shape, try naively some other positions as follows: 638 // CHECKSTYLE.OFF: SingleSpaceSeparator 639 final int x1 = pb.x + (int) (w/4.0); 640 final int x3 = pb.x + (int) (3*w/4.0); 641 final int y1 = pb.y + (int) (h/4.0); 642 final int y3 = pb.y + (int) (3*h/4.0); 643 // CHECKSTYLE.ON: SingleSpaceSeparator 644 // +-----------+ 645 // | 5 1 6 | 646 // | 4 C 2 | 647 // | 8 3 7 | 648 // +-----------+ 649 Rectangle[] candidates = new Rectangle[] { 650 new Rectangle(x2, y1, nbw, nbh), 651 new Rectangle(x3, y2, nbw, nbh), 652 new Rectangle(x2, y3, nbw, nbh), 653 new Rectangle(x1, y2, nbw, nbh), 654 new Rectangle(x1, y1, nbw, nbh), 655 new Rectangle(x3, y1, nbw, nbh), 656 new Rectangle(x3, y3, nbw, nbh), 657 new Rectangle(x1, y3, nbw, nbh) 658 }; 659 // Dumb algorithm to find a better placement. We could surely find a smarter one but it should 660 // solve most of building issues with only few calculations (8 at most) 661 for (int i = 0; i < candidates.length && !labelOK; i++) { 662 centeredNBounds = candidates[i]; 663 labelOK = area.contains(centeredNBounds); 664 } 665 } 666 if (labelOK) { 667 Font defaultFont = g.getFont(); 668 int x = (int) (centeredNBounds.getMinX() - nb.getMinX()); 669 int y = (int) (centeredNBounds.getMinY() - nb.getMinY()); 670 displayText(null, name, x, y, osm.isDisabled(), text); 671 g.setFont(defaultFont); 672 } else if (Main.isTraceEnabled()) { 673 Main.trace("Couldn't find a correct label placement for "+osm+" / "+name); 674 } 675 } 676 } 677 } 678 679 /** 680 * Draws a multipolygon area. 681 * @param r The multipolygon relation 682 * @param color The color to fill the area with. 683 * @param fillImage The image to fill the area with. Overrides color. 684 * @param extent if not null, area will be filled partially; specifies, how 685 * far to fill from the boundary towards the center of the area; 686 * if null, area will be filled completely 687 * @param extentThreshold if not null, determines if the partial filled should 688 * be replaced by plain fill, when it covers a certain fraction of the total area 689 * @param disabled If this should be drawn with a special disabled style. 690 * @param text The text to write on the area. 691 */ 692 public void drawArea(Relation r, Color color, MapImage fillImage, Float extent, Float extentThreshold, boolean disabled, TextLabel text) { 693 Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, r); 694 if (!r.isDisabled() && !multipolygon.getOuterWays().isEmpty()) { 695 for (PolyData pd : multipolygon.getCombinedPolygons()) { 696 Path2D.Double p = pd.get(); 697 Path2D.Double pfClip = null; 698 if (!isAreaVisible(p)) { 699 continue; 700 } 701 if (extent != null) { 702 if (!usePartialFill(pd.getAreaAndPerimeter(null), extent, extentThreshold)) { 703 extent = null; 704 } else if (!pd.isClosed()) { 705 pfClip = getPFClip(pd, extent * scale); 706 } 707 } 708 drawArea(r, p, 709 pd.isSelected() ? paintSettings.getRelationSelectedColor(color.getAlpha()) : color, 710 fillImage, extent, pfClip, disabled, text); 711 } 712 } 713 } 714 715 /** 716 * Draws an area defined by a way. They way does not need to be closed, but it should. 717 * @param w The way. 718 * @param color The color to fill the area with. 719 * @param fillImage The image to fill the area with. Overrides color. 720 * @param extent if not null, area will be filled partially; specifies, how 721 * far to fill from the boundary towards the center of the area; 722 * if null, area will be filled completely 723 * @param extentThreshold if not null, determines if the partial filled should 724 * be replaced by plain fill, when it covers a certain fraction of the total area 725 * @param disabled If this should be drawn with a special disabled style. 726 * @param text The text to write on the area. 727 */ 728 public void drawArea(Way w, Color color, MapImage fillImage, Float extent, Float extentThreshold, boolean disabled, TextLabel text) { 729 Path2D.Double pfClip = null; 730 if (extent != null) { 731 if (!usePartialFill(Geometry.getAreaAndPerimeter(w.getNodes()), extent, extentThreshold)) { 732 extent = null; 733 } else if (!w.isClosed()) { 734 pfClip = getPFClip(w, extent * scale); 735 } 736 } 737 drawArea(w, getPath(w), color, fillImage, extent, pfClip, disabled, text); 738 } 739 740 /** 741 * Determine, if partial fill should be turned off for this object, because 742 * only a small unfilled gap in the center of the area would be left. 743 * 744 * This is used to get a cleaner look for urban regions with many small 745 * areas like buildings, etc. 746 * @param ap the area and the perimeter of the object 747 * @param extent the "width" of partial fill 748 * @param threshold when the partial fill covers that much of the total 749 * area, the partial fill is turned off; can be greater than 100% as the 750 * covered area is estimated as <code>perimeter * extent</code> 751 * @return true, if the partial fill should be used, false otherwise 752 */ 753 private boolean usePartialFill(AreaAndPerimeter ap, float extent, Float threshold) { 754 if (threshold == null) return true; 755 return ap.getPerimeter() * extent * scale < threshold * ap.getArea(); 756 } 757 758 /** 759 * Draw a text onto a node 760 * @param n The node to draw the text on 761 * @param bs The text and it's alignment. 762 */ 763 public void drawBoxText(Node n, BoxTextElement bs) { 764 if (!isShowNames() || bs == null) 765 return; 766 767 MapViewPoint p = mapState.getPointFor(n); 768 TextLabel text = bs.text; 769 String s = text.labelCompositionStrategy.compose(n); 770 if (s == null) return; 771 772 Font defaultFont = g.getFont(); 773 g.setFont(text.font); 774 775 int x = (int) (Math.round(p.getInViewX()) + text.xOffset); 776 int y = (int) (Math.round(p.getInViewY()) + text.yOffset); 777 /** 778 * 779 * left-above __center-above___ right-above 780 * left-top| |right-top 781 * | | 782 * left-center| center-center |right-center 783 * | | 784 * left-bottom|_________________|right-bottom 785 * left-below center-below right-below 786 * 787 */ 788 Rectangle box = bs.getBox(); 789 if (bs.hAlign == HorizontalTextAlignment.RIGHT) { 790 x += box.x + box.width + 2; 791 } else { 792 FontRenderContext frc = g.getFontRenderContext(); 793 Rectangle2D bounds = text.font.getStringBounds(s, frc); 794 int textWidth = (int) bounds.getWidth(); 795 if (bs.hAlign == HorizontalTextAlignment.CENTER) { 796 x -= textWidth / 2; 797 } else if (bs.hAlign == HorizontalTextAlignment.LEFT) { 798 x -= -box.x + 4 + textWidth; 799 } else throw new AssertionError(); 800 } 801 802 if (bs.vAlign == VerticalTextAlignment.BOTTOM) { 803 y += box.y + box.height; 804 } else { 805 FontRenderContext frc = g.getFontRenderContext(); 806 LineMetrics metrics = text.font.getLineMetrics(s, frc); 807 if (bs.vAlign == VerticalTextAlignment.ABOVE) { 808 y -= -box.y + (int) metrics.getDescent(); 809 } else if (bs.vAlign == VerticalTextAlignment.TOP) { 810 y -= -box.y - (int) metrics.getAscent(); 811 } else if (bs.vAlign == VerticalTextAlignment.CENTER) { 812 y += (int) ((metrics.getAscent() - metrics.getDescent()) / 2); 813 } else if (bs.vAlign == VerticalTextAlignment.BELOW) { 814 y += box.y + box.height + (int) metrics.getAscent() + 2; 815 } else throw new AssertionError(); 816 } 817 displayText(null, s, x, y, n.isDisabled(), text); 818 g.setFont(defaultFont); 819 } 820 821 /** 822 * Draw an image along a way repeatedly. 823 * 824 * @param way the way 825 * @param pattern the image 826 * @param disabled If this should be drawn with a special disabled style. 827 * @param offset offset from the way 828 * @param spacing spacing between two images 829 * @param phase initial spacing 830 * @param align alignment of the image. The top, center or bottom edge can be aligned with the way. 831 */ 832 public void drawRepeatImage(Way way, MapImage pattern, boolean disabled, double offset, double spacing, double phase, 833 LineImageAlignment align) { 834 final int imgWidth = pattern.getWidth(); 835 final double repeat = imgWidth + spacing; 836 final int imgHeight = pattern.getHeight(); 837 838 int dy1 = (int) ((align.getAlignmentOffset() - .5) * imgHeight); 839 int dy2 = dy1 + imgHeight; 840 841 OffsetIterator it = new OffsetIterator(way.getNodes(), offset); 842 MapViewPath path = new MapViewPath(mapState); 843 if (it.hasNext()) { 844 path.moveTo(it.next()); 845 } 846 while (it.hasNext()) { 847 path.lineTo(it.next()); 848 } 849 850 double startOffset = phase % repeat; 851 if (startOffset < 0) { 852 startOffset += repeat; 853 } 854 855 BufferedImage image = pattern.getImage(disabled); 856 857 path.visitClippedLine(startOffset, repeat, (inLineOffset, start, end, startIsOldEnd) -> { 858 final double segmentLength = start.distanceToInView(end); 859 if (segmentLength < 0.1) { 860 // avoid odd patterns when zoomed out. 861 return; 862 } 863 if (segmentLength > repeat * 500) { 864 // simply skip drawing so many images - something must be wrong. 865 return; 866 } 867 AffineTransform saveTransform = g.getTransform(); 868 g.translate(start.getInViewX(), start.getInViewY()); 869 double dx = end.getInViewX() - start.getInViewX(); 870 double dy = end.getInViewY() - start.getInViewY(); 871 g.rotate(Math.atan2(dy, dx)); 872 873 // The start of the next image 874 double imageStart = -(inLineOffset % repeat); 875 876 while (imageStart < segmentLength) { 877 int x = (int) imageStart; 878 int sx1 = Math.max(0, -x); 879 int sx2 = imgWidth - Math.max(0, x + imgWidth - (int) Math.ceil(segmentLength)); 880 g.drawImage(image, x + sx1, dy1, x + sx2, dy2, sx1, 0, sx2, imgHeight, null); 881 imageStart += repeat; 882 } 883 884 g.setTransform(saveTransform); 885 }); 886 } 887 888 @Override 889 public void drawNode(Node n, Color color, int size, boolean fill) { 890 if (size <= 0 && !n.isHighlighted()) 891 return; 892 893 MapViewPoint p = mapState.getPointFor(n); 894 895 if (n.isHighlighted()) { 896 drawPointHighlight(p.getInView(), size); 897 } 898 899 if (size > 1 && p.isInView()) { 900 int radius = size / 2; 901 902 if (isInactiveMode || n.isDisabled()) { 903 g.setColor(inactiveColor); 904 } else { 905 g.setColor(color); 906 } 907 Rectangle2D rect = new Rectangle2D.Double(p.getInViewX()-radius-1, p.getInViewY()-radius-1, size + 1, size + 1); 908 if (fill) { 909 g.fill(rect); 910 } else { 911 g.draw(rect); 912 } 913 } 914 } 915 916 /** 917 * Draw the icon for a given node. 918 * @param n The node 919 * @param img The icon to draw at the node position 920 * @param disabled {@code} true to render disabled version, {@code false} for the standard version 921 * @param selected {@code} true to render it as selected, {@code false} otherwise 922 * @param member {@code} true to render it as a relation member, {@code false} otherwise 923 * @param theta the angle of rotation in radians 924 */ 925 public void drawNodeIcon(Node n, MapImage img, boolean disabled, boolean selected, boolean member, double theta) { 926 MapViewPoint p = mapState.getPointFor(n); 927 928 int w = img.getWidth(); 929 int h = img.getHeight(); 930 if (n.isHighlighted()) { 931 drawPointHighlight(p.getInView(), Math.max(w, h)); 932 } 933 934 float alpha = img.getAlphaFloat(); 935 936 Graphics2D temporaryGraphics = (Graphics2D) g.create(); 937 if (!Utils.equalsEpsilon(alpha, 1f)) { 938 temporaryGraphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)); 939 } 940 941 double x = Math.round(p.getInViewX()); 942 double y = Math.round(p.getInViewY()); 943 temporaryGraphics.translate(x, y); 944 temporaryGraphics.rotate(theta); 945 int drawX = -w/2 + img.offsetX; 946 int drawY = -h/2 + img.offsetY; 947 temporaryGraphics.drawImage(img.getImage(disabled), drawX, drawY, nc); 948 if (selected || member) { 949 Color color; 950 if (disabled) { 951 color = inactiveColor; 952 } else if (selected) { 953 color = selectedColor; 954 } else { 955 color = relationSelectedColor; 956 } 957 temporaryGraphics.setColor(color); 958 temporaryGraphics.draw(new Rectangle2D.Double(drawX - 2, drawY - 2, w + 4, h + 4)); 959 } 960 } 961 962 /** 963 * Draw the symbol and possibly a highlight marking on a given node. 964 * @param n The position to draw the symbol on 965 * @param s The symbol to draw 966 * @param fillColor The color to fill the symbol with 967 * @param strokeColor The color to use for the outer corner of the symbol 968 */ 969 public void drawNodeSymbol(Node n, Symbol s, Color fillColor, Color strokeColor) { 970 MapViewPoint p = mapState.getPointFor(n); 971 972 if (n.isHighlighted()) { 973 drawPointHighlight(p.getInView(), s.size); 974 } 975 976 if (fillColor != null || strokeColor != null) { 977 Shape shape = s.buildShapeAround(p.getInViewX(), p.getInViewY()); 978 979 if (fillColor != null) { 980 g.setColor(fillColor); 981 g.fill(shape); 982 } 983 if (s.stroke != null) { 984 g.setStroke(s.stroke); 985 g.setColor(strokeColor); 986 g.draw(shape); 987 g.setStroke(new BasicStroke()); 988 } 989 } 990 } 991 992 /** 993 * Draw a number of the order of the two consecutive nodes within the 994 * parents way 995 * 996 * @param n1 First node of the way segment. 997 * @param n2 Second node of the way segment. 998 * @param orderNumber The number of the segment in the way. 999 * @param clr The color to use for drawing the text. 1000 */ 1001 public void drawOrderNumber(Node n1, Node n2, int orderNumber, Color clr) { 1002 MapViewPoint p1 = mapState.getPointFor(n1); 1003 MapViewPoint p2 = mapState.getPointFor(n2); 1004 drawOrderNumber(p1, p2, orderNumber, clr); 1005 } 1006 1007 /** 1008 * highlights a given GeneralPath using the settings from BasicStroke to match the line's 1009 * style. Width of the highlight is hard coded. 1010 * @param path path to draw 1011 * @param line line style 1012 */ 1013 private void drawPathHighlight(MapViewPath path, BasicStroke line) { 1014 if (path == null) 1015 return; 1016 g.setColor(highlightColorTransparent); 1017 float w = line.getLineWidth() + highlightLineWidth; 1018 if (useWiderHighlight) w += widerHighlight; 1019 while (w >= line.getLineWidth()) { 1020 g.setStroke(new BasicStroke(w, line.getEndCap(), line.getLineJoin(), line.getMiterLimit())); 1021 g.draw(path); 1022 w -= highlightStep; 1023 } 1024 } 1025 1026 /** 1027 * highlights a given point by drawing a rounded rectangle around it. Give the 1028 * size of the object you want to be highlighted, width is added automatically. 1029 * @param p point 1030 * @param size highlight size 1031 */ 1032 private void drawPointHighlight(Point2D p, int size) { 1033 g.setColor(highlightColorTransparent); 1034 int s = size + highlightPointRadius; 1035 if (useWiderHighlight) s += widerHighlight; 1036 while (s >= size) { 1037 int r = (int) Math.floor(s/2d); 1038 g.fill(new RoundRectangle2D.Double(p.getX()-r, p.getY()-r, s, s, r, r)); 1039 s -= highlightStep; 1040 } 1041 } 1042 1043 public void drawRestriction(Image img, Point pVia, double vx, double vx2, double vy, double vy2, double angle, boolean selected) { 1044 // rotate image with direction last node in from to, and scale down image to 16*16 pixels 1045 Image smallImg = ImageProvider.createRotatedImage(img, angle, new Dimension(16, 16)); 1046 int w = smallImg.getWidth(null), h = smallImg.getHeight(null); 1047 g.drawImage(smallImg, (int) (pVia.x+vx+vx2)-w/2, (int) (pVia.y+vy+vy2)-h/2, nc); 1048 1049 if (selected) { 1050 g.setColor(isInactiveMode ? inactiveColor : relationSelectedColor); 1051 g.drawRect((int) (pVia.x+vx+vx2)-w/2-2, (int) (pVia.y+vy+vy2)-h/2-2, w+4, h+4); 1052 } 1053 } 1054 1055 public void drawRestriction(Relation r, MapImage icon, boolean disabled) { 1056 Way fromWay = null; 1057 Way toWay = null; 1058 OsmPrimitive via = null; 1059 1060 /* find the "from", "via" and "to" elements */ 1061 for (RelationMember m : r.getMembers()) { 1062 if (m.getMember().isIncomplete()) 1063 return; 1064 else { 1065 if (m.isWay()) { 1066 Way w = m.getWay(); 1067 if (w.getNodesCount() < 2) { 1068 continue; 1069 } 1070 1071 switch(m.getRole()) { 1072 case "from": 1073 if (fromWay == null) { 1074 fromWay = w; 1075 } 1076 break; 1077 case "to": 1078 if (toWay == null) { 1079 toWay = w; 1080 } 1081 break; 1082 case "via": 1083 if (via == null) { 1084 via = w; 1085 } 1086 break; 1087 default: // Do nothing 1088 } 1089 } else if (m.isNode()) { 1090 Node n = m.getNode(); 1091 if ("via".equals(m.getRole()) && via == null) { 1092 via = n; 1093 } 1094 } 1095 } 1096 } 1097 1098 if (fromWay == null || toWay == null || via == null) 1099 return; 1100 1101 Node viaNode; 1102 if (via instanceof Node) { 1103 viaNode = (Node) via; 1104 if (!fromWay.isFirstLastNode(viaNode)) 1105 return; 1106 } else { 1107 Way viaWay = (Way) via; 1108 Node firstNode = viaWay.firstNode(); 1109 Node lastNode = viaWay.lastNode(); 1110 Boolean onewayvia = Boolean.FALSE; 1111 1112 String onewayviastr = viaWay.get("oneway"); 1113 if (onewayviastr != null) { 1114 if ("-1".equals(onewayviastr)) { 1115 onewayvia = Boolean.TRUE; 1116 Node tmp = firstNode; 1117 firstNode = lastNode; 1118 lastNode = tmp; 1119 } else { 1120 onewayvia = OsmUtils.getOsmBoolean(onewayviastr); 1121 if (onewayvia == null) { 1122 onewayvia = Boolean.FALSE; 1123 } 1124 } 1125 } 1126 1127 if (fromWay.isFirstLastNode(firstNode)) { 1128 viaNode = firstNode; 1129 } else if (!onewayvia && fromWay.isFirstLastNode(lastNode)) { 1130 viaNode = lastNode; 1131 } else 1132 return; 1133 } 1134 1135 /* find the "direct" nodes before the via node */ 1136 Node fromNode; 1137 if (fromWay.firstNode() == via) { 1138 fromNode = fromWay.getNode(1); 1139 } else { 1140 fromNode = fromWay.getNode(fromWay.getNodesCount()-2); 1141 } 1142 1143 Point pFrom = nc.getPoint(fromNode); 1144 Point pVia = nc.getPoint(viaNode); 1145 1146 /* starting from via, go back the "from" way a few pixels 1147 (calculate the vector vx/vy with the specified length and the direction 1148 away from the "via" node along the first segment of the "from" way) 1149 */ 1150 double distanceFromVia = 14; 1151 double dx = pFrom.x >= pVia.x ? pFrom.x - pVia.x : pVia.x - pFrom.x; 1152 double dy = pFrom.y >= pVia.y ? pFrom.y - pVia.y : pVia.y - pFrom.y; 1153 1154 double fromAngle; 1155 if (dx == 0) { 1156 fromAngle = Math.PI/2; 1157 } else { 1158 fromAngle = Math.atan(dy / dx); 1159 } 1160 double fromAngleDeg = Math.toDegrees(fromAngle); 1161 1162 double vx = distanceFromVia * Math.cos(fromAngle); 1163 double vy = distanceFromVia * Math.sin(fromAngle); 1164 1165 if (pFrom.x < pVia.x) { 1166 vx = -vx; 1167 } 1168 if (pFrom.y < pVia.y) { 1169 vy = -vy; 1170 } 1171 1172 /* go a few pixels away from the way (in a right angle) 1173 (calculate the vx2/vy2 vector with the specified length and the direction 1174 90degrees away from the first segment of the "from" way) 1175 */ 1176 double distanceFromWay = 10; 1177 double vx2 = 0; 1178 double vy2 = 0; 1179 double iconAngle = 0; 1180 1181 if (pFrom.x >= pVia.x && pFrom.y >= pVia.y) { 1182 if (!leftHandTraffic) { 1183 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90)); 1184 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90)); 1185 } else { 1186 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90)); 1187 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90)); 1188 } 1189 iconAngle = 270+fromAngleDeg; 1190 } 1191 if (pFrom.x < pVia.x && pFrom.y >= pVia.y) { 1192 if (!leftHandTraffic) { 1193 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg)); 1194 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg)); 1195 } else { 1196 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180)); 1197 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180)); 1198 } 1199 iconAngle = 90-fromAngleDeg; 1200 } 1201 if (pFrom.x < pVia.x && pFrom.y < pVia.y) { 1202 if (!leftHandTraffic) { 1203 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90)); 1204 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90)); 1205 } else { 1206 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90)); 1207 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90)); 1208 } 1209 iconAngle = 90+fromAngleDeg; 1210 } 1211 if (pFrom.x >= pVia.x && pFrom.y < pVia.y) { 1212 if (!leftHandTraffic) { 1213 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180)); 1214 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180)); 1215 } else { 1216 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg)); 1217 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg)); 1218 } 1219 iconAngle = 270-fromAngleDeg; 1220 } 1221 1222 drawRestriction(icon.getImage(disabled), 1223 pVia, vx, vx2, vy, vy2, iconAngle, r.isSelected()); 1224 } 1225 1226 /** 1227 * A half segment that can be used to place text on it. Used in the drawTextOnPath algorithm. 1228 * @author Michael Zangl 1229 */ 1230 private static class HalfSegment { 1231 /** 1232 * start point of half segment (as length along the way) 1233 */ 1234 final double start; 1235 /** 1236 * end point of half segment (as length along the way) 1237 */ 1238 final double end; 1239 /** 1240 * quality factor (off screen / partly on screen / fully on screen) 1241 */ 1242 final double quality; 1243 1244 /** 1245 * Create a new half segment 1246 * @param start The start along the way 1247 * @param end The end of the segment 1248 * @param quality A quality factor. 1249 */ 1250 HalfSegment(double start, double end, double quality) { 1251 super(); 1252 this.start = start; 1253 this.end = end; 1254 this.quality = quality; 1255 } 1256 1257 @Override 1258 public String toString() { 1259 return "HalfSegment [start=" + start + ", end=" + end + ", quality=" + quality + "]"; 1260 } 1261 } 1262 1263 /** 1264 * Draws a text along a given way. 1265 * @param way The way to draw the text on. 1266 * @param text The text definition (font/.../text content) to draw. 1267 */ 1268 public void drawTextOnPath(Way way, TextLabel text) { 1269 if (way == null || text == null) 1270 return; 1271 String name = text.getString(way); 1272 if (name == null || name.isEmpty()) 1273 return; 1274 1275 FontMetrics fontMetrics = g.getFontMetrics(text.font); 1276 Rectangle2D rec = fontMetrics.getStringBounds(name, g); 1277 1278 Rectangle bounds = g.getClipBounds(); 1279 1280 List<MapViewPoint> points = way.getNodes().stream().map(mapState::getPointFor).collect(Collectors.toList()); 1281 1282 // find half segments that are long enough to draw text on (don't draw text over the cross hair in the center of each segment) 1283 List<HalfSegment> longHalfSegment = new ArrayList<>(); 1284 1285 double pathLength = computePath(2 * (rec.getWidth() + 4), bounds, points, longHalfSegment); 1286 1287 if (rec.getWidth() > pathLength) 1288 return; 1289 1290 double t1, t2; 1291 1292 if (!longHalfSegment.isEmpty()) { 1293 // find the segment with the best quality. If there are several with best quality, the one close to the center is prefered. 1294 Optional<HalfSegment> besto = longHalfSegment.stream().max( 1295 Comparator.comparingDouble(segment -> 1296 segment.quality - 1e-5 * Math.abs(0.5 * (segment.end + segment.start) - 0.5 * pathLength) 1297 )); 1298 if (!besto.isPresent()) 1299 throw new IllegalStateException("Unable to find the segment with the best quality for " + way); 1300 HalfSegment best = besto.get(); 1301 double remaining = best.end - best.start - rec.getWidth(); // total space left and right from the text 1302 // The space left and right of the text should be distributed 20% - 80% (towards the center), 1303 // but the smaller space should not be less than 7 px. 1304 // However, if the total remaining space is less than 14 px, then distribute it evenly. 1305 double smallerSpace = Math.min(Math.max(0.2 * remaining, 7), 0.5 * remaining); 1306 if ((best.end + best.start)/2 < pathLength/2) { 1307 t2 = best.end - smallerSpace; 1308 t1 = t2 - rec.getWidth(); 1309 } else { 1310 t1 = best.start + smallerSpace; 1311 t2 = t1 + rec.getWidth(); 1312 } 1313 } else { 1314 // doesn't fit into one half-segment -> just put it in the center of the way 1315 t1 = pathLength/2 - rec.getWidth()/2; 1316 t2 = pathLength/2 + rec.getWidth()/2; 1317 } 1318 t1 /= pathLength; 1319 t2 /= pathLength; 1320 1321 double[] p1 = pointAt(t1, points, pathLength); 1322 double[] p2 = pointAt(t2, points, pathLength); 1323 1324 if (p1 == null || p2 == null) 1325 return; 1326 1327 double angleOffset; 1328 double offsetSign; 1329 double tStart; 1330 1331 if (p1[0] < p2[0] && 1332 p1[2] < Math.PI/2 && 1333 p1[2] > -Math.PI/2) { 1334 angleOffset = 0; 1335 offsetSign = 1; 1336 tStart = t1; 1337 } else { 1338 angleOffset = Math.PI; 1339 offsetSign = -1; 1340 tStart = t2; 1341 } 1342 1343 List<GlyphVector> gvs = Utils.getGlyphVectorsBidi(name, text.font, g.getFontRenderContext()); 1344 double gvOffset = 0; 1345 for (GlyphVector gv : gvs) { 1346 double gvWidth = gv.getLogicalBounds().getBounds2D().getWidth(); 1347 for (int i = 0; i < gv.getNumGlyphs(); ++i) { 1348 Rectangle2D rect = gv.getGlyphLogicalBounds(i).getBounds2D(); 1349 double t = tStart + offsetSign * (gvOffset + rect.getX() + rect.getWidth()/2) / pathLength; 1350 double[] p = pointAt(t, points, pathLength); 1351 if (p != null) { 1352 AffineTransform trfm = AffineTransform.getTranslateInstance(p[0] - rect.getX(), p[1]); 1353 trfm.rotate(p[2]+angleOffset); 1354 double off = -rect.getY() - rect.getHeight()/2 + text.yOffset; 1355 trfm.translate(-rect.getWidth()/2, off); 1356 if (isGlyphVectorDoubleTranslationBug(text.font)) { 1357 // scale the translation components by one half 1358 AffineTransform tmp = AffineTransform.getTranslateInstance(-0.5 * trfm.getTranslateX(), -0.5 * trfm.getTranslateY()); 1359 tmp.concatenate(trfm); 1360 trfm = tmp; 1361 } 1362 gv.setGlyphTransform(i, trfm); 1363 } 1364 } 1365 displayText(gv, null, 0, 0, way.isDisabled(), text); 1366 gvOffset += gvWidth; 1367 } 1368 } 1369 1370 private static double computePath(double minSegmentLength, Rectangle bounds, List<MapViewPoint> points, 1371 List<HalfSegment> longHalfSegment) { 1372 MapViewPoint lastPoint = points.get(0); 1373 double pathLength = 0; 1374 for (MapViewPoint p : points.subList(1, points.size())) { 1375 double segmentLength = p.distanceToInView(lastPoint); 1376 if (segmentLength > minSegmentLength) { 1377 Point2D center = new Point2D.Double((lastPoint.getInViewX() + p.getInViewX())/2, (lastPoint.getInViewY() + p.getInViewY())/2); 1378 double q = computeQuality(bounds, lastPoint, center); 1379 // prefer the first one for quality equality. 1380 longHalfSegment.add(new HalfSegment(pathLength, pathLength + segmentLength / 2, q)); 1381 1382 q = 0; 1383 if (bounds != null) { 1384 if (bounds.contains(center) && bounds.contains(p.getInView())) { 1385 q = 2; 1386 } else if (bounds.contains(center) || bounds.contains(p.getInView())) { 1387 q = 1; 1388 } 1389 } 1390 longHalfSegment.add(new HalfSegment(pathLength + segmentLength / 2, pathLength + segmentLength, q)); 1391 } 1392 pathLength += segmentLength; 1393 lastPoint = p; 1394 } 1395 return pathLength; 1396 } 1397 1398 private static double computeQuality(Rectangle bounds, MapViewPoint p1, Point2D p2) { 1399 double q = 0; 1400 if (bounds != null) { 1401 if (bounds.contains(p1.getInView())) { 1402 q += 1; 1403 } 1404 if (bounds.contains(p2)) { 1405 q += 1; 1406 } 1407 } 1408 return q; 1409 } 1410 1411 /** 1412 * draw way. This method allows for two draw styles (line using color, dashes using dashedColor) to be passed. 1413 * @param way The way to draw 1414 * @param color The base color to draw the way in 1415 * @param line The line style to use. This is drawn using color. 1416 * @param dashes The dash style to use. This is drawn using dashedColor. <code>null</code> if unused. 1417 * @param dashedColor The color of the dashes. 1418 * @param offset The offset 1419 * @param showOrientation show arrows that indicate the technical orientation of 1420 * the way (defined by order of nodes) 1421 * @param showHeadArrowOnly True if only the arrow at the end of the line but not those on the segments should be displayed. 1422 * @param showOneway show symbols that indicate the direction of the feature, 1423 * e.g. oneway street or waterway 1424 * @param onewayReversed for oneway=-1 and similar 1425 */ 1426 public void drawWay(Way way, Color color, BasicStroke line, BasicStroke dashes, Color dashedColor, float offset, 1427 boolean showOrientation, boolean showHeadArrowOnly, 1428 boolean showOneway, boolean onewayReversed) { 1429 1430 MapViewPath path = new MapViewPath(mapState); 1431 MapViewPath orientationArrows = showOrientation ? new MapViewPath(mapState) : null; 1432 MapViewPath onewayArrows; 1433 MapViewPath onewayArrowsCasing; 1434 Rectangle bounds = g.getClipBounds(); 1435 if (bounds != null) { 1436 // avoid arrow heads at the border 1437 bounds.grow(100, 100); 1438 } 1439 1440 List<Node> wayNodes = way.getNodes(); 1441 if (wayNodes.size() < 2) return; 1442 1443 // only highlight the segment if the way itself is not highlighted 1444 if (!way.isHighlighted() && highlightWaySegments != null) { 1445 MapViewPath highlightSegs = null; 1446 for (WaySegment ws : highlightWaySegments) { 1447 if (ws.way != way || ws.lowerIndex < offset) { 1448 continue; 1449 } 1450 if (highlightSegs == null) { 1451 highlightSegs = new MapViewPath(mapState); 1452 } 1453 1454 highlightSegs.moveTo(ws.getFirstNode()); 1455 highlightSegs.lineTo(ws.getSecondNode()); 1456 } 1457 1458 drawPathHighlight(highlightSegs, line); 1459 } 1460 1461 MapViewPoint lastPoint = null; 1462 Iterator<MapViewPoint> it = new OffsetIterator(wayNodes, offset); 1463 boolean initialMoveToNeeded = true; 1464 while (it.hasNext()) { 1465 MapViewPoint p = it.next(); 1466 if (lastPoint != null) { 1467 MapViewPoint p1 = lastPoint; 1468 MapViewPoint p2 = p; 1469 1470 if (initialMoveToNeeded) { 1471 initialMoveToNeeded = false; 1472 path.moveTo(p1); 1473 } 1474 path.lineTo(p2); 1475 1476 /* draw arrow */ 1477 if (showHeadArrowOnly ? !it.hasNext() : showOrientation) { 1478 //TODO: Cache 1479 ArrowPaintHelper drawHelper = new ArrowPaintHelper(PHI, 10 + line.getLineWidth()); 1480 drawHelper.paintArrowAt(orientationArrows, p2, p1); 1481 } 1482 } 1483 lastPoint = p; 1484 } 1485 if (showOneway) { 1486 onewayArrows = new MapViewPath(mapState); 1487 onewayArrowsCasing = new MapViewPath(mapState); 1488 double interval = 60; 1489 1490 path.visitClippedLine(0, 60, (inLineOffset, start, end, startIsOldEnd) -> { 1491 double segmentLength = start.distanceToInView(end); 1492 if (segmentLength > 0.001) { 1493 final double nx = (end.getInViewX() - start.getInViewX()) / segmentLength; 1494 final double ny = (end.getInViewY() - start.getInViewY()) / segmentLength; 1495 1496 // distance from p1 1497 double dist = interval - (inLineOffset % interval); 1498 1499 while (dist < segmentLength) { 1500 appenOnewayPath(onewayReversed, start, nx, ny, dist, 3d, onewayArrowsCasing); 1501 appenOnewayPath(onewayReversed, start, nx, ny, dist, 2d, onewayArrows); 1502 dist += interval; 1503 } 1504 } 1505 }); 1506 } else { 1507 onewayArrows = null; 1508 onewayArrowsCasing = null; 1509 } 1510 1511 if (way.isHighlighted()) { 1512 drawPathHighlight(path, line); 1513 } 1514 displaySegments(path, orientationArrows, onewayArrows, onewayArrowsCasing, color, line, dashes, dashedColor); 1515 } 1516 1517 private static void appenOnewayPath(boolean onewayReversed, MapViewPoint p1, double nx, double ny, double dist, 1518 double onewaySize, Path2D onewayPath) { 1519 // scale such that border is 1 px 1520 final double fac = -(onewayReversed ? -1 : 1) * onewaySize * (1 + sinPHI) / (sinPHI * cosPHI); 1521 final double sx = nx * fac; 1522 final double sy = ny * fac; 1523 1524 // Attach the triangle at the incenter and not at the tip. 1525 // Makes the border even at all sides. 1526 final double x = p1.getInViewX() + nx * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI)); 1527 final double y = p1.getInViewY() + ny * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI)); 1528 1529 onewayPath.moveTo(x, y); 1530 onewayPath.lineTo(x + cosPHI * sx - sinPHI * sy, y + sinPHI * sx + cosPHI * sy); 1531 onewayPath.lineTo(x + cosPHI * sx + sinPHI * sy, y - sinPHI * sx + cosPHI * sy); 1532 onewayPath.lineTo(x, y); 1533 } 1534 1535 /** 1536 * Gets the "circum". This is the distance on the map in meters that 100 screen pixels represent. 1537 * @return The "circum" 1538 */ 1539 public double getCircum() { 1540 return circum; 1541 } 1542 1543 @Override 1544 public void getColors() { 1545 super.getColors(); 1546 this.highlightColorTransparent = new Color(highlightColor.getRed(), highlightColor.getGreen(), highlightColor.getBlue(), 100); 1547 this.backgroundColor = PaintColors.getBackgroundColor(); 1548 } 1549 1550 @Override 1551 public void getSettings(boolean virtual) { 1552 super.getSettings(virtual); 1553 paintSettings = MapPaintSettings.INSTANCE; 1554 1555 circum = nc.getDist100Pixel(); 1556 scale = nc.getScale(); 1557 1558 leftHandTraffic = Main.pref.getBoolean("mappaint.lefthandtraffic", false); 1559 1560 useStrokes = paintSettings.getUseStrokesDistance() > circum; 1561 showNames = paintSettings.getShowNamesDistance() > circum; 1562 showIcons = paintSettings.getShowIconsDistance() > circum; 1563 isOutlineOnly = paintSettings.isOutlineOnly(); 1564 orderFont = new Font(Main.pref.get("mappaint.font", "Droid Sans"), Font.PLAIN, Main.pref.getInteger("mappaint.fontsize", 8)); 1565 1566 antialiasing = Main.pref.getBoolean("mappaint.use-antialiasing", true) ? 1567 RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF; 1568 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing); 1569 1570 Object textAntialiasing; 1571 switch (Main.pref.get("mappaint.text-antialiasing", "default")) { 1572 case "on": 1573 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_ON; 1574 break; 1575 case "off": 1576 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_OFF; 1577 break; 1578 case "gasp": 1579 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_GASP; 1580 break; 1581 case "lcd-hrgb": 1582 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB; 1583 break; 1584 case "lcd-hbgr": 1585 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HBGR; 1586 break; 1587 case "lcd-vrgb": 1588 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VRGB; 1589 break; 1590 case "lcd-vbgr": 1591 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VBGR; 1592 break; 1593 default: 1594 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT; 1595 } 1596 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, textAntialiasing); 1597 1598 highlightLineWidth = Main.pref.getInteger("mappaint.highlight.width", 4); 1599 highlightPointRadius = Main.pref.getInteger("mappaint.highlight.radius", 7); 1600 widerHighlight = Main.pref.getInteger("mappaint.highlight.bigger-increment", 5); 1601 highlightStep = Main.pref.getInteger("mappaint.highlight.step", 4); 1602 } 1603 1604 private static Path2D.Double getPath(Way w) { 1605 Path2D.Double path = new Path2D.Double(); 1606 boolean initial = true; 1607 for (Node n : w.getNodes()) { 1608 EastNorth p = n.getEastNorth(); 1609 if (p != null) { 1610 if (initial) { 1611 path.moveTo(p.getX(), p.getY()); 1612 initial = false; 1613 } else { 1614 path.lineTo(p.getX(), p.getY()); 1615 } 1616 } 1617 } 1618 if (w.isClosed()) { 1619 path.closePath(); 1620 } 1621 return path; 1622 } 1623 1624 private static Path2D.Double getPFClip(Way w, double extent) { 1625 Path2D.Double clip = new Path2D.Double(); 1626 buildPFClip(clip, w.getNodes(), extent); 1627 return clip; 1628 } 1629 1630 private static Path2D.Double getPFClip(PolyData pd, double extent) { 1631 Path2D.Double clip = new Path2D.Double(); 1632 clip.setWindingRule(Path2D.WIND_EVEN_ODD); 1633 buildPFClip(clip, pd.getNodes(), extent); 1634 for (PolyData pdInner : pd.getInners()) { 1635 buildPFClip(clip, pdInner.getNodes(), extent); 1636 } 1637 return clip; 1638 } 1639 1640 /** 1641 * Fix the clipping area of unclosed polygons for partial fill. 1642 * 1643 * The current algorithm for partial fill simply strokes the polygon with a 1644 * large stroke width after masking the outside with a clipping area. 1645 * This works, but for unclosed polygons, the mask can crop the corners at 1646 * both ends (see #12104). 1647 * 1648 * This method fixes the clipping area by sort of adding the corners to the 1649 * clip outline. 1650 * 1651 * @param clip the clipping area to modify (initially empty) 1652 * @param nodes nodes of the polygon 1653 * @param extent the extent 1654 */ 1655 private static void buildPFClip(Path2D.Double clip, List<Node> nodes, double extent) { 1656 boolean initial = true; 1657 for (Node n : nodes) { 1658 EastNorth p = n.getEastNorth(); 1659 if (p != null) { 1660 if (initial) { 1661 clip.moveTo(p.getX(), p.getY()); 1662 initial = false; 1663 } else { 1664 clip.lineTo(p.getX(), p.getY()); 1665 } 1666 } 1667 } 1668 if (nodes.size() >= 3) { 1669 EastNorth fst = nodes.get(0).getEastNorth(); 1670 EastNorth snd = nodes.get(1).getEastNorth(); 1671 EastNorth lst = nodes.get(nodes.size() - 1).getEastNorth(); 1672 EastNorth lbo = nodes.get(nodes.size() - 2).getEastNorth(); 1673 1674 EastNorth cLst = getPFDisplacedEndPoint(lbo, lst, fst, extent); 1675 EastNorth cFst = getPFDisplacedEndPoint(snd, fst, cLst != null ? cLst : lst, extent); 1676 if (cLst == null && cFst != null) { 1677 cLst = getPFDisplacedEndPoint(lbo, lst, cFst, extent); 1678 } 1679 if (cLst != null) { 1680 clip.lineTo(cLst.getX(), cLst.getY()); 1681 } 1682 if (cFst != null) { 1683 clip.lineTo(cFst.getX(), cFst.getY()); 1684 } 1685 } 1686 } 1687 1688 /** 1689 * Get the point to add to the clipping area for partial fill of unclosed polygons. 1690 * 1691 * <code>(p1,p2)</code> is the first or last way segment and <code>p3</code> the 1692 * opposite endpoint. 1693 * 1694 * @param p1 1st point 1695 * @param p2 2nd point 1696 * @param p3 3rd point 1697 * @param extent the extent 1698 * @return a point q, such that p1,p2,q form a right angle 1699 * and the distance of q to p2 is <code>extent</code>. The point q lies on 1700 * the same side of the line p1,p2 as the point p3. 1701 * Returns null if p1,p2,p3 forms an angle greater 90 degrees. (In this case 1702 * the corner of the partial fill would not be cut off by the mask, so an 1703 * additional point is not necessary.) 1704 */ 1705 private static EastNorth getPFDisplacedEndPoint(EastNorth p1, EastNorth p2, EastNorth p3, double extent) { 1706 double dx1 = p2.getX() - p1.getX(); 1707 double dy1 = p2.getY() - p1.getY(); 1708 double dx2 = p3.getX() - p2.getX(); 1709 double dy2 = p3.getY() - p2.getY(); 1710 if (dx1 * dx2 + dy1 * dy2 < 0) { 1711 double len = Math.sqrt(dx1 * dx1 + dy1 * dy1); 1712 if (len == 0) return null; 1713 double dxm = -dy1 * extent / len; 1714 double dym = dx1 * extent / len; 1715 if (dx1 * dy2 - dx2 * dy1 < 0) { 1716 dxm = -dxm; 1717 dym = -dym; 1718 } 1719 return new EastNorth(p2.getX() + dxm, p2.getY() + dym); 1720 } 1721 return null; 1722 } 1723 1724 /** 1725 * Test if the area is visible 1726 * @param area The area, interpreted in east/north space. 1727 * @return true if it is visible. 1728 */ 1729 private boolean isAreaVisible(Path2D.Double area) { 1730 Rectangle2D bounds = area.getBounds2D(); 1731 if (bounds.isEmpty()) return false; 1732 MapViewPoint p = mapState.getPointFor(new EastNorth(bounds.getX(), bounds.getY())); 1733 if (p.getInViewX() > mapState.getViewWidth()) return false; 1734 if (p.getInViewY() < 0) return false; 1735 p = mapState.getPointFor(new EastNorth(bounds.getX() + bounds.getWidth(), bounds.getY() + bounds.getHeight())); 1736 if (p.getInViewX() < 0) return false; 1737 if (p.getInViewY() > mapState.getViewHeight()) return false; 1738 return true; 1739 } 1740 1741 public boolean isInactiveMode() { 1742 return isInactiveMode; 1743 } 1744 1745 public boolean isShowIcons() { 1746 return showIcons; 1747 } 1748 1749 public boolean isShowNames() { 1750 return showNames; 1751 } 1752 1753 private static double[] pointAt(double t, List<MapViewPoint> poly, double pathLength) { 1754 double totalLen = t * pathLength; 1755 double curLen = 0; 1756 double dx, dy; 1757 double segLen; 1758 1759 // Yes, it is inefficient to iterate from the beginning for each glyph. 1760 // Can be optimized if it turns out to be slow. 1761 for (int i = 1; i < poly.size(); ++i) { 1762 dx = poly.get(i).getInViewX() - poly.get(i - 1).getInViewX(); 1763 dy = poly.get(i).getInViewY() - poly.get(i - 1).getInViewY(); 1764 segLen = Math.sqrt(dx*dx + dy*dy); 1765 if (totalLen > curLen + segLen) { 1766 curLen += segLen; 1767 continue; 1768 } 1769 return new double[] { 1770 poly.get(i - 1).getInViewX() + (totalLen - curLen) / segLen * dx, 1771 poly.get(i - 1).getInViewY() + (totalLen - curLen) / segLen * dy, 1772 Math.atan2(dy, dx)}; 1773 } 1774 return null; 1775 } 1776 1777 /** 1778 * Computes the flags for a given OSM primitive. 1779 * @param primitive The primititve to compute the flags for. 1780 * @param checkOuterMember <code>true</code> if we should also add {@link #FLAG_OUTERMEMBER_OF_SELECTED} 1781 * @return The flag. 1782 */ 1783 public static int computeFlags(OsmPrimitive primitive, boolean checkOuterMember) { 1784 if (primitive.isDisabled()) { 1785 return FLAG_DISABLED; 1786 } else if (primitive.isSelected()) { 1787 return FLAG_SELECTED; 1788 } else if (checkOuterMember && primitive.isOuterMemberOfSelected()) { 1789 return FLAG_OUTERMEMBER_OF_SELECTED; 1790 } else if (primitive.isMemberOfSelected()) { 1791 return FLAG_MEMBER_OF_SELECTED; 1792 } else { 1793 return FLAG_NORMAL; 1794 } 1795 } 1796 1797 private static class ComputeStyleListWorker extends RecursiveTask<List<StyleRecord>> implements Visitor { 1798 private final transient List<? extends OsmPrimitive> input; 1799 private final transient List<StyleRecord> output; 1800 1801 private final transient ElemStyles styles = MapPaintStyles.getStyles(); 1802 private final int directExecutionTaskSize; 1803 private final double circum; 1804 private final NavigatableComponent nc; 1805 1806 private final boolean drawArea; 1807 private final boolean drawMultipolygon; 1808 private final boolean drawRestriction; 1809 1810 /** 1811 * Constructs a new {@code ComputeStyleListWorker}. 1812 * @param circum distance on the map in meters that 100 screen pixels represent 1813 * @param nc navigatable component 1814 * @param input the primitives to process 1815 * @param output the list of styles to which styles will be added 1816 * @param directExecutionTaskSize the threshold deciding whether to subdivide the tasks 1817 */ 1818 ComputeStyleListWorker(double circum, NavigatableComponent nc, 1819 final List<? extends OsmPrimitive> input, List<StyleRecord> output, int directExecutionTaskSize) { 1820 this.circum = circum; 1821 this.nc = nc; 1822 this.input = input; 1823 this.output = output; 1824 this.directExecutionTaskSize = directExecutionTaskSize; 1825 this.drawArea = circum <= Main.pref.getInteger("mappaint.fillareas", 10_000_000); 1826 this.drawMultipolygon = drawArea && Main.pref.getBoolean("mappaint.multipolygon", true); 1827 this.drawRestriction = Main.pref.getBoolean("mappaint.restriction", true); 1828 this.styles.setDrawMultipolygon(drawMultipolygon); 1829 } 1830 1831 @Override 1832 protected List<StyleRecord> compute() { 1833 if (input.size() <= directExecutionTaskSize) { 1834 return computeDirectly(); 1835 } else { 1836 final Collection<ForkJoinTask<List<StyleRecord>>> tasks = new ArrayList<>(); 1837 for (int fromIndex = 0; fromIndex < input.size(); fromIndex += directExecutionTaskSize) { 1838 final int toIndex = Math.min(fromIndex + directExecutionTaskSize, input.size()); 1839 final List<StyleRecord> output = new ArrayList<>(directExecutionTaskSize); 1840 tasks.add(new ComputeStyleListWorker(circum, nc, input.subList(fromIndex, toIndex), output, directExecutionTaskSize).fork()); 1841 } 1842 for (ForkJoinTask<List<StyleRecord>> task : tasks) { 1843 output.addAll(task.join()); 1844 } 1845 return output; 1846 } 1847 } 1848 1849 public List<StyleRecord> computeDirectly() { 1850 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock(); 1851 try { 1852 for (final OsmPrimitive osm : input) { 1853 acceptDrawable(osm); 1854 } 1855 return output; 1856 } catch (RuntimeException e) { 1857 throw BugReport.intercept(e).put("input-size", input.size()).put("output-size", output.size()); 1858 } finally { 1859 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock(); 1860 } 1861 } 1862 1863 private void acceptDrawable(final OsmPrimitive osm) { 1864 try { 1865 if (osm.isDrawable()) { 1866 osm.accept(this); 1867 } 1868 } catch (RuntimeException e) { 1869 throw BugReport.intercept(e).put("osm", osm); 1870 } 1871 } 1872 1873 @Override 1874 public void visit(Node n) { 1875 add(n, computeFlags(n, false)); 1876 } 1877 1878 @Override 1879 public void visit(Way w) { 1880 add(w, computeFlags(w, true)); 1881 } 1882 1883 @Override 1884 public void visit(Relation r) { 1885 add(r, computeFlags(r, true)); 1886 } 1887 1888 @Override 1889 public void visit(Changeset cs) { 1890 throw new UnsupportedOperationException(); 1891 } 1892 1893 public void add(Node osm, int flags) { 1894 StyleElementList sl = styles.get(osm, circum, nc); 1895 for (StyleElement s : sl) { 1896 output.add(new StyleRecord(s, osm, flags)); 1897 } 1898 } 1899 1900 public void add(Relation osm, int flags) { 1901 StyleElementList sl = styles.get(osm, circum, nc); 1902 for (StyleElement s : sl) { 1903 if (drawMultipolygon && drawArea && s instanceof AreaElement && (flags & FLAG_DISABLED) == 0) { 1904 output.add(new StyleRecord(s, osm, flags)); 1905 } else if (drawRestriction && s instanceof NodeElement) { 1906 output.add(new StyleRecord(s, osm, flags)); 1907 } 1908 } 1909 } 1910 1911 public void add(Way osm, int flags) { 1912 StyleElementList sl = styles.get(osm, circum, nc); 1913 for (StyleElement s : sl) { 1914 if (!(drawArea && (flags & FLAG_DISABLED) == 0) && s instanceof AreaElement) { 1915 continue; 1916 } 1917 output.add(new StyleRecord(s, osm, flags)); 1918 } 1919 } 1920 } 1921 1922 /** 1923 * Sets the factory that creates the benchmark data receivers. 1924 * @param benchmarkFactory The factory. 1925 * @since 10697 1926 */ 1927 public void setBenchmarkFactory(Supplier<RenderBenchmarkCollector> benchmarkFactory) { 1928 this.benchmarkFactory = benchmarkFactory; 1929 } 1930 1931 @Override 1932 public void render(final DataSet data, boolean renderVirtualNodes, Bounds bounds) { 1933 RenderBenchmarkCollector benchmark = benchmarkFactory.get(); 1934 BBox bbox = bounds.toBBox(); 1935 getSettings(renderVirtualNodes); 1936 1937 data.getReadLock().lock(); 1938 try { 1939 highlightWaySegments = data.getHighlightedWaySegments(); 1940 1941 benchmark.renderStart(circum); 1942 1943 List<Node> nodes = data.searchNodes(bbox); 1944 List<Way> ways = data.searchWays(bbox); 1945 List<Relation> relations = data.searchRelations(bbox); 1946 1947 final List<StyleRecord> allStyleElems = new ArrayList<>(nodes.size()+ways.size()+relations.size()); 1948 1949 // Need to process all relations first. 1950 // Reason: Make sure, ElemStyles.getStyleCacheWithRange is not called for the same primitive in parallel threads. 1951 // (Could be synchronized, but try to avoid this for performance reasons.) 1952 THREAD_POOL.invoke(new ComputeStyleListWorker(circum, nc, relations, allStyleElems, 1953 Math.max(20, relations.size() / THREAD_POOL.getParallelism() / 3))); 1954 THREAD_POOL.invoke(new ComputeStyleListWorker(circum, nc, new CompositeList<>(nodes, ways), allStyleElems, 1955 Math.max(100, (nodes.size() + ways.size()) / THREAD_POOL.getParallelism() / 3))); 1956 1957 if (!benchmark.renderSort()) { 1958 return; 1959 } 1960 1961 Collections.sort(allStyleElems); // TODO: try parallel sort when switching to Java 8 1962 1963 if (!benchmark.renderDraw(allStyleElems)) { 1964 return; 1965 } 1966 1967 for (StyleRecord record : allStyleElems) { 1968 paintRecord(record); 1969 } 1970 1971 drawVirtualNodes(data, bbox); 1972 1973 benchmark.renderDone(); 1974 } catch (RuntimeException e) { 1975 throw BugReport.intercept(e) 1976 .put("data", data) 1977 .put("circum", circum) 1978 .put("scale", scale) 1979 .put("paintSettings", paintSettings) 1980 .put("renderVirtualNodes", renderVirtualNodes); 1981 } finally { 1982 data.getReadLock().unlock(); 1983 } 1984 } 1985 1986 private void paintRecord(StyleRecord record) { 1987 try { 1988 record.paintPrimitive(paintSettings, this); 1989 } catch (RuntimeException e) { 1990 throw BugReport.intercept(e).put("record", record); 1991 } 1992 } 1993}