source: josm/trunk/src/org/openstreetmap/josm/data/osm/visitor/paint/MapPainter.java@ 4042

Last change on this file since 4042 was 4042, checked in by bastiK, 13 years ago

fixed #6218 - horizontal lines with larger dataset (openjdk does not like NaN in Path2D)

  • Property svn:eol-style set to native
  • Property svn:mime-type set to text/plain
File size: 38.5 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.osm.visitor.paint;
3
4import java.awt.AlphaComposite;
5import java.awt.BasicStroke;
6import java.awt.Color;
7import java.awt.Font;
8import java.awt.FontMetrics;
9import java.awt.Graphics2D;
10import java.awt.Image;
11import java.awt.Point;
12import java.awt.Polygon;
13import java.awt.Rectangle;
14import java.awt.Shape;
15import java.awt.TexturePaint;
16import java.awt.font.FontRenderContext;
17import java.awt.font.GlyphVector;
18import java.awt.font.LineMetrics;
19import java.awt.geom.AffineTransform;
20import java.awt.geom.GeneralPath;
21import java.awt.geom.Rectangle2D;
22import java.awt.image.BufferedImage;
23import java.util.Arrays;
24import java.util.Collection;
25import java.util.Iterator;
26
27import javax.swing.ImageIcon;
28
29import org.openstreetmap.josm.Main;
30import org.openstreetmap.josm.data.osm.Node;
31import org.openstreetmap.josm.data.osm.OsmPrimitive;
32import org.openstreetmap.josm.data.osm.OsmUtils;
33import org.openstreetmap.josm.data.osm.Relation;
34import org.openstreetmap.josm.data.osm.RelationMember;
35import org.openstreetmap.josm.data.osm.Way;
36import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
37import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData;
38import org.openstreetmap.josm.gui.NavigatableComponent;
39import org.openstreetmap.josm.gui.mappaint.NodeElemStyle;
40import org.openstreetmap.josm.gui.mappaint.NodeElemStyle.HorizontalTextAlignment;
41import org.openstreetmap.josm.gui.mappaint.NodeElemStyle.NodeTextElement;
42import org.openstreetmap.josm.gui.mappaint.NodeElemStyle.Symbol;
43import org.openstreetmap.josm.gui.mappaint.NodeElemStyle.VerticalTextAlignment;
44import org.openstreetmap.josm.gui.mappaint.TextElement;
45import org.openstreetmap.josm.tools.ImageProvider;
46import org.openstreetmap.josm.tools.Pair;
47
48public class MapPainter {
49
50 private final Graphics2D g;
51 private final NavigatableComponent nc;
52 private final boolean inactive;
53 private final MapPaintSettings settings;
54
55 private final boolean useStrokes;
56 private final boolean showNames;
57 private final boolean showIcons;
58
59 private final boolean isOutlineOnly;
60
61 private final Color inactiveColor;
62 private final Color selectedColor;
63 private final Color relationSelectedColor;
64 private final Color nodeColor;
65 private final Color backgroundColor;
66
67 private final Font orderFont;
68 private final int virtualNodeSize;
69 private final int virtualNodeSpace;
70 private final int segmentNumberSpace;
71
72 private final double circum;
73
74 private final boolean leftHandTraffic;
75
76 private static final double PHI = Math.toRadians(20);
77 private static final double cosPHI = Math.cos(PHI);
78 private static final double sinPHI = Math.sin(PHI);
79
80 public MapPainter(MapPaintSettings settings, Graphics2D g,
81 boolean inactive, NavigatableComponent nc, boolean virtual,
82 double circum, boolean leftHandTraffic)
83 {
84 this.settings = settings;
85 this.g = g;
86 this.inactive = inactive;
87 this.nc = nc;
88 this.useStrokes = settings.getUseStrokesDistance() > circum;
89 this.showNames = settings.getShowNamesDistance() > circum;
90 this.showIcons = settings.getShowIconsDistance() > circum;
91
92 this.isOutlineOnly = settings.isOutlineOnly();
93
94 this.inactiveColor = PaintColors.INACTIVE.get();
95 this.selectedColor = PaintColors.SELECTED.get();
96 this.relationSelectedColor = PaintColors.RELATIONSELECTED.get();
97 this.nodeColor = PaintColors.NODE.get();
98 this.backgroundColor = PaintColors.getBackgroundColor();
99
100 this.orderFont = new Font(Main.pref.get("mappaint.font", "Helvetica"), Font.PLAIN, Main.pref.getInteger("mappaint.fontsize", 8));
101 this.virtualNodeSize = virtual ? Main.pref.getInteger("mappaint.node.virtual-size", 8) / 2 : 0;
102 this.virtualNodeSpace = Main.pref.getInteger("mappaint.node.virtual-space", 70);
103 this.segmentNumberSpace = Main.pref.getInteger("mappaint.segmentnumber.space", 40);
104
105 this.circum = circum;
106 this.leftHandTraffic = leftHandTraffic;
107 }
108
109 /**
110 * draw way
111 * @param showOrientation show arrows that indicate the technical orientation of
112 * the way (defined by order of nodes)
113 * @param showOneway show symbols that indicate the direction of the feature,
114 * e.g. oneway street or waterway
115 * @param onewayReversed for oneway=-1 and similar
116 */
117 public void drawWay(Way way, Color color, BasicStroke line, BasicStroke dashes, Color dashedColor,
118 boolean showOrientation, boolean showHeadArrowOnly,
119 boolean showOneway, boolean onewayReversed) {
120
121 GeneralPath path = new GeneralPath();
122 GeneralPath orientationArrows = showOrientation ? new GeneralPath() : null;
123 GeneralPath onewayArrows = showOneway ? new GeneralPath() : null;
124 GeneralPath onewayArrowsCasing = showOneway ? new GeneralPath() : null;
125 Rectangle bounds = g.getClipBounds();
126 bounds.grow(100, 100); // avoid arrow heads at the border
127
128 double wayLength = 0;
129 Point lastPoint = null;
130 boolean initialMoveToNeeded = true;
131 Iterator<Node> it = way.getNodes().iterator();
132 while (it.hasNext()) {
133 Node n = it.next();
134 Point p = nc.getPoint(n);
135 if(lastPoint != null) {
136 Point p1 = lastPoint;
137 Point p2 = p;
138
139 /**
140 * Do custom clipping to work around openjdk bug. It leads to
141 * drawing artefacts when zooming in a lot. (#4289, #4424)
142 * (Looks like int overflow.)
143 */
144 LineClip clip = new LineClip(p1, p2, bounds);
145 if (clip.execute()) {
146 if (!p1.equals(clip.getP1())) {
147 p1 = clip.getP1();
148 path.moveTo(p1.x, p1.y);
149 } else if (initialMoveToNeeded) {
150 initialMoveToNeeded = false;
151 path.moveTo(p1.x, p1.y);
152 }
153 p2 = clip.getP2();
154 path.lineTo(p2.x, p2.y);
155
156 /* draw arrow */
157 if (showHeadArrowOnly ? !it.hasNext() : showOrientation) {
158 final double segmentLength = p1.distance(p2);
159 if (segmentLength != 0.0) {
160 final double l = (10. + line.getLineWidth()) / segmentLength;
161
162 final double sx = l * (p1.x - p2.x);
163 final double sy = l * (p1.y - p2.y);
164
165 double tmp = p2.x + cosPHI * sx - sinPHI * sy;
166 orientationArrows.moveTo (p2.x + cosPHI * sx - sinPHI * sy, p2.y + sinPHI * sx + cosPHI * sy);
167 orientationArrows.lineTo(p2.x, p2.y);
168 orientationArrows.lineTo (p2.x + cosPHI * sx + sinPHI * sy, p2.y - sinPHI * sx + cosPHI * sy);
169 }
170 }
171 if (showOneway) {
172 final double segmentLength = p1.distance(p2);
173 if (segmentLength != 0.0) {
174 final double nx = (p2.x - p1.x) / segmentLength;
175 final double ny = (p2.y - p1.y) / segmentLength;
176
177 final double interval = 60;
178 // distance from p1
179 double dist = interval - (wayLength % interval);
180
181 while (dist < segmentLength) {
182 for (Pair<Float, GeneralPath> sizeAndPath : Arrays.asList(new Pair[] {
183 new Pair<Float, GeneralPath>(3f, onewayArrowsCasing),
184 new Pair<Float, GeneralPath>(2f, onewayArrows)})) {
185
186 // scale such that border is 1 px
187 final double fac = - (onewayReversed ? -1 : 1) * sizeAndPath.a * (1 + sinPHI) / (sinPHI * cosPHI);
188 final double sx = nx * fac;
189 final double sy = ny * fac;
190
191 // Attach the triangle at the incenter and not at the tip.
192 // Makes the border even at all sides.
193 final double x = p1.x + nx * (dist + (onewayReversed ? -1 : 1) * (sizeAndPath.a / sinPHI));
194 final double y = p1.y + ny * (dist + (onewayReversed ? -1 : 1) * (sizeAndPath.a / sinPHI));
195
196 sizeAndPath.b.moveTo(x, y);
197 sizeAndPath.b.lineTo (x + cosPHI * sx - sinPHI * sy, y + sinPHI * sx + cosPHI * sy);
198 sizeAndPath.b.lineTo (x + cosPHI * sx + sinPHI * sy, y - sinPHI * sx + cosPHI * sy);
199 sizeAndPath.b.lineTo(x, y);
200 }
201 dist += interval;
202 }
203 }
204 wayLength += segmentLength;
205 }
206 }
207 }
208 lastPoint = p;
209 }
210 displaySegments(path, orientationArrows, onewayArrows, onewayArrowsCasing, color, line, dashes, dashedColor);
211 }
212
213 private void displaySegments(GeneralPath path, GeneralPath orientationArrows, GeneralPath onewayArrows, GeneralPath onewayArrowsCasing,
214 Color color, BasicStroke line, BasicStroke dashes, Color dashedColor) {
215 g.setColor(inactive ? inactiveColor : color);
216 if (useStrokes) {
217 g.setStroke(line);
218 }
219 g.draw(path);
220
221 if(!inactive && useStrokes && dashes != null) {
222 g.setColor(dashedColor);
223 g.setStroke(dashes);
224 g.draw(path);
225 }
226
227 if (orientationArrows != null) {
228 g.setColor(inactive ? inactiveColor : color);
229 g.setStroke(new BasicStroke(line.getLineWidth(), line.getEndCap(), BasicStroke.JOIN_MITER, line.getMiterLimit()));
230 g.draw(orientationArrows);
231 }
232
233 if (onewayArrows != null) {
234 g.setStroke(new BasicStroke(1, line.getEndCap(), BasicStroke.JOIN_MITER, line.getMiterLimit()));
235 g.fill(onewayArrowsCasing);
236 g.setColor(inactive ? inactiveColor : backgroundColor);
237 g.fill(onewayArrows);
238 }
239
240 if(useStrokes) {
241 g.setStroke(new BasicStroke());
242 }
243 }
244
245 private boolean isSegmentVisible(Point p1, Point p2) {
246 if ((p1.x < 0) && (p2.x < 0)) return false;
247 if ((p1.y < 0) && (p2.y < 0)) return false;
248 if ((p1.x > nc.getWidth()) && (p2.x > nc.getWidth())) return false;
249 if ((p1.y > nc.getHeight()) && (p2.y > nc.getHeight())) return false;
250 return true;
251 }
252
253 public void drawTextOnPath(Way way, TextElement text) {
254 if (text == null)
255 return;
256 String name = text.getString(way);
257 if (name == null || name.equals(""))
258 return;
259
260 Polygon poly = new Polygon();
261 Point lastPoint = null;
262 Iterator<Node> it = way.getNodes().iterator();
263 double pathLength = 0;
264 int dx, dy;
265 while (it.hasNext()) {
266 Node n = it.next();
267 Point p = nc.getPoint(n);
268 poly.addPoint(p.x, p.y);
269
270 if(lastPoint != null) {
271 dx = p.x - lastPoint.x;
272 dy = p.y - lastPoint.y;
273 pathLength += Math.sqrt(dx*dx + dy*dy);
274 }
275 lastPoint = p;
276 }
277
278 FontMetrics fontMetrics = g.getFontMetrics(text.font); // if slow, use cache
279 Rectangle2D rec = fontMetrics.getStringBounds(name, g); // if slow, approximate by strlen()*maxcharbounds(font)
280
281 if (rec.getWidth() > pathLength)
282 return;
283
284 double t1 = (pathLength/2 - rec.getWidth()/2) / pathLength;
285 double t2 = (pathLength/2 + rec.getWidth()/2) / pathLength;
286
287 double[] p1 = pointAt(t1, poly, pathLength);
288 double[] p2 = pointAt(t2, poly, pathLength);
289
290 double angleOffset;
291 double offsetSign;
292 double tStart;
293
294 if (p1[0] < p2[0] &&
295 p1[2] < Math.PI/2 &&
296 p1[2] > -Math.PI/2) {
297 angleOffset = 0;
298 offsetSign = 1;
299 tStart = t1;
300 } else {
301 angleOffset = Math.PI;
302 offsetSign = -1;
303 tStart = t2;
304 }
305
306 FontRenderContext frc = g.getFontRenderContext();
307 GlyphVector gv = text.font.createGlyphVector(frc, name);
308
309 for (int i=0; i<gv.getNumGlyphs(); ++i) {
310 Rectangle2D rect = gv.getGlyphLogicalBounds(i).getBounds2D();
311 double t = tStart + offsetSign * (rect.getX() + rect.getWidth()/2) / pathLength;
312 double[] p = pointAt(t, poly, pathLength);
313 AffineTransform trfm = AffineTransform.getTranslateInstance(p[0] - rect.getX(), p[1]);
314 trfm.rotate(p[2]+angleOffset);
315 double off = -rect.getY() - rect.getHeight()/2 + text.yOffset;
316 trfm.translate(-rect.getWidth()/2, off);
317 gv.setGlyphTransform(i, trfm);
318 }
319 if (text.haloRadius != null) {
320 Shape textOutline = gv.getOutline();
321 g.setStroke(new BasicStroke(2*text.haloRadius, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND));
322 g.setColor(text.haloColor);
323 g.draw(textOutline);
324 g.setStroke(new BasicStroke());
325 g.setColor(text.color);
326 g.fill(textOutline);
327 } else {
328 g.setColor(text.color);
329 g.drawGlyphVector(gv, 0, 0);
330 }
331 }
332
333 private double[] pointAt(double t, Polygon poly, double pathLength) {
334 double totalLen = t * pathLength;
335 double curLen = 0;
336 int dx, dy;
337 double segLen;
338
339 // Yes, it is ineffecient to iterate from the beginning for each glyph.
340 // Can be optimized if it turns out to be slow.
341 for (int i = 1; i < poly.npoints; ++i) {
342 dx = poly.xpoints[i] - poly.xpoints[i-1];
343 dy = poly.ypoints[i] - poly.ypoints[i-1];
344 segLen = Math.sqrt(dx*dx + dy*dy);
345 if (totalLen > curLen + segLen) {
346 curLen += segLen;
347 continue;
348 }
349 return new double[] {poly.xpoints[i-1]+(totalLen - curLen)/segLen*dx,
350 poly.ypoints[i-1]+(totalLen - curLen)/segLen*dy,
351 Math.atan2(dy, dx)};
352 }
353 return null;
354 }
355
356 public void drawLinePattern(Way way, ImageIcon pattern) {
357 final int width = pattern.getIconWidth();
358 final int height = pattern.getIconHeight();
359
360 Point lastP = null;
361 double wayLength = 0;
362
363 Iterator<Node> it = way.getNodes().iterator();
364 while (it.hasNext()) {
365 Node n = it.next();
366 Point thisP = nc.getPoint(n);
367
368 if (lastP != null) {
369 final double segmentLength = thisP.distance(lastP);
370
371 final double dx = thisP.x - lastP.x;
372 final double dy = thisP.y - lastP.y;
373
374 double dist = wayLength == 0 ? 0 : width - (wayLength % width);
375
376 AffineTransform saveTransform = g.getTransform();
377 g.translate(lastP.x, lastP.y);
378 g.rotate(Math.atan2(dy, dx));
379
380 if (dist > 0) {
381 g.drawImage(pattern.getImage(), 0, 0, (int) dist, height,
382 width - (int) dist, 0, width, height, null);
383 }
384 while (dist < segmentLength) {
385 if (dist + width > segmentLength) {
386 g.drawImage(pattern.getImage(), (int) dist, 0, (int) segmentLength, height,
387 0, 0, (int) segmentLength - (int) dist, height, null);
388 } else {
389 pattern.paintIcon(nc, g, (int) dist, 0);
390 }
391 dist += width;
392 }
393 g.setTransform(saveTransform);
394
395 wayLength += segmentLength;
396 }
397 lastP = thisP;
398 }
399 }
400
401 public void drawNodeIcon(Node n, ImageIcon icon, float iconAlpha, boolean selected, boolean member, NodeTextElement text) {
402 Point p = nc.getPoint(n);
403 if ((p.x < 0) || (p.y < 0) || (p.x > nc.getWidth()) || (p.y > nc.getHeight())) return;
404
405 int w = icon.getIconWidth(), h=icon.getIconHeight();
406 if (iconAlpha != 1f) {
407 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, iconAlpha));
408 }
409 icon.paintIcon ( nc, g, p.x-w/2, p.y-h/2 );
410 g.setPaintMode();
411 drawNodeText(n, text, p, w/2, h/2);
412 if (selected || member)
413 {
414 g.setColor(selected? selectedColor : relationSelectedColor);
415 g.drawRect(p.x-w/2-2, p.y-h/2-2, w+4, h+4);
416 }
417 }
418
419 private Polygon buildPolygon(Point center, int radius, int sides, double rotation) {
420 Polygon polygon = new Polygon();
421 for (int i = 0; i < sides; i++) {
422 double angle = ((2 * Math.PI / sides) * i) - rotation;
423 int x = (int) Math.round(center.x + radius * Math.cos(angle));
424 int y = (int) Math.round(center.y + radius * Math.sin(angle));
425 polygon.addPoint(x, y);
426 }
427 return polygon;
428 }
429
430 private Polygon buildPolygon(Point center, int radius, int sides) {
431 return buildPolygon(center, radius, sides, 0.0);
432 }
433
434 public void drawNodeSymbol(Node n, Symbol s, Color fillColor, Color strokeColor, NodeTextElement text) {
435 Point p = nc.getPoint(n);
436 if ((p.x < 0) || (p.y < 0) || (p.x > nc.getWidth()) || (p.y > nc.getHeight())) return;
437 int radius = s.size / 2;
438
439 if (fillColor != null) {
440 g.setColor(fillColor);
441 switch (s.symbol) {
442 case SQUARE:
443 g.fillRect(p.x - radius, p.y - radius, s.size, s.size);
444 break;
445 case CIRCLE:
446 g.fillOval(p.x - radius, p.y - radius, s.size, s.size);
447 break;
448 case TRIANGLE:
449 g.fillPolygon(buildPolygon(p, radius, 3, Math.PI / 2));
450 break;
451 case PENTAGON:
452 g.fillPolygon(buildPolygon(p, radius, 5, Math.PI / 2));
453 break;
454 case HEXAGON:
455 g.fillPolygon(buildPolygon(p, radius, 6));
456 break;
457 case HEPTAGON:
458 g.fillPolygon(buildPolygon(p, radius, 7, Math.PI / 2));
459 break;
460 case OCTAGON:
461 g.fillPolygon(buildPolygon(p, radius, 8, Math.PI / 8));
462 break;
463 case NONAGON:
464 g.fillPolygon(buildPolygon(p, radius, 9, Math.PI / 2));
465 break;
466 case DECAGON:
467 g.fillPolygon(buildPolygon(p, radius, 10));
468 break;
469 default:
470 throw new AssertionError();
471 }
472 }
473 if (s.stroke != null) {
474 g.setStroke(s.stroke);
475 g.setColor(strokeColor);
476 switch (s.symbol) {
477 case SQUARE:
478 g.drawRect(p.x - radius, p.y - radius, s.size - 1, s.size - 1);
479 break;
480 case CIRCLE:
481 g.drawOval(p.x - radius, p.y - radius, s.size - 1, s.size - 1);
482 break;
483 case TRIANGLE:
484 g.drawPolygon(buildPolygon(p, radius, 3, Math.PI / 2));
485 break;
486 case PENTAGON:
487 g.drawPolygon(buildPolygon(p, radius, 5, Math.PI / 2));
488 break;
489 case HEXAGON:
490 g.drawPolygon(buildPolygon(p, radius, 6));
491 break;
492 case HEPTAGON:
493 g.drawPolygon(buildPolygon(p, radius, 7, Math.PI / 2));
494 break;
495 case OCTAGON:
496 g.drawPolygon(buildPolygon(p, radius, 8, Math.PI / 8));
497 break;
498 case NONAGON:
499 g.drawPolygon(buildPolygon(p, radius, 9, Math.PI / 2));
500 break;
501 case DECAGON:
502 g.drawPolygon(buildPolygon(p, radius, 10));
503 break;
504 default:
505 throw new AssertionError();
506 }
507 g.setStroke(new BasicStroke());
508 }
509 drawNodeText(n, text, p, radius, radius);
510 }
511
512 /**
513 * Draw the node as small rectangle with the given color.
514 *
515 * @param n The node to draw.
516 * @param color The color of the node.
517 */
518 public void drawNode(Node n, Color color, int size, boolean fill, NodeTextElement text) {
519 if (size > 1) {
520 Point p = nc.getPoint(n);
521 if ((p.x < 0) || (p.y < 0) || (p.x > nc.getWidth()) || (p.y > nc.getHeight())) return;
522 int radius = size / 2;
523
524 if (inactive || n.isDisabled()) {
525 g.setColor(inactiveColor);
526 } else {
527 g.setColor(color);
528 }
529 if (fill) {
530 g.fillRect(p.x - radius, p.y - radius, size + 1, size + 1);
531 } else {
532 g.drawRect(p.x - radius, p.y - radius, size, size);
533 }
534
535 drawNodeText(n, text, p, radius, radius + 4);
536 }
537 }
538
539 private void drawNodeText(Node n, NodeTextElement text, Point p, int w_half, int h_half) {
540 if (!isShowNames() || text == null)
541 return;
542
543 /*
544 * abort if we can't compose the label to be rendered
545 */
546 if (text.labelCompositionStrategy == null) return;
547 String s = text.labelCompositionStrategy.compose(n);
548 if (s == null) return;
549
550 Font defaultFont = g.getFont();
551 g.setFont(text.font);
552
553 int x = p.x + text.xOffset;
554 int y = p.y + text.yOffset;
555 /**
556 *
557 * left-above __center-above___ right-above
558 * left-top| |right-top
559 * | |
560 * left-center| center-center |right-center
561 * | |
562 * left-bottom|_________________|right-bottom
563 * left-below center-below right-below
564 *
565 */
566 if (text.hAlign == HorizontalTextAlignment.RIGHT) {
567 x += w_half + 2;
568 } else {
569 FontRenderContext frc = g.getFontRenderContext();
570 Rectangle2D bounds = text.font.getStringBounds(s, frc);
571 int textWidth = (int) bounds.getWidth();
572 if (text.hAlign == HorizontalTextAlignment.CENTER) {
573 x -= textWidth / 2;
574 } else if (text.hAlign == HorizontalTextAlignment.LEFT) {
575 x -= w_half + 4 + textWidth;
576 } else throw new AssertionError();
577 }
578
579 if (text.vAlign == VerticalTextAlignment.BOTTOM) {
580 y += h_half - 2;
581 } else {
582 FontRenderContext frc = g.getFontRenderContext();
583 LineMetrics metrics = text.font.getLineMetrics(s, frc);
584 if (text.vAlign == VerticalTextAlignment.ABOVE) {
585 y -= h_half + metrics.getDescent();
586 } else if (text.vAlign == VerticalTextAlignment.TOP) {
587 y -= h_half - metrics.getAscent();
588 } else if (text.vAlign == VerticalTextAlignment.CENTER) {
589 y += (metrics.getAscent() - metrics.getDescent()) / 2;
590 } else if (text.vAlign == VerticalTextAlignment.BELOW) {
591 y += h_half + metrics.getAscent() + 2;
592 } else throw new AssertionError();
593 }
594 if (inactive || n.isDisabled()) {
595 g.setColor(inactiveColor);
596 } else {
597 g.setColor(text.color);
598 }
599 if (text.haloRadius != null) {
600 g.setStroke(new BasicStroke(2*text.haloRadius, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND));
601 g.setColor(text.haloColor);
602 FontRenderContext frc = g.getFontRenderContext();
603 GlyphVector gv = text.font.createGlyphVector(frc, s);
604 Shape textOutline = gv.getOutline(x, y);
605 g.draw(textOutline);
606 g.setStroke(new BasicStroke());
607 g.setColor(text.color);
608 g.fill(textOutline);
609 } else {
610 g.drawString(s, x, y);
611 }
612 g.setFont(defaultFont);
613 }
614
615 private Polygon getPolygon(Way w) {
616 Polygon polygon = new Polygon();
617
618 for (Node n : w.getNodes()) {
619 Point p = nc.getPoint(n);
620 polygon.addPoint(p.x,p.y);
621 }
622 return polygon;
623 }
624
625 public void drawArea(Way w, Color color, BufferedImage fillImage, float fillImageAlpha, TextElement text) {
626 Polygon polygon = getPolygon(w);
627 drawArea(w, polygon, color, fillImage, fillImageAlpha, text);
628 }
629
630 protected void drawArea(OsmPrimitive osm, Polygon polygon, Color color, BufferedImage fillImage, float fillImageAlpha, TextElement text) {
631
632 if (!isOutlineOnly) {
633 if (fillImage == null) {
634 g.setColor(color);
635 g.fillPolygon(polygon);
636 } else {
637 TexturePaint texture = new TexturePaint(fillImage,
638 new Rectangle(polygon.xpoints[0], polygon.ypoints[0], fillImage.getWidth(), fillImage.getHeight()));
639 g.setPaint(texture);
640 if (fillImageAlpha != 1f) {
641 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, fillImageAlpha));
642 }
643 g.fill(polygon);
644 g.setPaintMode();
645 }
646 }
647
648 if (text != null && isShowNames()) {
649 /*
650 * abort if we can't compose the label to be rendered
651 */
652 if (text.labelCompositionStrategy == null) return;
653 String name = text.labelCompositionStrategy.compose(osm);
654 if (name == null) return;
655
656 Rectangle pb = polygon.getBounds();
657 FontMetrics fontMetrics = g.getFontMetrics(orderFont); // if slow, use cache
658 Rectangle2D nb = fontMetrics.getStringBounds(name, g); // if slow, approximate by strlen()*maxcharbounds(font)
659
660 // Point2D c = getCentroid(polygon);
661 // Using the Centroid is Nicer for buildings like: +--------+
662 // but this needs to be fast. As most houses are | 42 |
663 // boxes anyway, the center of the bounding box +---++---+
664 // will have to do. ++
665 // Centroids are not optimal either, just imagine a U-shaped house.
666 // Point2D c = new Point2D.Double(pb.x + pb.width / 2.0, pb.y + pb.height / 2.0);
667 // Rectangle2D.Double centeredNBounds =
668 // new Rectangle2D.Double(c.getX() - nb.getWidth()/2,
669 // c.getY() - nb.getHeight()/2,
670 // nb.getWidth(),
671 // nb.getHeight());
672
673 Rectangle centeredNBounds = new Rectangle(pb.x + (int)((pb.width - nb.getWidth())/2.0),
674 pb.y + (int)((pb.height - nb.getHeight())/2.0),
675 (int)nb.getWidth(),
676 (int)nb.getHeight());
677
678 if ((pb.width >= nb.getWidth() && pb.height >= nb.getHeight()) && // quick check
679 polygon.contains(centeredNBounds) // slow but nice
680 ) {
681 g.setColor(text.color);
682 Font defaultFont = g.getFont();
683 g.setFont (text.font);
684 g.drawString (name,
685 (int)(centeredNBounds.getMinX() - nb.getMinX()),
686 (int)(centeredNBounds.getMinY() - nb.getMinY()));
687 g.setFont(defaultFont);
688 }
689 }
690 }
691
692 public void drawArea(Relation r, Color color, BufferedImage fillImage, float fillImageAlpha, TextElement text) {
693 Multipolygon multipolygon = new Multipolygon(nc);
694 multipolygon.load(r);
695 if(!r.isDisabled() && !multipolygon.getOuterWays().isEmpty()) {
696 for (PolyData pd : multipolygon.getCombinedPolygons()) {
697 Polygon p = pd.get();
698 if(!isPolygonVisible(p)) {
699 continue;
700 }
701 drawArea(r, p,
702 pd.selected ? settings.getRelationSelectedColor(color.getAlpha()) : color,
703 fillImage, fillImageAlpha, text);
704 }
705 }
706 }
707
708 private boolean isPolygonVisible(Polygon polygon) {
709 Rectangle bounds = polygon.getBounds();
710 if (bounds.width == 0 && bounds.height == 0) return false;
711 if (bounds.x > nc.getWidth()) return false;
712 if (bounds.y > nc.getHeight()) return false;
713 if (bounds.x + bounds.width < 0) return false;
714 if (bounds.y + bounds.height < 0) return false;
715 return true;
716 }
717
718 public void drawRestriction(ImageIcon icon, Point pVia, double vx, double vx2, double vy, double vy2, double iconAngle, boolean selected) {
719 /* rotate icon with direction last node in from to */
720 ImageIcon rotatedIcon = ImageProvider.createRotatedImage(null /*icon2*/, icon, iconAngle);
721
722 /* scale down icon to 16*16 pixels */
723 ImageIcon smallIcon = new ImageIcon(rotatedIcon.getImage().getScaledInstance(16 , 16, Image.SCALE_SMOOTH));
724 int w = smallIcon.getIconWidth(), h=smallIcon.getIconHeight();
725 smallIcon.paintIcon (nc, g, (int)(pVia.x+vx+vx2)-w/2, (int)(pVia.y+vy+vy2)-h/2 );
726
727 if (selected) {
728 g.setColor(relationSelectedColor);
729 g.drawRect((int)(pVia.x+vx+vx2)-w/2-2,(int)(pVia.y+vy+vy2)-h/2-2, w+4, h+4);
730 }
731 }
732
733 public void drawRestriction(Relation r, NodeElemStyle icon) {
734
735 Way fromWay = null;
736 Way toWay = null;
737 OsmPrimitive via = null;
738
739 /* find the "from", "via" and "to" elements */
740 for (RelationMember m : r.getMembers())
741 {
742 if(m.getMember().isIncomplete())
743 return;
744 else
745 {
746 if(m.isWay())
747 {
748 Way w = m.getWay();
749 if(w.getNodesCount() < 2) {
750 continue;
751 }
752
753 if("from".equals(m.getRole())) {
754 if(fromWay == null) {
755 fromWay = w;
756 }
757 } else if("to".equals(m.getRole())) {
758 if(toWay == null) {
759 toWay = w;
760 }
761 } else if("via".equals(m.getRole())) {
762 if(via == null) {
763 via = w;
764 }
765 }
766 }
767 else if(m.isNode())
768 {
769 Node n = m.getNode();
770 if("via".equals(m.getRole()) && via == null) {
771 via = n;
772 }
773 }
774 }
775 }
776
777 if (fromWay == null || toWay == null || via == null)
778 return;
779
780 Node viaNode;
781 if(via instanceof Node)
782 {
783 viaNode = (Node) via;
784 if(!fromWay.isFirstLastNode(viaNode))
785 return;
786 }
787 else
788 {
789 Way viaWay = (Way) via;
790 Node firstNode = viaWay.firstNode();
791 Node lastNode = viaWay.lastNode();
792 Boolean onewayvia = false;
793
794 String onewayviastr = viaWay.get("oneway");
795 if(onewayviastr != null)
796 {
797 if("-1".equals(onewayviastr)) {
798 onewayvia = true;
799 Node tmp = firstNode;
800 firstNode = lastNode;
801 lastNode = tmp;
802 } else {
803 onewayvia = OsmUtils.getOsmBoolean(onewayviastr);
804 if (onewayvia == null) {
805 onewayvia = false;
806 }
807 }
808 }
809
810 if(fromWay.isFirstLastNode(firstNode)) {
811 viaNode = firstNode;
812 } else if (!onewayvia && fromWay.isFirstLastNode(lastNode)) {
813 viaNode = lastNode;
814 } else
815 return;
816 }
817
818 /* find the "direct" nodes before the via node */
819 Node fromNode = null;
820 if(fromWay.firstNode() == via) {
821 fromNode = fromWay.getNode(1);
822 } else {
823 fromNode = fromWay.getNode(fromWay.getNodesCount()-2);
824 }
825
826 Point pFrom = nc.getPoint(fromNode);
827 Point pVia = nc.getPoint(viaNode);
828
829 /* starting from via, go back the "from" way a few pixels
830 (calculate the vector vx/vy with the specified length and the direction
831 away from the "via" node along the first segment of the "from" way)
832 */
833 double distanceFromVia=14;
834 double dx = (pFrom.x >= pVia.x) ? (pFrom.x - pVia.x) : (pVia.x - pFrom.x);
835 double dy = (pFrom.y >= pVia.y) ? (pFrom.y - pVia.y) : (pVia.y - pFrom.y);
836
837 double fromAngle;
838 if(dx == 0.0) {
839 fromAngle = Math.PI/2;
840 } else {
841 fromAngle = Math.atan(dy / dx);
842 }
843 double fromAngleDeg = Math.toDegrees(fromAngle);
844
845 double vx = distanceFromVia * Math.cos(fromAngle);
846 double vy = distanceFromVia * Math.sin(fromAngle);
847
848 if(pFrom.x < pVia.x) {
849 vx = -vx;
850 }
851 if(pFrom.y < pVia.y) {
852 vy = -vy;
853 }
854
855 /* go a few pixels away from the way (in a right angle)
856 (calculate the vx2/vy2 vector with the specified length and the direction
857 90degrees away from the first segment of the "from" way)
858 */
859 double distanceFromWay=10;
860 double vx2 = 0;
861 double vy2 = 0;
862 double iconAngle = 0;
863
864 if(pFrom.x >= pVia.x && pFrom.y >= pVia.y) {
865 if(!leftHandTraffic) {
866 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
867 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
868 } else {
869 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
870 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
871 }
872 iconAngle = 270+fromAngleDeg;
873 }
874 if(pFrom.x < pVia.x && pFrom.y >= pVia.y) {
875 if(!leftHandTraffic) {
876 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
877 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
878 } else {
879 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
880 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
881 }
882 iconAngle = 90-fromAngleDeg;
883 }
884 if(pFrom.x < pVia.x && pFrom.y < pVia.y) {
885 if(!leftHandTraffic) {
886 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
887 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
888 } else {
889 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
890 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
891 }
892 iconAngle = 90+fromAngleDeg;
893 }
894 if(pFrom.x >= pVia.x && pFrom.y < pVia.y) {
895 if(!leftHandTraffic) {
896 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
897 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
898 } else {
899 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
900 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
901 }
902 iconAngle = 270-fromAngleDeg;
903 }
904
905 drawRestriction(inactive || r.isDisabled() ? icon.getDisabledIcon() : icon.icon,
906 pVia, vx, vx2, vy, vy2, iconAngle, r.isSelected());
907 }
908
909 public void drawVirtualNodes(Collection<Way> ways) {
910
911 if (virtualNodeSize != 0) {
912 GeneralPath path = new GeneralPath();
913 for (Way osm: ways){
914 if (osm.isUsable() && !osm.isDisabled()) {
915 visitVirtual(path, osm);
916 }
917 }
918 g.setColor(nodeColor);
919 g.draw(path);
920 }
921 }
922
923 public void visitVirtual(GeneralPath path, Way w) {
924 Iterator<Node> it = w.getNodes().iterator();
925 if (it.hasNext()) {
926 Point lastP = nc.getPoint(it.next());
927 while(it.hasNext())
928 {
929 Point p = nc.getPoint(it.next());
930 if(isSegmentVisible(lastP, p) && isLargeSegment(lastP, p, virtualNodeSpace))
931 {
932 int x = (p.x+lastP.x)/2;
933 int y = (p.y+lastP.y)/2;
934 path.moveTo(x-virtualNodeSize, y);
935 path.lineTo(x+virtualNodeSize, y);
936 path.moveTo(x, y-virtualNodeSize);
937 path.lineTo(x, y+virtualNodeSize);
938 }
939 lastP = p;
940 }
941 }
942 }
943
944 private static boolean isLargeSegment(Point p1, Point p2, int space) {
945 int xd = p1.x-p2.x; if(xd < 0) {
946 xd = -xd;
947 }
948 int yd = p1.y-p2.y; if(yd < 0) {
949 yd = -yd;
950 }
951 return (xd+yd > space);
952 }
953
954 /**
955 * Draw a number of the order of the two consecutive nodes within the
956 * parents way
957 */
958 public void drawOrderNumber(Node n1, Node n2, int orderNumber, Color clr) {
959 Point p1 = nc.getPoint(n1);
960 Point p2 = nc.getPoint(n2);
961 drawOrderNumber(p1, p2, orderNumber, clr);
962 }
963
964 /**
965 * Draw an number of the order of the two consecutive nodes within the
966 * parents way
967 */
968 protected void drawOrderNumber(Point p1, Point p2, int orderNumber, Color clr) {
969 if (isSegmentVisible(p1, p2) && isLargeSegment(p1, p2, segmentNumberSpace)) {
970 String on = Integer.toString(orderNumber);
971 int strlen = on.length();
972 int x = (p1.x+p2.x)/2 - 4*strlen;
973 int y = (p1.y+p2.y)/2 + 4;
974
975 if(virtualNodeSize != 0 && isLargeSegment(p1, p2, virtualNodeSpace))
976 {
977 y = (p1.y+p2.y)/2 - virtualNodeSize - 3;
978 }
979
980 g.setColor(backgroundColor);
981 g.fillRect(x-1, y-12, 8*strlen+1, 14);
982 g.setColor(clr);
983 g.drawString(on, x, y);
984 }
985 }
986
987 public boolean isInactive() {
988 return inactive;
989 }
990
991 public boolean isShowNames() {
992 return showNames;
993 }
994
995 public double getCircum() {
996 return circum;
997 }
998
999 public boolean isShowIcons() {
1000 return showIcons;
1001 }
1002
1003}
Note: See TracBrowser for help on using the repository browser.