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