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