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, boolean disabled, 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(disabled), 443 new Rectangle(0, 0, fillImage.getWidth(), fillImage.getHeight())); 444 g.setPaint(texture); 445 Float alpha = fillImage.getAlphaFloat(); 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, boolean disabled, 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, disabled, text); 542 } 543 } 544 } 545 546 public void drawArea(Way w, Color color, MapImage fillImage, boolean disabled, TextElement text) { 547 drawArea(w, getPath(w), color, fillImage, disabled, 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, MapImage pattern, boolean disabled, float offset, float spacing, float phase, LineImageAlignment align) { 620 final int imgWidth = pattern.getWidth(); 621 final double repeat = imgWidth + spacing; 622 final int imgHeight = pattern.getHeight(); 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.getImage(disabled), 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.getImage(disabled), 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.getImage(disabled), (int) pos, dy1, (int) segmentLength, dy2, 685 0, 0, (int) segmentLength - (int) pos, imgHeight, null); 686 } else { 687 g.drawImage(pattern.getImage(disabled), (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, MapImage img, boolean disabled, boolean selected, boolean member) { 728 Point p = nc.getPoint(n); 729 730 final int w = img.getWidth(), h = img.getHeight(); 731 if(n.isHighlighted()) { 732 drawPointHighlight(p, Math.max(w, h)); 733 } 734 735 float alpha = img.getAlphaFloat(); 736 737 if (alpha != 1f) { 738 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)); 739 } 740 g.drawImage(img.getImage(disabled), p.x - w/2 + img.offsetX, p.y - h/2 + img.offsetY, nc); 741 g.setPaintMode(); 742 if (selected || member) 743 { 744 Color color; 745 if (disabled) { 746 color = inactiveColor; 747 } else if (selected) { 748 color = selectedColor; 749 } else { 750 color = relationSelectedColor; 751 } 752 g.setColor(color); 753 g.drawRect(p.x - w/2 + img.offsetX - 2, p.y - h/2 + img.offsetY - 2, w + 4, h + 4); 754 } 755 } 756 757 public void drawNodeSymbol(Node n, Symbol s, Color fillColor, Color strokeColor) { 758 Point p = nc.getPoint(n); 759 int radius = s.size / 2; 760 761 if(n.isHighlighted()) { 762 drawPointHighlight(p, s.size); 763 } 764 765 if (fillColor != null) { 766 g.setColor(fillColor); 767 switch (s.symbol) { 768 case SQUARE: 769 g.fillRect(p.x - radius, p.y - radius, s.size, s.size); 770 break; 771 case CIRCLE: 772 g.fillOval(p.x - radius, p.y - radius, s.size, s.size); 773 break; 774 case TRIANGLE: 775 g.fillPolygon(buildPolygon(p, radius, 3, Math.PI / 2)); 776 break; 777 case PENTAGON: 778 g.fillPolygon(buildPolygon(p, radius, 5, Math.PI / 2)); 779 break; 780 case HEXAGON: 781 g.fillPolygon(buildPolygon(p, radius, 6)); 782 break; 783 case HEPTAGON: 784 g.fillPolygon(buildPolygon(p, radius, 7, Math.PI / 2)); 785 break; 786 case OCTAGON: 787 g.fillPolygon(buildPolygon(p, radius, 8, Math.PI / 8)); 788 break; 789 case NONAGON: 790 g.fillPolygon(buildPolygon(p, radius, 9, Math.PI / 2)); 791 break; 792 case DECAGON: 793 g.fillPolygon(buildPolygon(p, radius, 10)); 794 break; 795 default: 796 throw new AssertionError(); 797 } 798 } 799 if (s.stroke != null) { 800 g.setStroke(s.stroke); 801 g.setColor(strokeColor); 802 switch (s.symbol) { 803 case SQUARE: 804 g.drawRect(p.x - radius, p.y - radius, s.size - 1, s.size - 1); 805 break; 806 case CIRCLE: 807 g.drawOval(p.x - radius, p.y - radius, s.size - 1, s.size - 1); 808 break; 809 case TRIANGLE: 810 g.drawPolygon(buildPolygon(p, radius, 3, Math.PI / 2)); 811 break; 812 case PENTAGON: 813 g.drawPolygon(buildPolygon(p, radius, 5, Math.PI / 2)); 814 break; 815 case HEXAGON: 816 g.drawPolygon(buildPolygon(p, radius, 6)); 817 break; 818 case HEPTAGON: 819 g.drawPolygon(buildPolygon(p, radius, 7, Math.PI / 2)); 820 break; 821 case OCTAGON: 822 g.drawPolygon(buildPolygon(p, radius, 8, Math.PI / 8)); 823 break; 824 case NONAGON: 825 g.drawPolygon(buildPolygon(p, radius, 9, Math.PI / 2)); 826 break; 827 case DECAGON: 828 g.drawPolygon(buildPolygon(p, radius, 10)); 829 break; 830 default: 831 throw new AssertionError(); 832 } 833 g.setStroke(new BasicStroke()); 834 } 835 } 836 837 /** 838 * Draw a number of the order of the two consecutive nodes within the 839 * parents way 840 */ 841 public void drawOrderNumber(Node n1, Node n2, int orderNumber, Color clr) { 842 Point p1 = nc.getPoint(n1); 843 Point p2 = nc.getPoint(n2); 844 StyledMapRenderer.this.drawOrderNumber(p1, p2, orderNumber, clr); 845 } 846 847 /** 848 * highlights a given GeneralPath using the settings from BasicStroke to match the line's 849 * style. Width of the highlight is hard coded. 850 * @param path 851 * @param line 852 */ 853 private void drawPathHighlight(GeneralPath path, BasicStroke line) { 854 if(path == null) 855 return; 856 g.setColor(highlightColorTransparent); 857 float w = (line.getLineWidth() + highlightLineWidth); 858 if (useWiderHighlight) w+=widerHighlight; 859 while(w >= line.getLineWidth()) { 860 g.setStroke(new BasicStroke(w, line.getEndCap(), line.getLineJoin(), line.getMiterLimit())); 861 g.draw(path); 862 w -= highlightStep; 863 } 864 } 865 /** 866 * highlights a given point by drawing a rounded rectangle around it. Give the 867 * size of the object you want to be highlighted, width is added automatically. 868 */ 869 private void drawPointHighlight(Point p, int size) { 870 g.setColor(highlightColorTransparent); 871 int s = size + highlightPointRadius; 872 if (useWiderHighlight) s+=widerHighlight; 873 while(s >= size) { 874 int r = (int) Math.floor(s/2); 875 g.fillRoundRect(p.x-r, p.y-r, s, s, r, r); 876 s -= highlightStep; 877 } 878 } 879 880 public void drawRestriction(Image img, Point pVia, double vx, double vx2, double vy, double vy2, double angle, boolean selected) { 881 // rotate image with direction last node in from to, and scale down image to 16*16 pixels 882 Image smallImg = ImageProvider.createRotatedImage(img, angle, new Dimension(16, 16)); 883 int w = smallImg.getWidth(null), h=smallImg.getHeight(null); 884 g.drawImage(smallImg, (int)(pVia.x+vx+vx2)-w/2, (int)(pVia.y+vy+vy2)-h/2, nc); 885 886 if (selected) { 887 g.setColor(isInactiveMode ? inactiveColor : relationSelectedColor); 888 g.drawRect((int)(pVia.x+vx+vx2)-w/2-2,(int)(pVia.y+vy+vy2)-h/2-2, w+4, h+4); 889 } 890 } 891 892 public void drawRestriction(Relation r, MapImage icon, boolean disabled) { 893 Way fromWay = null; 894 Way toWay = null; 895 OsmPrimitive via = null; 896 897 /* find the "from", "via" and "to" elements */ 898 for (RelationMember m : r.getMembers()) { 899 if(m.getMember().isIncomplete()) 900 return; 901 else { 902 if(m.isWay()) { 903 Way w = m.getWay(); 904 if(w.getNodesCount() < 2) { 905 continue; 906 } 907 908 switch(m.getRole()) { 909 case "from": 910 if(fromWay == null) { 911 fromWay = w; 912 } 913 break; 914 case "to": 915 if(toWay == null) { 916 toWay = w; 917 } 918 break; 919 case "via": 920 if(via == null) { 921 via = w; 922 } 923 } 924 } else if(m.isNode()) { 925 Node n = m.getNode(); 926 if("via".equals(m.getRole()) && via == null) { 927 via = n; 928 } 929 } 930 } 931 } 932 933 if (fromWay == null || toWay == null || via == null) 934 return; 935 936 Node viaNode; 937 if(via instanceof Node) 938 { 939 viaNode = (Node) via; 940 if(!fromWay.isFirstLastNode(viaNode)) 941 return; 942 } 943 else 944 { 945 Way viaWay = (Way) via; 946 Node firstNode = viaWay.firstNode(); 947 Node lastNode = viaWay.lastNode(); 948 Boolean onewayvia = false; 949 950 String onewayviastr = viaWay.get("oneway"); 951 if(onewayviastr != null) 952 { 953 if("-1".equals(onewayviastr)) { 954 onewayvia = true; 955 Node tmp = firstNode; 956 firstNode = lastNode; 957 lastNode = tmp; 958 } else { 959 onewayvia = OsmUtils.getOsmBoolean(onewayviastr); 960 if (onewayvia == null) { 961 onewayvia = false; 962 } 963 } 964 } 965 966 if(fromWay.isFirstLastNode(firstNode)) { 967 viaNode = firstNode; 968 } else if (!onewayvia && fromWay.isFirstLastNode(lastNode)) { 969 viaNode = lastNode; 970 } else 971 return; 972 } 973 974 /* find the "direct" nodes before the via node */ 975 Node fromNode; 976 if(fromWay.firstNode() == via) { 977 fromNode = fromWay.getNode(1); 978 } else { 979 fromNode = fromWay.getNode(fromWay.getNodesCount()-2); 980 } 981 982 Point pFrom = nc.getPoint(fromNode); 983 Point pVia = nc.getPoint(viaNode); 984 985 /* starting from via, go back the "from" way a few pixels 986 (calculate the vector vx/vy with the specified length and the direction 987 away from the "via" node along the first segment of the "from" way) 988 */ 989 double distanceFromVia=14; 990 double dx = (pFrom.x >= pVia.x) ? (pFrom.x - pVia.x) : (pVia.x - pFrom.x); 991 double dy = (pFrom.y >= pVia.y) ? (pFrom.y - pVia.y) : (pVia.y - pFrom.y); 992 993 double fromAngle; 994 if(dx == 0.0) { 995 fromAngle = Math.PI/2; 996 } else { 997 fromAngle = Math.atan(dy / dx); 998 } 999 double fromAngleDeg = Math.toDegrees(fromAngle); 1000 1001 double vx = distanceFromVia * Math.cos(fromAngle); 1002 double vy = distanceFromVia * Math.sin(fromAngle); 1003 1004 if(pFrom.x < pVia.x) { 1005 vx = -vx; 1006 } 1007 if(pFrom.y < pVia.y) { 1008 vy = -vy; 1009 } 1010 1011 /* go a few pixels away from the way (in a right angle) 1012 (calculate the vx2/vy2 vector with the specified length and the direction 1013 90degrees away from the first segment of the "from" way) 1014 */ 1015 double distanceFromWay=10; 1016 double vx2 = 0; 1017 double vy2 = 0; 1018 double iconAngle = 0; 1019 1020 if(pFrom.x >= pVia.x && pFrom.y >= pVia.y) { 1021 if(!leftHandTraffic) { 1022 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90)); 1023 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90)); 1024 } else { 1025 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90)); 1026 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90)); 1027 } 1028 iconAngle = 270+fromAngleDeg; 1029 } 1030 if(pFrom.x < pVia.x && pFrom.y >= pVia.y) { 1031 if(!leftHandTraffic) { 1032 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg)); 1033 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg)); 1034 } else { 1035 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180)); 1036 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180)); 1037 } 1038 iconAngle = 90-fromAngleDeg; 1039 } 1040 if(pFrom.x < pVia.x && pFrom.y < pVia.y) { 1041 if(!leftHandTraffic) { 1042 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90)); 1043 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90)); 1044 } else { 1045 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90)); 1046 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90)); 1047 } 1048 iconAngle = 90+fromAngleDeg; 1049 } 1050 if(pFrom.x >= pVia.x && pFrom.y < pVia.y) { 1051 if(!leftHandTraffic) { 1052 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180)); 1053 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180)); 1054 } else { 1055 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg)); 1056 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg)); 1057 } 1058 iconAngle = 270-fromAngleDeg; 1059 } 1060 1061 drawRestriction(icon.getImage(disabled), 1062 pVia, vx, vx2, vy, vy2, iconAngle, r.isSelected()); 1063 } 1064 1065 public void drawTextOnPath(Way way, TextElement text) { 1066 if (way == null || text == null) 1067 return; 1068 String name = text.getString(way); 1069 if (name == null || name.isEmpty()) 1070 return; 1071 1072 FontMetrics fontMetrics = g.getFontMetrics(text.font); 1073 Rectangle2D rec = fontMetrics.getStringBounds(name, g); 1074 1075 Rectangle bounds = g.getClipBounds(); 1076 1077 Polygon poly = new Polygon(); 1078 Point lastPoint = null; 1079 Iterator<Node> it = way.getNodes().iterator(); 1080 double pathLength = 0; 1081 long dx, dy; 1082 1083 // find half segments that are long enough to draw text on 1084 // (don't draw text over the cross hair in the center of each segment) 1085 List<Double> longHalfSegmentStart = new ArrayList<>(); // start point of half segment (as length along the way) 1086 List<Double> longHalfSegmentEnd = new ArrayList<>(); // end point of half segment (as length along the way) 1087 List<Double> longHalfsegmentQuality = new ArrayList<>(); // quality factor (off screen / partly on screen / fully on screen) 1088 1089 while (it.hasNext()) { 1090 Node n = it.next(); 1091 Point p = nc.getPoint(n); 1092 poly.addPoint(p.x, p.y); 1093 1094 if(lastPoint != null) { 1095 dx = p.x - lastPoint.x; 1096 dy = p.y - lastPoint.y; 1097 double segmentLength = Math.sqrt(dx*dx + dy*dy); 1098 if (segmentLength > 2*(rec.getWidth()+4)) { 1099 Point center = new Point((lastPoint.x + p.x)/2, (lastPoint.y + p.y)/2); 1100 double q = 0; 1101 if (bounds != null) { 1102 if (bounds.contains(lastPoint) && bounds.contains(center)) { 1103 q = 2; 1104 } else if (bounds.contains(lastPoint) || bounds.contains(center)) { 1105 q = 1; 1106 } 1107 } 1108 longHalfSegmentStart.add(pathLength); 1109 longHalfSegmentEnd.add(pathLength + segmentLength / 2); 1110 longHalfsegmentQuality.add(q); 1111 1112 q = 0; 1113 if (bounds != null) { 1114 if (bounds.contains(center) && bounds.contains(p)) { 1115 q = 2; 1116 } else if (bounds.contains(center) || bounds.contains(p)) { 1117 q = 1; 1118 } 1119 } 1120 longHalfSegmentStart.add(pathLength + segmentLength / 2); 1121 longHalfSegmentEnd.add(pathLength + segmentLength); 1122 longHalfsegmentQuality.add(q); 1123 } 1124 pathLength += segmentLength; 1125 } 1126 lastPoint = p; 1127 } 1128 1129 if (rec.getWidth() > pathLength) 1130 return; 1131 1132 double t1, t2; 1133 1134 if (!longHalfSegmentStart.isEmpty()) { 1135 if (way.getNodesCount() == 2) { 1136 // For 2 node ways, the two half segments are exactly 1137 // the same size and distance from the center. 1138 // Prefer the first one for consistency. 1139 longHalfsegmentQuality.set(0, longHalfsegmentQuality.get(0) + 0.5); 1140 } 1141 1142 // find the long half segment that is closest to the center of the way 1143 // candidates with higher quality value are preferred 1144 double bestStart = Double.NaN; 1145 double bestEnd = Double.NaN; 1146 double bestDistanceToCenter = Double.MAX_VALUE; 1147 double bestQuality = -1; 1148 for (int i=0; i<longHalfSegmentStart.size(); i++) { 1149 double start = longHalfSegmentStart.get(i); 1150 double end = longHalfSegmentEnd.get(i); 1151 double dist = Math.abs(0.5 * (end + start) - 0.5 * pathLength); 1152 if (longHalfsegmentQuality.get(i) > bestQuality || (dist < bestDistanceToCenter && longHalfsegmentQuality.get(i) == bestQuality)) { 1153 bestStart = start; 1154 bestEnd = end; 1155 bestDistanceToCenter = dist; 1156 bestQuality = longHalfsegmentQuality.get(i); 1157 } 1158 } 1159 double remaining = bestEnd - bestStart - rec.getWidth(); // total space left and right from the text 1160 // The space left and right of the text should be distributed 20% - 80% (towards the center), 1161 // but the smaller space should not be less than 7 px. 1162 // However, if the total remaining space is less than 14 px, then distribute it evenly. 1163 double smallerSpace = Math.min(Math.max(0.2 * remaining, 7), 0.5 * remaining); 1164 if ((bestEnd + bestStart)/2 < pathLength/2) { 1165 t2 = bestEnd - smallerSpace; 1166 t1 = t2 - rec.getWidth(); 1167 } else { 1168 t1 = bestStart + smallerSpace; 1169 t2 = t1 + rec.getWidth(); 1170 } 1171 } else { 1172 // doesn't fit into one half-segment -> just put it in the center of the way 1173 t1 = pathLength/2 - rec.getWidth()/2; 1174 t2 = pathLength/2 + rec.getWidth()/2; 1175 } 1176 t1 /= pathLength; 1177 t2 /= pathLength; 1178 1179 double[] p1 = pointAt(t1, poly, pathLength); 1180 double[] p2 = pointAt(t2, poly, pathLength); 1181 1182 if (p1 == null || p2 == null) 1183 return; 1184 1185 double angleOffset; 1186 double offsetSign; 1187 double tStart; 1188 1189 if (p1[0] < p2[0] && 1190 p1[2] < Math.PI/2 && 1191 p1[2] > -Math.PI/2) { 1192 angleOffset = 0; 1193 offsetSign = 1; 1194 tStart = t1; 1195 } else { 1196 angleOffset = Math.PI; 1197 offsetSign = -1; 1198 tStart = t2; 1199 } 1200 1201 FontRenderContext frc = g.getFontRenderContext(); 1202 GlyphVector gv = text.font.createGlyphVector(frc, name); 1203 1204 for (int i=0; i<gv.getNumGlyphs(); ++i) { 1205 Rectangle2D rect = gv.getGlyphLogicalBounds(i).getBounds2D(); 1206 double t = tStart + offsetSign * (rect.getX() + rect.getWidth()/2) / pathLength; 1207 double[] p = pointAt(t, poly, pathLength); 1208 if (p != null) { 1209 AffineTransform trfm = AffineTransform.getTranslateInstance(p[0] - rect.getX(), p[1]); 1210 trfm.rotate(p[2]+angleOffset); 1211 double off = -rect.getY() - rect.getHeight()/2 + text.yOffset; 1212 trfm.translate(-rect.getWidth()/2, off); 1213 if (isGlyphVectorDoubleTranslationBug(text.font)) { 1214 // scale the translation components by one half 1215 AffineTransform tmp = AffineTransform.getTranslateInstance(-0.5 * trfm.getTranslateX(), -0.5 * trfm.getTranslateY()); 1216 tmp.concatenate(trfm); 1217 trfm = tmp; 1218 } 1219 gv.setGlyphTransform(i, trfm); 1220 } 1221 } 1222 displayText(gv, null, 0, 0, way.isDisabled(), text); 1223 } 1224 1225 /** 1226 * draw way 1227 * @param showOrientation show arrows that indicate the technical orientation of 1228 * the way (defined by order of nodes) 1229 * @param showOneway show symbols that indicate the direction of the feature, 1230 * e.g. oneway street or waterway 1231 * @param onewayReversed for oneway=-1 and similar 1232 */ 1233 public void drawWay(Way way, Color color, BasicStroke line, BasicStroke dashes, Color dashedColor, float offset, 1234 boolean showOrientation, boolean showHeadArrowOnly, 1235 boolean showOneway, boolean onewayReversed) { 1236 1237 GeneralPath path = new GeneralPath(); 1238 GeneralPath orientationArrows = showOrientation ? new GeneralPath() : null; 1239 GeneralPath onewayArrows = showOneway ? new GeneralPath() : null; 1240 GeneralPath onewayArrowsCasing = showOneway ? new GeneralPath() : null; 1241 Rectangle bounds = g.getClipBounds(); 1242 if (bounds != null) { 1243 // avoid arrow heads at the border 1244 bounds.grow(100, 100); 1245 } 1246 1247 double wayLength = 0; 1248 Point lastPoint = null; 1249 boolean initialMoveToNeeded = true; 1250 List<Node> wayNodes = way.getNodes(); 1251 if (wayNodes.size() < 2) return; 1252 1253 // only highlight the segment if the way itself is not highlighted 1254 if (!way.isHighlighted() && highlightWaySegments != null) { 1255 GeneralPath highlightSegs = null; 1256 for (WaySegment ws : highlightWaySegments) { 1257 if (ws.way != way || ws.lowerIndex < offset) { 1258 continue; 1259 } 1260 if(highlightSegs == null) { 1261 highlightSegs = new GeneralPath(); 1262 } 1263 1264 Point p1 = nc.getPoint(ws.getFirstNode()); 1265 Point p2 = nc.getPoint(ws.getSecondNode()); 1266 highlightSegs.moveTo(p1.x, p1.y); 1267 highlightSegs.lineTo(p2.x, p2.y); 1268 } 1269 1270 drawPathHighlight(highlightSegs, line); 1271 } 1272 1273 Iterator<Point> it = new OffsetIterator(wayNodes, offset); 1274 while (it.hasNext()) { 1275 Point p = it.next(); 1276 if (lastPoint != null) { 1277 Point p1 = lastPoint; 1278 Point p2 = p; 1279 1280 /** 1281 * Do custom clipping to work around openjdk bug. It leads to 1282 * drawing artefacts when zooming in a lot. (#4289, #4424) 1283 * (Looks like int overflow.) 1284 */ 1285 LineClip clip = new LineClip(p1, p2, bounds); 1286 if (clip.execute()) { 1287 if (!p1.equals(clip.getP1())) { 1288 p1 = clip.getP1(); 1289 path.moveTo(p1.x, p1.y); 1290 } else if (initialMoveToNeeded) { 1291 initialMoveToNeeded = false; 1292 path.moveTo(p1.x, p1.y); 1293 } 1294 p2 = clip.getP2(); 1295 path.lineTo(p2.x, p2.y); 1296 1297 /* draw arrow */ 1298 if (showHeadArrowOnly ? !it.hasNext() : showOrientation) { 1299 final double segmentLength = p1.distance(p2); 1300 if (segmentLength != 0.0) { 1301 final double l = (10. + line.getLineWidth()) / segmentLength; 1302 1303 final double sx = l * (p1.x - p2.x); 1304 final double sy = l * (p1.y - p2.y); 1305 1306 orientationArrows.moveTo (p2.x + cosPHI * sx - sinPHI * sy, p2.y + sinPHI * sx + cosPHI * sy); 1307 orientationArrows.lineTo(p2.x, p2.y); 1308 orientationArrows.lineTo (p2.x + cosPHI * sx + sinPHI * sy, p2.y - sinPHI * sx + cosPHI * sy); 1309 } 1310 } 1311 if (showOneway) { 1312 final double segmentLength = p1.distance(p2); 1313 if (segmentLength != 0.0) { 1314 final double nx = (p2.x - p1.x) / segmentLength; 1315 final double ny = (p2.y - p1.y) / segmentLength; 1316 1317 final double interval = 60; 1318 // distance from p1 1319 double dist = interval - (wayLength % interval); 1320 1321 while (dist < segmentLength) { 1322 for (int i=0; i<2; ++i) { 1323 float onewaySize = i == 0 ? 3f : 2f; 1324 GeneralPath onewayPath = i == 0 ? onewayArrowsCasing : onewayArrows; 1325 1326 // scale such that border is 1 px 1327 final double fac = - (onewayReversed ? -1 : 1) * onewaySize * (1 + sinPHI) / (sinPHI * cosPHI); 1328 final double sx = nx * fac; 1329 final double sy = ny * fac; 1330 1331 // Attach the triangle at the incenter and not at the tip. 1332 // Makes the border even at all sides. 1333 final double x = p1.x + nx * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI)); 1334 final double y = p1.y + ny * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI)); 1335 1336 onewayPath.moveTo(x, y); 1337 onewayPath.lineTo (x + cosPHI * sx - sinPHI * sy, y + sinPHI * sx + cosPHI * sy); 1338 onewayPath.lineTo (x + cosPHI * sx + sinPHI * sy, y - sinPHI * sx + cosPHI * sy); 1339 onewayPath.lineTo(x, y); 1340 } 1341 dist += interval; 1342 } 1343 } 1344 wayLength += segmentLength; 1345 } 1346 } 1347 } 1348 lastPoint = p; 1349 } 1350 if(way.isHighlighted()) { 1351 drawPathHighlight(path, line); 1352 } 1353 displaySegments(path, orientationArrows, onewayArrows, onewayArrowsCasing, color, line, dashes, dashedColor); 1354 } 1355 1356 public double getCircum() { 1357 return circum; 1358 } 1359 1360 @Override 1361 public void getColors() { 1362 super.getColors(); 1363 this.highlightColorTransparent = new Color(highlightColor.getRed(), highlightColor.getGreen(), highlightColor.getBlue(), 100); 1364 this.backgroundColor = PaintColors.getBackgroundColor(); 1365 } 1366 1367 @Override 1368 public void getSettings(boolean virtual) { 1369 super.getSettings(virtual); 1370 paintSettings = MapPaintSettings.INSTANCE; 1371 1372 circum = nc.getDist100Pixel(); 1373 1374 leftHandTraffic = Main.pref.getBoolean("mappaint.lefthandtraffic", false); 1375 1376 useStrokes = paintSettings.getUseStrokesDistance() > circum; 1377 showNames = paintSettings.getShowNamesDistance() > circum; 1378 showIcons = paintSettings.getShowIconsDistance() > circum; 1379 isOutlineOnly = paintSettings.isOutlineOnly(); 1380 orderFont = new Font(Main.pref.get("mappaint.font", "Droid Sans"), Font.PLAIN, Main.pref.getInteger("mappaint.fontsize", 8)); 1381 1382 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 1383 Main.pref.getBoolean("mappaint.use-antialiasing", true) ? 1384 RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF); 1385 1386 highlightLineWidth = Main.pref.getInteger("mappaint.highlight.width", 4); 1387 highlightPointRadius = Main.pref.getInteger("mappaint.highlight.radius", 7); 1388 widerHighlight = Main.pref.getInteger("mappaint.highlight.bigger-increment", 5); 1389 highlightStep = Main.pref.getInteger("mappaint.highlight.step", 4); 1390 } 1391 1392 private Path2D.Double getPath(Way w) { 1393 Path2D.Double path = new Path2D.Double(); 1394 boolean initial = true; 1395 for (Node n : w.getNodes()) { 1396 EastNorth p = n.getEastNorth(); 1397 if (p != null) { 1398 if (initial) { 1399 path.moveTo(p.getX(), p.getY()); 1400 initial = false; 1401 } else { 1402 path.lineTo(p.getX(), p.getY()); 1403 } 1404 } 1405 } 1406 return path; 1407 } 1408 1409 private boolean isAreaVisible(Path2D.Double area) { 1410 Rectangle2D bounds = area.getBounds2D(); 1411 if (bounds.isEmpty()) return false; 1412 Point2D p = nc.getPoint2D(new EastNorth(bounds.getX(), bounds.getY())); 1413 if (p.getX() > nc.getWidth()) return false; 1414 if (p.getY() < 0) return false; 1415 p = nc.getPoint2D(new EastNorth(bounds.getX() + bounds.getWidth(), bounds.getY() + bounds.getHeight())); 1416 if (p.getX() < 0) return false; 1417 if (p.getY() > nc.getHeight()) return false; 1418 return true; 1419 } 1420 1421 public boolean isInactiveMode() { 1422 return isInactiveMode; 1423 } 1424 1425 public boolean isShowIcons() { 1426 return showIcons; 1427 } 1428 1429 public boolean isShowNames() { 1430 return showNames; 1431 } 1432 1433 private double[] pointAt(double t, Polygon poly, double pathLength) { 1434 double totalLen = t * pathLength; 1435 double curLen = 0; 1436 long dx, dy; 1437 double segLen; 1438 1439 // Yes, it is inefficient to iterate from the beginning for each glyph. 1440 // Can be optimized if it turns out to be slow. 1441 for (int i = 1; i < poly.npoints; ++i) { 1442 dx = poly.xpoints[i] - poly.xpoints[i-1]; 1443 dy = poly.ypoints[i] - poly.ypoints[i-1]; 1444 segLen = Math.sqrt(dx*dx + dy*dy); 1445 if (totalLen > curLen + segLen) { 1446 curLen += segLen; 1447 continue; 1448 } 1449 return new double[] { 1450 poly.xpoints[i-1]+(totalLen - curLen)/segLen*dx, 1451 poly.ypoints[i-1]+(totalLen - curLen)/segLen*dy, 1452 Math.atan2(dy, dx)}; 1453 } 1454 return null; 1455 } 1456 1457 private class ComputeStyleListWorker implements Callable<List<StyleRecord>>, Visitor { 1458 private final List<? extends OsmPrimitive> input; 1459 private final int from; 1460 private final int to; 1461 private final List<StyleRecord> output; 1462 1463 private final ElemStyles styles = MapPaintStyles.getStyles(); 1464 1465 private final boolean drawArea = circum <= Main.pref.getInteger("mappaint.fillareas", 10000000); 1466 private final boolean drawMultipolygon = drawArea && Main.pref.getBoolean("mappaint.multipolygon", true); 1467 private final boolean drawRestriction = Main.pref.getBoolean("mappaint.restriction", true); 1468 1469 /** 1470 * Constructs a new {@code ComputeStyleListWorker}. 1471 * @param input the primitives to process 1472 * @param from first index of <code>input</code> to use 1473 * @param to last index + 1 1474 * @param output the list of styles to which styles will be added 1475 */ 1476 public ComputeStyleListWorker(final List<? extends OsmPrimitive> input, int from, int to, List<StyleRecord> output) { 1477 this.input = input; 1478 this.from = from; 1479 this.to = to; 1480 this.output = output; 1481 this.styles.setDrawMultipolygon(drawMultipolygon); 1482 } 1483 1484 @Override 1485 public List<StyleRecord> call() throws Exception { 1486 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock(); 1487 try { 1488 for (int i = from; i<to; i++) { 1489 OsmPrimitive osm = input.get(i); 1490 if (osm.isDrawable()) { 1491 osm.accept(this); 1492 } 1493 } 1494 return output; 1495 } finally { 1496 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock(); 1497 } 1498 } 1499 1500 @Override 1501 public void visit(Node n) { 1502 if (n.isDisabled()) { 1503 add(n, FLAG_DISABLED); 1504 } else if (n.isSelected()) { 1505 add(n, FLAG_SELECTED); 1506 } else if (n.isMemberOfSelected()) { 1507 add(n, FLAG_MEMBER_OF_SELECTED); 1508 } else { 1509 add(n, FLAG_NORMAL); 1510 } 1511 } 1512 1513 @Override 1514 public void visit(Way w) { 1515 if (w.isDisabled()) { 1516 add(w, FLAG_DISABLED); 1517 } else if (w.isSelected()) { 1518 add(w, FLAG_SELECTED); 1519 } else if (w.isOuterMemberOfSelected()) { 1520 add(w, FLAG_OUTERMEMBER_OF_SELECTED); 1521 } else if (w.isMemberOfSelected()) { 1522 add(w, FLAG_MEMBER_OF_SELECTED); 1523 } else { 1524 add(w, FLAG_NORMAL); 1525 } 1526 } 1527 1528 @Override 1529 public void visit(Relation r) { 1530 if (r.isDisabled()) { 1531 add(r, FLAG_DISABLED); 1532 } else if (r.isSelected()) { 1533 add(r, FLAG_SELECTED); 1534 } else if (r.isOuterMemberOfSelected()) { 1535 add(r, FLAG_OUTERMEMBER_OF_SELECTED); 1536 } else if (r.isMemberOfSelected()) { 1537 add(r, FLAG_MEMBER_OF_SELECTED); 1538 } else { 1539 add(r, FLAG_NORMAL); 1540 } 1541 } 1542 1543 @Override 1544 public void visit(Changeset cs) { 1545 throw new UnsupportedOperationException(); 1546 } 1547 1548 public void add(Node osm, int flags) { 1549 StyleList sl = styles.get(osm, circum, nc); 1550 for (ElemStyle s : sl) { 1551 output.add(new StyleRecord(s, osm, flags)); 1552 } 1553 } 1554 1555 public void add(Relation osm, int flags) { 1556 StyleList sl = styles.get(osm, circum, nc); 1557 for (ElemStyle s : sl) { 1558 if (drawMultipolygon && drawArea && s instanceof AreaElemStyle && (flags & FLAG_DISABLED) == 0) { 1559 output.add(new StyleRecord(s, osm, flags)); 1560 } else if (drawRestriction && s instanceof NodeElemStyle) { 1561 output.add(new StyleRecord(s, osm, flags)); 1562 } 1563 } 1564 } 1565 1566 public void add(Way osm, int flags) { 1567 StyleList sl = styles.get(osm, circum, nc); 1568 for (ElemStyle s : sl) { 1569 if (!(drawArea && (flags & FLAG_DISABLED) == 0) && s instanceof AreaElemStyle) { 1570 continue; 1571 } 1572 output.add(new StyleRecord(s, osm, flags)); 1573 } 1574 } 1575 } 1576 1577 private class ConcurrentTasksHelper { 1578 1579 private final List<StyleRecord> allStyleElems; 1580 private final DataSet data; 1581 1582 public ConcurrentTasksHelper(List<StyleRecord> allStyleElems, DataSet data) { 1583 this.allStyleElems = allStyleElems; 1584 this.data = data; 1585 } 1586 1587 void process(List<? extends OsmPrimitive> prims) { 1588 final List<ComputeStyleListWorker> tasks = new ArrayList<>(); 1589 final int bucketsize = Math.max(100, prims.size()/THREAD_POOL.a/3); 1590 final int noBuckets = (prims.size() + bucketsize - 1) / bucketsize; 1591 final boolean singleThread = THREAD_POOL.a == 1 || noBuckets == 1; 1592 for (int i=0; i<noBuckets; i++) { 1593 int from = i*bucketsize; 1594 int to = Math.min((i+1)*bucketsize, prims.size()); 1595 List<StyleRecord> target = singleThread ? allStyleElems : new ArrayList<StyleRecord>(to - from); 1596 tasks.add(new ComputeStyleListWorker(prims, from, to, target)); 1597 } 1598 if (singleThread) { 1599 try { 1600 for (ComputeStyleListWorker task : tasks) { 1601 task.call(); 1602 } 1603 } catch (Exception ex) { 1604 throw new RuntimeException(ex); 1605 } 1606 } else if (!tasks.isEmpty()) { 1607 try { 1608 for (Future<List<StyleRecord>> future : THREAD_POOL.b.invokeAll(tasks)) { 1609 allStyleElems.addAll(future.get()); 1610 } 1611 } catch (InterruptedException | ExecutionException ex) { 1612 throw new RuntimeException(ex); 1613 } 1614 } 1615 } 1616 } 1617 1618 @Override 1619 public void render(final DataSet data, boolean renderVirtualNodes, Bounds bounds) { 1620 BBox bbox = bounds.toBBox(); 1621 getSettings(renderVirtualNodes); 1622 1623 data.getReadLock().lock(); 1624 try { 1625 highlightWaySegments = data.getHighlightedWaySegments(); 1626 1627 long timeStart=0, timePhase1=0, timeFinished; 1628 if (Main.isTraceEnabled()) { 1629 timeStart = System.currentTimeMillis(); 1630 System.err.print("BENCHMARK: rendering "); 1631 Main.debug(null); 1632 } 1633 1634 List<Node> nodes = data.searchNodes(bbox); 1635 List<Way> ways = data.searchWays(bbox); 1636 List<Relation> relations = data.searchRelations(bbox); 1637 1638 final List<StyleRecord> allStyleElems = new ArrayList<>(nodes.size()+ways.size()+relations.size()); 1639 1640 ConcurrentTasksHelper helper = new ConcurrentTasksHelper(allStyleElems, data); 1641 1642 // Need to process all relations first. 1643 // Reason: Make sure, ElemStyles.getStyleCacheWithRange is 1644 // not called for the same primitive in parallel threads. 1645 // (Could be synchronized, but try to avoid this for 1646 // performance reasons.) 1647 helper.process(relations); 1648 helper.process(new CompositeList<>(nodes, ways)); 1649 1650 if (Main.isTraceEnabled()) { 1651 timePhase1 = System.currentTimeMillis(); 1652 System.err.print("phase 1 (calculate styles): " + (timePhase1 - timeStart) + " ms"); 1653 } 1654 1655 Collections.sort(allStyleElems); // TODO: try parallel sort when switching to Java 8 1656 1657 for (StyleRecord r : allStyleElems) { 1658 r.style.paintPrimitive( 1659 r.osm, 1660 paintSettings, 1661 StyledMapRenderer.this, 1662 (r.flags & FLAG_SELECTED) != 0, 1663 (r.flags & FLAG_OUTERMEMBER_OF_SELECTED) != 0, 1664 (r.flags & FLAG_MEMBER_OF_SELECTED) != 0 1665 ); 1666 } 1667 1668 if (Main.isTraceEnabled()) { 1669 timeFinished = System.currentTimeMillis(); 1670 System.err.println("; phase 2 (draw): " + (timeFinished - timePhase1) + " ms; total: " + (timeFinished - timeStart) + " ms" + 1671 " (scale: " + circum + " zoom level: " + Selector.GeneralSelector.scale2level(circum) + ")"); 1672 } 1673 1674 drawVirtualNodes(data, bbox); 1675 } finally { 1676 data.getReadLock().unlock(); 1677 } 1678 } 1679}