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