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