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 public void drawBoxText(Node n, BoxTextElement bs) { 759 if (!isShowNames() || bs == null) 760 return; 761 762 MapViewPoint p = mapState.getPointFor(n); 763 TextLabel text = bs.text; 764 String s = text.labelCompositionStrategy.compose(n); 765 if (s == null) return; 766 767 Font defaultFont = g.getFont(); 768 g.setFont(text.font); 769 770 int x = (int) (Math.round(p.getInViewX()) + text.xOffset); 771 int y = (int) (Math.round(p.getInViewY()) + text.yOffset); 772 /** 773 * 774 * left-above __center-above___ right-above 775 * left-top| |right-top 776 * | | 777 * left-center| center-center |right-center 778 * | | 779 * left-bottom|_________________|right-bottom 780 * left-below center-below right-below 781 * 782 */ 783 Rectangle box = bs.getBox(); 784 if (bs.hAlign == HorizontalTextAlignment.RIGHT) { 785 x += box.x + box.width + 2; 786 } else { 787 FontRenderContext frc = g.getFontRenderContext(); 788 Rectangle2D bounds = text.font.getStringBounds(s, frc); 789 int textWidth = (int) bounds.getWidth(); 790 if (bs.hAlign == HorizontalTextAlignment.CENTER) { 791 x -= textWidth / 2; 792 } else if (bs.hAlign == HorizontalTextAlignment.LEFT) { 793 x -= -box.x + 4 + textWidth; 794 } else throw new AssertionError(); 795 } 796 797 if (bs.vAlign == VerticalTextAlignment.BOTTOM) { 798 y += box.y + box.height; 799 } else { 800 FontRenderContext frc = g.getFontRenderContext(); 801 LineMetrics metrics = text.font.getLineMetrics(s, frc); 802 if (bs.vAlign == VerticalTextAlignment.ABOVE) { 803 y -= -box.y + metrics.getDescent(); 804 } else if (bs.vAlign == VerticalTextAlignment.TOP) { 805 y -= -box.y - metrics.getAscent(); 806 } else if (bs.vAlign == VerticalTextAlignment.CENTER) { 807 y += (metrics.getAscent() - metrics.getDescent()) / 2; 808 } else if (bs.vAlign == VerticalTextAlignment.BELOW) { 809 y += box.y + box.height + metrics.getAscent() + 2; 810 } else throw new AssertionError(); 811 } 812 displayText(null, s, x, y, n.isDisabled(), text); 813 g.setFont(defaultFont); 814 } 815 816 /** 817 * Draw an image along a way repeatedly. 818 * 819 * @param way the way 820 * @param pattern the image 821 * @param disabled If this should be drawn with a special disabled style. 822 * @param offset offset from the way 823 * @param spacing spacing between two images 824 * @param phase initial spacing 825 * @param align alignment of the image. The top, center or bottom edge can be aligned with the way. 826 */ 827 public void drawRepeatImage(Way way, MapImage pattern, boolean disabled, double offset, double spacing, double phase, 828 LineImageAlignment align) { 829 final int imgWidth = pattern.getWidth(); 830 final double repeat = imgWidth + spacing; 831 final int imgHeight = pattern.getHeight(); 832 833 int dy1 = (int) ((align.getAlignmentOffset() - .5) * imgHeight); 834 int dy2 = dy1 + imgHeight; 835 836 OffsetIterator it = new OffsetIterator(way.getNodes(), offset); 837 MapViewPath path = new MapViewPath(mapState); 838 if (it.hasNext()) { 839 path.moveTo(it.next()); 840 } 841 while (it.hasNext()) { 842 path.lineTo(it.next()); 843 } 844 845 double startOffset = phase % repeat; 846 if (startOffset < 0) { 847 startOffset += repeat; 848 } 849 850 BufferedImage image = pattern.getImage(disabled); 851 852 path.visitClippedLine(startOffset, repeat, (inLineOffset, start, end, startIsOldEnd) -> { 853 final double segmentLength = start.distanceToInView(end); 854 if (segmentLength < 0.1) { 855 // avoid odd patterns when zoomed out. 856 return; 857 } 858 if (segmentLength > repeat * 500) { 859 // simply skip drawing so many images - something must be wrong. 860 return; 861 } 862 AffineTransform saveTransform = g.getTransform(); 863 g.translate(start.getInViewX(), start.getInViewY()); 864 double dx = end.getInViewX() - start.getInViewX(); 865 double dy = end.getInViewY() - start.getInViewY(); 866 g.rotate(Math.atan2(dy, dx)); 867 868 // The start of the next image 869 double imageStart = -(inLineOffset % repeat); 870 871 while (imageStart < segmentLength) { 872 int x = (int) imageStart; 873 int sx1 = Math.max(0, -x); 874 int sx2 = imgWidth - Math.max(0, x + imgWidth - (int) Math.ceil(segmentLength)); 875 g.drawImage(image, x + sx1, dy1, x + sx2, dy2, sx1, 0, sx2, imgHeight, null); 876 imageStart += repeat; 877 } 878 879 g.setTransform(saveTransform); 880 }); 881 } 882 883 @Override 884 public void drawNode(Node n, Color color, int size, boolean fill) { 885 if (size <= 0 && !n.isHighlighted()) 886 return; 887 888 MapViewPoint p = mapState.getPointFor(n); 889 890 if (n.isHighlighted()) { 891 drawPointHighlight(p.getInView(), size); 892 } 893 894 if (size > 1 && p.isInView()) { 895 int radius = size / 2; 896 897 if (isInactiveMode || n.isDisabled()) { 898 g.setColor(inactiveColor); 899 } else { 900 g.setColor(color); 901 } 902 Rectangle2D rect = new Rectangle2D.Double(p.getInViewX()-radius-1, p.getInViewY()-radius-1, size + 1, size + 1); 903 if (fill) { 904 g.fill(rect); 905 } else { 906 g.draw(rect); 907 } 908 } 909 } 910 911 /** 912 * Draw the icon for a given node. 913 * @param n The node 914 * @param img The icon to draw at the node position 915 * @param disabled {@code} true to render disabled version, {@code false} for the standard version 916 * @param selected {@code} true to render it as selected, {@code false} otherwise 917 * @param member {@code} true to render it as a relation member, {@code false} otherwise 918 * @param theta the angle of rotation in radians 919 */ 920 public void drawNodeIcon(Node n, MapImage img, boolean disabled, boolean selected, boolean member, double theta) { 921 MapViewPoint p = mapState.getPointFor(n); 922 923 int w = img.getWidth(); 924 int h = img.getHeight(); 925 if (n.isHighlighted()) { 926 drawPointHighlight(p.getInView(), Math.max(w, h)); 927 } 928 929 float alpha = img.getAlphaFloat(); 930 931 Graphics2D temporaryGraphics = (Graphics2D) g.create(); 932 if (!Utils.equalsEpsilon(alpha, 1f)) { 933 temporaryGraphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)); 934 } 935 936 double x = Math.round(p.getInViewX()); 937 double y = Math.round(p.getInViewY()); 938 temporaryGraphics.translate(x, y); 939 temporaryGraphics.rotate(theta); 940 int drawX = -w/2 + img.offsetX; 941 int drawY = -h/2 + img.offsetY; 942 temporaryGraphics.drawImage(img.getImage(disabled), drawX, drawY, nc); 943 if (selected || member) { 944 Color color; 945 if (disabled) { 946 color = inactiveColor; 947 } else if (selected) { 948 color = selectedColor; 949 } else { 950 color = relationSelectedColor; 951 } 952 temporaryGraphics.setColor(color); 953 temporaryGraphics.draw(new Rectangle2D.Double(drawX - 2, drawY - 2, w + 4, h + 4)); 954 } 955 } 956 957 /** 958 * Draw the symbol and possibly a highlight marking on a given node. 959 * @param n The position to draw the symbol on 960 * @param s The symbol to draw 961 * @param fillColor The color to fill the symbol with 962 * @param strokeColor The color to use for the outer corner of the symbol 963 */ 964 public void drawNodeSymbol(Node n, Symbol s, Color fillColor, Color strokeColor) { 965 MapViewPoint p = mapState.getPointFor(n); 966 967 if (n.isHighlighted()) { 968 drawPointHighlight(p.getInView(), s.size); 969 } 970 971 if (fillColor != null || strokeColor != null) { 972 Shape shape = s.buildShapeAround(p.getInViewX(), p.getInViewY()); 973 974 if (fillColor != null) { 975 g.setColor(fillColor); 976 g.fill(shape); 977 } 978 if (s.stroke != null) { 979 g.setStroke(s.stroke); 980 g.setColor(strokeColor); 981 g.draw(shape); 982 g.setStroke(new BasicStroke()); 983 } 984 } 985 } 986 987 /** 988 * Draw a number of the order of the two consecutive nodes within the 989 * parents way 990 * 991 * @param n1 First node of the way segment. 992 * @param n2 Second node of the way segment. 993 * @param orderNumber The number of the segment in the way. 994 * @param clr The color to use for drawing the text. 995 */ 996 public void drawOrderNumber(Node n1, Node n2, int orderNumber, Color clr) { 997 MapViewPoint p1 = mapState.getPointFor(n1); 998 MapViewPoint p2 = mapState.getPointFor(n2); 999 drawOrderNumber(p1, p2, orderNumber, clr); 1000 } 1001 1002 /** 1003 * highlights a given GeneralPath using the settings from BasicStroke to match the line's 1004 * style. Width of the highlight is hard coded. 1005 * @param path path to draw 1006 * @param line line style 1007 */ 1008 private void drawPathHighlight(MapViewPath path, BasicStroke line) { 1009 if (path == null) 1010 return; 1011 g.setColor(highlightColorTransparent); 1012 float w = line.getLineWidth() + highlightLineWidth; 1013 if (useWiderHighlight) w += widerHighlight; 1014 while (w >= line.getLineWidth()) { 1015 g.setStroke(new BasicStroke(w, line.getEndCap(), line.getLineJoin(), line.getMiterLimit())); 1016 g.draw(path); 1017 w -= highlightStep; 1018 } 1019 } 1020 1021 /** 1022 * highlights a given point by drawing a rounded rectangle around it. Give the 1023 * size of the object you want to be highlighted, width is added automatically. 1024 * @param p point 1025 * @param size highlight size 1026 */ 1027 private void drawPointHighlight(Point2D p, int size) { 1028 g.setColor(highlightColorTransparent); 1029 int s = size + highlightPointRadius; 1030 if (useWiderHighlight) s += widerHighlight; 1031 while (s >= size) { 1032 int r = (int) Math.floor(s/2d); 1033 g.fill(new RoundRectangle2D.Double(p.getX()-r, p.getY()-r, s, s, r, r)); 1034 s -= highlightStep; 1035 } 1036 } 1037 1038 public void drawRestriction(Image img, Point pVia, double vx, double vx2, double vy, double vy2, double angle, boolean selected) { 1039 // rotate image with direction last node in from to, and scale down image to 16*16 pixels 1040 Image smallImg = ImageProvider.createRotatedImage(img, angle, new Dimension(16, 16)); 1041 int w = smallImg.getWidth(null), h = smallImg.getHeight(null); 1042 g.drawImage(smallImg, (int) (pVia.x+vx+vx2)-w/2, (int) (pVia.y+vy+vy2)-h/2, nc); 1043 1044 if (selected) { 1045 g.setColor(isInactiveMode ? inactiveColor : relationSelectedColor); 1046 g.drawRect((int) (pVia.x+vx+vx2)-w/2-2, (int) (pVia.y+vy+vy2)-h/2-2, w+4, h+4); 1047 } 1048 } 1049 1050 public void drawRestriction(Relation r, MapImage icon, boolean disabled) { 1051 Way fromWay = null; 1052 Way toWay = null; 1053 OsmPrimitive via = null; 1054 1055 /* find the "from", "via" and "to" elements */ 1056 for (RelationMember m : r.getMembers()) { 1057 if (m.getMember().isIncomplete()) 1058 return; 1059 else { 1060 if (m.isWay()) { 1061 Way w = m.getWay(); 1062 if (w.getNodesCount() < 2) { 1063 continue; 1064 } 1065 1066 switch(m.getRole()) { 1067 case "from": 1068 if (fromWay == null) { 1069 fromWay = w; 1070 } 1071 break; 1072 case "to": 1073 if (toWay == null) { 1074 toWay = w; 1075 } 1076 break; 1077 case "via": 1078 if (via == null) { 1079 via = w; 1080 } 1081 break; 1082 default: // Do nothing 1083 } 1084 } else if (m.isNode()) { 1085 Node n = m.getNode(); 1086 if ("via".equals(m.getRole()) && via == null) { 1087 via = n; 1088 } 1089 } 1090 } 1091 } 1092 1093 if (fromWay == null || toWay == null || via == null) 1094 return; 1095 1096 Node viaNode; 1097 if (via instanceof Node) { 1098 viaNode = (Node) via; 1099 if (!fromWay.isFirstLastNode(viaNode)) 1100 return; 1101 } else { 1102 Way viaWay = (Way) via; 1103 Node firstNode = viaWay.firstNode(); 1104 Node lastNode = viaWay.lastNode(); 1105 Boolean onewayvia = Boolean.FALSE; 1106 1107 String onewayviastr = viaWay.get("oneway"); 1108 if (onewayviastr != null) { 1109 if ("-1".equals(onewayviastr)) { 1110 onewayvia = Boolean.TRUE; 1111 Node tmp = firstNode; 1112 firstNode = lastNode; 1113 lastNode = tmp; 1114 } else { 1115 onewayvia = OsmUtils.getOsmBoolean(onewayviastr); 1116 if (onewayvia == null) { 1117 onewayvia = Boolean.FALSE; 1118 } 1119 } 1120 } 1121 1122 if (fromWay.isFirstLastNode(firstNode)) { 1123 viaNode = firstNode; 1124 } else if (!onewayvia && fromWay.isFirstLastNode(lastNode)) { 1125 viaNode = lastNode; 1126 } else 1127 return; 1128 } 1129 1130 /* find the "direct" nodes before the via node */ 1131 Node fromNode; 1132 if (fromWay.firstNode() == via) { 1133 fromNode = fromWay.getNode(1); 1134 } else { 1135 fromNode = fromWay.getNode(fromWay.getNodesCount()-2); 1136 } 1137 1138 Point pFrom = nc.getPoint(fromNode); 1139 Point pVia = nc.getPoint(viaNode); 1140 1141 /* starting from via, go back the "from" way a few pixels 1142 (calculate the vector vx/vy with the specified length and the direction 1143 away from the "via" node along the first segment of the "from" way) 1144 */ 1145 double distanceFromVia = 14; 1146 double dx = pFrom.x >= pVia.x ? pFrom.x - pVia.x : pVia.x - pFrom.x; 1147 double dy = pFrom.y >= pVia.y ? pFrom.y - pVia.y : pVia.y - pFrom.y; 1148 1149 double fromAngle; 1150 if (dx == 0) { 1151 fromAngle = Math.PI/2; 1152 } else { 1153 fromAngle = Math.atan(dy / dx); 1154 } 1155 double fromAngleDeg = Math.toDegrees(fromAngle); 1156 1157 double vx = distanceFromVia * Math.cos(fromAngle); 1158 double vy = distanceFromVia * Math.sin(fromAngle); 1159 1160 if (pFrom.x < pVia.x) { 1161 vx = -vx; 1162 } 1163 if (pFrom.y < pVia.y) { 1164 vy = -vy; 1165 } 1166 1167 /* go a few pixels away from the way (in a right angle) 1168 (calculate the vx2/vy2 vector with the specified length and the direction 1169 90degrees away from the first segment of the "from" way) 1170 */ 1171 double distanceFromWay = 10; 1172 double vx2 = 0; 1173 double vy2 = 0; 1174 double iconAngle = 0; 1175 1176 if (pFrom.x >= pVia.x && pFrom.y >= pVia.y) { 1177 if (!leftHandTraffic) { 1178 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90)); 1179 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90)); 1180 } else { 1181 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90)); 1182 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90)); 1183 } 1184 iconAngle = 270+fromAngleDeg; 1185 } 1186 if (pFrom.x < pVia.x && pFrom.y >= pVia.y) { 1187 if (!leftHandTraffic) { 1188 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg)); 1189 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg)); 1190 } else { 1191 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180)); 1192 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180)); 1193 } 1194 iconAngle = 90-fromAngleDeg; 1195 } 1196 if (pFrom.x < pVia.x && pFrom.y < pVia.y) { 1197 if (!leftHandTraffic) { 1198 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90)); 1199 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90)); 1200 } else { 1201 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90)); 1202 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90)); 1203 } 1204 iconAngle = 90+fromAngleDeg; 1205 } 1206 if (pFrom.x >= pVia.x && pFrom.y < pVia.y) { 1207 if (!leftHandTraffic) { 1208 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180)); 1209 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180)); 1210 } else { 1211 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg)); 1212 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg)); 1213 } 1214 iconAngle = 270-fromAngleDeg; 1215 } 1216 1217 drawRestriction(icon.getImage(disabled), 1218 pVia, vx, vx2, vy, vy2, iconAngle, r.isSelected()); 1219 } 1220 1221 /** 1222 * A half segment that can be used to place text on it. Used in the drawTextOnPath algorithm. 1223 * @author Michael Zangl 1224 */ 1225 private static class HalfSegment { 1226 /** 1227 * start point of half segment (as length along the way) 1228 */ 1229 final double start; 1230 /** 1231 * end point of half segment (as length along the way) 1232 */ 1233 final double end; 1234 /** 1235 * quality factor (off screen / partly on screen / fully on screen) 1236 */ 1237 final double quality; 1238 1239 /** 1240 * Create a new half segment 1241 * @param start The start along the way 1242 * @param end The end of the segment 1243 * @param quality A quality factor. 1244 */ 1245 HalfSegment(double start, double end, double quality) { 1246 super(); 1247 this.start = start; 1248 this.end = end; 1249 this.quality = quality; 1250 } 1251 1252 @Override 1253 public String toString() { 1254 return "HalfSegment [start=" + start + ", end=" + end + ", quality=" + quality + "]"; 1255 } 1256 } 1257 1258 /** 1259 * Draws a text along a given way. 1260 * @param way The way to draw the text on. 1261 * @param text The text definition (font/.../text content) to draw. 1262 */ 1263 public void drawTextOnPath(Way way, TextLabel text) { 1264 if (way == null || text == null) 1265 return; 1266 String name = text.getString(way); 1267 if (name == null || name.isEmpty()) 1268 return; 1269 1270 FontMetrics fontMetrics = g.getFontMetrics(text.font); 1271 Rectangle2D rec = fontMetrics.getStringBounds(name, g); 1272 1273 Rectangle bounds = g.getClipBounds(); 1274 1275 List<MapViewPoint> points = way.getNodes().stream().map(mapState::getPointFor).collect(Collectors.toList()); 1276 1277 // 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) 1278 List<HalfSegment> longHalfSegment = new ArrayList<>(); 1279 1280 double pathLength = computePath(2 * (rec.getWidth() + 4), bounds, points, longHalfSegment); 1281 1282 if (rec.getWidth() > pathLength) 1283 return; 1284 1285 double t1, t2; 1286 1287 if (!longHalfSegment.isEmpty()) { 1288 // find the segment with the best quality. If there are several with best quality, the one close to the center is prefered. 1289 Optional<HalfSegment> besto = longHalfSegment.stream().max( 1290 Comparator.comparingDouble(segment -> 1291 segment.quality - 1e-5 * Math.abs(0.5 * (segment.end + segment.start) - 0.5 * pathLength) 1292 )); 1293 if (!besto.isPresent()) 1294 throw new IllegalStateException("Unable to find the segment with the best quality for " + way); 1295 HalfSegment best = besto.get(); 1296 double remaining = best.end - best.start - rec.getWidth(); // total space left and right from the text 1297 // The space left and right of the text should be distributed 20% - 80% (towards the center), 1298 // but the smaller space should not be less than 7 px. 1299 // However, if the total remaining space is less than 14 px, then distribute it evenly. 1300 double smallerSpace = Math.min(Math.max(0.2 * remaining, 7), 0.5 * remaining); 1301 if ((best.end + best.start)/2 < pathLength/2) { 1302 t2 = best.end - smallerSpace; 1303 t1 = t2 - rec.getWidth(); 1304 } else { 1305 t1 = best.start + smallerSpace; 1306 t2 = t1 + rec.getWidth(); 1307 } 1308 } else { 1309 // doesn't fit into one half-segment -> just put it in the center of the way 1310 t1 = pathLength/2 - rec.getWidth()/2; 1311 t2 = pathLength/2 + rec.getWidth()/2; 1312 } 1313 t1 /= pathLength; 1314 t2 /= pathLength; 1315 1316 double[] p1 = pointAt(t1, points, pathLength); 1317 double[] p2 = pointAt(t2, points, pathLength); 1318 1319 if (p1 == null || p2 == null) 1320 return; 1321 1322 double angleOffset; 1323 double offsetSign; 1324 double tStart; 1325 1326 if (p1[0] < p2[0] && 1327 p1[2] < Math.PI/2 && 1328 p1[2] > -Math.PI/2) { 1329 angleOffset = 0; 1330 offsetSign = 1; 1331 tStart = t1; 1332 } else { 1333 angleOffset = Math.PI; 1334 offsetSign = -1; 1335 tStart = t2; 1336 } 1337 1338 List<GlyphVector> gvs = Utils.getGlyphVectorsBidi(name, text.font, g.getFontRenderContext()); 1339 double gvOffset = 0; 1340 for (GlyphVector gv : gvs) { 1341 double gvWidth = gv.getLogicalBounds().getBounds2D().getWidth(); 1342 for (int i = 0; i < gv.getNumGlyphs(); ++i) { 1343 Rectangle2D rect = gv.getGlyphLogicalBounds(i).getBounds2D(); 1344 double t = tStart + offsetSign * (gvOffset + rect.getX() + rect.getWidth()/2) / pathLength; 1345 double[] p = pointAt(t, points, pathLength); 1346 if (p != null) { 1347 AffineTransform trfm = AffineTransform.getTranslateInstance(p[0] - rect.getX(), p[1]); 1348 trfm.rotate(p[2]+angleOffset); 1349 double off = -rect.getY() - rect.getHeight()/2 + text.yOffset; 1350 trfm.translate(-rect.getWidth()/2, off); 1351 if (isGlyphVectorDoubleTranslationBug(text.font)) { 1352 // scale the translation components by one half 1353 AffineTransform tmp = AffineTransform.getTranslateInstance(-0.5 * trfm.getTranslateX(), -0.5 * trfm.getTranslateY()); 1354 tmp.concatenate(trfm); 1355 trfm = tmp; 1356 } 1357 gv.setGlyphTransform(i, trfm); 1358 } 1359 } 1360 displayText(gv, null, 0, 0, way.isDisabled(), text); 1361 gvOffset += gvWidth; 1362 } 1363 } 1364 1365 private static double computePath(double minSegmentLength, Rectangle bounds, List<MapViewPoint> points, 1366 List<HalfSegment> longHalfSegment) { 1367 MapViewPoint lastPoint = points.get(0); 1368 double pathLength = 0; 1369 for (MapViewPoint p : points.subList(1, points.size())) { 1370 double segmentLength = p.distanceToInView(lastPoint); 1371 if (segmentLength > minSegmentLength) { 1372 Point2D center = new Point2D.Double((lastPoint.getInViewX() + p.getInViewX())/2, (lastPoint.getInViewY() + p.getInViewY())/2); 1373 double q = computeQuality(bounds, lastPoint, center); 1374 // prefer the first one for quality equality. 1375 longHalfSegment.add(new HalfSegment(pathLength, pathLength + segmentLength / 2, q)); 1376 1377 q = 0; 1378 if (bounds != null) { 1379 if (bounds.contains(center) && bounds.contains(p.getInView())) { 1380 q = 2; 1381 } else if (bounds.contains(center) || bounds.contains(p.getInView())) { 1382 q = 1; 1383 } 1384 } 1385 longHalfSegment.add(new HalfSegment(pathLength + segmentLength / 2, pathLength + segmentLength, q)); 1386 } 1387 pathLength += segmentLength; 1388 lastPoint = p; 1389 } 1390 return pathLength; 1391 } 1392 1393 private static double computeQuality(Rectangle bounds, MapViewPoint p1, Point2D p2) { 1394 double q = 0; 1395 if (bounds != null) { 1396 if (bounds.contains(p1.getInView())) { 1397 q += 1; 1398 } 1399 if (bounds.contains(p2)) { 1400 q += 1; 1401 } 1402 } 1403 return q; 1404 } 1405 1406 /** 1407 * draw way. This method allows for two draw styles (line using color, dashes using dashedColor) to be passed. 1408 * @param way The way to draw 1409 * @param color The base color to draw the way in 1410 * @param line The line style to use. This is drawn using color. 1411 * @param dashes The dash style to use. This is drawn using dashedColor. <code>null</code> if unused. 1412 * @param dashedColor The color of the dashes. 1413 * @param offset The offset 1414 * @param showOrientation show arrows that indicate the technical orientation of 1415 * the way (defined by order of nodes) 1416 * @param showHeadArrowOnly True if only the arrow at the end of the line but not those on the segments should be displayed. 1417 * @param showOneway show symbols that indicate the direction of the feature, 1418 * e.g. oneway street or waterway 1419 * @param onewayReversed for oneway=-1 and similar 1420 */ 1421 public void drawWay(Way way, Color color, BasicStroke line, BasicStroke dashes, Color dashedColor, float offset, 1422 boolean showOrientation, boolean showHeadArrowOnly, 1423 boolean showOneway, boolean onewayReversed) { 1424 1425 MapViewPath path = new MapViewPath(mapState); 1426 MapViewPath orientationArrows = showOrientation ? new MapViewPath(mapState) : null; 1427 MapViewPath onewayArrows; 1428 MapViewPath onewayArrowsCasing; 1429 Rectangle bounds = g.getClipBounds(); 1430 if (bounds != null) { 1431 // avoid arrow heads at the border 1432 bounds.grow(100, 100); 1433 } 1434 1435 List<Node> wayNodes = way.getNodes(); 1436 if (wayNodes.size() < 2) return; 1437 1438 // only highlight the segment if the way itself is not highlighted 1439 if (!way.isHighlighted() && highlightWaySegments != null) { 1440 MapViewPath highlightSegs = null; 1441 for (WaySegment ws : highlightWaySegments) { 1442 if (ws.way != way || ws.lowerIndex < offset) { 1443 continue; 1444 } 1445 if (highlightSegs == null) { 1446 highlightSegs = new MapViewPath(mapState); 1447 } 1448 1449 highlightSegs.moveTo(ws.getFirstNode()); 1450 highlightSegs.lineTo(ws.getSecondNode()); 1451 } 1452 1453 drawPathHighlight(highlightSegs, line); 1454 } 1455 1456 MapViewPoint lastPoint = null; 1457 Iterator<MapViewPoint> it = new OffsetIterator(wayNodes, offset); 1458 boolean initialMoveToNeeded = true; 1459 while (it.hasNext()) { 1460 MapViewPoint p = it.next(); 1461 if (lastPoint != null) { 1462 MapViewPoint p1 = lastPoint; 1463 MapViewPoint p2 = p; 1464 1465 if (initialMoveToNeeded) { 1466 initialMoveToNeeded = false; 1467 path.moveTo(p1); 1468 } 1469 path.lineTo(p2); 1470 1471 /* draw arrow */ 1472 if (showHeadArrowOnly ? !it.hasNext() : showOrientation) { 1473 //TODO: Cache 1474 ArrowPaintHelper drawHelper = new ArrowPaintHelper(PHI, 10 + line.getLineWidth()); 1475 drawHelper.paintArrowAt(orientationArrows, p2, p1); 1476 } 1477 } 1478 lastPoint = p; 1479 } 1480 if (showOneway) { 1481 onewayArrows = new MapViewPath(mapState); 1482 onewayArrowsCasing = new MapViewPath(mapState); 1483 double interval = 60; 1484 1485 path.visitClippedLine(0, 60, (inLineOffset, start, end, startIsOldEnd) -> { 1486 double segmentLength = start.distanceToInView(end); 1487 if (segmentLength > 0.001) { 1488 final double nx = (end.getInViewX() - start.getInViewX()) / segmentLength; 1489 final double ny = (end.getInViewY() - start.getInViewY()) / segmentLength; 1490 1491 // distance from p1 1492 double dist = interval - (inLineOffset % interval); 1493 1494 while (dist < segmentLength) { 1495 appenOnewayPath(onewayReversed, start, nx, ny, dist, 3d, onewayArrowsCasing); 1496 appenOnewayPath(onewayReversed, start, nx, ny, dist, 2d, onewayArrows); 1497 dist += interval; 1498 } 1499 } 1500 }); 1501 } else { 1502 onewayArrows = null; 1503 onewayArrowsCasing = null; 1504 } 1505 1506 if (way.isHighlighted()) { 1507 drawPathHighlight(path, line); 1508 } 1509 displaySegments(path, orientationArrows, onewayArrows, onewayArrowsCasing, color, line, dashes, dashedColor); 1510 } 1511 1512 private static void appenOnewayPath(boolean onewayReversed, MapViewPoint p1, double nx, double ny, double dist, 1513 double onewaySize, Path2D onewayPath) { 1514 // scale such that border is 1 px 1515 final double fac = -(onewayReversed ? -1 : 1) * onewaySize * (1 + sinPHI) / (sinPHI * cosPHI); 1516 final double sx = nx * fac; 1517 final double sy = ny * fac; 1518 1519 // Attach the triangle at the incenter and not at the tip. 1520 // Makes the border even at all sides. 1521 final double x = p1.getInViewX() + nx * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI)); 1522 final double y = p1.getInViewY() + ny * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI)); 1523 1524 onewayPath.moveTo(x, y); 1525 onewayPath.lineTo(x + cosPHI * sx - sinPHI * sy, y + sinPHI * sx + cosPHI * sy); 1526 onewayPath.lineTo(x + cosPHI * sx + sinPHI * sy, y - sinPHI * sx + cosPHI * sy); 1527 onewayPath.lineTo(x, y); 1528 } 1529 1530 /** 1531 * Gets the "circum". This is the distance on the map in meters that 100 screen pixels represent. 1532 * @return The "circum" 1533 */ 1534 public double getCircum() { 1535 return circum; 1536 } 1537 1538 @Override 1539 public void getColors() { 1540 super.getColors(); 1541 this.highlightColorTransparent = new Color(highlightColor.getRed(), highlightColor.getGreen(), highlightColor.getBlue(), 100); 1542 this.backgroundColor = PaintColors.getBackgroundColor(); 1543 } 1544 1545 @Override 1546 public void getSettings(boolean virtual) { 1547 super.getSettings(virtual); 1548 paintSettings = MapPaintSettings.INSTANCE; 1549 1550 circum = nc.getDist100Pixel(); 1551 scale = nc.getScale(); 1552 1553 leftHandTraffic = Main.pref.getBoolean("mappaint.lefthandtraffic", false); 1554 1555 useStrokes = paintSettings.getUseStrokesDistance() > circum; 1556 showNames = paintSettings.getShowNamesDistance() > circum; 1557 showIcons = paintSettings.getShowIconsDistance() > circum; 1558 isOutlineOnly = paintSettings.isOutlineOnly(); 1559 orderFont = new Font(Main.pref.get("mappaint.font", "Droid Sans"), Font.PLAIN, Main.pref.getInteger("mappaint.fontsize", 8)); 1560 1561 antialiasing = Main.pref.getBoolean("mappaint.use-antialiasing", true) ? 1562 RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF; 1563 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing); 1564 1565 Object textAntialiasing; 1566 switch (Main.pref.get("mappaint.text-antialiasing", "default")) { 1567 case "on": 1568 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_ON; 1569 break; 1570 case "off": 1571 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_OFF; 1572 break; 1573 case "gasp": 1574 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_GASP; 1575 break; 1576 case "lcd-hrgb": 1577 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB; 1578 break; 1579 case "lcd-hbgr": 1580 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HBGR; 1581 break; 1582 case "lcd-vrgb": 1583 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VRGB; 1584 break; 1585 case "lcd-vbgr": 1586 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VBGR; 1587 break; 1588 default: 1589 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT; 1590 } 1591 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, textAntialiasing); 1592 1593 highlightLineWidth = Main.pref.getInteger("mappaint.highlight.width", 4); 1594 highlightPointRadius = Main.pref.getInteger("mappaint.highlight.radius", 7); 1595 widerHighlight = Main.pref.getInteger("mappaint.highlight.bigger-increment", 5); 1596 highlightStep = Main.pref.getInteger("mappaint.highlight.step", 4); 1597 } 1598 1599 private static Path2D.Double getPath(Way w) { 1600 Path2D.Double path = new Path2D.Double(); 1601 boolean initial = true; 1602 for (Node n : w.getNodes()) { 1603 EastNorth p = n.getEastNorth(); 1604 if (p != null) { 1605 if (initial) { 1606 path.moveTo(p.getX(), p.getY()); 1607 initial = false; 1608 } else { 1609 path.lineTo(p.getX(), p.getY()); 1610 } 1611 } 1612 } 1613 if (w.isClosed()) { 1614 path.closePath(); 1615 } 1616 return path; 1617 } 1618 1619 private static Path2D.Double getPFClip(Way w, double extent) { 1620 Path2D.Double clip = new Path2D.Double(); 1621 buildPFClip(clip, w.getNodes(), extent); 1622 return clip; 1623 } 1624 1625 private static Path2D.Double getPFClip(PolyData pd, double extent) { 1626 Path2D.Double clip = new Path2D.Double(); 1627 clip.setWindingRule(Path2D.WIND_EVEN_ODD); 1628 buildPFClip(clip, pd.getNodes(), extent); 1629 for (PolyData pdInner : pd.getInners()) { 1630 buildPFClip(clip, pdInner.getNodes(), extent); 1631 } 1632 return clip; 1633 } 1634 1635 /** 1636 * Fix the clipping area of unclosed polygons for partial fill. 1637 * 1638 * The current algorithm for partial fill simply strokes the polygon with a 1639 * large stroke width after masking the outside with a clipping area. 1640 * This works, but for unclosed polygons, the mask can crop the corners at 1641 * both ends (see #12104). 1642 * 1643 * This method fixes the clipping area by sort of adding the corners to the 1644 * clip outline. 1645 * 1646 * @param clip the clipping area to modify (initially empty) 1647 * @param nodes nodes of the polygon 1648 * @param extent the extent 1649 */ 1650 private static void buildPFClip(Path2D.Double clip, List<Node> nodes, double extent) { 1651 boolean initial = true; 1652 for (Node n : nodes) { 1653 EastNorth p = n.getEastNorth(); 1654 if (p != null) { 1655 if (initial) { 1656 clip.moveTo(p.getX(), p.getY()); 1657 initial = false; 1658 } else { 1659 clip.lineTo(p.getX(), p.getY()); 1660 } 1661 } 1662 } 1663 if (nodes.size() >= 3) { 1664 EastNorth fst = nodes.get(0).getEastNorth(); 1665 EastNorth snd = nodes.get(1).getEastNorth(); 1666 EastNorth lst = nodes.get(nodes.size() - 1).getEastNorth(); 1667 EastNorth lbo = nodes.get(nodes.size() - 2).getEastNorth(); 1668 1669 EastNorth cLst = getPFDisplacedEndPoint(lbo, lst, fst, extent); 1670 EastNorth cFst = getPFDisplacedEndPoint(snd, fst, cLst != null ? cLst : lst, extent); 1671 if (cLst == null && cFst != null) { 1672 cLst = getPFDisplacedEndPoint(lbo, lst, cFst, extent); 1673 } 1674 if (cLst != null) { 1675 clip.lineTo(cLst.getX(), cLst.getY()); 1676 } 1677 if (cFst != null) { 1678 clip.lineTo(cFst.getX(), cFst.getY()); 1679 } 1680 } 1681 } 1682 1683 /** 1684 * Get the point to add to the clipping area for partial fill of unclosed polygons. 1685 * 1686 * <code>(p1,p2)</code> is the first or last way segment and <code>p3</code> the 1687 * opposite endpoint. 1688 * 1689 * @param p1 1st point 1690 * @param p2 2nd point 1691 * @param p3 3rd point 1692 * @param extent the extent 1693 * @return a point q, such that p1,p2,q form a right angle 1694 * and the distance of q to p2 is <code>extent</code>. The point q lies on 1695 * the same side of the line p1,p2 as the point p3. 1696 * Returns null if p1,p2,p3 forms an angle greater 90 degrees. (In this case 1697 * the corner of the partial fill would not be cut off by the mask, so an 1698 * additional point is not necessary.) 1699 */ 1700 private static EastNorth getPFDisplacedEndPoint(EastNorth p1, EastNorth p2, EastNorth p3, double extent) { 1701 double dx1 = p2.getX() - p1.getX(); 1702 double dy1 = p2.getY() - p1.getY(); 1703 double dx2 = p3.getX() - p2.getX(); 1704 double dy2 = p3.getY() - p2.getY(); 1705 if (dx1 * dx2 + dy1 * dy2 < 0) { 1706 double len = Math.sqrt(dx1 * dx1 + dy1 * dy1); 1707 if (len == 0) return null; 1708 double dxm = -dy1 * extent / len; 1709 double dym = dx1 * extent / len; 1710 if (dx1 * dy2 - dx2 * dy1 < 0) { 1711 dxm = -dxm; 1712 dym = -dym; 1713 } 1714 return new EastNorth(p2.getX() + dxm, p2.getY() + dym); 1715 } 1716 return null; 1717 } 1718 1719 /** 1720 * Test if the area is visible 1721 * @param area The area, interpreted in east/north space. 1722 * @return true if it is visible. 1723 */ 1724 private boolean isAreaVisible(Path2D.Double area) { 1725 Rectangle2D bounds = area.getBounds2D(); 1726 if (bounds.isEmpty()) return false; 1727 MapViewPoint p = mapState.getPointFor(new EastNorth(bounds.getX(), bounds.getY())); 1728 if (p.getInViewX() > mapState.getViewWidth()) return false; 1729 if (p.getInViewY() < 0) return false; 1730 p = mapState.getPointFor(new EastNorth(bounds.getX() + bounds.getWidth(), bounds.getY() + bounds.getHeight())); 1731 if (p.getInViewX() < 0) return false; 1732 if (p.getInViewY() > mapState.getViewHeight()) return false; 1733 return true; 1734 } 1735 1736 public boolean isInactiveMode() { 1737 return isInactiveMode; 1738 } 1739 1740 public boolean isShowIcons() { 1741 return showIcons; 1742 } 1743 1744 public boolean isShowNames() { 1745 return showNames; 1746 } 1747 1748 private static double[] pointAt(double t, List<MapViewPoint> poly, double pathLength) { 1749 double totalLen = t * pathLength; 1750 double curLen = 0; 1751 double dx, dy; 1752 double segLen; 1753 1754 // Yes, it is inefficient to iterate from the beginning for each glyph. 1755 // Can be optimized if it turns out to be slow. 1756 for (int i = 1; i < poly.size(); ++i) { 1757 dx = poly.get(i).getInViewX() - poly.get(i - 1).getInViewX(); 1758 dy = poly.get(i).getInViewY() - poly.get(i - 1).getInViewY(); 1759 segLen = Math.sqrt(dx*dx + dy*dy); 1760 if (totalLen > curLen + segLen) { 1761 curLen += segLen; 1762 continue; 1763 } 1764 return new double[] { 1765 poly.get(i - 1).getInViewX() + (totalLen - curLen) / segLen * dx, 1766 poly.get(i - 1).getInViewY() + (totalLen - curLen) / segLen * dy, 1767 Math.atan2(dy, dx)}; 1768 } 1769 return null; 1770 } 1771 1772 /** 1773 * Computes the flags for a given OSM primitive. 1774 * @param primitive The primititve to compute the flags for. 1775 * @param checkOuterMember <code>true</code> if we should also add {@link #FLAG_OUTERMEMBER_OF_SELECTED} 1776 * @return The flag. 1777 */ 1778 public static int computeFlags(OsmPrimitive primitive, boolean checkOuterMember) { 1779 if (primitive.isDisabled()) { 1780 return FLAG_DISABLED; 1781 } else if (primitive.isSelected()) { 1782 return FLAG_SELECTED; 1783 } else if (checkOuterMember && primitive.isOuterMemberOfSelected()) { 1784 return FLAG_OUTERMEMBER_OF_SELECTED; 1785 } else if (primitive.isMemberOfSelected()) { 1786 return FLAG_MEMBER_OF_SELECTED; 1787 } else { 1788 return FLAG_NORMAL; 1789 } 1790 } 1791 1792 private class ComputeStyleListWorker extends RecursiveTask<List<StyleRecord>> implements Visitor { 1793 private final transient List<? extends OsmPrimitive> input; 1794 private final transient List<StyleRecord> output; 1795 1796 private final transient ElemStyles styles = MapPaintStyles.getStyles(); 1797 private final int directExecutionTaskSize; 1798 1799 private final boolean drawArea = circum <= Main.pref.getInteger("mappaint.fillareas", 10_000_000); 1800 private final boolean drawMultipolygon = drawArea && Main.pref.getBoolean("mappaint.multipolygon", true); 1801 private final boolean drawRestriction = Main.pref.getBoolean("mappaint.restriction", true); 1802 1803 /** 1804 * Constructs a new {@code ComputeStyleListWorker}. 1805 * @param input the primitives to process 1806 * @param output the list of styles to which styles will be added 1807 * @param directExecutionTaskSize the threshold deciding whether to subdivide the tasks 1808 */ 1809 ComputeStyleListWorker(final List<? extends OsmPrimitive> input, List<StyleRecord> output, int directExecutionTaskSize) { 1810 this.input = input; 1811 this.output = output; 1812 this.directExecutionTaskSize = directExecutionTaskSize; 1813 this.styles.setDrawMultipolygon(drawMultipolygon); 1814 } 1815 1816 @Override 1817 protected List<StyleRecord> compute() { 1818 if (input.size() <= directExecutionTaskSize) { 1819 return computeDirectly(); 1820 } else { 1821 final Collection<ForkJoinTask<List<StyleRecord>>> tasks = new ArrayList<>(); 1822 for (int fromIndex = 0; fromIndex < input.size(); fromIndex += directExecutionTaskSize) { 1823 final int toIndex = Math.min(fromIndex + directExecutionTaskSize, input.size()); 1824 final List<StyleRecord> output = new ArrayList<>(directExecutionTaskSize); 1825 tasks.add(new ComputeStyleListWorker(input.subList(fromIndex, toIndex), output, directExecutionTaskSize).fork()); 1826 } 1827 for (ForkJoinTask<List<StyleRecord>> task : tasks) { 1828 output.addAll(task.join()); 1829 } 1830 return output; 1831 } 1832 } 1833 1834 public List<StyleRecord> computeDirectly() { 1835 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock(); 1836 try { 1837 for (final OsmPrimitive osm : input) { 1838 acceptDrawable(osm); 1839 } 1840 return output; 1841 } catch (RuntimeException e) { 1842 throw BugReport.intercept(e).put("input-size", input.size()).put("output-size", output.size()); 1843 } finally { 1844 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock(); 1845 } 1846 } 1847 1848 private void acceptDrawable(final OsmPrimitive osm) { 1849 try { 1850 if (osm.isDrawable()) { 1851 osm.accept(this); 1852 } 1853 } catch (RuntimeException e) { 1854 throw BugReport.intercept(e).put("osm", osm); 1855 } 1856 } 1857 1858 @Override 1859 public void visit(Node n) { 1860 add(n, computeFlags(n, false)); 1861 } 1862 1863 @Override 1864 public void visit(Way w) { 1865 add(w, computeFlags(w, true)); 1866 } 1867 1868 @Override 1869 public void visit(Relation r) { 1870 add(r, computeFlags(r, true)); 1871 } 1872 1873 @Override 1874 public void visit(Changeset cs) { 1875 throw new UnsupportedOperationException(); 1876 } 1877 1878 public void add(Node osm, int flags) { 1879 StyleElementList sl = styles.get(osm, circum, nc); 1880 for (StyleElement s : sl) { 1881 output.add(new StyleRecord(s, osm, flags)); 1882 } 1883 } 1884 1885 public void add(Relation osm, int flags) { 1886 StyleElementList sl = styles.get(osm, circum, nc); 1887 for (StyleElement s : sl) { 1888 if (drawMultipolygon && drawArea && s instanceof AreaElement && (flags & FLAG_DISABLED) == 0) { 1889 output.add(new StyleRecord(s, osm, flags)); 1890 } else if (drawRestriction && s instanceof NodeElement) { 1891 output.add(new StyleRecord(s, osm, flags)); 1892 } 1893 } 1894 } 1895 1896 public void add(Way osm, int flags) { 1897 StyleElementList sl = styles.get(osm, circum, nc); 1898 for (StyleElement s : sl) { 1899 if (!(drawArea && (flags & FLAG_DISABLED) == 0) && s instanceof AreaElement) { 1900 continue; 1901 } 1902 output.add(new StyleRecord(s, osm, flags)); 1903 } 1904 } 1905 } 1906 1907 /** 1908 * Sets the factory that creates the benchmark data receivers. 1909 * @param benchmarkFactory The factory. 1910 * @since 10697 1911 */ 1912 public void setBenchmarkFactory(Supplier<RenderBenchmarkCollector> benchmarkFactory) { 1913 this.benchmarkFactory = benchmarkFactory; 1914 } 1915 1916 @Override 1917 public void render(final DataSet data, boolean renderVirtualNodes, Bounds bounds) { 1918 RenderBenchmarkCollector benchmark = benchmarkFactory.get(); 1919 BBox bbox = bounds.toBBox(); 1920 getSettings(renderVirtualNodes); 1921 1922 data.getReadLock().lock(); 1923 try { 1924 highlightWaySegments = data.getHighlightedWaySegments(); 1925 1926 benchmark.renderStart(circum); 1927 1928 List<Node> nodes = data.searchNodes(bbox); 1929 List<Way> ways = data.searchWays(bbox); 1930 List<Relation> relations = data.searchRelations(bbox); 1931 1932 final List<StyleRecord> allStyleElems = new ArrayList<>(nodes.size()+ways.size()+relations.size()); 1933 1934 // Need to process all relations first. 1935 // Reason: Make sure, ElemStyles.getStyleCacheWithRange is 1936 // not called for the same primitive in parallel threads. 1937 // (Could be synchronized, but try to avoid this for 1938 // performance reasons.) 1939 THREAD_POOL.invoke(new ComputeStyleListWorker(relations, allStyleElems, 1940 Math.max(20, relations.size() / THREAD_POOL.getParallelism() / 3))); 1941 THREAD_POOL.invoke(new ComputeStyleListWorker(new CompositeList<>(nodes, ways), allStyleElems, 1942 Math.max(100, (nodes.size() + ways.size()) / THREAD_POOL.getParallelism() / 3))); 1943 1944 if (!benchmark.renderSort()) { 1945 return; 1946 } 1947 1948 Collections.sort(allStyleElems); // TODO: try parallel sort when switching to Java 8 1949 1950 if (!benchmark.renderDraw(allStyleElems)) { 1951 return; 1952 } 1953 1954 for (StyleRecord record : allStyleElems) { 1955 paintRecord(record); 1956 } 1957 1958 drawVirtualNodes(data, bbox); 1959 1960 benchmark.renderDone(); 1961 } catch (RuntimeException e) { 1962 throw BugReport.intercept(e) 1963 .put("data", data) 1964 .put("circum", circum) 1965 .put("scale", scale) 1966 .put("paintSettings", paintSettings) 1967 .put("renderVirtualNodes", renderVirtualNodes); 1968 } finally { 1969 data.getReadLock().unlock(); 1970 } 1971 } 1972 1973 private void paintRecord(StyleRecord record) { 1974 try { 1975 record.paintPrimitive(paintSettings, this); 1976 } catch (RuntimeException e) { 1977 throw BugReport.intercept(e).put("record", record); 1978 } 1979 } 1980}