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

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

fixes for unit tests

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