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

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

see #6797: load map images in background, in case they are loaded over a network. Show temporary image in the meantime.

  • Property svn:eol-style set to native
File size: 44.9 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.Path2D;
22import java.awt.geom.Point2D;
23import java.awt.geom.Rectangle2D;
24import java.util.Arrays;
25import java.util.Collection;
26import java.util.Iterator;
27import java.util.List;
28
29import javax.swing.ImageIcon;
30
31import org.openstreetmap.josm.Main;
32import org.openstreetmap.josm.data.coor.EastNorth;
33import org.openstreetmap.josm.data.osm.Node;
34import org.openstreetmap.josm.data.osm.OsmPrimitive;
35import org.openstreetmap.josm.data.osm.OsmUtils;
36import org.openstreetmap.josm.data.osm.Relation;
37import org.openstreetmap.josm.data.osm.RelationMember;
38import org.openstreetmap.josm.data.osm.Way;
39import org.openstreetmap.josm.data.osm.WaySegment;
40import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
41import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData;
42import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
43import org.openstreetmap.josm.gui.NavigatableComponent;
44import org.openstreetmap.josm.gui.mappaint.BoxTextElemStyle;
45import org.openstreetmap.josm.gui.mappaint.BoxTextElemStyle.HorizontalTextAlignment;
46import org.openstreetmap.josm.gui.mappaint.BoxTextElemStyle.VerticalTextAlignment;
47import org.openstreetmap.josm.gui.mappaint.MapImage;
48import org.openstreetmap.josm.gui.mappaint.NodeElemStyle.Symbol;
49import org.openstreetmap.josm.gui.mappaint.TextElement;
50import org.openstreetmap.josm.tools.ImageProvider;
51import org.openstreetmap.josm.tools.Pair;
52import org.openstreetmap.josm.tools.Utils;
53
54public class MapPainter {
55
56 private final Graphics2D g;
57 private final NavigatableComponent nc;
58 private final boolean inactive;
59 private final MapPaintSettings settings;
60 private final Collection<WaySegment> highlightWaySegments;
61
62 private final boolean useStrokes;
63 private final boolean showNames;
64 private final boolean showIcons;
65
66 private final boolean isOutlineOnly;
67
68 private final Color inactiveColor;
69 private final Color selectedColor;
70 private final Color relationSelectedColor;
71 private final Color nodeColor;
72 private final Color highlightColor;
73 private final Color highlightColorTransparent;
74 private final Color backgroundColor;
75
76 private final Font orderFont;
77 private final int virtualNodeSize;
78 private final int virtualNodeSpace;
79 private final int segmentNumberSpace;
80
81 private final double circum;
82
83 private final boolean leftHandTraffic;
84
85 private static final double PHI = Math.toRadians(20);
86 private static final double cosPHI = Math.cos(PHI);
87 private static final double sinPHI = Math.sin(PHI);
88
89 public MapPainter(MapPaintSettings settings, Graphics2D g,
90 boolean inactive, NavigatableComponent nc, boolean virtual,
91 double circum, boolean leftHandTraffic,
92 Collection<WaySegment> highlightWaySegments){
93 this.settings = settings;
94 this.g = g;
95 this.inactive = inactive;
96 this.nc = nc;
97 this.highlightWaySegments = highlightWaySegments;
98 this.useStrokes = settings.getUseStrokesDistance() > circum;
99 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, int 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 int 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, int 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 (offset == 0) 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 public void drawTextOnPath(Way way, TextElement text) {
405 if (text == null)
406 return;
407 String name = text.getString(way);
408 if (name == null || name.equals(""))
409 return;
410
411 Polygon poly = new Polygon();
412 Point lastPoint = null;
413 Iterator<Node> it = way.getNodes().iterator();
414 double pathLength = 0;
415 long dx, dy;
416 while (it.hasNext()) {
417 Node n = it.next();
418 Point p = nc.getPoint(n);
419 poly.addPoint(p.x, p.y);
420
421 if(lastPoint != null) {
422 dx = p.x - lastPoint.x;
423 dy = p.y - lastPoint.y;
424 pathLength += Math.sqrt(dx*dx + dy*dy);
425 }
426 lastPoint = p;
427 }
428
429 FontMetrics fontMetrics = g.getFontMetrics(text.font); // if slow, use cache
430 Rectangle2D rec = fontMetrics.getStringBounds(name, g); // if slow, approximate by strlen()*maxcharbounds(font)
431
432 if (rec.getWidth() > pathLength)
433 return;
434
435 double t1 = (pathLength/2 - rec.getWidth()/2) / pathLength;
436 double t2 = (pathLength/2 + rec.getWidth()/2) / pathLength;
437
438 double[] p1 = pointAt(t1, poly, pathLength);
439 double[] p2 = pointAt(t2, poly, pathLength);
440
441 double angleOffset;
442 double offsetSign;
443 double tStart;
444
445 if (p1[0] < p2[0] &&
446 p1[2] < Math.PI/2 &&
447 p1[2] > -Math.PI/2) {
448 angleOffset = 0;
449 offsetSign = 1;
450 tStart = t1;
451 } else {
452 angleOffset = Math.PI;
453 offsetSign = -1;
454 tStart = t2;
455 }
456
457 FontRenderContext frc = g.getFontRenderContext();
458 GlyphVector gv = text.font.createGlyphVector(frc, name);
459
460 for (int i=0; i<gv.getNumGlyphs(); ++i) {
461 Rectangle2D rect = gv.getGlyphLogicalBounds(i).getBounds2D();
462 double t = tStart + offsetSign * (rect.getX() + rect.getWidth()/2) / pathLength;
463 double[] p = pointAt(t, poly, pathLength);
464 AffineTransform trfm = AffineTransform.getTranslateInstance(p[0] - rect.getX(), p[1]);
465 trfm.rotate(p[2]+angleOffset);
466 double off = -rect.getY() - rect.getHeight()/2 + text.yOffset;
467 trfm.translate(-rect.getWidth()/2, off);
468 gv.setGlyphTransform(i, trfm);
469 }
470 if (text.haloRadius != null) {
471 Shape textOutline = gv.getOutline();
472 g.setStroke(new BasicStroke(2*text.haloRadius, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND));
473 g.setColor(text.haloColor);
474 g.draw(textOutline);
475 g.setStroke(new BasicStroke());
476 g.setColor(text.color);
477 g.fill(textOutline);
478 } else {
479 g.setColor(text.color);
480 g.drawGlyphVector(gv, 0, 0);
481 }
482 }
483
484 private double[] pointAt(double t, Polygon poly, double pathLength) {
485 double totalLen = t * pathLength;
486 double curLen = 0;
487 long dx, dy;
488 double segLen;
489
490 // Yes, it is ineffecient to iterate from the beginning for each glyph.
491 // Can be optimized if it turns out to be slow.
492 for (int i = 1; i < poly.npoints; ++i) {
493 dx = poly.xpoints[i] - poly.xpoints[i-1];
494 dy = poly.ypoints[i] - poly.ypoints[i-1];
495 segLen = Math.sqrt(dx*dx + dy*dy);
496 if (totalLen > curLen + segLen) {
497 curLen += segLen;
498 continue;
499 }
500 return new double[] {poly.xpoints[i-1]+(totalLen - curLen)/segLen*dx,
501 poly.ypoints[i-1]+(totalLen - curLen)/segLen*dy,
502 Math.atan2(dy, dx)};
503 }
504 return null;
505 }
506
507 public void drawLinePattern(Way way, Image pattern) {
508 final int width = pattern.getWidth(null);
509 final int height = pattern.getHeight(null);
510
511 Point lastP = null;
512 double wayLength = 0;
513
514 Iterator<Node> it = way.getNodes().iterator();
515 while (it.hasNext()) {
516 Node n = it.next();
517 Point thisP = nc.getPoint(n);
518
519 if (lastP != null) {
520 final double segmentLength = thisP.distance(lastP);
521
522 final double dx = thisP.x - lastP.x;
523 final double dy = thisP.y - lastP.y;
524
525 double dist = wayLength == 0 ? 0 : width - (wayLength % width);
526
527 AffineTransform saveTransform = g.getTransform();
528 g.translate(lastP.x, lastP.y);
529 g.rotate(Math.atan2(dy, dx));
530
531 if (dist > 0) {
532 g.drawImage(pattern, 0, 0, (int) dist, height,
533 width - (int) dist, 0, width, height, null);
534 }
535 while (dist < segmentLength) {
536 if (dist + width > segmentLength) {
537 g.drawImage(pattern, (int) dist, 0, (int) segmentLength, height,
538 0, 0, (int) segmentLength - (int) dist, height, null);
539 } else {
540 g.drawImage(pattern, (int) dist, 0, nc);
541 }
542 dist += width;
543 }
544 g.setTransform(saveTransform);
545
546 wayLength += segmentLength;
547 }
548 lastP = thisP;
549 }
550 }
551
552 public void drawNodeIcon(Node n, Image img, float alpha, boolean selected, boolean member) {
553 Point p = nc.getPoint(n);
554
555 final int w = img.getWidth(null), h=img.getHeight(null);
556 if(n.isHighlighted()) {
557 drawPointHighlight(p, Math.max(w, h));
558 }
559
560 if (alpha != 1f) {
561 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
562 }
563 g.drawImage(img, p.x-w/2, p.y-h/2, nc);
564 g.setPaintMode();
565 if (selected || member)
566 {
567 g.setColor(selected? selectedColor : relationSelectedColor);
568 g.drawRect(p.x-w/2-2, p.y-h/2-2, w+4, h+4);
569 }
570 }
571
572 private Polygon buildPolygon(Point center, int radius, int sides, double rotation) {
573 Polygon polygon = new Polygon();
574 for (int i = 0; i < sides; i++) {
575 double angle = ((2 * Math.PI / sides) * i) - rotation;
576 int x = (int) Math.round(center.x + radius * Math.cos(angle));
577 int y = (int) Math.round(center.y + radius * Math.sin(angle));
578 polygon.addPoint(x, y);
579 }
580 return polygon;
581 }
582
583 private Polygon buildPolygon(Point center, int radius, int sides) {
584 return buildPolygon(center, radius, sides, 0.0);
585 }
586
587 public void drawNodeSymbol(Node n, Symbol s, Color fillColor, Color strokeColor) {
588 Point p = nc.getPoint(n);
589 int radius = s.size / 2;
590
591 if(n.isHighlighted()) {
592 drawPointHighlight(p, s.size);
593 }
594
595 if (fillColor != null) {
596 g.setColor(fillColor);
597 switch (s.symbol) {
598 case SQUARE:
599 g.fillRect(p.x - radius, p.y - radius, s.size, s.size);
600 break;
601 case CIRCLE:
602 g.fillOval(p.x - radius, p.y - radius, s.size, s.size);
603 break;
604 case TRIANGLE:
605 g.fillPolygon(buildPolygon(p, radius, 3, Math.PI / 2));
606 break;
607 case PENTAGON:
608 g.fillPolygon(buildPolygon(p, radius, 5, Math.PI / 2));
609 break;
610 case HEXAGON:
611 g.fillPolygon(buildPolygon(p, radius, 6));
612 break;
613 case HEPTAGON:
614 g.fillPolygon(buildPolygon(p, radius, 7, Math.PI / 2));
615 break;
616 case OCTAGON:
617 g.fillPolygon(buildPolygon(p, radius, 8, Math.PI / 8));
618 break;
619 case NONAGON:
620 g.fillPolygon(buildPolygon(p, radius, 9, Math.PI / 2));
621 break;
622 case DECAGON:
623 g.fillPolygon(buildPolygon(p, radius, 10));
624 break;
625 default:
626 throw new AssertionError();
627 }
628 }
629 if (s.stroke != null) {
630 g.setStroke(s.stroke);
631 g.setColor(strokeColor);
632 switch (s.symbol) {
633 case SQUARE:
634 g.drawRect(p.x - radius, p.y - radius, s.size - 1, s.size - 1);
635 break;
636 case CIRCLE:
637 g.drawOval(p.x - radius, p.y - radius, s.size - 1, s.size - 1);
638 break;
639 case TRIANGLE:
640 g.drawPolygon(buildPolygon(p, radius, 3, Math.PI / 2));
641 break;
642 case PENTAGON:
643 g.drawPolygon(buildPolygon(p, radius, 5, Math.PI / 2));
644 break;
645 case HEXAGON:
646 g.drawPolygon(buildPolygon(p, radius, 6));
647 break;
648 case HEPTAGON:
649 g.drawPolygon(buildPolygon(p, radius, 7, Math.PI / 2));
650 break;
651 case OCTAGON:
652 g.drawPolygon(buildPolygon(p, radius, 8, Math.PI / 8));
653 break;
654 case NONAGON:
655 g.drawPolygon(buildPolygon(p, radius, 9, Math.PI / 2));
656 break;
657 case DECAGON:
658 g.drawPolygon(buildPolygon(p, radius, 10));
659 break;
660 default:
661 throw new AssertionError();
662 }
663 g.setStroke(new BasicStroke());
664 }
665 }
666
667 /**
668 * Draw the node as small rectangle with the given color.
669 *
670 * @param n The node to draw.
671 * @param color The color of the node.
672 */
673 public void drawNode(Node n, Color color, int size, boolean fill) {
674 if(size <= 0 && !n.isHighlighted())
675 return;
676
677 Point p = nc.getPoint(n);
678
679 if(n.isHighlighted()) {
680 drawPointHighlight(p, size);
681 }
682
683 if (size > 1) {
684 if ((p.x < 0) || (p.y < 0) || (p.x > nc.getWidth()) || (p.y > nc.getHeight())) return;
685 int radius = size / 2;
686
687 if (inactive || n.isDisabled()) {
688 g.setColor(inactiveColor);
689 } else {
690 g.setColor(color);
691 }
692 if (fill) {
693 g.fillRect(p.x-radius-1, p.y-radius-1, size + 1, size + 1);
694 } else {
695 g.drawRect(p.x-radius-1, p.y-radius-1, size, size);
696 }
697 }
698 }
699
700 /**
701 * highlights a given point by drawing a rounded rectangle around it. Give the
702 * size of the object you want to be highlighted, width is added automatically.
703 */
704 private void drawPointHighlight(Point p, int size) {
705 g.setColor(highlightColorTransparent);
706 int s = size + 7;
707 while(s >= size) {
708 int r = (int) Math.floor(s/2);
709 g.fillRoundRect(p.x-r, p.y-r, s, s, r, r);
710 s -= 4;
711 }
712 }
713
714 public void drawBoxText(Node n, BoxTextElemStyle bs) {
715 if (!isShowNames() || bs == null)
716 return;
717
718 Point p = nc.getPoint(n);
719 TextElement text = bs.text;
720 String s = text.labelCompositionStrategy.compose(n);
721 if (s == null) return;
722
723 Font defaultFont = g.getFont();
724 g.setFont(text.font);
725
726 int x = p.x + text.xOffset;
727 int y = p.y + text.yOffset;
728 /**
729 *
730 * left-above __center-above___ right-above
731 * left-top| |right-top
732 * | |
733 * left-center| center-center |right-center
734 * | |
735 * left-bottom|_________________|right-bottom
736 * left-below center-below right-below
737 *
738 */
739 Rectangle box = bs.getBox();
740 if (bs.hAlign == HorizontalTextAlignment.RIGHT) {
741 x += box.x + box.width + 2;
742 } else {
743 FontRenderContext frc = g.getFontRenderContext();
744 Rectangle2D bounds = text.font.getStringBounds(s, frc);
745 int textWidth = (int) bounds.getWidth();
746 if (bs.hAlign == HorizontalTextAlignment.CENTER) {
747 x -= textWidth / 2;
748 } else if (bs.hAlign == HorizontalTextAlignment.LEFT) {
749 x -= - box.x + 4 + textWidth;
750 } else throw new AssertionError();
751 }
752
753 if (bs.vAlign == VerticalTextAlignment.BOTTOM) {
754 y += box.y + box.height;
755 } else {
756 FontRenderContext frc = g.getFontRenderContext();
757 LineMetrics metrics = text.font.getLineMetrics(s, frc);
758 if (bs.vAlign == VerticalTextAlignment.ABOVE) {
759 y -= - box.y + metrics.getDescent();
760 } else if (bs.vAlign == VerticalTextAlignment.TOP) {
761 y -= - box.y - metrics.getAscent();
762 } else if (bs.vAlign == VerticalTextAlignment.CENTER) {
763 y += (metrics.getAscent() - metrics.getDescent()) / 2;
764 } else if (bs.vAlign == VerticalTextAlignment.BELOW) {
765 y += box.y + box.height + metrics.getAscent() + 2;
766 } else throw new AssertionError();
767 }
768 if (inactive || n.isDisabled()) {
769 g.setColor(inactiveColor);
770 } else {
771 g.setColor(text.color);
772 }
773 if (text.haloRadius != null) {
774 g.setStroke(new BasicStroke(2*text.haloRadius, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND));
775 g.setColor(text.haloColor);
776 FontRenderContext frc = g.getFontRenderContext();
777 GlyphVector gv = text.font.createGlyphVector(frc, s);
778 Shape textOutline = gv.getOutline(x, y);
779 g.draw(textOutline);
780 g.setStroke(new BasicStroke());
781 g.setColor(text.color);
782 g.fill(textOutline);
783 } else {
784 g.drawString(s, x, y);
785 }
786 g.setFont(defaultFont);
787 }
788
789 private Path2D.Double getPath(Way w) {
790 Path2D.Double path = new Path2D.Double();
791 boolean initial = true;
792 for (Node n : w.getNodes())
793 {
794 Point2D p = n.getEastNorth();
795 if (initial) {
796 path.moveTo(p.getX(), p.getY());
797 initial = false;
798 } else {
799 path.lineTo(p.getX(), p.getY());
800 }
801 }
802 return path;
803 }
804
805 public void drawArea(Way w, Color color, MapImage fillImage, TextElement text) {
806 drawArea(w, getPath(w), color, fillImage, text);
807 }
808
809 protected void drawArea(OsmPrimitive osm, Path2D.Double path, Color color, MapImage fillImage, TextElement text) {
810
811 Shape area = path.createTransformedShape(nc.getAffineTransform());
812
813 if (!isOutlineOnly) {
814 if (fillImage == null) {
815 g.setColor(color);
816 g.fill(area);
817 } else {
818 TexturePaint texture = new TexturePaint(fillImage.getImage(),
819 // new Rectangle(polygon.xpoints[0], polygon.ypoints[0], fillImage.getWidth(), fillImage.getHeight()));
820 new Rectangle(0, 0, fillImage.getWidth(), fillImage.getHeight()));
821 g.setPaint(texture);
822 Float alpha = Utils.color_int2float(fillImage.alpha);
823 if (alpha != 1f) {
824 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
825 }
826 g.fill(area);
827 g.setPaintMode();
828 }
829 }
830
831 if (text != null && isShowNames()) {
832 /*
833 * abort if we can't compose the label to be rendered
834 */
835 if (text.labelCompositionStrategy == null) return;
836 String name = text.labelCompositionStrategy.compose(osm);
837 if (name == null) return;
838
839 Rectangle pb = area.getBounds();
840 FontMetrics fontMetrics = g.getFontMetrics(orderFont); // if slow, use cache
841 Rectangle2D nb = fontMetrics.getStringBounds(name, g); // if slow, approximate by strlen()*maxcharbounds(font)
842
843 // Point2D c = getCentroid(polygon);
844 // Using the Centroid is Nicer for buildings like: +--------+
845 // but this needs to be fast. As most houses are | 42 |
846 // boxes anyway, the center of the bounding box +---++---+
847 // will have to do. ++
848 // Centroids are not optimal either, just imagine a U-shaped house.
849 // Point2D c = new Point2D.Double(pb.x + pb.width / 2.0, pb.y + pb.height / 2.0);
850 // Rectangle2D.Double centeredNBounds =
851 // new Rectangle2D.Double(c.getX() - nb.getWidth()/2,
852 // c.getY() - nb.getHeight()/2,
853 // nb.getWidth(),
854 // nb.getHeight());
855
856 Rectangle centeredNBounds = new Rectangle(pb.x + (int)((pb.width - nb.getWidth())/2.0),
857 pb.y + (int)((pb.height - nb.getHeight())/2.0),
858 (int)nb.getWidth(),
859 (int)nb.getHeight());
860
861 if ((pb.width >= nb.getWidth() && pb.height >= nb.getHeight()) && // quick check
862 area.contains(centeredNBounds) // slow but nice
863 ) {
864 g.setColor(text.color);
865 Font defaultFont = g.getFont();
866 g.setFont (text.font);
867 g.drawString (name,
868 (int)(centeredNBounds.getMinX() - nb.getMinX()),
869 (int)(centeredNBounds.getMinY() - nb.getMinY()));
870 g.setFont(defaultFont);
871 }
872 }
873 }
874
875 public void drawArea(Relation r, Color color, MapImage fillImage, TextElement text) {
876 Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, r);
877 if (!r.isDisabled() && !multipolygon.getOuterWays().isEmpty()) {
878 for (PolyData pd : multipolygon.getCombinedPolygons()) {
879 Path2D.Double p = pd.get();
880 if (!isAreaVisible(p)) {
881 continue;
882 }
883 drawArea(r, p,
884 pd.selected ? settings.getRelationSelectedColor(color.getAlpha()) : color,
885 fillImage, text);
886 }
887 }
888 }
889
890 private boolean isAreaVisible(Path2D.Double area) {
891 Rectangle2D bounds = area.getBounds2D();
892 if (bounds.isEmpty()) return false;
893 Point2D p = nc.getPoint2D(new EastNorth(bounds.getX(), bounds.getY()));
894 if (p.getX() > nc.getWidth()) return false;
895 if (p.getY() < 0) return false;
896 p = nc.getPoint2D(new EastNorth(bounds.getX() + bounds.getWidth(), bounds.getY() + bounds.getHeight()));
897 if (p.getX() < 0) return false;
898 if (p.getY() > nc.getHeight()) return false;
899 return true;
900 }
901
902 public void drawRestriction(Image img, Point pVia, double vx, double vx2, double vy, double vy2, double angle, boolean selected) {
903 /* rotate image with direction last node in from to */
904 Image rotatedImg = ImageProvider.createRotatedImage(null , img, angle);
905
906 /* scale down image to 16*16 pixels */
907 Image smallImg = new ImageIcon(rotatedImg.getScaledInstance(16 , 16, Image.SCALE_SMOOTH)).getImage();
908 int w = smallImg.getWidth(null), h=smallImg.getHeight(null);
909 g.drawImage(smallImg, (int)(pVia.x+vx+vx2)-w/2, (int)(pVia.y+vy+vy2)-h/2, nc);
910
911 if (selected) {
912 g.setColor(relationSelectedColor);
913 g.drawRect((int)(pVia.x+vx+vx2)-w/2-2,(int)(pVia.y+vy+vy2)-h/2-2, w+4, h+4);
914 }
915 }
916
917 public void drawRestriction(Relation r, MapImage icon) {
918 Way fromWay = null;
919 Way toWay = null;
920 OsmPrimitive via = null;
921
922 /* find the "from", "via" and "to" elements */
923 for (RelationMember m : r.getMembers())
924 {
925 if(m.getMember().isIncomplete())
926 return;
927 else
928 {
929 if(m.isWay())
930 {
931 Way w = m.getWay();
932 if(w.getNodesCount() < 2) {
933 continue;
934 }
935
936 if("from".equals(m.getRole())) {
937 if(fromWay == null) {
938 fromWay = w;
939 }
940 } else if("to".equals(m.getRole())) {
941 if(toWay == null) {
942 toWay = w;
943 }
944 } else if("via".equals(m.getRole())) {
945 if(via == null) {
946 via = w;
947 }
948 }
949 }
950 else if(m.isNode())
951 {
952 Node n = m.getNode();
953 if("via".equals(m.getRole()) && via == null) {
954 via = n;
955 }
956 }
957 }
958 }
959
960 if (fromWay == null || toWay == null || via == null)
961 return;
962
963 Node viaNode;
964 if(via instanceof Node)
965 {
966 viaNode = (Node) via;
967 if(!fromWay.isFirstLastNode(viaNode))
968 return;
969 }
970 else
971 {
972 Way viaWay = (Way) via;
973 Node firstNode = viaWay.firstNode();
974 Node lastNode = viaWay.lastNode();
975 Boolean onewayvia = false;
976
977 String onewayviastr = viaWay.get("oneway");
978 if(onewayviastr != null)
979 {
980 if("-1".equals(onewayviastr)) {
981 onewayvia = true;
982 Node tmp = firstNode;
983 firstNode = lastNode;
984 lastNode = tmp;
985 } else {
986 onewayvia = OsmUtils.getOsmBoolean(onewayviastr);
987 if (onewayvia == null) {
988 onewayvia = false;
989 }
990 }
991 }
992
993 if(fromWay.isFirstLastNode(firstNode)) {
994 viaNode = firstNode;
995 } else if (!onewayvia && fromWay.isFirstLastNode(lastNode)) {
996 viaNode = lastNode;
997 } else
998 return;
999 }
1000
1001 /* find the "direct" nodes before the via node */
1002 Node fromNode = null;
1003 if(fromWay.firstNode() == via) {
1004 fromNode = fromWay.getNode(1);
1005 } else {
1006 fromNode = fromWay.getNode(fromWay.getNodesCount()-2);
1007 }
1008
1009 Point pFrom = nc.getPoint(fromNode);
1010 Point pVia = nc.getPoint(viaNode);
1011
1012 /* starting from via, go back the "from" way a few pixels
1013 (calculate the vector vx/vy with the specified length and the direction
1014 away from the "via" node along the first segment of the "from" way)
1015 */
1016 double distanceFromVia=14;
1017 double dx = (pFrom.x >= pVia.x) ? (pFrom.x - pVia.x) : (pVia.x - pFrom.x);
1018 double dy = (pFrom.y >= pVia.y) ? (pFrom.y - pVia.y) : (pVia.y - pFrom.y);
1019
1020 double fromAngle;
1021 if(dx == 0.0) {
1022 fromAngle = Math.PI/2;
1023 } else {
1024 fromAngle = Math.atan(dy / dx);
1025 }
1026 double fromAngleDeg = Math.toDegrees(fromAngle);
1027
1028 double vx = distanceFromVia * Math.cos(fromAngle);
1029 double vy = distanceFromVia * Math.sin(fromAngle);
1030
1031 if(pFrom.x < pVia.x) {
1032 vx = -vx;
1033 }
1034 if(pFrom.y < pVia.y) {
1035 vy = -vy;
1036 }
1037
1038 /* go a few pixels away from the way (in a right angle)
1039 (calculate the vx2/vy2 vector with the specified length and the direction
1040 90degrees away from the first segment of the "from" way)
1041 */
1042 double distanceFromWay=10;
1043 double vx2 = 0;
1044 double vy2 = 0;
1045 double iconAngle = 0;
1046
1047 if(pFrom.x >= pVia.x && pFrom.y >= pVia.y) {
1048 if(!leftHandTraffic) {
1049 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
1050 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
1051 } else {
1052 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
1053 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
1054 }
1055 iconAngle = 270+fromAngleDeg;
1056 }
1057 if(pFrom.x < pVia.x && pFrom.y >= pVia.y) {
1058 if(!leftHandTraffic) {
1059 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
1060 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
1061 } else {
1062 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
1063 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
1064 }
1065 iconAngle = 90-fromAngleDeg;
1066 }
1067 if(pFrom.x < pVia.x && pFrom.y < pVia.y) {
1068 if(!leftHandTraffic) {
1069 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
1070 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
1071 } else {
1072 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
1073 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
1074 }
1075 iconAngle = 90+fromAngleDeg;
1076 }
1077 if(pFrom.x >= pVia.x && pFrom.y < pVia.y) {
1078 if(!leftHandTraffic) {
1079 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
1080 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
1081 } else {
1082 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
1083 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
1084 }
1085 iconAngle = 270-fromAngleDeg;
1086 }
1087
1088 drawRestriction(inactive || r.isDisabled() ? icon.getDisabled() : icon.getImage(),
1089 pVia, vx, vx2, vy, vy2, iconAngle, r.isSelected());
1090 }
1091
1092 public void drawVirtualNodes(Collection<Way> ways, Collection<WaySegment> highlightVirtualNodes) {
1093 if (virtualNodeSize == 0)
1094 return;
1095 // print normal virtual nodes
1096 GeneralPath path = new GeneralPath();
1097 for (Way osm: ways){
1098 if (osm.isUsable() && !osm.isDisabled()) {
1099 visitVirtual(path, osm);
1100 }
1101 }
1102 g.setColor(nodeColor);
1103 g.draw(path);
1104 // print highlighted virtual nodes. Since only the color changes, simply
1105 // drawing them over the existing ones works fine (at least in their current
1106 // simple style)
1107 path = new GeneralPath();
1108 for (WaySegment wseg: highlightVirtualNodes){
1109 if (wseg.way.isUsable() && !wseg.way.isDisabled()) {
1110 visitVirtual(path, wseg.toWay());
1111 }
1112 }
1113 g.setColor(highlightColor);
1114 g.draw(path);
1115 }
1116
1117 public void visitVirtual(GeneralPath path, Way w) {
1118 Iterator<Node> it = w.getNodes().iterator();
1119 if (it.hasNext()) {
1120 Point lastP = nc.getPoint(it.next());
1121 while(it.hasNext())
1122 {
1123 Point p = nc.getPoint(it.next());
1124 if(isSegmentVisible(lastP, p) && isLargeSegment(lastP, p, virtualNodeSpace))
1125 {
1126 int x = (p.x+lastP.x)/2;
1127 int y = (p.y+lastP.y)/2;
1128 path.moveTo(x-virtualNodeSize, y);
1129 path.lineTo(x+virtualNodeSize, y);
1130 path.moveTo(x, y-virtualNodeSize);
1131 path.lineTo(x, y+virtualNodeSize);
1132 }
1133 lastP = p;
1134 }
1135 }
1136 }
1137
1138 private static boolean isLargeSegment(Point p1, Point p2, int space) {
1139 int xd = p1.x-p2.x; if(xd < 0) {
1140 xd = -xd;
1141 }
1142 int yd = p1.y-p2.y; if(yd < 0) {
1143 yd = -yd;
1144 }
1145 return (xd+yd > space);
1146 }
1147
1148 /**
1149 * Draw a number of the order of the two consecutive nodes within the
1150 * parents way
1151 */
1152 public void drawOrderNumber(Node n1, Node n2, int orderNumber, Color clr) {
1153 Point p1 = nc.getPoint(n1);
1154 Point p2 = nc.getPoint(n2);
1155 drawOrderNumber(p1, p2, orderNumber, clr);
1156 }
1157
1158 /**
1159 * Draw an number of the order of the two consecutive nodes within the
1160 * parents way
1161 */
1162 protected void drawOrderNumber(Point p1, Point p2, int orderNumber, Color clr) {
1163 if (isSegmentVisible(p1, p2) && isLargeSegment(p1, p2, segmentNumberSpace)) {
1164 String on = Integer.toString(orderNumber);
1165 int strlen = on.length();
1166 int x = (p1.x+p2.x)/2 - 4*strlen;
1167 int y = (p1.y+p2.y)/2 + 4;
1168
1169 if(virtualNodeSize != 0 && isLargeSegment(p1, p2, virtualNodeSpace))
1170 {
1171 y = (p1.y+p2.y)/2 - virtualNodeSize - 3;
1172 }
1173
1174 g.setColor(backgroundColor);
1175 g.fillRect(x-1, y-12, 8*strlen+1, 14);
1176 g.setColor(clr);
1177 g.drawString(on, x, y);
1178 }
1179 }
1180
1181 public boolean isShowNames() {
1182 return showNames;
1183 }
1184
1185 public double getCircum() {
1186 return circum;
1187 }
1188
1189 public boolean isShowIcons() {
1190 return showIcons;
1191 }
1192
1193 public boolean isInactiveMode() {
1194 return inactive;
1195 }
1196}
Note: See TracBrowser for help on using the repository browser.