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