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