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