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.geom.AffineTransform; 023import java.awt.geom.GeneralPath; 024import java.awt.geom.Path2D; 025import java.awt.geom.Point2D; 026import java.awt.geom.Rectangle2D; 027import java.util.ArrayList; 028import java.util.Collection; 029import java.util.Collections; 030import java.util.Iterator; 031import java.util.List; 032 033import javax.swing.AbstractButton; 034import javax.swing.FocusManager; 035 036import org.openstreetmap.josm.Main; 037import org.openstreetmap.josm.data.Bounds; 038import org.openstreetmap.josm.data.coor.EastNorth; 039import org.openstreetmap.josm.data.osm.BBox; 040import org.openstreetmap.josm.data.osm.DataSet; 041import org.openstreetmap.josm.data.osm.Node; 042import org.openstreetmap.josm.data.osm.OsmPrimitive; 043import org.openstreetmap.josm.data.osm.OsmUtils; 044import org.openstreetmap.josm.data.osm.Relation; 045import org.openstreetmap.josm.data.osm.RelationMember; 046import org.openstreetmap.josm.data.osm.Way; 047import org.openstreetmap.josm.data.osm.WaySegment; 048import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon; 049import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData; 050import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache; 051import org.openstreetmap.josm.gui.NavigatableComponent; 052import org.openstreetmap.josm.gui.mappaint.AreaElemStyle; 053import org.openstreetmap.josm.gui.mappaint.BoxTextElemStyle; 054import org.openstreetmap.josm.gui.mappaint.BoxTextElemStyle.HorizontalTextAlignment; 055import org.openstreetmap.josm.gui.mappaint.BoxTextElemStyle.VerticalTextAlignment; 056import org.openstreetmap.josm.gui.mappaint.ElemStyle; 057import org.openstreetmap.josm.gui.mappaint.ElemStyles; 058import org.openstreetmap.josm.gui.mappaint.MapImage; 059import org.openstreetmap.josm.gui.mappaint.MapPaintStyles; 060import org.openstreetmap.josm.gui.mappaint.NodeElemStyle; 061import org.openstreetmap.josm.gui.mappaint.NodeElemStyle.Symbol; 062import org.openstreetmap.josm.gui.mappaint.RepeatImageElemStyle.LineImageAlignment; 063import org.openstreetmap.josm.gui.mappaint.StyleCache.StyleList; 064import org.openstreetmap.josm.gui.mappaint.TextElement; 065import org.openstreetmap.josm.tools.ImageProvider; 066import org.openstreetmap.josm.tools.Utils; 067 068/** 069 * <p>A map renderer which renders a map according to style rules in a set of style sheets.</p> 070 * 071 */ 072public class StyledMapRenderer extends AbstractMapRenderer { 073 074 /** 075 * Iterates over a list of Way Nodes and returns screen coordinates that 076 * represent a line that is shifted by a certain offset perpendicular 077 * to the way direction. 078 * 079 * There is no intention, to handle consecutive duplicate Nodes in a 080 * perfect way, but it is should not throw an exception. 081 */ 082 private class OffsetIterator implements Iterator<Point> { 083 084 private List<Node> nodes; 085 private float offset; 086 private int idx; 087 088 private Point prev = null; 089 /* 'prev0' is a point that has distance 'offset' from 'prev' and the 090 * line from 'prev' to 'prev0' is perpendicular to the way segment from 091 * 'prev' to the next point. 092 */ 093 private int x_prev0, y_prev0; 094 095 public OffsetIterator(List<Node> nodes, float offset) { 096 this.nodes = nodes; 097 this.offset = offset; 098 idx = 0; 099 } 100 101 @Override 102 public boolean hasNext() { 103 return idx < nodes.size(); 104 } 105 106 @Override 107 public Point next() { 108 if (Math.abs(offset) < 0.1f) return nc.getPoint(nodes.get(idx++)); 109 110 Point current = nc.getPoint(nodes.get(idx)); 111 112 if (idx == nodes.size() - 1) { 113 ++idx; 114 return new Point(x_prev0 + current.x - prev.x, y_prev0 + current.y - prev.y); 115 } 116 117 Point next = nc.getPoint(nodes.get(idx+1)); 118 119 int dx_next = next.x - current.x; 120 int dy_next = next.y - current.y; 121 double len_next = Math.sqrt(dx_next*dx_next + dy_next*dy_next); 122 123 if (len_next == 0) { 124 len_next = 1; // value does not matter, because dy_next and dx_next is 0 125 } 126 127 int x_current0 = current.x + (int) Math.round(offset * dy_next / len_next); 128 int y_current0 = current.y - (int) Math.round(offset * dx_next / len_next); 129 130 if (idx==0) { 131 ++idx; 132 prev = current; 133 x_prev0 = x_current0; 134 y_prev0 = y_current0; 135 return new Point(x_current0, y_current0); 136 } else { 137 int dx_prev = current.x - prev.x; 138 int dy_prev = current.y - prev.y; 139 140 // determine intersection of the lines parallel to the two 141 // segments 142 int det = dx_next*dy_prev - dx_prev*dy_next; 143 144 if (det == 0) { 145 ++idx; 146 prev = current; 147 x_prev0 = x_current0; 148 y_prev0 = y_current0; 149 return new Point(x_current0, y_current0); 150 } 151 152 int m = dx_next*(y_current0 - y_prev0) - dy_next*(x_current0 - x_prev0); 153 154 int cx_ = x_prev0 + Math.round((float)m * dx_prev / det); 155 int cy_ = y_prev0 + Math.round((float)m * dy_prev / det); 156 ++idx; 157 prev = current; 158 x_prev0 = x_current0; 159 y_prev0 = y_current0; 160 return new Point(cx_, cy_); 161 } 162 } 163 164 @Override 165 public void remove() { 166 throw new UnsupportedOperationException(); 167 } 168 } 169 170 private class StyleCollector { 171 private final boolean drawArea; 172 private final boolean drawMultipolygon; 173 private final boolean drawRestriction; 174 175 private final List<StyleRecord> styleElems; 176 177 public StyleCollector(boolean drawArea, boolean drawMultipolygon, boolean drawRestriction) { 178 this.drawArea = drawArea; 179 this.drawMultipolygon = drawMultipolygon; 180 this.drawRestriction = drawRestriction; 181 styleElems = new ArrayList<StyleRecord>(); 182 } 183 184 public void add(Node osm, int flags) { 185 StyleList sl = styles.get(osm, circum, nc); 186 for (ElemStyle s : sl) { 187 styleElems.add(new StyleRecord(s, osm, flags)); 188 } 189 } 190 191 public void add(Relation osm, int flags) { 192 StyleList sl = styles.get(osm, circum, nc); 193 for (ElemStyle s : sl) { 194 if (drawMultipolygon && drawArea && s instanceof AreaElemStyle && (flags & FLAG_DISABLED) == 0) { 195 styleElems.add(new StyleRecord(s, osm, flags)); 196 } else if (drawRestriction && s instanceof NodeElemStyle) { 197 styleElems.add(new StyleRecord(s, osm, flags)); 198 } 199 } 200 } 201 202 public void add(Way osm, int flags) { 203 StyleList sl = styles.get(osm, circum, nc); 204 for (ElemStyle s : sl) { 205 if (!(drawArea && (flags & FLAG_DISABLED) == 0) && s instanceof AreaElemStyle) { 206 continue; 207 } 208 styleElems.add(new StyleRecord(s, osm, flags)); 209 } 210 } 211 212 public void drawAll() { 213 Collections.sort(styleElems); 214 for (StyleRecord r : styleElems) { 215 r.style.paintPrimitive( 216 r.osm, 217 paintSettings, 218 StyledMapRenderer.this, 219 (r.flags & FLAG_SELECTED) != 0, 220 (r.flags & FLAG_MEMBER_OF_SELECTED) != 0 221 ); 222 } 223 } 224 } 225 226 private static class StyleRecord implements Comparable<StyleRecord> { 227 final ElemStyle style; 228 final OsmPrimitive osm; 229 final int flags; 230 231 public StyleRecord(ElemStyle style, OsmPrimitive osm, int flags) { 232 this.style = style; 233 this.osm = osm; 234 this.flags = flags; 235 } 236 237 @Override 238 public int compareTo(StyleRecord other) { 239 if ((this.flags & FLAG_DISABLED) != 0 && (other.flags & FLAG_DISABLED) == 0) 240 return -1; 241 if ((this.flags & FLAG_DISABLED) == 0 && (other.flags & FLAG_DISABLED) != 0) 242 return 1; 243 244 int d0 = Float.compare(this.style.major_z_index, other.style.major_z_index); 245 if (d0 != 0) 246 return d0; 247 248 // selected on top of member of selected on top of unselected 249 // FLAG_DISABLED bit is the same at this point 250 if (this.flags > other.flags) 251 return 1; 252 if (this.flags < other.flags) 253 return -1; 254 255 int dz = Float.compare(this.style.z_index, other.style.z_index); 256 if (dz != 0) 257 return dz; 258 259 // simple node on top of icons and shapes 260 if (this.style == NodeElemStyle.SIMPLE_NODE_ELEMSTYLE && other.style != NodeElemStyle.SIMPLE_NODE_ELEMSTYLE) 261 return 1; 262 if (this.style != NodeElemStyle.SIMPLE_NODE_ELEMSTYLE && other.style == NodeElemStyle.SIMPLE_NODE_ELEMSTYLE) 263 return -1; 264 265 // newer primitives to the front 266 long id = this.osm.getUniqueId() - other.osm.getUniqueId(); 267 if (id > 0) 268 return 1; 269 if (id < 0) 270 return -1; 271 272 return Float.compare(this.style.object_z_index, other.style.object_z_index); 273 } 274 } 275 276 private static Boolean IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG = null; 277 278 /** 279 * Check, if this System has the GlyphVector double translation bug. 280 * 281 * With this bug, <code>gv.setGlyphTransform(i, trfm)</code> has a different 282 * effect than on most other systems, namely the translation components 283 * ("m02" & "m12", {@link AffineTransform}) appear to be twice as large, as 284 * they actually are. The rotation is unaffected (scale & shear not tested 285 * so far). 286 * 287 * This bug has only been observed on Mac OS X, see #7841. 288 * 289 * @return true, if the GlyphVector double translation bug is present on 290 * this System 291 */ 292 public static boolean isGlyphVectorDoubleTranslationBug() { 293 if (IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG != null) 294 return IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG; 295 FontRenderContext frc = new FontRenderContext(null, false, false); 296 Font font = new Font("Dialog", Font.PLAIN, 12); 297 GlyphVector gv = font.createGlyphVector(frc, "x"); 298 gv.setGlyphTransform(0, AffineTransform.getTranslateInstance(1000, 1000)); 299 Shape shape = gv.getGlyphOutline(0); 300 // x is about 1000 on normal stystems and about 2000 when the bug occurs 301 int x = shape.getBounds().x; 302 IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG = x > 1500; 303 return IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG; 304 } 305 306 private ElemStyles styles; 307 private double circum; 308 309 private MapPaintSettings paintSettings; 310 311 private Color relationSelectedColor; 312 private Color highlightColorTransparent; 313 314 private static final int FLAG_NORMAL = 0; 315 private static final int FLAG_DISABLED = 1; 316 private static final int FLAG_MEMBER_OF_SELECTED = 2; 317 private static final int FLAG_SELECTED = 4; 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 343 public StyledMapRenderer(Graphics2D g, NavigatableComponent nc, boolean isInactiveMode) { 344 super(g, nc, isInactiveMode); 345 346 if (nc!=null) { 347 Component focusOwner = FocusManager.getCurrentManager().getFocusOwner(); 348 useWiderHighlight = !(focusOwner instanceof AbstractButton || focusOwner == nc); 349 } 350 } 351 352 private Polygon buildPolygon(Point center, int radius, int sides) { 353 return buildPolygon(center, radius, sides, 0.0); 354 } 355 356 private Polygon buildPolygon(Point center, int radius, int sides, double rotation) { 357 Polygon polygon = new Polygon(); 358 for (int i = 0; i < sides; i++) { 359 double angle = ((2 * Math.PI / sides) * i) - rotation; 360 int x = (int) Math.round(center.x + radius * Math.cos(angle)); 361 int y = (int) Math.round(center.y + radius * Math.sin(angle)); 362 polygon.addPoint(x, y); 363 } 364 return polygon; 365 } 366 367 private void collectNodeStyles(DataSet data, StyleCollector sc, BBox bbox) { 368 for (final Node n: data.searchNodes(bbox)) { 369 if (n.isDrawable()) { 370 if (n.isDisabled()) { 371 sc.add(n, FLAG_DISABLED); 372 } else if (data.isSelected(n)) { 373 sc.add(n, FLAG_SELECTED); 374 } else if (n.isMemberOfSelected()) { 375 sc.add(n, FLAG_MEMBER_OF_SELECTED); 376 } else { 377 sc.add(n, FLAG_NORMAL); 378 } 379 } 380 } 381 } 382 383 private void collectWayStyles(DataSet data, StyleCollector sc, BBox bbox) { 384 for (final Way w : data.searchWays(bbox)) { 385 if (w.isDrawable()) { 386 if (w.isDisabled()) { 387 sc.add(w, FLAG_DISABLED); 388 } else if (data.isSelected(w)) { 389 sc.add(w, FLAG_SELECTED); 390 } else if (w.isMemberOfSelected()) { 391 sc.add(w, FLAG_MEMBER_OF_SELECTED); 392 } else { 393 sc.add(w, FLAG_NORMAL); 394 } 395 } 396 } 397 } 398 399 private void collectRelationStyles(DataSet data, StyleCollector sc, BBox bbox) { 400 for (Relation r: data.searchRelations(bbox)) { 401 if (r.isDrawable()) { 402 if (r.isDisabled()) { 403 sc.add(r, FLAG_DISABLED); 404 } else if (data.isSelected(r)) { 405 sc.add(r, FLAG_SELECTED); 406 } else { 407 sc.add(r, FLAG_NORMAL); 408 } 409 } 410 } 411 } 412 413 private void displaySegments(GeneralPath path, GeneralPath orientationArrows, GeneralPath onewayArrows, GeneralPath onewayArrowsCasing, 414 Color color, BasicStroke line, BasicStroke dashes, Color dashedColor) { 415 g.setColor(isInactiveMode ? inactiveColor : color); 416 if (useStrokes) { 417 g.setStroke(line); 418 } 419 g.draw(path); 420 421 if(!isInactiveMode && useStrokes && dashes != null) { 422 g.setColor(dashedColor); 423 g.setStroke(dashes); 424 g.draw(path); 425 } 426 427 if (orientationArrows != null) { 428 g.setColor(isInactiveMode ? inactiveColor : color); 429 g.setStroke(new BasicStroke(line.getLineWidth(), line.getEndCap(), BasicStroke.JOIN_MITER, line.getMiterLimit())); 430 g.draw(orientationArrows); 431 } 432 433 if (onewayArrows != null) { 434 g.setStroke(new BasicStroke(1, line.getEndCap(), BasicStroke.JOIN_MITER, line.getMiterLimit())); 435 g.fill(onewayArrowsCasing); 436 g.setColor(isInactiveMode ? inactiveColor : backgroundColor); 437 g.fill(onewayArrows); 438 } 439 440 if (useStrokes) { 441 g.setStroke(new BasicStroke()); 442 } 443 } 444 445 /** 446 * Displays text at specified position including its halo, if applicable. 447 * 448 * @param gv Text's glyphs to display. If {@code null}, use text from {@code s} instead. 449 * @param s text to display if {@code gv} is {@code null} 450 * @param x X position 451 * @param y Y position 452 * @param disabled {@code true} if element is disabled (filtered out) 453 * @param text text style to use 454 */ 455 private void displayText(GlyphVector gv, String s, int x, int y, boolean disabled, TextElement text) { 456 if (isInactiveMode || disabled) { 457 g.setColor(inactiveColor); 458 if (gv != null) { 459 g.drawGlyphVector(gv, x, y); 460 } else { 461 g.setFont(text.font); 462 g.drawString(s, x, y); 463 } 464 } else if (text.haloRadius != null) { 465 g.setStroke(new BasicStroke(2*text.haloRadius, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND)); 466 g.setColor(text.haloColor); 467 if (gv == null) { 468 FontRenderContext frc = g.getFontRenderContext(); 469 gv = text.font.createGlyphVector(frc, s); 470 } 471 Shape textOutline = gv.getOutline(x, y); 472 g.draw(textOutline); 473 g.setStroke(new BasicStroke()); 474 g.setColor(text.color); 475 g.fill(textOutline); 476 } else { 477 g.setColor(text.color); 478 if (gv != null) { 479 g.drawGlyphVector(gv, x, y); 480 } else { 481 g.setFont(text.font); 482 g.drawString(s, x, y); 483 } 484 } 485 } 486 487 protected void drawArea(OsmPrimitive osm, Path2D.Double path, Color color, MapImage fillImage, TextElement text) { 488 489 Shape area = path.createTransformedShape(nc.getAffineTransform()); 490 491 if (!isOutlineOnly) { 492 if (fillImage == null) { 493 if (isInactiveMode) { 494 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.33f)); 495 } 496 g.setColor(color); 497 g.fill(area); 498 } else { 499 TexturePaint texture = new TexturePaint(fillImage.getImage(), 500 new Rectangle(0, 0, fillImage.getWidth(), fillImage.getHeight())); 501 g.setPaint(texture); 502 Float alpha = Utils.color_int2float(fillImage.alpha); 503 if (alpha != 1f) { 504 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)); 505 } 506 g.fill(area); 507 g.setPaintMode(); 508 } 509 } 510 511 if (text != null && isShowNames()) { 512 // abort if we can't compose the label to be rendered 513 if (text.labelCompositionStrategy == null) return; 514 String name = text.labelCompositionStrategy.compose(osm); 515 if (name == null) return; 516 517 Rectangle pb = area.getBounds(); 518 FontMetrics fontMetrics = g.getFontMetrics(orderFont); // if slow, use cache 519 Rectangle2D nb = fontMetrics.getStringBounds(name, g); // if slow, approximate by strlen()*maxcharbounds(font) 520 521 // Using the Centroid is Nicer for buildings like: +--------+ 522 // but this needs to be fast. As most houses are | 42 | 523 // boxes anyway, the center of the bounding box +---++---+ 524 // will have to do. ++ 525 // Centroids are not optimal either, just imagine a U-shaped house. 526 527 Rectangle centeredNBounds = new Rectangle(pb.x + (int)((pb.width - nb.getWidth())/2.0), 528 pb.y + (int)((pb.height - nb.getHeight())/2.0), 529 (int)nb.getWidth(), 530 (int)nb.getHeight()); 531 532 if ((pb.width >= nb.getWidth() && pb.height >= nb.getHeight()) && // quick check 533 area.contains(centeredNBounds) // slow but nice 534 ) { 535 Font defaultFont = g.getFont(); 536 int x = (int)(centeredNBounds.getMinX() - nb.getMinX()); 537 int y = (int)(centeredNBounds.getMinY() - nb.getMinY()); 538 displayText(null, name, x, y, osm.isDisabled(), text); 539 g.setFont(defaultFont); 540 } 541 } 542 } 543 544 public void drawArea(Relation r, Color color, MapImage fillImage, TextElement text) { 545 Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, r); 546 if (!r.isDisabled() && !multipolygon.getOuterWays().isEmpty()) { 547 for (PolyData pd : multipolygon.getCombinedPolygons()) { 548 Path2D.Double p = pd.get(); 549 if (!isAreaVisible(p)) { 550 continue; 551 } 552 drawArea(r, p, 553 pd.selected ? paintSettings.getRelationSelectedColor(color.getAlpha()) : color, 554 fillImage, text); 555 } 556 } 557 } 558 559 public void drawArea(Way w, Color color, MapImage fillImage, TextElement text) { 560 drawArea(w, getPath(w), color, fillImage, text); 561 } 562 563 public void drawBoxText(Node n, BoxTextElemStyle bs) { 564 if (!isShowNames() || bs == null) 565 return; 566 567 Point p = nc.getPoint(n); 568 TextElement text = bs.text; 569 String s = text.labelCompositionStrategy.compose(n); 570 if (s == null) return; 571 572 Font defaultFont = g.getFont(); 573 g.setFont(text.font); 574 575 int x = p.x + text.xOffset; 576 int y = p.y + text.yOffset; 577 /** 578 * 579 * left-above __center-above___ right-above 580 * left-top| |right-top 581 * | | 582 * left-center| center-center |right-center 583 * | | 584 * left-bottom|_________________|right-bottom 585 * left-below center-below right-below 586 * 587 */ 588 Rectangle box = bs.getBox(); 589 if (bs.hAlign == HorizontalTextAlignment.RIGHT) { 590 x += box.x + box.width + 2; 591 } else { 592 FontRenderContext frc = g.getFontRenderContext(); 593 Rectangle2D bounds = text.font.getStringBounds(s, frc); 594 int textWidth = (int) bounds.getWidth(); 595 if (bs.hAlign == HorizontalTextAlignment.CENTER) { 596 x -= textWidth / 2; 597 } else if (bs.hAlign == HorizontalTextAlignment.LEFT) { 598 x -= - box.x + 4 + textWidth; 599 } else throw new AssertionError(); 600 } 601 602 if (bs.vAlign == VerticalTextAlignment.BOTTOM) { 603 y += box.y + box.height; 604 } else { 605 FontRenderContext frc = g.getFontRenderContext(); 606 LineMetrics metrics = text.font.getLineMetrics(s, frc); 607 if (bs.vAlign == VerticalTextAlignment.ABOVE) { 608 y -= - box.y + metrics.getDescent(); 609 } else if (bs.vAlign == VerticalTextAlignment.TOP) { 610 y -= - box.y - metrics.getAscent(); 611 } else if (bs.vAlign == VerticalTextAlignment.CENTER) { 612 y += (metrics.getAscent() - metrics.getDescent()) / 2; 613 } else if (bs.vAlign == VerticalTextAlignment.BELOW) { 614 y += box.y + box.height + metrics.getAscent() + 2; 615 } else throw new AssertionError(); 616 } 617 displayText(null, s, x, y, n.isDisabled(), text); 618 g.setFont(defaultFont); 619 } 620 621 @Deprecated 622 public void drawLinePattern(Way way, Image pattern) { 623 drawRepeatImage(way, pattern, 0f, 0f, 0f, LineImageAlignment.TOP); 624 } 625 626 /** 627 * Draw an image along a way repeatedly. 628 * 629 * @param way the way 630 * @param pattern the image 631 * @param offset offset from the way 632 * @param spacing spacing between two images 633 * @param phase initial spacing 634 * @param align alignment of the image. The top, center or bottom edge 635 * can be aligned with the way. 636 */ 637 public void drawRepeatImage(Way way, Image pattern, float offset, float spacing, float phase, LineImageAlignment align) { 638 final int imgWidth = pattern.getWidth(null); 639 final double repeat = imgWidth + spacing; 640 final int imgHeight = pattern.getHeight(null); 641 642 Point lastP = null; 643 double currentWayLength = phase % repeat; 644 if (currentWayLength < 0) { 645 currentWayLength += repeat; 646 } 647 648 int dy1, dy2; 649 switch (align) { 650 case TOP: 651 dy1 = 0; 652 dy2 = imgHeight; 653 break; 654 case CENTER: 655 dy1 = - imgHeight / 2; 656 dy2 = imgHeight + dy1; 657 break; 658 case BOTTOM: 659 dy1 = -imgHeight; 660 dy2 = 0; 661 break; 662 default: 663 throw new AssertionError(); 664 } 665 666 OffsetIterator it = new OffsetIterator(way.getNodes(), offset); 667 while (it.hasNext()) { 668 Point thisP = it.next(); 669 670 if (lastP != null) { 671 final double segmentLength = thisP.distance(lastP); 672 673 final double dx = thisP.x - lastP.x; 674 final double dy = thisP.y - lastP.y; 675 676 // pos is the position from the beginning of the current segment 677 // where an image should be painted 678 double pos = repeat - (currentWayLength % repeat); 679 680 AffineTransform saveTransform = g.getTransform(); 681 g.translate(lastP.x, lastP.y); 682 g.rotate(Math.atan2(dy, dx)); 683 684 // draw the rest of the image from the last segment in case it 685 // is cut off 686 if (pos > spacing) { 687 // segment is too short for a complete image 688 if (pos > segmentLength + spacing) { 689 g.drawImage(pattern, 0, dy1, (int) segmentLength, dy2, 690 (int) (repeat - pos), 0, 691 (int) (repeat - pos + segmentLength), imgHeight, null); 692 // rest of the image fits fully on the current segment 693 } else { 694 g.drawImage(pattern, 0, dy1, (int) (pos - spacing), dy2, 695 (int) (repeat - pos), 0, imgWidth, imgHeight, null); 696 } 697 } 698 // draw remaining images for this segment 699 while (pos < segmentLength) { 700 // cut off at the end? 701 if (pos + imgWidth > segmentLength) { 702 g.drawImage(pattern, (int) pos, dy1, (int) segmentLength, dy2, 703 0, 0, (int) segmentLength - (int) pos, imgHeight, null); 704 } else { 705 g.drawImage(pattern, (int) pos, dy1, nc); 706 } 707 pos += repeat; 708 } 709 g.setTransform(saveTransform); 710 711 currentWayLength += segmentLength; 712 } 713 lastP = thisP; 714 } 715 } 716 717 @Override 718 public void drawNode(Node n, Color color, int size, boolean fill) { 719 if(size <= 0 && !n.isHighlighted()) 720 return; 721 722 Point p = nc.getPoint(n); 723 724 if(n.isHighlighted()) { 725 drawPointHighlight(p, size); 726 } 727 728 if (size > 1) { 729 if ((p.x < 0) || (p.y < 0) || (p.x > nc.getWidth()) || (p.y > nc.getHeight())) return; 730 int radius = size / 2; 731 732 if (isInactiveMode || n.isDisabled()) { 733 g.setColor(inactiveColor); 734 } else { 735 g.setColor(color); 736 } 737 if (fill) { 738 g.fillRect(p.x-radius-1, p.y-radius-1, size + 1, size + 1); 739 } else { 740 g.drawRect(p.x-radius-1, p.y-radius-1, size, size); 741 } 742 } 743 } 744 745 public void drawNodeIcon(Node n, Image img, float alpha, boolean selected, boolean member) { 746 Point p = nc.getPoint(n); 747 748 final int w = img.getWidth(null), h=img.getHeight(null); 749 if(n.isHighlighted()) { 750 drawPointHighlight(p, Math.max(w, h)); 751 } 752 753 if (alpha != 1f) { 754 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)); 755 } 756 g.drawImage(img, p.x-w/2, p.y-h/2, nc); 757 g.setPaintMode(); 758 if (selected || member) 759 { 760 Color color; 761 if (isInactiveMode || n.isDisabled()) { 762 color = inactiveColor; 763 } else if (selected) { 764 color = selectedColor; 765 } else { 766 color = relationSelectedColor; 767 } 768 g.setColor(color); 769 g.drawRect(p.x-w/2-2, p.y-h/2-2, w+4, h+4); 770 } 771 } 772 773 public void drawNodeSymbol(Node n, Symbol s, Color fillColor, Color strokeColor) { 774 Point p = nc.getPoint(n); 775 int radius = s.size / 2; 776 777 if(n.isHighlighted()) { 778 drawPointHighlight(p, s.size); 779 } 780 781 if (fillColor != null) { 782 g.setColor(fillColor); 783 switch (s.symbol) { 784 case SQUARE: 785 g.fillRect(p.x - radius, p.y - radius, s.size, s.size); 786 break; 787 case CIRCLE: 788 g.fillOval(p.x - radius, p.y - radius, s.size, s.size); 789 break; 790 case TRIANGLE: 791 g.fillPolygon(buildPolygon(p, radius, 3, Math.PI / 2)); 792 break; 793 case PENTAGON: 794 g.fillPolygon(buildPolygon(p, radius, 5, Math.PI / 2)); 795 break; 796 case HEXAGON: 797 g.fillPolygon(buildPolygon(p, radius, 6)); 798 break; 799 case HEPTAGON: 800 g.fillPolygon(buildPolygon(p, radius, 7, Math.PI / 2)); 801 break; 802 case OCTAGON: 803 g.fillPolygon(buildPolygon(p, radius, 8, Math.PI / 8)); 804 break; 805 case NONAGON: 806 g.fillPolygon(buildPolygon(p, radius, 9, Math.PI / 2)); 807 break; 808 case DECAGON: 809 g.fillPolygon(buildPolygon(p, radius, 10)); 810 break; 811 default: 812 throw new AssertionError(); 813 } 814 } 815 if (s.stroke != null) { 816 g.setStroke(s.stroke); 817 g.setColor(strokeColor); 818 switch (s.symbol) { 819 case SQUARE: 820 g.drawRect(p.x - radius, p.y - radius, s.size - 1, s.size - 1); 821 break; 822 case CIRCLE: 823 g.drawOval(p.x - radius, p.y - radius, s.size - 1, s.size - 1); 824 break; 825 case TRIANGLE: 826 g.drawPolygon(buildPolygon(p, radius, 3, Math.PI / 2)); 827 break; 828 case PENTAGON: 829 g.drawPolygon(buildPolygon(p, radius, 5, Math.PI / 2)); 830 break; 831 case HEXAGON: 832 g.drawPolygon(buildPolygon(p, radius, 6)); 833 break; 834 case HEPTAGON: 835 g.drawPolygon(buildPolygon(p, radius, 7, Math.PI / 2)); 836 break; 837 case OCTAGON: 838 g.drawPolygon(buildPolygon(p, radius, 8, Math.PI / 8)); 839 break; 840 case NONAGON: 841 g.drawPolygon(buildPolygon(p, radius, 9, Math.PI / 2)); 842 break; 843 case DECAGON: 844 g.drawPolygon(buildPolygon(p, radius, 10)); 845 break; 846 default: 847 throw new AssertionError(); 848 } 849 g.setStroke(new BasicStroke()); 850 } 851 } 852 853 /** 854 * Draw a number of the order of the two consecutive nodes within the 855 * parents way 856 */ 857 public void drawOrderNumber(Node n1, Node n2, int orderNumber, Color clr) { 858 Point p1 = nc.getPoint(n1); 859 Point p2 = nc.getPoint(n2); 860 StyledMapRenderer.this.drawOrderNumber(p1, p2, orderNumber, clr); 861 } 862 863 /** 864 * highlights a given GeneralPath using the settings from BasicStroke to match the line's 865 * style. Width of the highlight is hard coded. 866 * @param path 867 * @param line 868 */ 869 private void drawPathHighlight(GeneralPath path, BasicStroke line) { 870 if(path == null) 871 return; 872 g.setColor(highlightColorTransparent); 873 float w = (line.getLineWidth() + highlightLineWidth); 874 if (useWiderHighlight) w+=widerHighlight; 875 while(w >= line.getLineWidth()) { 876 g.setStroke(new BasicStroke(w, line.getEndCap(), line.getLineJoin(), line.getMiterLimit())); 877 g.draw(path); 878 w -= highlightStep; 879 } 880 } 881 /** 882 * highlights a given point by drawing a rounded rectangle around it. Give the 883 * size of the object you want to be highlighted, width is added automatically. 884 */ 885 private void drawPointHighlight(Point p, int size) { 886 g.setColor(highlightColorTransparent); 887 int s = size + highlightPointRadius; 888 if (useWiderHighlight) s+=widerHighlight; 889 while(s >= size) { 890 int r = (int) Math.floor(s/2); 891 g.fillRoundRect(p.x-r, p.y-r, s, s, r, r); 892 s -= highlightStep; 893 } 894 } 895 896 public void drawRestriction(Image img, Point pVia, double vx, double vx2, double vy, double vy2, double angle, boolean selected) { 897 // rotate image with direction last node in from to, and scale down image to 16*16 pixels 898 Image smallImg = ImageProvider.createRotatedImage(img, angle, new Dimension(16, 16)); 899 int w = smallImg.getWidth(null), h=smallImg.getHeight(null); 900 g.drawImage(smallImg, (int)(pVia.x+vx+vx2)-w/2, (int)(pVia.y+vy+vy2)-h/2, nc); 901 902 if (selected) { 903 g.setColor(isInactiveMode ? inactiveColor : relationSelectedColor); 904 g.drawRect((int)(pVia.x+vx+vx2)-w/2-2,(int)(pVia.y+vy+vy2)-h/2-2, w+4, h+4); 905 } 906 } 907 908 public void drawRestriction(Relation r, MapImage icon) { 909 Way fromWay = null; 910 Way toWay = null; 911 OsmPrimitive via = null; 912 913 /* find the "from", "via" and "to" elements */ 914 for (RelationMember m : r.getMembers()) 915 { 916 if(m.getMember().isIncomplete()) 917 return; 918 else 919 { 920 if(m.isWay()) 921 { 922 Way w = m.getWay(); 923 if(w.getNodesCount() < 2) { 924 continue; 925 } 926 927 if("from".equals(m.getRole())) { 928 if(fromWay == null) { 929 fromWay = w; 930 } 931 } else if("to".equals(m.getRole())) { 932 if(toWay == null) { 933 toWay = w; 934 } 935 } else if("via".equals(m.getRole())) { 936 if(via == null) { 937 via = w; 938 } 939 } 940 } 941 else if(m.isNode()) 942 { 943 Node n = m.getNode(); 944 if("via".equals(m.getRole()) && via == null) { 945 via = n; 946 } 947 } 948 } 949 } 950 951 if (fromWay == null || toWay == null || via == null) 952 return; 953 954 Node viaNode; 955 if(via instanceof Node) 956 { 957 viaNode = (Node) via; 958 if(!fromWay.isFirstLastNode(viaNode)) 959 return; 960 } 961 else 962 { 963 Way viaWay = (Way) via; 964 Node firstNode = viaWay.firstNode(); 965 Node lastNode = viaWay.lastNode(); 966 Boolean onewayvia = false; 967 968 String onewayviastr = viaWay.get("oneway"); 969 if(onewayviastr != null) 970 { 971 if("-1".equals(onewayviastr)) { 972 onewayvia = true; 973 Node tmp = firstNode; 974 firstNode = lastNode; 975 lastNode = tmp; 976 } else { 977 onewayvia = OsmUtils.getOsmBoolean(onewayviastr); 978 if (onewayvia == null) { 979 onewayvia = false; 980 } 981 } 982 } 983 984 if(fromWay.isFirstLastNode(firstNode)) { 985 viaNode = firstNode; 986 } else if (!onewayvia && fromWay.isFirstLastNode(lastNode)) { 987 viaNode = lastNode; 988 } else 989 return; 990 } 991 992 /* find the "direct" nodes before the via node */ 993 Node fromNode; 994 if(fromWay.firstNode() == via) { 995 fromNode = fromWay.getNode(1); 996 } else { 997 fromNode = fromWay.getNode(fromWay.getNodesCount()-2); 998 } 999 1000 Point pFrom = nc.getPoint(fromNode); 1001 Point pVia = nc.getPoint(viaNode); 1002 1003 /* starting from via, go back the "from" way a few pixels 1004 (calculate the vector vx/vy with the specified length and the direction 1005 away from the "via" node along the first segment of the "from" way) 1006 */ 1007 double distanceFromVia=14; 1008 double dx = (pFrom.x >= pVia.x) ? (pFrom.x - pVia.x) : (pVia.x - pFrom.x); 1009 double dy = (pFrom.y >= pVia.y) ? (pFrom.y - pVia.y) : (pVia.y - pFrom.y); 1010 1011 double fromAngle; 1012 if(dx == 0.0) { 1013 fromAngle = Math.PI/2; 1014 } else { 1015 fromAngle = Math.atan(dy / dx); 1016 } 1017 double fromAngleDeg = Math.toDegrees(fromAngle); 1018 1019 double vx = distanceFromVia * Math.cos(fromAngle); 1020 double vy = distanceFromVia * Math.sin(fromAngle); 1021 1022 if(pFrom.x < pVia.x) { 1023 vx = -vx; 1024 } 1025 if(pFrom.y < pVia.y) { 1026 vy = -vy; 1027 } 1028 1029 /* go a few pixels away from the way (in a right angle) 1030 (calculate the vx2/vy2 vector with the specified length and the direction 1031 90degrees away from the first segment of the "from" way) 1032 */ 1033 double distanceFromWay=10; 1034 double vx2 = 0; 1035 double vy2 = 0; 1036 double iconAngle = 0; 1037 1038 if(pFrom.x >= pVia.x && pFrom.y >= pVia.y) { 1039 if(!leftHandTraffic) { 1040 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90)); 1041 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90)); 1042 } else { 1043 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90)); 1044 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90)); 1045 } 1046 iconAngle = 270+fromAngleDeg; 1047 } 1048 if(pFrom.x < pVia.x && pFrom.y >= pVia.y) { 1049 if(!leftHandTraffic) { 1050 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg)); 1051 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg)); 1052 } else { 1053 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180)); 1054 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180)); 1055 } 1056 iconAngle = 90-fromAngleDeg; 1057 } 1058 if(pFrom.x < pVia.x && pFrom.y < pVia.y) { 1059 if(!leftHandTraffic) { 1060 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90)); 1061 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90)); 1062 } else { 1063 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90)); 1064 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90)); 1065 } 1066 iconAngle = 90+fromAngleDeg; 1067 } 1068 if(pFrom.x >= pVia.x && pFrom.y < pVia.y) { 1069 if(!leftHandTraffic) { 1070 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180)); 1071 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180)); 1072 } else { 1073 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg)); 1074 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg)); 1075 } 1076 iconAngle = 270-fromAngleDeg; 1077 } 1078 1079 drawRestriction(isInactiveMode || r.isDisabled() ? icon.getDisabled() : icon.getImage(), 1080 pVia, vx, vx2, vy, vy2, iconAngle, r.isSelected()); 1081 } 1082 1083 public void drawTextOnPath(Way way, TextElement text) { 1084 if (way == null || text == null) 1085 return; 1086 String name = text.getString(way); 1087 if (name == null || name.isEmpty()) 1088 return; 1089 1090 Polygon poly = new Polygon(); 1091 Point lastPoint = null; 1092 Iterator<Node> it = way.getNodes().iterator(); 1093 double pathLength = 0; 1094 long dx, dy; 1095 while (it.hasNext()) { 1096 Node n = it.next(); 1097 Point p = nc.getPoint(n); 1098 poly.addPoint(p.x, p.y); 1099 1100 if(lastPoint != null) { 1101 dx = p.x - lastPoint.x; 1102 dy = p.y - lastPoint.y; 1103 pathLength += Math.sqrt(dx*dx + dy*dy); 1104 } 1105 lastPoint = p; 1106 } 1107 1108 FontMetrics fontMetrics = g.getFontMetrics(text.font); // if slow, use cache 1109 Rectangle2D rec = fontMetrics.getStringBounds(name, g); // if slow, approximate by strlen()*maxcharbounds(font) 1110 1111 if (rec.getWidth() > pathLength) 1112 return; 1113 1114 double t1 = (pathLength/2 - rec.getWidth()/2) / pathLength; 1115 double t2 = (pathLength/2 + rec.getWidth()/2) / pathLength; 1116 1117 double[] p1 = pointAt(t1, poly, pathLength); 1118 double[] p2 = pointAt(t2, poly, pathLength); 1119 1120 if (p1 == null || p2 == null) 1121 return; 1122 1123 double angleOffset; 1124 double offsetSign; 1125 double tStart; 1126 1127 if (p1[0] < p2[0] && 1128 p1[2] < Math.PI/2 && 1129 p1[2] > -Math.PI/2) { 1130 angleOffset = 0; 1131 offsetSign = 1; 1132 tStart = t1; 1133 } else { 1134 angleOffset = Math.PI; 1135 offsetSign = -1; 1136 tStart = t2; 1137 } 1138 1139 FontRenderContext frc = g.getFontRenderContext(); 1140 GlyphVector gv = text.font.createGlyphVector(frc, name); 1141 1142 for (int i=0; i<gv.getNumGlyphs(); ++i) { 1143 Rectangle2D rect = gv.getGlyphLogicalBounds(i).getBounds2D(); 1144 double t = tStart + offsetSign * (rect.getX() + rect.getWidth()/2) / pathLength; 1145 double[] p = pointAt(t, poly, pathLength); 1146 if (p != null) { 1147 AffineTransform trfm = AffineTransform.getTranslateInstance(p[0] - rect.getX(), p[1]); 1148 trfm.rotate(p[2]+angleOffset); 1149 double off = -rect.getY() - rect.getHeight()/2 + text.yOffset; 1150 trfm.translate(-rect.getWidth()/2, off); 1151 if (isGlyphVectorDoubleTranslationBug()) { 1152 // scale the translation components by one half 1153 AffineTransform tmp = AffineTransform.getTranslateInstance(-0.5 * trfm.getTranslateX(), -0.5 * trfm.getTranslateY()); 1154 tmp.concatenate(trfm); 1155 trfm = tmp; 1156 } 1157 gv.setGlyphTransform(i, trfm); 1158 } 1159 } 1160 displayText(gv, null, 0, 0, way.isDisabled(), text); 1161 } 1162 1163 /** 1164 * draw way 1165 * @param showOrientation show arrows that indicate the technical orientation of 1166 * the way (defined by order of nodes) 1167 * @param showOneway show symbols that indicate the direction of the feature, 1168 * e.g. oneway street or waterway 1169 * @param onewayReversed for oneway=-1 and similar 1170 */ 1171 public void drawWay(Way way, Color color, BasicStroke line, BasicStroke dashes, Color dashedColor, float offset, 1172 boolean showOrientation, boolean showHeadArrowOnly, 1173 boolean showOneway, boolean onewayReversed) { 1174 1175 GeneralPath path = new GeneralPath(); 1176 GeneralPath orientationArrows = showOrientation ? new GeneralPath() : null; 1177 GeneralPath onewayArrows = showOneway ? new GeneralPath() : null; 1178 GeneralPath onewayArrowsCasing = showOneway ? new GeneralPath() : null; 1179 Rectangle bounds = g.getClipBounds(); 1180 bounds.grow(100, 100); // avoid arrow heads at the border 1181 1182 double wayLength = 0; 1183 Point lastPoint = null; 1184 boolean initialMoveToNeeded = true; 1185 List<Node> wayNodes = way.getNodes(); 1186 if (wayNodes.size() < 2) return; 1187 1188 // only highlight the segment if the way itself is not highlighted 1189 if (!way.isHighlighted()) { 1190 GeneralPath highlightSegs = null; 1191 for (WaySegment ws : highlightWaySegments) { 1192 if (ws.way != way || ws.lowerIndex < offset) { 1193 continue; 1194 } 1195 if(highlightSegs == null) { 1196 highlightSegs = new GeneralPath(); 1197 } 1198 1199 Point p1 = nc.getPoint(ws.getFirstNode()); 1200 Point p2 = nc.getPoint(ws.getSecondNode()); 1201 highlightSegs.moveTo(p1.x, p1.y); 1202 highlightSegs.lineTo(p2.x, p2.y); 1203 } 1204 1205 drawPathHighlight(highlightSegs, line); 1206 } 1207 1208 Iterator<Point> it = new OffsetIterator(wayNodes, offset); 1209 while (it.hasNext()) { 1210 Point p = it.next(); 1211 if (lastPoint != null) { 1212 Point p1 = lastPoint; 1213 Point p2 = p; 1214 1215 /** 1216 * Do custom clipping to work around openjdk bug. It leads to 1217 * drawing artefacts when zooming in a lot. (#4289, #4424) 1218 * (Looks like int overflow.) 1219 */ 1220 LineClip clip = new LineClip(p1, p2, bounds); 1221 if (clip.execute()) { 1222 if (!p1.equals(clip.getP1())) { 1223 p1 = clip.getP1(); 1224 path.moveTo(p1.x, p1.y); 1225 } else if (initialMoveToNeeded) { 1226 initialMoveToNeeded = false; 1227 path.moveTo(p1.x, p1.y); 1228 } 1229 p2 = clip.getP2(); 1230 path.lineTo(p2.x, p2.y); 1231 1232 /* draw arrow */ 1233 if (showHeadArrowOnly ? !it.hasNext() : showOrientation) { 1234 final double segmentLength = p1.distance(p2); 1235 if (segmentLength != 0.0) { 1236 final double l = (10. + line.getLineWidth()) / segmentLength; 1237 1238 final double sx = l * (p1.x - p2.x); 1239 final double sy = l * (p1.y - p2.y); 1240 1241 orientationArrows.moveTo (p2.x + cosPHI * sx - sinPHI * sy, p2.y + sinPHI * sx + cosPHI * sy); 1242 orientationArrows.lineTo(p2.x, p2.y); 1243 orientationArrows.lineTo (p2.x + cosPHI * sx + sinPHI * sy, p2.y - sinPHI * sx + cosPHI * sy); 1244 } 1245 } 1246 if (showOneway) { 1247 final double segmentLength = p1.distance(p2); 1248 if (segmentLength != 0.0) { 1249 final double nx = (p2.x - p1.x) / segmentLength; 1250 final double ny = (p2.y - p1.y) / segmentLength; 1251 1252 final double interval = 60; 1253 // distance from p1 1254 double dist = interval - (wayLength % interval); 1255 1256 while (dist < segmentLength) { 1257 for (int i=0; i<2; ++i) { 1258 float onewaySize = i == 0 ? 3f : 2f; 1259 GeneralPath onewayPath = i == 0 ? onewayArrowsCasing : onewayArrows; 1260 1261 // scale such that border is 1 px 1262 final double fac = - (onewayReversed ? -1 : 1) * onewaySize * (1 + sinPHI) / (sinPHI * cosPHI); 1263 final double sx = nx * fac; 1264 final double sy = ny * fac; 1265 1266 // Attach the triangle at the incenter and not at the tip. 1267 // Makes the border even at all sides. 1268 final double x = p1.x + nx * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI)); 1269 final double y = p1.y + ny * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI)); 1270 1271 onewayPath.moveTo(x, y); 1272 onewayPath.lineTo (x + cosPHI * sx - sinPHI * sy, y + sinPHI * sx + cosPHI * sy); 1273 onewayPath.lineTo (x + cosPHI * sx + sinPHI * sy, y - sinPHI * sx + cosPHI * sy); 1274 onewayPath.lineTo(x, y); 1275 } 1276 dist += interval; 1277 } 1278 } 1279 wayLength += segmentLength; 1280 } 1281 } 1282 } 1283 lastPoint = p; 1284 } 1285 if(way.isHighlighted()) { 1286 drawPathHighlight(path, line); 1287 } 1288 displaySegments(path, orientationArrows, onewayArrows, onewayArrowsCasing, color, line, dashes, dashedColor); 1289 } 1290 1291 public double getCircum() { 1292 return circum; 1293 } 1294 1295 @Override 1296 public void getColors() { 1297 super.getColors(); 1298 this.relationSelectedColor = PaintColors.RELATIONSELECTED.get(); 1299 this.highlightColorTransparent = new Color(highlightColor.getRed(), highlightColor.getGreen(), highlightColor.getBlue(), 100); 1300 this.backgroundColor = PaintColors.getBackgroundColor(); 1301 } 1302 1303 @Override 1304 protected void getSettings(boolean virtual) { 1305 super.getSettings(virtual); 1306 paintSettings = MapPaintSettings.INSTANCE; 1307 1308 circum = nc.getDist100Pixel(); 1309 1310 leftHandTraffic = Main.pref.getBoolean("mappaint.lefthandtraffic", false); 1311 1312 useStrokes = paintSettings.getUseStrokesDistance() > circum; 1313 showNames = paintSettings.getShowNamesDistance() > circum; 1314 showIcons = paintSettings.getShowIconsDistance() > circum; 1315 isOutlineOnly = paintSettings.isOutlineOnly(); 1316 orderFont = new Font(Main.pref.get("mappaint.font", "Helvetica"), Font.PLAIN, Main.pref.getInteger("mappaint.fontsize", 8)); 1317 1318 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 1319 Main.pref.getBoolean("mappaint.use-antialiasing", true) ? 1320 RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF); 1321 1322 highlightLineWidth = Main.pref.getInteger("mappaint.highlight.width", 4); 1323 highlightPointRadius = Main.pref.getInteger("mappaint.highlight.radius", 7); 1324 widerHighlight = Main.pref.getInteger("mappaint.highlight.bigger-increment", 5); 1325 highlightStep = Main.pref.getInteger("mappaint.highlight.step", 4); 1326 } 1327 1328 private Path2D.Double getPath(Way w) { 1329 Path2D.Double path = new Path2D.Double(); 1330 boolean initial = true; 1331 for (Node n : w.getNodes()) { 1332 EastNorth p = n.getEastNorth(); 1333 if (p != null) { 1334 if (initial) { 1335 path.moveTo(p.getX(), p.getY()); 1336 initial = false; 1337 } else { 1338 path.lineTo(p.getX(), p.getY()); 1339 } 1340 } 1341 } 1342 return path; 1343 } 1344 1345 private boolean isAreaVisible(Path2D.Double area) { 1346 Rectangle2D bounds = area.getBounds2D(); 1347 if (bounds.isEmpty()) return false; 1348 Point2D p = nc.getPoint2D(new EastNorth(bounds.getX(), bounds.getY())); 1349 if (p.getX() > nc.getWidth()) return false; 1350 if (p.getY() < 0) return false; 1351 p = nc.getPoint2D(new EastNorth(bounds.getX() + bounds.getWidth(), bounds.getY() + bounds.getHeight())); 1352 if (p.getX() < 0) return false; 1353 if (p.getY() > nc.getHeight()) return false; 1354 return true; 1355 } 1356 1357 public boolean isInactiveMode() { 1358 return isInactiveMode; 1359 } 1360 1361 public boolean isShowIcons() { 1362 return showIcons; 1363 } 1364 1365 public boolean isShowNames() { 1366 return showNames; 1367 } 1368 1369 private double[] pointAt(double t, Polygon poly, double pathLength) { 1370 double totalLen = t * pathLength; 1371 double curLen = 0; 1372 long dx, dy; 1373 double segLen; 1374 1375 // Yes, it is inefficient to iterate from the beginning for each glyph. 1376 // Can be optimized if it turns out to be slow. 1377 for (int i = 1; i < poly.npoints; ++i) { 1378 dx = poly.xpoints[i] - poly.xpoints[i-1]; 1379 dy = poly.ypoints[i] - poly.ypoints[i-1]; 1380 segLen = Math.sqrt(dx*dx + dy*dy); 1381 if (totalLen > curLen + segLen) { 1382 curLen += segLen; 1383 continue; 1384 } 1385 return new double[] { 1386 poly.xpoints[i-1]+(totalLen - curLen)/segLen*dx, 1387 poly.ypoints[i-1]+(totalLen - curLen)/segLen*dy, 1388 Math.atan2(dy, dx)}; 1389 } 1390 return null; 1391 } 1392 1393 @Override 1394 public void render(final DataSet data, boolean renderVirtualNodes, Bounds bounds) { 1395 BBox bbox = bounds.toBBox(); 1396 getSettings(renderVirtualNodes); 1397 1398 boolean drawArea = circum <= Main.pref.getInteger("mappaint.fillareas", 10000000); 1399 boolean drawMultipolygon = drawArea && Main.pref.getBoolean("mappaint.multipolygon", true); 1400 boolean drawRestriction = Main.pref.getBoolean("mappaint.restriction", true); 1401 1402 styles = MapPaintStyles.getStyles(); 1403 styles.setDrawMultipolygon(drawMultipolygon); 1404 1405 highlightWaySegments = data.getHighlightedWaySegments(); 1406 1407 StyleCollector sc = new StyleCollector(drawArea, drawMultipolygon, drawRestriction); 1408 collectNodeStyles(data, sc, bbox); 1409 collectWayStyles(data, sc, bbox); 1410 collectRelationStyles(data, sc, bbox); 1411 sc.drawAll(); 1412 sc = null; 1413 drawVirtualNodes(data, bbox); 1414 } 1415}