source: josm/trunk/src/org/openstreetmap/josm/data/osm/visitor/paint/StyledMapRenderer.java@ 7005

Last change on this file since 7005 was 7005, checked in by Don-vip, 10 years ago

see #8465 - use diamond operator where applicable

  • Property svn:eol-style set to native
File size: 53.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.Component;
8import java.awt.Dimension;
9import java.awt.Font;
10import java.awt.FontMetrics;
11import java.awt.Graphics2D;
12import java.awt.Image;
13import java.awt.Point;
14import java.awt.Polygon;
15import java.awt.Rectangle;
16import java.awt.RenderingHints;
17import java.awt.Shape;
18import java.awt.TexturePaint;
19import java.awt.font.FontRenderContext;
20import java.awt.font.GlyphVector;
21import java.awt.font.LineMetrics;
22import java.awt.geom.AffineTransform;
23import java.awt.geom.GeneralPath;
24import java.awt.geom.Path2D;
25import java.awt.geom.Point2D;
26import java.awt.geom.Rectangle2D;
27import java.util.ArrayList;
28import java.util.Collection;
29import java.util.Collections;
30import java.util.Iterator;
31import java.util.List;
32
33import javax.swing.AbstractButton;
34import javax.swing.FocusManager;
35
36import org.openstreetmap.josm.Main;
37import org.openstreetmap.josm.data.Bounds;
38import org.openstreetmap.josm.data.coor.EastNorth;
39import org.openstreetmap.josm.data.osm.BBox;
40import org.openstreetmap.josm.data.osm.DataSet;
41import org.openstreetmap.josm.data.osm.Node;
42import org.openstreetmap.josm.data.osm.OsmPrimitive;
43import org.openstreetmap.josm.data.osm.OsmUtils;
44import org.openstreetmap.josm.data.osm.Relation;
45import org.openstreetmap.josm.data.osm.RelationMember;
46import org.openstreetmap.josm.data.osm.Way;
47import org.openstreetmap.josm.data.osm.WaySegment;
48import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
49import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData;
50import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
51import org.openstreetmap.josm.gui.NavigatableComponent;
52import org.openstreetmap.josm.gui.mappaint.AreaElemStyle;
53import org.openstreetmap.josm.gui.mappaint.BoxTextElemStyle;
54import org.openstreetmap.josm.gui.mappaint.BoxTextElemStyle.HorizontalTextAlignment;
55import org.openstreetmap.josm.gui.mappaint.BoxTextElemStyle.VerticalTextAlignment;
56import org.openstreetmap.josm.gui.mappaint.ElemStyle;
57import org.openstreetmap.josm.gui.mappaint.ElemStyles;
58import org.openstreetmap.josm.gui.mappaint.MapImage;
59import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
60import org.openstreetmap.josm.gui.mappaint.NodeElemStyle;
61import org.openstreetmap.josm.gui.mappaint.NodeElemStyle.Symbol;
62import org.openstreetmap.josm.gui.mappaint.RepeatImageElemStyle.LineImageAlignment;
63import org.openstreetmap.josm.gui.mappaint.StyleCache.StyleList;
64import org.openstreetmap.josm.gui.mappaint.TextElement;
65import org.openstreetmap.josm.tools.ImageProvider;
66import org.openstreetmap.josm.tools.Utils;
67
68/**
69 * <p>A map renderer which renders a map according to style rules in a set of style sheets.</p>
70 *
71 */
72public class StyledMapRenderer extends AbstractMapRenderer {
73
74 /**
75 * Iterates over a list of Way Nodes and returns screen coordinates that
76 * represent a line that is shifted by a certain offset perpendicular
77 * to the way direction.
78 *
79 * There is no intention, to handle consecutive duplicate Nodes in a
80 * perfect way, but it is should not throw an exception.
81 */
82 private class OffsetIterator implements Iterator<Point> {
83
84 private List<Node> nodes;
85 private float offset;
86 private int idx;
87
88 private Point prev = null;
89 /* 'prev0' is a point that has distance 'offset' from 'prev' and the
90 * line from 'prev' to 'prev0' is perpendicular to the way segment from
91 * 'prev' to the next point.
92 */
93 private int x_prev0, y_prev0;
94
95 public OffsetIterator(List<Node> nodes, float offset) {
96 this.nodes = nodes;
97 this.offset = offset;
98 idx = 0;
99 }
100
101 @Override
102 public boolean hasNext() {
103 return idx < nodes.size();
104 }
105
106 @Override
107 public Point next() {
108 if (Math.abs(offset) < 0.1f) return nc.getPoint(nodes.get(idx++));
109
110 Point current = nc.getPoint(nodes.get(idx));
111
112 if (idx == nodes.size() - 1) {
113 ++idx;
114 if (prev != null) {
115 return new Point(x_prev0 + current.x - prev.x, y_prev0 + current.y - prev.y);
116 } else {
117 return current;
118 }
119 }
120
121 Point next = nc.getPoint(nodes.get(idx+1));
122
123 int dx_next = next.x - current.x;
124 int dy_next = next.y - current.y;
125 double len_next = Math.sqrt(dx_next*dx_next + dy_next*dy_next);
126
127 if (len_next == 0) {
128 len_next = 1; // value does not matter, because dy_next and dx_next is 0
129 }
130
131 int x_current0 = current.x + (int) Math.round(offset * dy_next / len_next);
132 int y_current0 = current.y - (int) Math.round(offset * dx_next / len_next);
133
134 if (idx==0) {
135 ++idx;
136 prev = current;
137 x_prev0 = x_current0;
138 y_prev0 = y_current0;
139 return new Point(x_current0, y_current0);
140 } else {
141 int dx_prev = current.x - prev.x;
142 int dy_prev = current.y - prev.y;
143
144 // determine intersection of the lines parallel to the two
145 // segments
146 int det = dx_next*dy_prev - dx_prev*dy_next;
147
148 if (det == 0) {
149 ++idx;
150 prev = current;
151 x_prev0 = x_current0;
152 y_prev0 = y_current0;
153 return new Point(x_current0, y_current0);
154 }
155
156 int m = dx_next*(y_current0 - y_prev0) - dy_next*(x_current0 - x_prev0);
157
158 int cx_ = x_prev0 + Math.round((float)m * dx_prev / det);
159 int cy_ = y_prev0 + Math.round((float)m * dy_prev / det);
160 ++idx;
161 prev = current;
162 x_prev0 = x_current0;
163 y_prev0 = y_current0;
164 return new Point(cx_, cy_);
165 }
166 }
167
168 @Override
169 public void remove() {
170 throw new UnsupportedOperationException();
171 }
172 }
173
174 private class StyleCollector {
175 private final boolean drawArea;
176 private final boolean drawMultipolygon;
177 private final boolean drawRestriction;
178
179 private final List<StyleRecord> styleElems;
180
181 public StyleCollector(boolean drawArea, boolean drawMultipolygon, boolean drawRestriction) {
182 this.drawArea = drawArea;
183 this.drawMultipolygon = drawMultipolygon;
184 this.drawRestriction = drawRestriction;
185 styleElems = new ArrayList<>();
186 }
187
188 public void add(Node osm, int flags) {
189 StyleList sl = styles.get(osm, circum, nc);
190 for (ElemStyle s : sl) {
191 styleElems.add(new StyleRecord(s, osm, flags));
192 }
193 }
194
195 public void add(Relation osm, int flags) {
196 StyleList sl = styles.get(osm, circum, nc);
197 for (ElemStyle s : sl) {
198 if (drawMultipolygon && drawArea && s instanceof AreaElemStyle && (flags & FLAG_DISABLED) == 0) {
199 styleElems.add(new StyleRecord(s, osm, flags));
200 } else if (drawRestriction && s instanceof NodeElemStyle) {
201 styleElems.add(new StyleRecord(s, osm, flags));
202 }
203 }
204 }
205
206 public void add(Way osm, int flags) {
207 StyleList sl = styles.get(osm, circum, nc);
208 for (ElemStyle s : sl) {
209 if (!(drawArea && (flags & FLAG_DISABLED) == 0) && s instanceof AreaElemStyle) {
210 continue;
211 }
212 styleElems.add(new StyleRecord(s, osm, flags));
213 }
214 }
215
216 public void drawAll() {
217 Collections.sort(styleElems);
218 for (StyleRecord r : styleElems) {
219 r.style.paintPrimitive(
220 r.osm,
221 paintSettings,
222 StyledMapRenderer.this,
223 (r.flags & FLAG_SELECTED) != 0,
224 (r.flags & FLAG_MEMBER_OF_SELECTED) != 0
225 );
226 }
227 }
228 }
229
230 private static class StyleRecord implements Comparable<StyleRecord> {
231 final ElemStyle style;
232 final OsmPrimitive osm;
233 final int flags;
234
235 public StyleRecord(ElemStyle style, OsmPrimitive osm, int flags) {
236 this.style = style;
237 this.osm = osm;
238 this.flags = flags;
239 }
240
241 @Override
242 public int compareTo(StyleRecord other) {
243 if ((this.flags & FLAG_DISABLED) != 0 && (other.flags & FLAG_DISABLED) == 0)
244 return -1;
245 if ((this.flags & FLAG_DISABLED) == 0 && (other.flags & FLAG_DISABLED) != 0)
246 return 1;
247
248 int d0 = Float.compare(this.style.major_z_index, other.style.major_z_index);
249 if (d0 != 0)
250 return d0;
251
252 // selected on top of member of selected on top of unselected
253 // FLAG_DISABLED bit is the same at this point
254 if (this.flags > other.flags)
255 return 1;
256 if (this.flags < other.flags)
257 return -1;
258
259 int dz = Float.compare(this.style.z_index, other.style.z_index);
260 if (dz != 0)
261 return dz;
262
263 // simple node on top of icons and shapes
264 if (this.style == NodeElemStyle.SIMPLE_NODE_ELEMSTYLE && other.style != NodeElemStyle.SIMPLE_NODE_ELEMSTYLE)
265 return 1;
266 if (this.style != NodeElemStyle.SIMPLE_NODE_ELEMSTYLE && other.style == NodeElemStyle.SIMPLE_NODE_ELEMSTYLE)
267 return -1;
268
269 // newer primitives to the front
270 long id = this.osm.getUniqueId() - other.osm.getUniqueId();
271 if (id > 0)
272 return 1;
273 if (id < 0)
274 return -1;
275
276 return Float.compare(this.style.object_z_index, other.style.object_z_index);
277 }
278 }
279
280 private static Boolean IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG = null;
281
282 /**
283 * Check, if this System has the GlyphVector double translation bug.
284 *
285 * With this bug, <code>gv.setGlyphTransform(i, trfm)</code> has a different
286 * effect than on most other systems, namely the translation components
287 * ("m02" &amp; "m12", {@link AffineTransform}) appear to be twice as large, as
288 * they actually are. The rotation is unaffected (scale &amp; shear not tested
289 * so far).
290 *
291 * This bug has only been observed on Mac OS X, see #7841.
292 *
293 * @return true, if the GlyphVector double translation bug is present on
294 * this System
295 */
296 public static boolean isGlyphVectorDoubleTranslationBug() {
297 if (IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG != null)
298 return IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG;
299 FontRenderContext frc = new FontRenderContext(null, false, false);
300 Font font = new Font("Dialog", Font.PLAIN, 12);
301 GlyphVector gv = font.createGlyphVector(frc, "x");
302 gv.setGlyphTransform(0, AffineTransform.getTranslateInstance(1000, 1000));
303 Shape shape = gv.getGlyphOutline(0);
304 // x is about 1000 on normal stystems and about 2000 when the bug occurs
305 int x = shape.getBounds().x;
306 IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG = x > 1500;
307 return IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG;
308 }
309
310 private ElemStyles styles;
311 private double circum;
312
313 private MapPaintSettings paintSettings;
314
315 private Color relationSelectedColor;
316 private Color highlightColorTransparent;
317
318 private static final int FLAG_NORMAL = 0;
319 private static final int FLAG_DISABLED = 1;
320 private static final int FLAG_MEMBER_OF_SELECTED = 2;
321 private static final int FLAG_SELECTED = 4;
322
323 private static final double PHI = Math.toRadians(20);
324 private static final double cosPHI = Math.cos(PHI);
325 private static final double sinPHI = Math.sin(PHI);
326
327 private Collection<WaySegment> highlightWaySegments;
328
329 // highlight customization fields
330 private int highlightLineWidth;
331 private int highlightPointRadius;
332 private int widerHighlight;
333 private int highlightStep;
334
335 //flag that activate wider highlight mode
336 private boolean useWiderHighlight;
337
338 private boolean useStrokes;
339 private boolean showNames;
340 private boolean showIcons;
341 private boolean isOutlineOnly;
342
343 private Font orderFont;
344
345 private boolean leftHandTraffic;
346
347 public StyledMapRenderer(Graphics2D g, NavigatableComponent nc, boolean isInactiveMode) {
348 super(g, nc, isInactiveMode);
349
350 if (nc!=null) {
351 Component focusOwner = FocusManager.getCurrentManager().getFocusOwner();
352 useWiderHighlight = !(focusOwner instanceof AbstractButton || focusOwner == nc);
353 }
354 }
355
356 private Polygon buildPolygon(Point center, int radius, int sides) {
357 return buildPolygon(center, radius, sides, 0.0);
358 }
359
360 private Polygon buildPolygon(Point center, int radius, int sides, double rotation) {
361 Polygon polygon = new Polygon();
362 for (int i = 0; i < sides; i++) {
363 double angle = ((2 * Math.PI / sides) * i) - rotation;
364 int x = (int) Math.round(center.x + radius * Math.cos(angle));
365 int y = (int) Math.round(center.y + radius * Math.sin(angle));
366 polygon.addPoint(x, y);
367 }
368 return polygon;
369 }
370
371 private void collectNodeStyles(DataSet data, StyleCollector sc, BBox bbox) {
372 for (final Node n: data.searchNodes(bbox)) {
373 if (n.isDrawable()) {
374 if (n.isDisabled()) {
375 sc.add(n, FLAG_DISABLED);
376 } else if (data.isSelected(n)) {
377 sc.add(n, FLAG_SELECTED);
378 } else if (n.isMemberOfSelected()) {
379 sc.add(n, FLAG_MEMBER_OF_SELECTED);
380 } else {
381 sc.add(n, FLAG_NORMAL);
382 }
383 }
384 }
385 }
386
387 private void collectWayStyles(DataSet data, StyleCollector sc, BBox bbox) {
388 for (final Way w : data.searchWays(bbox)) {
389 if (w.isDrawable()) {
390 if (w.isDisabled()) {
391 sc.add(w, FLAG_DISABLED);
392 } else if (data.isSelected(w)) {
393 sc.add(w, FLAG_SELECTED);
394 } else if (w.isMemberOfSelected()) {
395 sc.add(w, FLAG_MEMBER_OF_SELECTED);
396 } else {
397 sc.add(w, FLAG_NORMAL);
398 }
399 }
400 }
401 }
402
403 private void collectRelationStyles(DataSet data, StyleCollector sc, BBox bbox) {
404 for (Relation r: data.searchRelations(bbox)) {
405 if (r.isDrawable()) {
406 if (r.isDisabled()) {
407 sc.add(r, FLAG_DISABLED);
408 } else if (data.isSelected(r)) {
409 sc.add(r, FLAG_SELECTED);
410 } else {
411 sc.add(r, FLAG_NORMAL);
412 }
413 }
414 }
415 }
416
417 private void displaySegments(GeneralPath path, GeneralPath orientationArrows, GeneralPath onewayArrows, GeneralPath onewayArrowsCasing,
418 Color color, BasicStroke line, BasicStroke dashes, Color dashedColor) {
419 g.setColor(isInactiveMode ? inactiveColor : color);
420 if (useStrokes) {
421 g.setStroke(line);
422 }
423 g.draw(path);
424
425 if(!isInactiveMode && useStrokes && dashes != null) {
426 g.setColor(dashedColor);
427 g.setStroke(dashes);
428 g.draw(path);
429 }
430
431 if (orientationArrows != null) {
432 g.setColor(isInactiveMode ? inactiveColor : color);
433 g.setStroke(new BasicStroke(line.getLineWidth(), line.getEndCap(), BasicStroke.JOIN_MITER, line.getMiterLimit()));
434 g.draw(orientationArrows);
435 }
436
437 if (onewayArrows != null) {
438 g.setStroke(new BasicStroke(1, line.getEndCap(), BasicStroke.JOIN_MITER, line.getMiterLimit()));
439 g.fill(onewayArrowsCasing);
440 g.setColor(isInactiveMode ? inactiveColor : backgroundColor);
441 g.fill(onewayArrows);
442 }
443
444 if (useStrokes) {
445 g.setStroke(new BasicStroke());
446 }
447 }
448
449 /**
450 * Displays text at specified position including its halo, if applicable.
451 *
452 * @param gv Text's glyphs to display. If {@code null}, use text from {@code s} instead.
453 * @param s text to display if {@code gv} is {@code null}
454 * @param x X position
455 * @param y Y position
456 * @param disabled {@code true} if element is disabled (filtered out)
457 * @param text text style to use
458 */
459 private void displayText(GlyphVector gv, String s, int x, int y, boolean disabled, TextElement text) {
460 if (isInactiveMode || disabled) {
461 g.setColor(inactiveColor);
462 if (gv != null) {
463 g.drawGlyphVector(gv, x, y);
464 } else {
465 g.setFont(text.font);
466 g.drawString(s, x, y);
467 }
468 } else if (text.haloRadius != null) {
469 g.setStroke(new BasicStroke(2*text.haloRadius, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND));
470 g.setColor(text.haloColor);
471 if (gv == null) {
472 FontRenderContext frc = g.getFontRenderContext();
473 gv = text.font.createGlyphVector(frc, s);
474 }
475 Shape textOutline = gv.getOutline(x, y);
476 g.draw(textOutline);
477 g.setStroke(new BasicStroke());
478 g.setColor(text.color);
479 g.fill(textOutline);
480 } else {
481 g.setColor(text.color);
482 if (gv != null) {
483 g.drawGlyphVector(gv, x, y);
484 } else {
485 g.setFont(text.font);
486 g.drawString(s, x, y);
487 }
488 }
489 }
490
491 protected void drawArea(OsmPrimitive osm, Path2D.Double path, Color color, MapImage fillImage, TextElement text) {
492
493 Shape area = path.createTransformedShape(nc.getAffineTransform());
494
495 if (!isOutlineOnly) {
496 if (fillImage == null) {
497 if (isInactiveMode) {
498 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.33f));
499 }
500 g.setColor(color);
501 g.fill(area);
502 } else {
503 TexturePaint texture = new TexturePaint(fillImage.getImage(),
504 new Rectangle(0, 0, fillImage.getWidth(), fillImage.getHeight()));
505 g.setPaint(texture);
506 Float alpha = Utils.color_int2float(fillImage.alpha);
507 if (alpha != 1f) {
508 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
509 }
510 g.fill(area);
511 g.setPaintMode();
512 }
513 }
514
515 if (text != null && isShowNames()) {
516 // abort if we can't compose the label to be rendered
517 if (text.labelCompositionStrategy == null) return;
518 String name = text.labelCompositionStrategy.compose(osm);
519 if (name == null) return;
520
521 Rectangle pb = area.getBounds();
522 FontMetrics fontMetrics = g.getFontMetrics(orderFont); // if slow, use cache
523 Rectangle2D nb = fontMetrics.getStringBounds(name, g); // if slow, approximate by strlen()*maxcharbounds(font)
524
525 // Using the Centroid is Nicer for buildings like: +--------+
526 // but this needs to be fast. As most houses are | 42 |
527 // boxes anyway, the center of the bounding box +---++---+
528 // will have to do. ++
529 // Centroids are not optimal either, just imagine a U-shaped house.
530
531 Rectangle centeredNBounds = new Rectangle(pb.x + (int)((pb.width - nb.getWidth())/2.0),
532 pb.y + (int)((pb.height - nb.getHeight())/2.0),
533 (int)nb.getWidth(),
534 (int)nb.getHeight());
535
536 if ((pb.width >= nb.getWidth() && pb.height >= nb.getHeight()) && // quick check
537 area.contains(centeredNBounds) // slow but nice
538 ) {
539 Font defaultFont = g.getFont();
540 int x = (int)(centeredNBounds.getMinX() - nb.getMinX());
541 int y = (int)(centeredNBounds.getMinY() - nb.getMinY());
542 displayText(null, name, x, y, osm.isDisabled(), text);
543 g.setFont(defaultFont);
544 }
545 }
546 }
547
548 public void drawArea(Relation r, Color color, MapImage fillImage, TextElement text) {
549 Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, r);
550 if (!r.isDisabled() && !multipolygon.getOuterWays().isEmpty()) {
551 for (PolyData pd : multipolygon.getCombinedPolygons()) {
552 Path2D.Double p = pd.get();
553 if (!isAreaVisible(p)) {
554 continue;
555 }
556 drawArea(r, p,
557 pd.selected ? paintSettings.getRelationSelectedColor(color.getAlpha()) : color,
558 fillImage, text);
559 }
560 }
561 }
562
563 public void drawArea(Way w, Color color, MapImage fillImage, TextElement text) {
564 drawArea(w, getPath(w), color, fillImage, text);
565 }
566
567 public void drawBoxText(Node n, BoxTextElemStyle bs) {
568 if (!isShowNames() || bs == null)
569 return;
570
571 Point p = nc.getPoint(n);
572 TextElement text = bs.text;
573 String s = text.labelCompositionStrategy.compose(n);
574 if (s == null) return;
575
576 Font defaultFont = g.getFont();
577 g.setFont(text.font);
578
579 int x = p.x + text.xOffset;
580 int y = p.y + text.yOffset;
581 /**
582 *
583 * left-above __center-above___ right-above
584 * left-top| |right-top
585 * | |
586 * left-center| center-center |right-center
587 * | |
588 * left-bottom|_________________|right-bottom
589 * left-below center-below right-below
590 *
591 */
592 Rectangle box = bs.getBox();
593 if (bs.hAlign == HorizontalTextAlignment.RIGHT) {
594 x += box.x + box.width + 2;
595 } else {
596 FontRenderContext frc = g.getFontRenderContext();
597 Rectangle2D bounds = text.font.getStringBounds(s, frc);
598 int textWidth = (int) bounds.getWidth();
599 if (bs.hAlign == HorizontalTextAlignment.CENTER) {
600 x -= textWidth / 2;
601 } else if (bs.hAlign == HorizontalTextAlignment.LEFT) {
602 x -= - box.x + 4 + textWidth;
603 } else throw new AssertionError();
604 }
605
606 if (bs.vAlign == VerticalTextAlignment.BOTTOM) {
607 y += box.y + box.height;
608 } else {
609 FontRenderContext frc = g.getFontRenderContext();
610 LineMetrics metrics = text.font.getLineMetrics(s, frc);
611 if (bs.vAlign == VerticalTextAlignment.ABOVE) {
612 y -= - box.y + metrics.getDescent();
613 } else if (bs.vAlign == VerticalTextAlignment.TOP) {
614 y -= - box.y - metrics.getAscent();
615 } else if (bs.vAlign == VerticalTextAlignment.CENTER) {
616 y += (metrics.getAscent() - metrics.getDescent()) / 2;
617 } else if (bs.vAlign == VerticalTextAlignment.BELOW) {
618 y += box.y + box.height + metrics.getAscent() + 2;
619 } else throw new AssertionError();
620 }
621 displayText(null, s, x, y, n.isDisabled(), text);
622 g.setFont(defaultFont);
623 }
624
625 @Deprecated
626 public void drawLinePattern(Way way, Image pattern) {
627 drawRepeatImage(way, pattern, 0f, 0f, 0f, LineImageAlignment.TOP);
628 }
629
630 /**
631 * Draw an image along a way repeatedly.
632 *
633 * @param way the way
634 * @param pattern the image
635 * @param offset offset from the way
636 * @param spacing spacing between two images
637 * @param phase initial spacing
638 * @param align alignment of the image. The top, center or bottom edge
639 * can be aligned with the way.
640 */
641 public void drawRepeatImage(Way way, Image pattern, float offset, float spacing, float phase, LineImageAlignment align) {
642 final int imgWidth = pattern.getWidth(null);
643 final double repeat = imgWidth + spacing;
644 final int imgHeight = pattern.getHeight(null);
645
646 Point lastP = null;
647 double currentWayLength = phase % repeat;
648 if (currentWayLength < 0) {
649 currentWayLength += repeat;
650 }
651
652 int dy1, dy2;
653 switch (align) {
654 case TOP:
655 dy1 = 0;
656 dy2 = imgHeight;
657 break;
658 case CENTER:
659 dy1 = - imgHeight / 2;
660 dy2 = imgHeight + dy1;
661 break;
662 case BOTTOM:
663 dy1 = -imgHeight;
664 dy2 = 0;
665 break;
666 default:
667 throw new AssertionError();
668 }
669
670 OffsetIterator it = new OffsetIterator(way.getNodes(), offset);
671 while (it.hasNext()) {
672 Point thisP = it.next();
673
674 if (lastP != null) {
675 final double segmentLength = thisP.distance(lastP);
676
677 final double dx = thisP.x - lastP.x;
678 final double dy = thisP.y - lastP.y;
679
680 // pos is the position from the beginning of the current segment
681 // where an image should be painted
682 double pos = repeat - (currentWayLength % repeat);
683
684 AffineTransform saveTransform = g.getTransform();
685 g.translate(lastP.x, lastP.y);
686 g.rotate(Math.atan2(dy, dx));
687
688 // draw the rest of the image from the last segment in case it
689 // is cut off
690 if (pos > spacing) {
691 // segment is too short for a complete image
692 if (pos > segmentLength + spacing) {
693 g.drawImage(pattern, 0, dy1, (int) segmentLength, dy2,
694 (int) (repeat - pos), 0,
695 (int) (repeat - pos + segmentLength), imgHeight, null);
696 // rest of the image fits fully on the current segment
697 } else {
698 g.drawImage(pattern, 0, dy1, (int) (pos - spacing), dy2,
699 (int) (repeat - pos), 0, imgWidth, imgHeight, null);
700 }
701 }
702 // draw remaining images for this segment
703 while (pos < segmentLength) {
704 // cut off at the end?
705 if (pos + imgWidth > segmentLength) {
706 g.drawImage(pattern, (int) pos, dy1, (int) segmentLength, dy2,
707 0, 0, (int) segmentLength - (int) pos, imgHeight, null);
708 } else {
709 g.drawImage(pattern, (int) pos, dy1, nc);
710 }
711 pos += repeat;
712 }
713 g.setTransform(saveTransform);
714
715 currentWayLength += segmentLength;
716 }
717 lastP = thisP;
718 }
719 }
720
721 @Override
722 public void drawNode(Node n, Color color, int size, boolean fill) {
723 if(size <= 0 && !n.isHighlighted())
724 return;
725
726 Point p = nc.getPoint(n);
727
728 if(n.isHighlighted()) {
729 drawPointHighlight(p, size);
730 }
731
732 if (size > 1) {
733 if ((p.x < 0) || (p.y < 0) || (p.x > nc.getWidth()) || (p.y > nc.getHeight())) return;
734 int radius = size / 2;
735
736 if (isInactiveMode || n.isDisabled()) {
737 g.setColor(inactiveColor);
738 } else {
739 g.setColor(color);
740 }
741 if (fill) {
742 g.fillRect(p.x-radius-1, p.y-radius-1, size + 1, size + 1);
743 } else {
744 g.drawRect(p.x-radius-1, p.y-radius-1, size, size);
745 }
746 }
747 }
748
749 public void drawNodeIcon(Node n, Image img, float alpha, boolean selected, boolean member) {
750 Point p = nc.getPoint(n);
751
752 final int w = img.getWidth(null), h=img.getHeight(null);
753 if(n.isHighlighted()) {
754 drawPointHighlight(p, Math.max(w, h));
755 }
756
757 if (alpha != 1f) {
758 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
759 }
760 g.drawImage(img, p.x-w/2, p.y-h/2, nc);
761 g.setPaintMode();
762 if (selected || member)
763 {
764 Color color;
765 if (isInactiveMode || n.isDisabled()) {
766 color = inactiveColor;
767 } else if (selected) {
768 color = selectedColor;
769 } else {
770 color = relationSelectedColor;
771 }
772 g.setColor(color);
773 g.drawRect(p.x-w/2-2, p.y-h/2-2, w+4, h+4);
774 }
775 }
776
777 public void drawNodeSymbol(Node n, Symbol s, Color fillColor, Color strokeColor) {
778 Point p = nc.getPoint(n);
779 int radius = s.size / 2;
780
781 if(n.isHighlighted()) {
782 drawPointHighlight(p, s.size);
783 }
784
785 if (fillColor != null) {
786 g.setColor(fillColor);
787 switch (s.symbol) {
788 case SQUARE:
789 g.fillRect(p.x - radius, p.y - radius, s.size, s.size);
790 break;
791 case CIRCLE:
792 g.fillOval(p.x - radius, p.y - radius, s.size, s.size);
793 break;
794 case TRIANGLE:
795 g.fillPolygon(buildPolygon(p, radius, 3, Math.PI / 2));
796 break;
797 case PENTAGON:
798 g.fillPolygon(buildPolygon(p, radius, 5, Math.PI / 2));
799 break;
800 case HEXAGON:
801 g.fillPolygon(buildPolygon(p, radius, 6));
802 break;
803 case HEPTAGON:
804 g.fillPolygon(buildPolygon(p, radius, 7, Math.PI / 2));
805 break;
806 case OCTAGON:
807 g.fillPolygon(buildPolygon(p, radius, 8, Math.PI / 8));
808 break;
809 case NONAGON:
810 g.fillPolygon(buildPolygon(p, radius, 9, Math.PI / 2));
811 break;
812 case DECAGON:
813 g.fillPolygon(buildPolygon(p, radius, 10));
814 break;
815 default:
816 throw new AssertionError();
817 }
818 }
819 if (s.stroke != null) {
820 g.setStroke(s.stroke);
821 g.setColor(strokeColor);
822 switch (s.symbol) {
823 case SQUARE:
824 g.drawRect(p.x - radius, p.y - radius, s.size - 1, s.size - 1);
825 break;
826 case CIRCLE:
827 g.drawOval(p.x - radius, p.y - radius, s.size - 1, s.size - 1);
828 break;
829 case TRIANGLE:
830 g.drawPolygon(buildPolygon(p, radius, 3, Math.PI / 2));
831 break;
832 case PENTAGON:
833 g.drawPolygon(buildPolygon(p, radius, 5, Math.PI / 2));
834 break;
835 case HEXAGON:
836 g.drawPolygon(buildPolygon(p, radius, 6));
837 break;
838 case HEPTAGON:
839 g.drawPolygon(buildPolygon(p, radius, 7, Math.PI / 2));
840 break;
841 case OCTAGON:
842 g.drawPolygon(buildPolygon(p, radius, 8, Math.PI / 8));
843 break;
844 case NONAGON:
845 g.drawPolygon(buildPolygon(p, radius, 9, Math.PI / 2));
846 break;
847 case DECAGON:
848 g.drawPolygon(buildPolygon(p, radius, 10));
849 break;
850 default:
851 throw new AssertionError();
852 }
853 g.setStroke(new BasicStroke());
854 }
855 }
856
857 /**
858 * Draw a number of the order of the two consecutive nodes within the
859 * parents way
860 */
861 public void drawOrderNumber(Node n1, Node n2, int orderNumber, Color clr) {
862 Point p1 = nc.getPoint(n1);
863 Point p2 = nc.getPoint(n2);
864 StyledMapRenderer.this.drawOrderNumber(p1, p2, orderNumber, clr);
865 }
866
867 /**
868 * highlights a given GeneralPath using the settings from BasicStroke to match the line's
869 * style. Width of the highlight is hard coded.
870 * @param path
871 * @param line
872 */
873 private void drawPathHighlight(GeneralPath path, BasicStroke line) {
874 if(path == null)
875 return;
876 g.setColor(highlightColorTransparent);
877 float w = (line.getLineWidth() + highlightLineWidth);
878 if (useWiderHighlight) w+=widerHighlight;
879 while(w >= line.getLineWidth()) {
880 g.setStroke(new BasicStroke(w, line.getEndCap(), line.getLineJoin(), line.getMiterLimit()));
881 g.draw(path);
882 w -= highlightStep;
883 }
884 }
885 /**
886 * highlights a given point by drawing a rounded rectangle around it. Give the
887 * size of the object you want to be highlighted, width is added automatically.
888 */
889 private void drawPointHighlight(Point p, int size) {
890 g.setColor(highlightColorTransparent);
891 int s = size + highlightPointRadius;
892 if (useWiderHighlight) s+=widerHighlight;
893 while(s >= size) {
894 int r = (int) Math.floor(s/2);
895 g.fillRoundRect(p.x-r, p.y-r, s, s, r, r);
896 s -= highlightStep;
897 }
898 }
899
900 public void drawRestriction(Image img, Point pVia, double vx, double vx2, double vy, double vy2, double angle, boolean selected) {
901 // rotate image with direction last node in from to, and scale down image to 16*16 pixels
902 Image smallImg = ImageProvider.createRotatedImage(img, angle, new Dimension(16, 16));
903 int w = smallImg.getWidth(null), h=smallImg.getHeight(null);
904 g.drawImage(smallImg, (int)(pVia.x+vx+vx2)-w/2, (int)(pVia.y+vy+vy2)-h/2, nc);
905
906 if (selected) {
907 g.setColor(isInactiveMode ? inactiveColor : relationSelectedColor);
908 g.drawRect((int)(pVia.x+vx+vx2)-w/2-2,(int)(pVia.y+vy+vy2)-h/2-2, w+4, h+4);
909 }
910 }
911
912 public void drawRestriction(Relation r, MapImage icon) {
913 Way fromWay = null;
914 Way toWay = null;
915 OsmPrimitive via = null;
916
917 /* find the "from", "via" and "to" elements */
918 for (RelationMember m : r.getMembers())
919 {
920 if(m.getMember().isIncomplete())
921 return;
922 else
923 {
924 if(m.isWay())
925 {
926 Way w = m.getWay();
927 if(w.getNodesCount() < 2) {
928 continue;
929 }
930
931 if("from".equals(m.getRole())) {
932 if(fromWay == null) {
933 fromWay = w;
934 }
935 } else if("to".equals(m.getRole())) {
936 if(toWay == null) {
937 toWay = w;
938 }
939 } else if("via".equals(m.getRole())) {
940 if(via == null) {
941 via = w;
942 }
943 }
944 }
945 else if(m.isNode())
946 {
947 Node n = m.getNode();
948 if("via".equals(m.getRole()) && via == null) {
949 via = n;
950 }
951 }
952 }
953 }
954
955 if (fromWay == null || toWay == null || via == null)
956 return;
957
958 Node viaNode;
959 if(via instanceof Node)
960 {
961 viaNode = (Node) via;
962 if(!fromWay.isFirstLastNode(viaNode))
963 return;
964 }
965 else
966 {
967 Way viaWay = (Way) via;
968 Node firstNode = viaWay.firstNode();
969 Node lastNode = viaWay.lastNode();
970 Boolean onewayvia = false;
971
972 String onewayviastr = viaWay.get("oneway");
973 if(onewayviastr != null)
974 {
975 if("-1".equals(onewayviastr)) {
976 onewayvia = true;
977 Node tmp = firstNode;
978 firstNode = lastNode;
979 lastNode = tmp;
980 } else {
981 onewayvia = OsmUtils.getOsmBoolean(onewayviastr);
982 if (onewayvia == null) {
983 onewayvia = false;
984 }
985 }
986 }
987
988 if(fromWay.isFirstLastNode(firstNode)) {
989 viaNode = firstNode;
990 } else if (!onewayvia && fromWay.isFirstLastNode(lastNode)) {
991 viaNode = lastNode;
992 } else
993 return;
994 }
995
996 /* find the "direct" nodes before the via node */
997 Node fromNode;
998 if(fromWay.firstNode() == via) {
999 fromNode = fromWay.getNode(1);
1000 } else {
1001 fromNode = fromWay.getNode(fromWay.getNodesCount()-2);
1002 }
1003
1004 Point pFrom = nc.getPoint(fromNode);
1005 Point pVia = nc.getPoint(viaNode);
1006
1007 /* starting from via, go back the "from" way a few pixels
1008 (calculate the vector vx/vy with the specified length and the direction
1009 away from the "via" node along the first segment of the "from" way)
1010 */
1011 double distanceFromVia=14;
1012 double dx = (pFrom.x >= pVia.x) ? (pFrom.x - pVia.x) : (pVia.x - pFrom.x);
1013 double dy = (pFrom.y >= pVia.y) ? (pFrom.y - pVia.y) : (pVia.y - pFrom.y);
1014
1015 double fromAngle;
1016 if(dx == 0.0) {
1017 fromAngle = Math.PI/2;
1018 } else {
1019 fromAngle = Math.atan(dy / dx);
1020 }
1021 double fromAngleDeg = Math.toDegrees(fromAngle);
1022
1023 double vx = distanceFromVia * Math.cos(fromAngle);
1024 double vy = distanceFromVia * Math.sin(fromAngle);
1025
1026 if(pFrom.x < pVia.x) {
1027 vx = -vx;
1028 }
1029 if(pFrom.y < pVia.y) {
1030 vy = -vy;
1031 }
1032
1033 /* go a few pixels away from the way (in a right angle)
1034 (calculate the vx2/vy2 vector with the specified length and the direction
1035 90degrees away from the first segment of the "from" way)
1036 */
1037 double distanceFromWay=10;
1038 double vx2 = 0;
1039 double vy2 = 0;
1040 double iconAngle = 0;
1041
1042 if(pFrom.x >= pVia.x && pFrom.y >= pVia.y) {
1043 if(!leftHandTraffic) {
1044 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
1045 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
1046 } else {
1047 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
1048 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
1049 }
1050 iconAngle = 270+fromAngleDeg;
1051 }
1052 if(pFrom.x < pVia.x && pFrom.y >= pVia.y) {
1053 if(!leftHandTraffic) {
1054 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
1055 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
1056 } else {
1057 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
1058 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
1059 }
1060 iconAngle = 90-fromAngleDeg;
1061 }
1062 if(pFrom.x < pVia.x && pFrom.y < pVia.y) {
1063 if(!leftHandTraffic) {
1064 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
1065 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
1066 } else {
1067 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
1068 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
1069 }
1070 iconAngle = 90+fromAngleDeg;
1071 }
1072 if(pFrom.x >= pVia.x && pFrom.y < pVia.y) {
1073 if(!leftHandTraffic) {
1074 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
1075 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
1076 } else {
1077 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
1078 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
1079 }
1080 iconAngle = 270-fromAngleDeg;
1081 }
1082
1083 drawRestriction(isInactiveMode || r.isDisabled() ? icon.getDisabled() : icon.getImage(),
1084 pVia, vx, vx2, vy, vy2, iconAngle, r.isSelected());
1085 }
1086
1087 public void drawTextOnPath(Way way, TextElement text) {
1088 if (way == null || text == null)
1089 return;
1090 String name = text.getString(way);
1091 if (name == null || name.isEmpty())
1092 return;
1093
1094 Polygon poly = new Polygon();
1095 Point lastPoint = null;
1096 Iterator<Node> it = way.getNodes().iterator();
1097 double pathLength = 0;
1098 long dx, dy;
1099 while (it.hasNext()) {
1100 Node n = it.next();
1101 Point p = nc.getPoint(n);
1102 poly.addPoint(p.x, p.y);
1103
1104 if(lastPoint != null) {
1105 dx = p.x - lastPoint.x;
1106 dy = p.y - lastPoint.y;
1107 pathLength += Math.sqrt(dx*dx + dy*dy);
1108 }
1109 lastPoint = p;
1110 }
1111
1112 FontMetrics fontMetrics = g.getFontMetrics(text.font); // if slow, use cache
1113 Rectangle2D rec = fontMetrics.getStringBounds(name, g); // if slow, approximate by strlen()*maxcharbounds(font)
1114
1115 if (rec.getWidth() > pathLength)
1116 return;
1117
1118 double t1 = (pathLength/2 - rec.getWidth()/2) / pathLength;
1119 double t2 = (pathLength/2 + rec.getWidth()/2) / pathLength;
1120
1121 double[] p1 = pointAt(t1, poly, pathLength);
1122 double[] p2 = pointAt(t2, poly, pathLength);
1123
1124 if (p1 == null || p2 == null)
1125 return;
1126
1127 double angleOffset;
1128 double offsetSign;
1129 double tStart;
1130
1131 if (p1[0] < p2[0] &&
1132 p1[2] < Math.PI/2 &&
1133 p1[2] > -Math.PI/2) {
1134 angleOffset = 0;
1135 offsetSign = 1;
1136 tStart = t1;
1137 } else {
1138 angleOffset = Math.PI;
1139 offsetSign = -1;
1140 tStart = t2;
1141 }
1142
1143 FontRenderContext frc = g.getFontRenderContext();
1144 GlyphVector gv = text.font.createGlyphVector(frc, name);
1145
1146 for (int i=0; i<gv.getNumGlyphs(); ++i) {
1147 Rectangle2D rect = gv.getGlyphLogicalBounds(i).getBounds2D();
1148 double t = tStart + offsetSign * (rect.getX() + rect.getWidth()/2) / pathLength;
1149 double[] p = pointAt(t, poly, pathLength);
1150 if (p != null) {
1151 AffineTransform trfm = AffineTransform.getTranslateInstance(p[0] - rect.getX(), p[1]);
1152 trfm.rotate(p[2]+angleOffset);
1153 double off = -rect.getY() - rect.getHeight()/2 + text.yOffset;
1154 trfm.translate(-rect.getWidth()/2, off);
1155 if (isGlyphVectorDoubleTranslationBug()) {
1156 // scale the translation components by one half
1157 AffineTransform tmp = AffineTransform.getTranslateInstance(-0.5 * trfm.getTranslateX(), -0.5 * trfm.getTranslateY());
1158 tmp.concatenate(trfm);
1159 trfm = tmp;
1160 }
1161 gv.setGlyphTransform(i, trfm);
1162 }
1163 }
1164 displayText(gv, null, 0, 0, way.isDisabled(), text);
1165 }
1166
1167 /**
1168 * draw way
1169 * @param showOrientation show arrows that indicate the technical orientation of
1170 * the way (defined by order of nodes)
1171 * @param showOneway show symbols that indicate the direction of the feature,
1172 * e.g. oneway street or waterway
1173 * @param onewayReversed for oneway=-1 and similar
1174 */
1175 public void drawWay(Way way, Color color, BasicStroke line, BasicStroke dashes, Color dashedColor, float offset,
1176 boolean showOrientation, boolean showHeadArrowOnly,
1177 boolean showOneway, boolean onewayReversed) {
1178
1179 GeneralPath path = new GeneralPath();
1180 GeneralPath orientationArrows = showOrientation ? new GeneralPath() : null;
1181 GeneralPath onewayArrows = showOneway ? new GeneralPath() : null;
1182 GeneralPath onewayArrowsCasing = showOneway ? new GeneralPath() : null;
1183 Rectangle bounds = g.getClipBounds();
1184 bounds.grow(100, 100); // avoid arrow heads at the border
1185
1186 double wayLength = 0;
1187 Point lastPoint = null;
1188 boolean initialMoveToNeeded = true;
1189 List<Node> wayNodes = way.getNodes();
1190 if (wayNodes.size() < 2) return;
1191
1192 // only highlight the segment if the way itself is not highlighted
1193 if (!way.isHighlighted()) {
1194 GeneralPath highlightSegs = null;
1195 for (WaySegment ws : highlightWaySegments) {
1196 if (ws.way != way || ws.lowerIndex < offset) {
1197 continue;
1198 }
1199 if(highlightSegs == null) {
1200 highlightSegs = new GeneralPath();
1201 }
1202
1203 Point p1 = nc.getPoint(ws.getFirstNode());
1204 Point p2 = nc.getPoint(ws.getSecondNode());
1205 highlightSegs.moveTo(p1.x, p1.y);
1206 highlightSegs.lineTo(p2.x, p2.y);
1207 }
1208
1209 drawPathHighlight(highlightSegs, line);
1210 }
1211
1212 Iterator<Point> it = new OffsetIterator(wayNodes, offset);
1213 while (it.hasNext()) {
1214 Point p = it.next();
1215 if (lastPoint != null) {
1216 Point p1 = lastPoint;
1217 Point p2 = p;
1218
1219 /**
1220 * Do custom clipping to work around openjdk bug. It leads to
1221 * drawing artefacts when zooming in a lot. (#4289, #4424)
1222 * (Looks like int overflow.)
1223 */
1224 LineClip clip = new LineClip(p1, p2, bounds);
1225 if (clip.execute()) {
1226 if (!p1.equals(clip.getP1())) {
1227 p1 = clip.getP1();
1228 path.moveTo(p1.x, p1.y);
1229 } else if (initialMoveToNeeded) {
1230 initialMoveToNeeded = false;
1231 path.moveTo(p1.x, p1.y);
1232 }
1233 p2 = clip.getP2();
1234 path.lineTo(p2.x, p2.y);
1235
1236 /* draw arrow */
1237 if (showHeadArrowOnly ? !it.hasNext() : showOrientation) {
1238 final double segmentLength = p1.distance(p2);
1239 if (segmentLength != 0.0) {
1240 final double l = (10. + line.getLineWidth()) / segmentLength;
1241
1242 final double sx = l * (p1.x - p2.x);
1243 final double sy = l * (p1.y - p2.y);
1244
1245 orientationArrows.moveTo (p2.x + cosPHI * sx - sinPHI * sy, p2.y + sinPHI * sx + cosPHI * sy);
1246 orientationArrows.lineTo(p2.x, p2.y);
1247 orientationArrows.lineTo (p2.x + cosPHI * sx + sinPHI * sy, p2.y - sinPHI * sx + cosPHI * sy);
1248 }
1249 }
1250 if (showOneway) {
1251 final double segmentLength = p1.distance(p2);
1252 if (segmentLength != 0.0) {
1253 final double nx = (p2.x - p1.x) / segmentLength;
1254 final double ny = (p2.y - p1.y) / segmentLength;
1255
1256 final double interval = 60;
1257 // distance from p1
1258 double dist = interval - (wayLength % interval);
1259
1260 while (dist < segmentLength) {
1261 for (int i=0; i<2; ++i) {
1262 float onewaySize = i == 0 ? 3f : 2f;
1263 GeneralPath onewayPath = i == 0 ? onewayArrowsCasing : onewayArrows;
1264
1265 // scale such that border is 1 px
1266 final double fac = - (onewayReversed ? -1 : 1) * onewaySize * (1 + sinPHI) / (sinPHI * cosPHI);
1267 final double sx = nx * fac;
1268 final double sy = ny * fac;
1269
1270 // Attach the triangle at the incenter and not at the tip.
1271 // Makes the border even at all sides.
1272 final double x = p1.x + nx * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI));
1273 final double y = p1.y + ny * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI));
1274
1275 onewayPath.moveTo(x, y);
1276 onewayPath.lineTo (x + cosPHI * sx - sinPHI * sy, y + sinPHI * sx + cosPHI * sy);
1277 onewayPath.lineTo (x + cosPHI * sx + sinPHI * sy, y - sinPHI * sx + cosPHI * sy);
1278 onewayPath.lineTo(x, y);
1279 }
1280 dist += interval;
1281 }
1282 }
1283 wayLength += segmentLength;
1284 }
1285 }
1286 }
1287 lastPoint = p;
1288 }
1289 if(way.isHighlighted()) {
1290 drawPathHighlight(path, line);
1291 }
1292 displaySegments(path, orientationArrows, onewayArrows, onewayArrowsCasing, color, line, dashes, dashedColor);
1293 }
1294
1295 public double getCircum() {
1296 return circum;
1297 }
1298
1299 @Override
1300 public void getColors() {
1301 super.getColors();
1302 this.relationSelectedColor = PaintColors.RELATIONSELECTED.get();
1303 this.highlightColorTransparent = new Color(highlightColor.getRed(), highlightColor.getGreen(), highlightColor.getBlue(), 100);
1304 this.backgroundColor = PaintColors.getBackgroundColor();
1305 }
1306
1307 @Override
1308 protected void getSettings(boolean virtual) {
1309 super.getSettings(virtual);
1310 paintSettings = MapPaintSettings.INSTANCE;
1311
1312 circum = nc.getDist100Pixel();
1313
1314 leftHandTraffic = Main.pref.getBoolean("mappaint.lefthandtraffic", false);
1315
1316 useStrokes = paintSettings.getUseStrokesDistance() > circum;
1317 showNames = paintSettings.getShowNamesDistance() > circum;
1318 showIcons = paintSettings.getShowIconsDistance() > circum;
1319 isOutlineOnly = paintSettings.isOutlineOnly();
1320 orderFont = new Font(Main.pref.get("mappaint.font", "Helvetica"), Font.PLAIN, Main.pref.getInteger("mappaint.fontsize", 8));
1321
1322 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1323 Main.pref.getBoolean("mappaint.use-antialiasing", true) ?
1324 RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
1325
1326 highlightLineWidth = Main.pref.getInteger("mappaint.highlight.width", 4);
1327 highlightPointRadius = Main.pref.getInteger("mappaint.highlight.radius", 7);
1328 widerHighlight = Main.pref.getInteger("mappaint.highlight.bigger-increment", 5);
1329 highlightStep = Main.pref.getInteger("mappaint.highlight.step", 4);
1330 }
1331
1332 private Path2D.Double getPath(Way w) {
1333 Path2D.Double path = new Path2D.Double();
1334 boolean initial = true;
1335 for (Node n : w.getNodes()) {
1336 EastNorth p = n.getEastNorth();
1337 if (p != null) {
1338 if (initial) {
1339 path.moveTo(p.getX(), p.getY());
1340 initial = false;
1341 } else {
1342 path.lineTo(p.getX(), p.getY());
1343 }
1344 }
1345 }
1346 return path;
1347 }
1348
1349 private boolean isAreaVisible(Path2D.Double area) {
1350 Rectangle2D bounds = area.getBounds2D();
1351 if (bounds.isEmpty()) return false;
1352 Point2D p = nc.getPoint2D(new EastNorth(bounds.getX(), bounds.getY()));
1353 if (p.getX() > nc.getWidth()) return false;
1354 if (p.getY() < 0) return false;
1355 p = nc.getPoint2D(new EastNorth(bounds.getX() + bounds.getWidth(), bounds.getY() + bounds.getHeight()));
1356 if (p.getX() < 0) return false;
1357 if (p.getY() > nc.getHeight()) return false;
1358 return true;
1359 }
1360
1361 public boolean isInactiveMode() {
1362 return isInactiveMode;
1363 }
1364
1365 public boolean isShowIcons() {
1366 return showIcons;
1367 }
1368
1369 public boolean isShowNames() {
1370 return showNames;
1371 }
1372
1373 private double[] pointAt(double t, Polygon poly, double pathLength) {
1374 double totalLen = t * pathLength;
1375 double curLen = 0;
1376 long dx, dy;
1377 double segLen;
1378
1379 // Yes, it is inefficient to iterate from the beginning for each glyph.
1380 // Can be optimized if it turns out to be slow.
1381 for (int i = 1; i < poly.npoints; ++i) {
1382 dx = poly.xpoints[i] - poly.xpoints[i-1];
1383 dy = poly.ypoints[i] - poly.ypoints[i-1];
1384 segLen = Math.sqrt(dx*dx + dy*dy);
1385 if (totalLen > curLen + segLen) {
1386 curLen += segLen;
1387 continue;
1388 }
1389 return new double[] {
1390 poly.xpoints[i-1]+(totalLen - curLen)/segLen*dx,
1391 poly.ypoints[i-1]+(totalLen - curLen)/segLen*dy,
1392 Math.atan2(dy, dx)};
1393 }
1394 return null;
1395 }
1396
1397 @Override
1398 public void render(final DataSet data, boolean renderVirtualNodes, Bounds bounds) {
1399 BBox bbox = bounds.toBBox();
1400 getSettings(renderVirtualNodes);
1401
1402 boolean drawArea = circum <= Main.pref.getInteger("mappaint.fillareas", 10000000);
1403 boolean drawMultipolygon = drawArea && Main.pref.getBoolean("mappaint.multipolygon", true);
1404 boolean drawRestriction = Main.pref.getBoolean("mappaint.restriction", true);
1405
1406 styles = MapPaintStyles.getStyles();
1407 styles.setDrawMultipolygon(drawMultipolygon);
1408
1409 highlightWaySegments = data.getHighlightedWaySegments();
1410
1411 StyleCollector sc = new StyleCollector(drawArea, drawMultipolygon, drawRestriction);
1412 collectNodeStyles(data, sc, bbox);
1413 collectWayStyles(data, sc, bbox);
1414 collectRelationStyles(data, sc, bbox);
1415 sc.drawAll();
1416 sc = null;
1417 drawVirtualNodes(data, bbox);
1418 }
1419}
Note: See TracBrowser for help on using the repository browser.