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

Last change on this file since 7107 was 7107, checked in by bastiK, 10 years ago

mappaint: hold read lock on Dataset during painting, so modifications do not lead to inconsistent state (fixes #8671, see #9691)

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