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