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

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

fix #13258 - Pull benchmark code out of renderer (patch by michael2402) - gsoc-core

  • Property svn:eol-style set to native
File size: 78.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.font.TextLayout;
23import java.awt.geom.AffineTransform;
24import java.awt.geom.GeneralPath;
25import java.awt.geom.Path2D;
26import java.awt.geom.Point2D;
27import java.awt.geom.Rectangle2D;
28import java.util.ArrayList;
29import java.util.Collection;
30import java.util.Collections;
31import java.util.HashMap;
32import java.util.Iterator;
33import java.util.List;
34import java.util.Map;
35import java.util.NoSuchElementException;
36import java.util.concurrent.ForkJoinPool;
37import java.util.concurrent.ForkJoinTask;
38import java.util.concurrent.RecursiveTask;
39import java.util.function.Supplier;
40
41import javax.swing.AbstractButton;
42import javax.swing.FocusManager;
43
44import org.openstreetmap.josm.Main;
45import org.openstreetmap.josm.data.Bounds;
46import org.openstreetmap.josm.data.coor.EastNorth;
47import org.openstreetmap.josm.data.osm.BBox;
48import org.openstreetmap.josm.data.osm.Changeset;
49import org.openstreetmap.josm.data.osm.DataSet;
50import org.openstreetmap.josm.data.osm.Node;
51import org.openstreetmap.josm.data.osm.OsmPrimitive;
52import org.openstreetmap.josm.data.osm.OsmUtils;
53import org.openstreetmap.josm.data.osm.Relation;
54import org.openstreetmap.josm.data.osm.RelationMember;
55import org.openstreetmap.josm.data.osm.Way;
56import org.openstreetmap.josm.data.osm.WaySegment;
57import org.openstreetmap.josm.data.osm.visitor.Visitor;
58import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
59import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData;
60import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
61import org.openstreetmap.josm.gui.NavigatableComponent;
62import org.openstreetmap.josm.gui.mappaint.ElemStyles;
63import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
64import org.openstreetmap.josm.gui.mappaint.StyleElementList;
65import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
66import org.openstreetmap.josm.gui.mappaint.styleelement.AreaElement;
67import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement;
68import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement.HorizontalTextAlignment;
69import org.openstreetmap.josm.gui.mappaint.styleelement.BoxTextElement.VerticalTextAlignment;
70import org.openstreetmap.josm.gui.mappaint.styleelement.MapImage;
71import org.openstreetmap.josm.gui.mappaint.styleelement.NodeElement;
72import org.openstreetmap.josm.gui.mappaint.styleelement.NodeElement.Symbol;
73import org.openstreetmap.josm.gui.mappaint.styleelement.RepeatImageElement.LineImageAlignment;
74import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement;
75import org.openstreetmap.josm.gui.mappaint.styleelement.TextLabel;
76import org.openstreetmap.josm.tools.CompositeList;
77import org.openstreetmap.josm.tools.Geometry;
78import org.openstreetmap.josm.tools.Geometry.AreaAndPerimeter;
79import org.openstreetmap.josm.tools.ImageProvider;
80import org.openstreetmap.josm.tools.Utils;
81
82/**
83 * A map renderer which renders a map according to style rules in a set of style sheets.
84 * @since 486
85 */
86public class StyledMapRenderer extends AbstractMapRenderer {
87
88 private static final ForkJoinPool THREAD_POOL =
89 Utils.newForkJoinPool("mappaint.StyledMapRenderer.style_creation.numberOfThreads", "styled-map-renderer-%d", Thread.NORM_PRIORITY);
90
91 /**
92 * Iterates over a list of Way Nodes and returns screen coordinates that
93 * represent a line that is shifted by a certain offset perpendicular
94 * to the way direction.
95 *
96 * There is no intention, to handle consecutive duplicate Nodes in a
97 * perfect way, but it should not throw an exception.
98 */
99 private class OffsetIterator implements Iterator<Point> {
100
101 private final List<Node> nodes;
102 private final double offset;
103 private int idx;
104
105 private Point prev;
106 /* 'prev0' is a point that has distance 'offset' from 'prev' and the
107 * line from 'prev' to 'prev0' is perpendicular to the way segment from
108 * 'prev' to the next point.
109 */
110 private int xPrev0, yPrev0;
111
112 OffsetIterator(List<Node> nodes, double offset) {
113 this.nodes = nodes;
114 this.offset = offset;
115 idx = 0;
116 }
117
118 @Override
119 public boolean hasNext() {
120 return idx < nodes.size();
121 }
122
123 @Override
124 public Point next() {
125 if (!hasNext())
126 throw new NoSuchElementException();
127
128 if (Math.abs(offset) < 0.1d)
129 return nc.getPoint(nodes.get(idx++));
130
131 Point current = nc.getPoint(nodes.get(idx));
132
133 if (idx == nodes.size() - 1) {
134 ++idx;
135 if (prev != null) {
136 return new Point(xPrev0 + current.x - prev.x, yPrev0 + current.y - prev.y);
137 } else {
138 return current;
139 }
140 }
141
142 Point next = nc.getPoint(nodes.get(idx+1));
143
144 int dxNext = next.x - current.x;
145 int dyNext = next.y - current.y;
146 double lenNext = Math.sqrt((double) dxNext*dxNext + (double) dyNext*dyNext);
147
148 if (lenNext == 0) {
149 lenNext = 1; // value does not matter, because dy_next and dx_next is 0
150 }
151
152 int xCurrent0 = current.x + (int) Math.round(offset * dyNext / lenNext);
153 int yCurrent0 = current.y - (int) Math.round(offset * dxNext / lenNext);
154
155 if (idx == 0) {
156 ++idx;
157 prev = current;
158 xPrev0 = xCurrent0;
159 yPrev0 = yCurrent0;
160 return new Point(xCurrent0, yCurrent0);
161 } else {
162 int dxPrev = current.x - prev.x;
163 int dyPrev = current.y - prev.y;
164
165 // determine intersection of the lines parallel to the two segments
166 int det = dxNext*dyPrev - dxPrev*dyNext;
167
168 if (det == 0) {
169 ++idx;
170 prev = current;
171 xPrev0 = xCurrent0;
172 yPrev0 = yCurrent0;
173 return new Point(xCurrent0, yCurrent0);
174 }
175
176 int m = dxNext*(yCurrent0 - yPrev0) - dyNext*(xCurrent0 - xPrev0);
177
178 int cx = xPrev0 + (int) Math.round((double) m * dxPrev / det);
179 int cy = yPrev0 + (int) Math.round((double) m * dyPrev / det);
180 ++idx;
181 prev = current;
182 xPrev0 = xCurrent0;
183 yPrev0 = yCurrent0;
184 return new Point(cx, cy);
185 }
186 }
187
188 @Override
189 public void remove() {
190 throw new UnsupportedOperationException();
191 }
192 }
193
194 public static class StyleRecord implements Comparable<StyleRecord> {
195 private final StyleElement style;
196 private final OsmPrimitive osm;
197 private final int flags;
198
199 StyleRecord(StyleElement style, OsmPrimitive osm, int flags) {
200 this.style = style;
201 this.osm = osm;
202 this.flags = flags;
203 }
204
205 @Override
206 public int compareTo(StyleRecord other) {
207 if ((this.flags & FLAG_DISABLED) != 0 && (other.flags & FLAG_DISABLED) == 0)
208 return -1;
209 if ((this.flags & FLAG_DISABLED) == 0 && (other.flags & FLAG_DISABLED) != 0)
210 return 1;
211
212 int d0 = Float.compare(this.style.majorZIndex, other.style.majorZIndex);
213 if (d0 != 0)
214 return d0;
215
216 // selected on top of member of selected on top of unselected
217 // FLAG_DISABLED bit is the same at this point
218 if (this.flags > other.flags)
219 return 1;
220 if (this.flags < other.flags)
221 return -1;
222
223 int dz = Float.compare(this.style.zIndex, other.style.zIndex);
224 if (dz != 0)
225 return dz;
226
227 // simple node on top of icons and shapes
228 if (NodeElement.SIMPLE_NODE_ELEMSTYLE.equals(this.style) && !NodeElement.SIMPLE_NODE_ELEMSTYLE.equals(other.style))
229 return 1;
230 if (!NodeElement.SIMPLE_NODE_ELEMSTYLE.equals(this.style) && NodeElement.SIMPLE_NODE_ELEMSTYLE.equals(other.style))
231 return -1;
232
233 // newer primitives to the front
234 long id = this.osm.getUniqueId() - other.osm.getUniqueId();
235 if (id > 0)
236 return 1;
237 if (id < 0)
238 return -1;
239
240 return Float.compare(this.style.objectZIndex, other.style.objectZIndex);
241 }
242
243 /**
244 * Get the style for this style element.
245 * @return The style
246 */
247 public StyleElement getStyle() {
248 return style;
249 }
250 }
251
252 private static Map<Font, Boolean> IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG = new HashMap<>();
253
254 /**
255 * Check, if this System has the GlyphVector double translation bug.
256 *
257 * With this bug, <code>gv.setGlyphTransform(i, trfm)</code> has a different
258 * effect than on most other systems, namely the translation components
259 * ("m02" &amp; "m12", {@link AffineTransform}) appear to be twice as large, as
260 * they actually are. The rotation is unaffected (scale &amp; shear not tested
261 * so far).
262 *
263 * This bug has only been observed on Mac OS X, see #7841.
264 *
265 * After switch to Java 7, this test is a false positive on Mac OS X (see #10446),
266 * i.e. it returns true, but the real rendering code does not require any special
267 * handling.
268 * It hasn't been further investigated why the test reports a wrong result in
269 * this case, but the method has been changed to simply return false by default.
270 * (This can be changed with a setting in the advanced preferences.)
271 *
272 * @param font The font to check.
273 * @return false by default, but depends on the value of the advanced
274 * preference glyph-bug=false|true|auto, where auto is the automatic detection
275 * method which apparently no longer gives a useful result for Java 7.
276 */
277 public static boolean isGlyphVectorDoubleTranslationBug(Font font) {
278 Boolean cached = IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG.get(font);
279 if (cached != null)
280 return cached;
281 String overridePref = Main.pref.get("glyph-bug", "auto");
282 if ("auto".equals(overridePref)) {
283 FontRenderContext frc = new FontRenderContext(null, false, false);
284 GlyphVector gv = font.createGlyphVector(frc, "x");
285 gv.setGlyphTransform(0, AffineTransform.getTranslateInstance(1000, 1000));
286 Shape shape = gv.getGlyphOutline(0);
287 if (Main.isTraceEnabled()) {
288 Main.trace("#10446: shape: "+shape.getBounds());
289 }
290 // x is about 1000 on normal stystems and about 2000 when the bug occurs
291 int x = shape.getBounds().x;
292 boolean isBug = x > 1500;
293 IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG.put(font, isBug);
294 return isBug;
295 } else {
296 boolean override = Boolean.parseBoolean(overridePref);
297 IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG.put(font, override);
298 return override;
299 }
300 }
301
302 private double circum;
303 private double scale;
304
305 private MapPaintSettings paintSettings;
306
307 private Color highlightColorTransparent;
308
309 /**
310 * Flags used to store the primitive state along with the style. This is the normal style.
311 * <p>
312 * Not used in any public interfaces.
313 */
314 private static final int FLAG_NORMAL = 0;
315 /**
316 * A primitive with {@link OsmPrimitive#isDisabled()}
317 */
318 private static final int FLAG_DISABLED = 1;
319 /**
320 * A primitive with {@link OsmPrimitive#isMemberOfSelected()}
321 */
322 private static final int FLAG_MEMBER_OF_SELECTED = 2;
323 /**
324 * A primitive with {@link OsmPrimitive#isSelected()}
325 */
326 private static final int FLAG_SELECTED = 4;
327 /**
328 * A primitive with {@link OsmPrimitive#isOuterMemberOfSelected()}
329 */
330 private static final int FLAG_OUTERMEMBER_OF_SELECTED = 8;
331
332 private static final double PHI = Math.toRadians(20);
333 private static final double cosPHI = Math.cos(PHI);
334 private static final double sinPHI = Math.sin(PHI);
335
336 private Collection<WaySegment> highlightWaySegments;
337
338 // highlight customization fields
339 private int highlightLineWidth;
340 private int highlightPointRadius;
341 private int widerHighlight;
342 private int highlightStep;
343
344 //flag that activate wider highlight mode
345 private boolean useWiderHighlight;
346
347 private boolean useStrokes;
348 private boolean showNames;
349 private boolean showIcons;
350 private boolean isOutlineOnly;
351
352 private Font orderFont;
353
354 private boolean leftHandTraffic;
355 private Object antialiasing;
356
357 private Supplier<RenderBenchmarkCollector> benchmarkFactory = RenderBenchmarkCollector.defaultBenchmarkSupplier();
358
359 /**
360 * Constructs a new {@code StyledMapRenderer}.
361 *
362 * @param g the graphics context. Must not be null.
363 * @param nc the map viewport. Must not be null.
364 * @param isInactiveMode if true, the paint visitor shall render OSM objects such that they
365 * look inactive. Example: rendering of data in an inactive layer using light gray as color only.
366 * @throws IllegalArgumentException if {@code g} is null
367 * @throws IllegalArgumentException if {@code nc} is null
368 */
369 public StyledMapRenderer(Graphics2D g, NavigatableComponent nc, boolean isInactiveMode) {
370 super(g, nc, isInactiveMode);
371
372 if (nc != null) {
373 Component focusOwner = FocusManager.getCurrentManager().getFocusOwner();
374 useWiderHighlight = !(focusOwner instanceof AbstractButton || focusOwner == nc);
375 }
376 }
377
378 private static Polygon buildPolygon(Point center, int radius, int sides) {
379 return buildPolygon(center, radius, sides, 0.0);
380 }
381
382 private static Polygon buildPolygon(Point center, int radius, int sides, double rotation) {
383 Polygon polygon = new Polygon();
384 for (int i = 0; i < sides; i++) {
385 double angle = ((2 * Math.PI / sides) * i) - rotation;
386 int x = (int) Math.round(center.x + radius * Math.cos(angle));
387 int y = (int) Math.round(center.y + radius * Math.sin(angle));
388 polygon.addPoint(x, y);
389 }
390 return polygon;
391 }
392
393 private void displaySegments(GeneralPath path, GeneralPath orientationArrows, GeneralPath onewayArrows, GeneralPath onewayArrowsCasing,
394 Color color, BasicStroke line, BasicStroke dashes, Color dashedColor) {
395 g.setColor(isInactiveMode ? inactiveColor : color);
396 if (useStrokes) {
397 g.setStroke(line);
398 }
399 g.draw(path);
400
401 if (!isInactiveMode && useStrokes && dashes != null) {
402 g.setColor(dashedColor);
403 g.setStroke(dashes);
404 g.draw(path);
405 }
406
407 if (orientationArrows != null) {
408 g.setColor(isInactiveMode ? inactiveColor : color);
409 g.setStroke(new BasicStroke(line.getLineWidth(), line.getEndCap(), BasicStroke.JOIN_MITER, line.getMiterLimit()));
410 g.draw(orientationArrows);
411 }
412
413 if (onewayArrows != null) {
414 g.setStroke(new BasicStroke(1, line.getEndCap(), BasicStroke.JOIN_MITER, line.getMiterLimit()));
415 g.fill(onewayArrowsCasing);
416 g.setColor(isInactiveMode ? inactiveColor : backgroundColor);
417 g.fill(onewayArrows);
418 }
419
420 if (useStrokes) {
421 g.setStroke(new BasicStroke());
422 }
423 }
424
425 /**
426 * Displays text at specified position including its halo, if applicable.
427 *
428 * @param gv Text's glyphs to display. If {@code null}, use text from {@code s} instead.
429 * @param s text to display if {@code gv} is {@code null}
430 * @param x X position
431 * @param y Y position
432 * @param disabled {@code true} if element is disabled (filtered out)
433 * @param text text style to use
434 */
435 private void displayText(GlyphVector gv, String s, int x, int y, boolean disabled, TextLabel text) {
436 if (gv == null && s.isEmpty()) return;
437 if (isInactiveMode || disabled) {
438 g.setColor(inactiveColor);
439 if (gv != null) {
440 g.drawGlyphVector(gv, x, y);
441 } else {
442 g.setFont(text.font);
443 g.drawString(s, x, y);
444 }
445 } else if (text.haloRadius != null) {
446 g.setStroke(new BasicStroke(2*text.haloRadius, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND));
447 g.setColor(text.haloColor);
448 Shape textOutline;
449 if (gv == null) {
450 FontRenderContext frc = g.getFontRenderContext();
451 TextLayout tl = new TextLayout(s, text.font, frc);
452 textOutline = tl.getOutline(AffineTransform.getTranslateInstance(x, y));
453 } else {
454 textOutline = gv.getOutline(x, y);
455 }
456 g.draw(textOutline);
457 g.setStroke(new BasicStroke());
458 g.setColor(text.color);
459 g.fill(textOutline);
460 } else {
461 g.setColor(text.color);
462 if (gv != null) {
463 g.drawGlyphVector(gv, x, y);
464 } else {
465 g.setFont(text.font);
466 g.drawString(s, x, y);
467 }
468 }
469 }
470
471 /**
472 * Worker function for drawing areas.
473 *
474 * @param osm the primitive
475 * @param path the path object for the area that should be drawn; in case
476 * of multipolygons, this can path can be a complex shape with one outer
477 * polygon and one or more inner polygons
478 * @param color The color to fill the area with.
479 * @param fillImage The image to fill the area with. Overrides color.
480 * @param extent if not null, area will be filled partially; specifies, how
481 * far to fill from the boundary towards the center of the area;
482 * if null, area will be filled completely
483 * @param pfClip clipping area for partial fill (only needed for unclosed
484 * polygons)
485 * @param disabled If this should be drawn with a special disabled style.
486 * @param text The text to write on the area.
487 */
488 protected void drawArea(OsmPrimitive osm, Path2D.Double path, Color color,
489 MapImage fillImage, Float extent, Path2D.Double pfClip, boolean disabled, TextLabel text) {
490
491 Shape area = path.createTransformedShape(nc.getAffineTransform());
492
493 if (!isOutlineOnly) {
494 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
495 if (fillImage == null) {
496 if (isInactiveMode) {
497 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.33f));
498 }
499 g.setColor(color);
500 if (extent == null) {
501 g.fill(area);
502 } else {
503 Shape oldClip = g.getClip();
504 Shape clip = area;
505 if (pfClip != null) {
506 clip = pfClip.createTransformedShape(nc.getAffineTransform());
507 }
508 g.clip(clip);
509 g.setStroke(new BasicStroke(2 * extent, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 4));
510 g.draw(area);
511 g.setClip(oldClip);
512 }
513 } else {
514 TexturePaint texture = new TexturePaint(fillImage.getImage(disabled),
515 new Rectangle(0, 0, fillImage.getWidth(), fillImage.getHeight()));
516 g.setPaint(texture);
517 Float alpha = fillImage.getAlphaFloat();
518 if (!Utils.equalsEpsilon(alpha, 1f)) {
519 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
520 }
521 if (extent == null) {
522 g.fill(area);
523 } else {
524 Shape oldClip = g.getClip();
525 BasicStroke stroke = new BasicStroke(2 * extent, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
526 g.clip(stroke.createStrokedShape(area));
527 Shape fill = area;
528 if (pfClip != null) {
529 fill = pfClip.createTransformedShape(nc.getAffineTransform());
530 }
531 g.fill(fill);
532 g.setClip(oldClip);
533 }
534 g.setPaintMode();
535 }
536 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing);
537 }
538
539 drawAreaText(osm, text, area);
540 }
541
542 private void drawAreaText(OsmPrimitive osm, TextLabel text, Shape area) {
543 if (text != null && isShowNames()) {
544 // abort if we can't compose the label to be rendered
545 if (text.labelCompositionStrategy == null) return;
546 String name = text.labelCompositionStrategy.compose(osm);
547 if (name == null) return;
548
549 Rectangle pb = area.getBounds();
550 FontMetrics fontMetrics = g.getFontMetrics(orderFont); // if slow, use cache
551 Rectangle2D nb = fontMetrics.getStringBounds(name, g); // if slow, approximate by strlen()*maxcharbounds(font)
552
553 // Using the Centroid is Nicer for buildings like: +--------+
554 // but this needs to be fast. As most houses are | 42 |
555 // boxes anyway, the center of the bounding box +---++---+
556 // will have to do. ++
557 // Centroids are not optimal either, just imagine a U-shaped house.
558
559 // quick check to see if label box is smaller than primitive box
560 if (pb.width >= nb.getWidth() && pb.height >= nb.getHeight()) {
561
562 final double w = pb.width - nb.getWidth();
563 final double h = pb.height - nb.getHeight();
564
565 final int x2 = pb.x + (int) (w/2.0);
566 final int y2 = pb.y + (int) (h/2.0);
567
568 final int nbw = (int) nb.getWidth();
569 final int nbh = (int) nb.getHeight();
570
571 Rectangle centeredNBounds = new Rectangle(x2, y2, nbw, nbh);
572
573 // slower check to see if label is displayed inside primitive shape
574 boolean labelOK = area.contains(centeredNBounds);
575 if (!labelOK) {
576 // if center position (C) is not inside osm shape, try naively some other positions as follows:
577 // CHECKSTYLE.OFF: SingleSpaceSeparator
578 final int x1 = pb.x + (int) (w/4.0);
579 final int x3 = pb.x + (int) (3*w/4.0);
580 final int y1 = pb.y + (int) (h/4.0);
581 final int y3 = pb.y + (int) (3*h/4.0);
582 // CHECKSTYLE.ON: SingleSpaceSeparator
583 // +-----------+
584 // | 5 1 6 |
585 // | 4 C 2 |
586 // | 8 3 7 |
587 // +-----------+
588 Rectangle[] candidates = new Rectangle[] {
589 new Rectangle(x2, y1, nbw, nbh),
590 new Rectangle(x3, y2, nbw, nbh),
591 new Rectangle(x2, y3, nbw, nbh),
592 new Rectangle(x1, y2, nbw, nbh),
593 new Rectangle(x1, y1, nbw, nbh),
594 new Rectangle(x3, y1, nbw, nbh),
595 new Rectangle(x3, y3, nbw, nbh),
596 new Rectangle(x1, y3, nbw, nbh)
597 };
598 // Dumb algorithm to find a better placement. We could surely find a smarter one but it should
599 // solve most of building issues with only few calculations (8 at most)
600 for (int i = 0; i < candidates.length && !labelOK; i++) {
601 centeredNBounds = candidates[i];
602 labelOK = area.contains(centeredNBounds);
603 }
604 }
605 if (labelOK) {
606 Font defaultFont = g.getFont();
607 int x = (int) (centeredNBounds.getMinX() - nb.getMinX());
608 int y = (int) (centeredNBounds.getMinY() - nb.getMinY());
609 displayText(null, name, x, y, osm.isDisabled(), text);
610 g.setFont(defaultFont);
611 } else if (Main.isTraceEnabled()) {
612 Main.trace("Couldn't find a correct label placement for "+osm+" / "+name);
613 }
614 }
615 }
616 }
617
618 /**
619 * Draws a multipolygon area.
620 * @param r The multipolygon relation
621 * @param color The color to fill the area with.
622 * @param fillImage The image to fill the area with. Overrides color.
623 * @param extent if not null, area will be filled partially; specifies, how
624 * far to fill from the boundary towards the center of the area;
625 * if null, area will be filled completely
626 * @param extentThreshold if not null, determines if the partial filled should
627 * be replaced by plain fill, when it covers a certain fraction of the total area
628 * @param disabled If this should be drawn with a special disabled style.
629 * @param text The text to write on the area.
630 */
631 public void drawArea(Relation r, Color color, MapImage fillImage, Float extent, Float extentThreshold, boolean disabled, TextLabel text) {
632 Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, r);
633 if (!r.isDisabled() && !multipolygon.getOuterWays().isEmpty()) {
634 for (PolyData pd : multipolygon.getCombinedPolygons()) {
635 Path2D.Double p = pd.get();
636 Path2D.Double pfClip = null;
637 if (!isAreaVisible(p)) {
638 continue;
639 }
640 if (extent != null) {
641 if (!usePartialFill(pd.getAreaAndPerimeter(null), extent, extentThreshold)) {
642 extent = null;
643 } else if (!pd.isClosed()) {
644 pfClip = getPFClip(pd, extent * scale);
645 }
646 }
647 drawArea(r, p,
648 pd.isSelected() ? paintSettings.getRelationSelectedColor(color.getAlpha()) : color,
649 fillImage, extent, pfClip, disabled, text);
650 }
651 }
652 }
653
654 /**
655 * Draws an area defined by a way. They way does not need to be closed, but it should.
656 * @param w The way.
657 * @param color The color to fill the area with.
658 * @param fillImage The image to fill the area with. Overrides color.
659 * @param extent if not null, area will be filled partially; specifies, how
660 * far to fill from the boundary towards the center of the area;
661 * if null, area will be filled completely
662 * @param extentThreshold if not null, determines if the partial filled should
663 * be replaced by plain fill, when it covers a certain fraction of the total area
664 * @param disabled If this should be drawn with a special disabled style.
665 * @param text The text to write on the area.
666 */
667 public void drawArea(Way w, Color color, MapImage fillImage, Float extent, Float extentThreshold, boolean disabled, TextLabel text) {
668 Path2D.Double pfClip = null;
669 if (extent != null) {
670 if (!usePartialFill(Geometry.getAreaAndPerimeter(w.getNodes()), extent, extentThreshold)) {
671 extent = null;
672 } else if (!w.isClosed()) {
673 pfClip = getPFClip(w, extent * scale);
674 }
675 }
676 drawArea(w, getPath(w), color, fillImage, extent, pfClip, disabled, text);
677 }
678
679 /**
680 * Determine, if partial fill should be turned off for this object, because
681 * only a small unfilled gap in the center of the area would be left.
682 *
683 * This is used to get a cleaner look for urban regions with many small
684 * areas like buildings, etc.
685 * @param ap the area and the perimeter of the object
686 * @param extent the "width" of partial fill
687 * @param threshold when the partial fill covers that much of the total
688 * area, the partial fill is turned off; can be greater than 100% as the
689 * covered area is estimated as <code>perimeter * extent</code>
690 * @return true, if the partial fill should be used, false otherwise
691 */
692 private boolean usePartialFill(AreaAndPerimeter ap, float extent, Float threshold) {
693 if (threshold == null) return true;
694 return ap.getPerimeter() * extent * scale < threshold * ap.getArea();
695 }
696
697 public void drawBoxText(Node n, BoxTextElement bs) {
698 if (!isShowNames() || bs == null)
699 return;
700
701 Point p = nc.getPoint(n);
702 TextLabel text = bs.text;
703 String s = text.labelCompositionStrategy.compose(n);
704 if (s == null) return;
705
706 Font defaultFont = g.getFont();
707 g.setFont(text.font);
708
709 int x = p.x + text.xOffset;
710 int y = p.y + text.yOffset;
711 /**
712 *
713 * left-above __center-above___ right-above
714 * left-top| |right-top
715 * | |
716 * left-center| center-center |right-center
717 * | |
718 * left-bottom|_________________|right-bottom
719 * left-below center-below right-below
720 *
721 */
722 Rectangle box = bs.getBox();
723 if (bs.hAlign == HorizontalTextAlignment.RIGHT) {
724 x += box.x + box.width + 2;
725 } else {
726 FontRenderContext frc = g.getFontRenderContext();
727 Rectangle2D bounds = text.font.getStringBounds(s, frc);
728 int textWidth = (int) bounds.getWidth();
729 if (bs.hAlign == HorizontalTextAlignment.CENTER) {
730 x -= textWidth / 2;
731 } else if (bs.hAlign == HorizontalTextAlignment.LEFT) {
732 x -= -box.x + 4 + textWidth;
733 } else throw new AssertionError();
734 }
735
736 if (bs.vAlign == VerticalTextAlignment.BOTTOM) {
737 y += box.y + box.height;
738 } else {
739 FontRenderContext frc = g.getFontRenderContext();
740 LineMetrics metrics = text.font.getLineMetrics(s, frc);
741 if (bs.vAlign == VerticalTextAlignment.ABOVE) {
742 y -= -box.y + metrics.getDescent();
743 } else if (bs.vAlign == VerticalTextAlignment.TOP) {
744 y -= -box.y - metrics.getAscent();
745 } else if (bs.vAlign == VerticalTextAlignment.CENTER) {
746 y += (metrics.getAscent() - metrics.getDescent()) / 2;
747 } else if (bs.vAlign == VerticalTextAlignment.BELOW) {
748 y += box.y + box.height + metrics.getAscent() + 2;
749 } else throw new AssertionError();
750 }
751 displayText(null, s, x, y, n.isDisabled(), text);
752 g.setFont(defaultFont);
753 }
754
755 /**
756 * Draw an image along a way repeatedly.
757 *
758 * @param way the way
759 * @param pattern the image
760 * @param disabled If this should be drawn with a special disabled style.
761 * @param offset offset from the way
762 * @param spacing spacing between two images
763 * @param phase initial spacing
764 * @param align alignment of the image. The top, center or bottom edge can be aligned with the way.
765 */
766 public void drawRepeatImage(Way way, MapImage pattern, boolean disabled, double offset, double spacing, double phase,
767 LineImageAlignment align) {
768 final int imgWidth = pattern.getWidth();
769 final double repeat = imgWidth + spacing;
770 final int imgHeight = pattern.getHeight();
771
772 Point lastP = null;
773 double currentWayLength = phase % repeat;
774 if (currentWayLength < 0) {
775 currentWayLength += repeat;
776 }
777
778 int dy1, dy2;
779 switch (align) {
780 case TOP:
781 dy1 = 0;
782 dy2 = imgHeight;
783 break;
784 case CENTER:
785 dy1 = -imgHeight / 2;
786 dy2 = imgHeight + dy1;
787 break;
788 case BOTTOM:
789 dy1 = -imgHeight;
790 dy2 = 0;
791 break;
792 default:
793 throw new AssertionError();
794 }
795
796 OffsetIterator it = new OffsetIterator(way.getNodes(), offset);
797 while (it.hasNext()) {
798 Point thisP = it.next();
799
800 if (lastP != null) {
801 final double segmentLength = thisP.distance(lastP);
802
803 final double dx = (double) thisP.x - lastP.x;
804 final double dy = (double) thisP.y - lastP.y;
805
806 // pos is the position from the beginning of the current segment
807 // where an image should be painted
808 double pos = repeat - (currentWayLength % repeat);
809
810 AffineTransform saveTransform = g.getTransform();
811 g.translate(lastP.x, lastP.y);
812 g.rotate(Math.atan2(dy, dx));
813
814 // draw the rest of the image from the last segment in case it
815 // is cut off
816 if (pos > spacing) {
817 // segment is too short for a complete image
818 if (pos > segmentLength + spacing) {
819 g.drawImage(pattern.getImage(disabled), 0, dy1, (int) segmentLength, dy2,
820 (int) (repeat - pos), 0,
821 (int) (repeat - pos + segmentLength), imgHeight, null);
822 } else {
823 // rest of the image fits fully on the current segment
824 g.drawImage(pattern.getImage(disabled), 0, dy1, (int) (pos - spacing), dy2,
825 (int) (repeat - pos), 0, imgWidth, imgHeight, null);
826 }
827 }
828 // draw remaining images for this segment
829 while (pos < segmentLength) {
830 // cut off at the end?
831 if (pos + imgWidth > segmentLength) {
832 g.drawImage(pattern.getImage(disabled), (int) pos, dy1, (int) segmentLength, dy2,
833 0, 0, (int) segmentLength - (int) pos, imgHeight, null);
834 } else {
835 g.drawImage(pattern.getImage(disabled), (int) pos, dy1, nc);
836 }
837 pos += repeat;
838 }
839 g.setTransform(saveTransform);
840
841 currentWayLength += segmentLength;
842 }
843 lastP = thisP;
844 }
845 }
846
847 @Override
848 public void drawNode(Node n, Color color, int size, boolean fill) {
849 if (size <= 0 && !n.isHighlighted())
850 return;
851
852 Point p = nc.getPoint(n);
853
854 if (n.isHighlighted()) {
855 drawPointHighlight(p, size);
856 }
857
858 if (size > 1) {
859 if ((p.x < 0) || (p.y < 0) || (p.x > nc.getWidth()) || (p.y > nc.getHeight())) return;
860 int radius = size / 2;
861
862 if (isInactiveMode || n.isDisabled()) {
863 g.setColor(inactiveColor);
864 } else {
865 g.setColor(color);
866 }
867 if (fill) {
868 g.fillRect(p.x-radius-1, p.y-radius-1, size + 1, size + 1);
869 } else {
870 g.drawRect(p.x-radius-1, p.y-radius-1, size, size);
871 }
872 }
873 }
874
875 public void drawNodeIcon(Node n, MapImage img, boolean disabled, boolean selected, boolean member, double theta) {
876 Point p = nc.getPoint(n);
877
878 final int w = img.getWidth(), h = img.getHeight();
879 if (n.isHighlighted()) {
880 drawPointHighlight(p, Math.max(w, h));
881 }
882
883 float alpha = img.getAlphaFloat();
884
885 if (!Utils.equalsEpsilon(alpha, 1f)) {
886 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
887 }
888 g.rotate(theta, p.x, p.y);
889 g.drawImage(img.getImage(disabled), p.x - w/2 + img.offsetX, p.y - h/2 + img.offsetY, nc);
890 g.rotate(-theta, p.x, p.y);
891 g.setPaintMode();
892 if (selected || member) {
893 Color color;
894 if (disabled) {
895 color = inactiveColor;
896 } else if (selected) {
897 color = selectedColor;
898 } else {
899 color = relationSelectedColor;
900 }
901 g.setColor(color);
902 g.drawRect(p.x - w/2 + img.offsetX - 2, p.y - h/2 + img.offsetY - 2, w + 4, h + 4);
903 }
904 }
905
906 public void drawNodeSymbol(Node n, Symbol s, Color fillColor, Color strokeColor) {
907 Point p = nc.getPoint(n);
908 int radius = s.size / 2;
909
910 if (n.isHighlighted()) {
911 drawPointHighlight(p, s.size);
912 }
913
914 if (fillColor != null) {
915 g.setColor(fillColor);
916 switch (s.symbol) {
917 case SQUARE:
918 g.fillRect(p.x - radius, p.y - radius, s.size, s.size);
919 break;
920 case CIRCLE:
921 g.fillOval(p.x - radius, p.y - radius, s.size, s.size);
922 break;
923 case TRIANGLE:
924 g.fillPolygon(buildPolygon(p, radius, 3, Math.PI / 2));
925 break;
926 case PENTAGON:
927 g.fillPolygon(buildPolygon(p, radius, 5, Math.PI / 2));
928 break;
929 case HEXAGON:
930 g.fillPolygon(buildPolygon(p, radius, 6));
931 break;
932 case HEPTAGON:
933 g.fillPolygon(buildPolygon(p, radius, 7, Math.PI / 2));
934 break;
935 case OCTAGON:
936 g.fillPolygon(buildPolygon(p, radius, 8, Math.PI / 8));
937 break;
938 case NONAGON:
939 g.fillPolygon(buildPolygon(p, radius, 9, Math.PI / 2));
940 break;
941 case DECAGON:
942 g.fillPolygon(buildPolygon(p, radius, 10));
943 break;
944 default:
945 throw new AssertionError();
946 }
947 }
948 if (s.stroke != null) {
949 g.setStroke(s.stroke);
950 g.setColor(strokeColor);
951 switch (s.symbol) {
952 case SQUARE:
953 g.drawRect(p.x - radius, p.y - radius, s.size - 1, s.size - 1);
954 break;
955 case CIRCLE:
956 g.drawOval(p.x - radius, p.y - radius, s.size - 1, s.size - 1);
957 break;
958 case TRIANGLE:
959 g.drawPolygon(buildPolygon(p, radius, 3, Math.PI / 2));
960 break;
961 case PENTAGON:
962 g.drawPolygon(buildPolygon(p, radius, 5, Math.PI / 2));
963 break;
964 case HEXAGON:
965 g.drawPolygon(buildPolygon(p, radius, 6));
966 break;
967 case HEPTAGON:
968 g.drawPolygon(buildPolygon(p, radius, 7, Math.PI / 2));
969 break;
970 case OCTAGON:
971 g.drawPolygon(buildPolygon(p, radius, 8, Math.PI / 8));
972 break;
973 case NONAGON:
974 g.drawPolygon(buildPolygon(p, radius, 9, Math.PI / 2));
975 break;
976 case DECAGON:
977 g.drawPolygon(buildPolygon(p, radius, 10));
978 break;
979 default:
980 throw new AssertionError();
981 }
982 g.setStroke(new BasicStroke());
983 }
984 }
985
986 /**
987 * Draw a number of the order of the two consecutive nodes within the
988 * parents way
989 *
990 * @param n1 First node of the way segment.
991 * @param n2 Second node of the way segment.
992 * @param orderNumber The number of the segment in the way.
993 * @param clr The color to use for drawing the text.
994 */
995 public void drawOrderNumber(Node n1, Node n2, int orderNumber, Color clr) {
996 Point p1 = nc.getPoint(n1);
997 Point p2 = nc.getPoint(n2);
998 drawOrderNumber(p1, p2, orderNumber, clr);
999 }
1000
1001 /**
1002 * highlights a given GeneralPath using the settings from BasicStroke to match the line's
1003 * style. Width of the highlight is hard coded.
1004 * @param path path to draw
1005 * @param line line style
1006 */
1007 private void drawPathHighlight(GeneralPath path, BasicStroke line) {
1008 if (path == null)
1009 return;
1010 g.setColor(highlightColorTransparent);
1011 float w = line.getLineWidth() + highlightLineWidth;
1012 if (useWiderHighlight) w += widerHighlight;
1013 while (w >= line.getLineWidth()) {
1014 g.setStroke(new BasicStroke(w, line.getEndCap(), line.getLineJoin(), line.getMiterLimit()));
1015 g.draw(path);
1016 w -= highlightStep;
1017 }
1018 }
1019
1020 /**
1021 * highlights a given point by drawing a rounded rectangle around it. Give the
1022 * size of the object you want to be highlighted, width is added automatically.
1023 * @param p point
1024 * @param size highlight size
1025 */
1026 private void drawPointHighlight(Point p, int size) {
1027 g.setColor(highlightColorTransparent);
1028 int s = size + highlightPointRadius;
1029 if (useWiderHighlight) s += widerHighlight;
1030 while (s >= size) {
1031 int r = (int) Math.floor(s/2d);
1032 g.fillRoundRect(p.x-r, p.y-r, s, s, r, r);
1033 s -= highlightStep;
1034 }
1035 }
1036
1037 public void drawRestriction(Image img, Point pVia, double vx, double vx2, double vy, double vy2, double angle, boolean selected) {
1038 // rotate image with direction last node in from to, and scale down image to 16*16 pixels
1039 Image smallImg = ImageProvider.createRotatedImage(img, angle, new Dimension(16, 16));
1040 int w = smallImg.getWidth(null), h = smallImg.getHeight(null);
1041 g.drawImage(smallImg, (int) (pVia.x+vx+vx2)-w/2, (int) (pVia.y+vy+vy2)-h/2, nc);
1042
1043 if (selected) {
1044 g.setColor(isInactiveMode ? inactiveColor : relationSelectedColor);
1045 g.drawRect((int) (pVia.x+vx+vx2)-w/2-2, (int) (pVia.y+vy+vy2)-h/2-2, w+4, h+4);
1046 }
1047 }
1048
1049 public void drawRestriction(Relation r, MapImage icon, boolean disabled) {
1050 Way fromWay = null;
1051 Way toWay = null;
1052 OsmPrimitive via = null;
1053
1054 /* find the "from", "via" and "to" elements */
1055 for (RelationMember m : r.getMembers()) {
1056 if (m.getMember().isIncomplete())
1057 return;
1058 else {
1059 if (m.isWay()) {
1060 Way w = m.getWay();
1061 if (w.getNodesCount() < 2) {
1062 continue;
1063 }
1064
1065 switch(m.getRole()) {
1066 case "from":
1067 if (fromWay == null) {
1068 fromWay = w;
1069 }
1070 break;
1071 case "to":
1072 if (toWay == null) {
1073 toWay = w;
1074 }
1075 break;
1076 case "via":
1077 if (via == null) {
1078 via = w;
1079 }
1080 break;
1081 default: // Do nothing
1082 }
1083 } else if (m.isNode()) {
1084 Node n = m.getNode();
1085 if ("via".equals(m.getRole()) && via == null) {
1086 via = n;
1087 }
1088 }
1089 }
1090 }
1091
1092 if (fromWay == null || toWay == null || via == null)
1093 return;
1094
1095 Node viaNode;
1096 if (via instanceof Node) {
1097 viaNode = (Node) via;
1098 if (!fromWay.isFirstLastNode(viaNode))
1099 return;
1100 } else {
1101 Way viaWay = (Way) via;
1102 Node firstNode = viaWay.firstNode();
1103 Node lastNode = viaWay.lastNode();
1104 Boolean onewayvia = Boolean.FALSE;
1105
1106 String onewayviastr = viaWay.get("oneway");
1107 if (onewayviastr != null) {
1108 if ("-1".equals(onewayviastr)) {
1109 onewayvia = Boolean.TRUE;
1110 Node tmp = firstNode;
1111 firstNode = lastNode;
1112 lastNode = tmp;
1113 } else {
1114 onewayvia = OsmUtils.getOsmBoolean(onewayviastr);
1115 if (onewayvia == null) {
1116 onewayvia = Boolean.FALSE;
1117 }
1118 }
1119 }
1120
1121 if (fromWay.isFirstLastNode(firstNode)) {
1122 viaNode = firstNode;
1123 } else if (!onewayvia && fromWay.isFirstLastNode(lastNode)) {
1124 viaNode = lastNode;
1125 } else
1126 return;
1127 }
1128
1129 /* find the "direct" nodes before the via node */
1130 Node fromNode;
1131 if (fromWay.firstNode() == via) {
1132 fromNode = fromWay.getNode(1);
1133 } else {
1134 fromNode = fromWay.getNode(fromWay.getNodesCount()-2);
1135 }
1136
1137 Point pFrom = nc.getPoint(fromNode);
1138 Point pVia = nc.getPoint(viaNode);
1139
1140 /* starting from via, go back the "from" way a few pixels
1141 (calculate the vector vx/vy with the specified length and the direction
1142 away from the "via" node along the first segment of the "from" way)
1143 */
1144 double distanceFromVia = 14;
1145 double dx = pFrom.x >= pVia.x ? pFrom.x - pVia.x : pVia.x - pFrom.x;
1146 double dy = pFrom.y >= pVia.y ? pFrom.y - pVia.y : pVia.y - pFrom.y;
1147
1148 double fromAngle;
1149 if (dx == 0) {
1150 fromAngle = Math.PI/2;
1151 } else {
1152 fromAngle = Math.atan(dy / dx);
1153 }
1154 double fromAngleDeg = Math.toDegrees(fromAngle);
1155
1156 double vx = distanceFromVia * Math.cos(fromAngle);
1157 double vy = distanceFromVia * Math.sin(fromAngle);
1158
1159 if (pFrom.x < pVia.x) {
1160 vx = -vx;
1161 }
1162 if (pFrom.y < pVia.y) {
1163 vy = -vy;
1164 }
1165
1166 /* go a few pixels away from the way (in a right angle)
1167 (calculate the vx2/vy2 vector with the specified length and the direction
1168 90degrees away from the first segment of the "from" way)
1169 */
1170 double distanceFromWay = 10;
1171 double vx2 = 0;
1172 double vy2 = 0;
1173 double iconAngle = 0;
1174
1175 if (pFrom.x >= pVia.x && pFrom.y >= pVia.y) {
1176 if (!leftHandTraffic) {
1177 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
1178 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
1179 } else {
1180 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
1181 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
1182 }
1183 iconAngle = 270+fromAngleDeg;
1184 }
1185 if (pFrom.x < pVia.x && pFrom.y >= pVia.y) {
1186 if (!leftHandTraffic) {
1187 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
1188 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
1189 } else {
1190 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
1191 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
1192 }
1193 iconAngle = 90-fromAngleDeg;
1194 }
1195 if (pFrom.x < pVia.x && pFrom.y < pVia.y) {
1196 if (!leftHandTraffic) {
1197 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
1198 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
1199 } else {
1200 vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
1201 vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
1202 }
1203 iconAngle = 90+fromAngleDeg;
1204 }
1205 if (pFrom.x >= pVia.x && pFrom.y < pVia.y) {
1206 if (!leftHandTraffic) {
1207 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
1208 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
1209 } else {
1210 vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
1211 vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
1212 }
1213 iconAngle = 270-fromAngleDeg;
1214 }
1215
1216 drawRestriction(icon.getImage(disabled),
1217 pVia, vx, vx2, vy, vy2, iconAngle, r.isSelected());
1218 }
1219
1220 /**
1221 * Draws a text along a given way.
1222 * @param way The way to draw the text on.
1223 * @param text The text definition (font/.../text content) to draw.
1224 */
1225 public void drawTextOnPath(Way way, TextLabel text) {
1226 if (way == null || text == null)
1227 return;
1228 String name = text.getString(way);
1229 if (name == null || name.isEmpty())
1230 return;
1231
1232 FontMetrics fontMetrics = g.getFontMetrics(text.font);
1233 Rectangle2D rec = fontMetrics.getStringBounds(name, g);
1234
1235 Rectangle bounds = g.getClipBounds();
1236
1237 Polygon poly = new Polygon();
1238 Point lastPoint = null;
1239 Iterator<Node> it = way.getNodes().iterator();
1240 double pathLength = 0;
1241 long dx, dy;
1242
1243 // find half segments that are long enough to draw text on (don't draw text over the cross hair in the center of each segment)
1244 List<Double> longHalfSegmentStart = new ArrayList<>(); // start point of half segment (as length along the way)
1245 List<Double> longHalfSegmentEnd = new ArrayList<>(); // end point of half segment (as length along the way)
1246 List<Double> longHalfsegmentQuality = new ArrayList<>(); // quality factor (off screen / partly on screen / fully on screen)
1247
1248 while (it.hasNext()) {
1249 Node n = it.next();
1250 Point p = nc.getPoint(n);
1251 poly.addPoint(p.x, p.y);
1252
1253 if (lastPoint != null) {
1254 dx = (long) p.x - lastPoint.x;
1255 dy = (long) p.y - lastPoint.y;
1256 double segmentLength = Math.sqrt(dx*dx + dy*dy);
1257 if (segmentLength > 2*(rec.getWidth()+4)) {
1258 Point center = new Point((lastPoint.x + p.x)/2, (lastPoint.y + p.y)/2);
1259 double q = 0;
1260 if (bounds != null) {
1261 if (bounds.contains(lastPoint) && bounds.contains(center)) {
1262 q = 2;
1263 } else if (bounds.contains(lastPoint) || bounds.contains(center)) {
1264 q = 1;
1265 }
1266 }
1267 longHalfSegmentStart.add(pathLength);
1268 longHalfSegmentEnd.add(pathLength + segmentLength / 2);
1269 longHalfsegmentQuality.add(q);
1270
1271 q = 0;
1272 if (bounds != null) {
1273 if (bounds.contains(center) && bounds.contains(p)) {
1274 q = 2;
1275 } else if (bounds.contains(center) || bounds.contains(p)) {
1276 q = 1;
1277 }
1278 }
1279 longHalfSegmentStart.add(pathLength + segmentLength / 2);
1280 longHalfSegmentEnd.add(pathLength + segmentLength);
1281 longHalfsegmentQuality.add(q);
1282 }
1283 pathLength += segmentLength;
1284 }
1285 lastPoint = p;
1286 }
1287
1288 if (rec.getWidth() > pathLength)
1289 return;
1290
1291 double t1, t2;
1292
1293 if (!longHalfSegmentStart.isEmpty()) {
1294 if (way.getNodesCount() == 2) {
1295 // For 2 node ways, the two half segments are exactly the same size and distance from the center.
1296 // Prefer the first one for consistency.
1297 longHalfsegmentQuality.set(0, longHalfsegmentQuality.get(0) + 0.5);
1298 }
1299
1300 // find the long half segment that is closest to the center of the way
1301 // candidates with higher quality value are preferred
1302 double bestStart = Double.NaN;
1303 double bestEnd = Double.NaN;
1304 double bestDistanceToCenter = Double.MAX_VALUE;
1305 double bestQuality = -1;
1306 for (int i = 0; i < longHalfSegmentStart.size(); i++) {
1307 double start = longHalfSegmentStart.get(i);
1308 double end = longHalfSegmentEnd.get(i);
1309 double dist = Math.abs(0.5 * (end + start) - 0.5 * pathLength);
1310 if (longHalfsegmentQuality.get(i) > bestQuality
1311 || (dist < bestDistanceToCenter && Utils.equalsEpsilon(longHalfsegmentQuality.get(i), bestQuality))) {
1312 bestStart = start;
1313 bestEnd = end;
1314 bestDistanceToCenter = dist;
1315 bestQuality = longHalfsegmentQuality.get(i);
1316 }
1317 }
1318 double remaining = bestEnd - bestStart - rec.getWidth(); // total space left and right from the text
1319 // The space left and right of the text should be distributed 20% - 80% (towards the center),
1320 // but the smaller space should not be less than 7 px.
1321 // However, if the total remaining space is less than 14 px, then distribute it evenly.
1322 double smallerSpace = Math.min(Math.max(0.2 * remaining, 7), 0.5 * remaining);
1323 if ((bestEnd + bestStart)/2 < pathLength/2) {
1324 t2 = bestEnd - smallerSpace;
1325 t1 = t2 - rec.getWidth();
1326 } else {
1327 t1 = bestStart + smallerSpace;
1328 t2 = t1 + rec.getWidth();
1329 }
1330 } else {
1331 // doesn't fit into one half-segment -> just put it in the center of the way
1332 t1 = pathLength/2 - rec.getWidth()/2;
1333 t2 = pathLength/2 + rec.getWidth()/2;
1334 }
1335 t1 /= pathLength;
1336 t2 /= pathLength;
1337
1338 double[] p1 = pointAt(t1, poly, pathLength);
1339 double[] p2 = pointAt(t2, poly, pathLength);
1340
1341 if (p1 == null || p2 == null)
1342 return;
1343
1344 double angleOffset;
1345 double offsetSign;
1346 double tStart;
1347
1348 if (p1[0] < p2[0] &&
1349 p1[2] < Math.PI/2 &&
1350 p1[2] > -Math.PI/2) {
1351 angleOffset = 0;
1352 offsetSign = 1;
1353 tStart = t1;
1354 } else {
1355 angleOffset = Math.PI;
1356 offsetSign = -1;
1357 tStart = t2;
1358 }
1359
1360 List<GlyphVector> gvs = Utils.getGlyphVectorsBidi(name, text.font, g.getFontRenderContext());
1361 double gvOffset = 0;
1362 for (GlyphVector gv : gvs) {
1363 double gvWidth = gv.getLogicalBounds().getBounds2D().getWidth();
1364 for (int i = 0; i < gv.getNumGlyphs(); ++i) {
1365 Rectangle2D rect = gv.getGlyphLogicalBounds(i).getBounds2D();
1366 double t = tStart + offsetSign * (gvOffset + rect.getX() + rect.getWidth()/2) / pathLength;
1367 double[] p = pointAt(t, poly, pathLength);
1368 if (p != null) {
1369 AffineTransform trfm = AffineTransform.getTranslateInstance(p[0] - rect.getX(), p[1]);
1370 trfm.rotate(p[2]+angleOffset);
1371 double off = -rect.getY() - rect.getHeight()/2 + text.yOffset;
1372 trfm.translate(-rect.getWidth()/2, off);
1373 if (isGlyphVectorDoubleTranslationBug(text.font)) {
1374 // scale the translation components by one half
1375 AffineTransform tmp = AffineTransform.getTranslateInstance(-0.5 * trfm.getTranslateX(), -0.5 * trfm.getTranslateY());
1376 tmp.concatenate(trfm);
1377 trfm = tmp;
1378 }
1379 gv.setGlyphTransform(i, trfm);
1380 }
1381 }
1382 displayText(gv, null, 0, 0, way.isDisabled(), text);
1383 gvOffset += gvWidth;
1384 }
1385 }
1386
1387 /**
1388 * draw way. This method allows for two draw styles (line using color, dashes using dashedColor) to be passed.
1389 * @param way The way to draw
1390 * @param color The base color to draw the way in
1391 * @param line The line style to use. This is drawn using color.
1392 * @param dashes The dash style to use. This is drawn using dashedColor. <code>null</code> if unused.
1393 * @param dashedColor The color of the dashes.
1394 * @param offset The offset
1395 * @param showOrientation show arrows that indicate the technical orientation of
1396 * the way (defined by order of nodes)
1397 * @param showHeadArrowOnly True if only the arrow at the end of the line but not those on the segments should be displayed.
1398 * @param showOneway show symbols that indicate the direction of the feature,
1399 * e.g. oneway street or waterway
1400 * @param onewayReversed for oneway=-1 and similar
1401 */
1402 public void drawWay(Way way, Color color, BasicStroke line, BasicStroke dashes, Color dashedColor, float offset,
1403 boolean showOrientation, boolean showHeadArrowOnly,
1404 boolean showOneway, boolean onewayReversed) {
1405
1406 GeneralPath path = new GeneralPath();
1407 GeneralPath orientationArrows = showOrientation ? new GeneralPath() : null;
1408 GeneralPath onewayArrows = showOneway ? new GeneralPath() : null;
1409 GeneralPath onewayArrowsCasing = showOneway ? new GeneralPath() : null;
1410 Rectangle bounds = g.getClipBounds();
1411 if (bounds != null) {
1412 // avoid arrow heads at the border
1413 bounds.grow(100, 100);
1414 }
1415
1416 double wayLength = 0;
1417 Point lastPoint = null;
1418 boolean initialMoveToNeeded = true;
1419 List<Node> wayNodes = way.getNodes();
1420 if (wayNodes.size() < 2) return;
1421
1422 // only highlight the segment if the way itself is not highlighted
1423 if (!way.isHighlighted() && highlightWaySegments != null) {
1424 GeneralPath highlightSegs = null;
1425 for (WaySegment ws : highlightWaySegments) {
1426 if (ws.way != way || ws.lowerIndex < offset) {
1427 continue;
1428 }
1429 if (highlightSegs == null) {
1430 highlightSegs = new GeneralPath();
1431 }
1432
1433 Point p1 = nc.getPoint(ws.getFirstNode());
1434 Point p2 = nc.getPoint(ws.getSecondNode());
1435 highlightSegs.moveTo(p1.x, p1.y);
1436 highlightSegs.lineTo(p2.x, p2.y);
1437 }
1438
1439 drawPathHighlight(highlightSegs, line);
1440 }
1441
1442 Iterator<Point> it = new OffsetIterator(wayNodes, offset);
1443 while (it.hasNext()) {
1444 Point p = it.next();
1445 if (lastPoint != null) {
1446 Point p1 = lastPoint;
1447 Point p2 = p;
1448
1449 /**
1450 * Do custom clipping to work around openjdk bug. It leads to
1451 * drawing artefacts when zooming in a lot. (#4289, #4424)
1452 * (Looks like int overflow.)
1453 */
1454 LineClip clip = new LineClip(p1, p2, bounds);
1455 if (clip.execute()) {
1456 if (!p1.equals(clip.getP1())) {
1457 p1 = clip.getP1();
1458 path.moveTo(p1.x, p1.y);
1459 } else if (initialMoveToNeeded) {
1460 initialMoveToNeeded = false;
1461 path.moveTo(p1.x, p1.y);
1462 }
1463 p2 = clip.getP2();
1464 path.lineTo(p2.x, p2.y);
1465
1466 /* draw arrow */
1467 if (showHeadArrowOnly ? !it.hasNext() : showOrientation) {
1468 final double segmentLength = p1.distance(p2);
1469 if (segmentLength != 0) {
1470 final double l = (10. + line.getLineWidth()) / segmentLength;
1471
1472 final double sx = l * (p1.x - p2.x);
1473 final double sy = l * (p1.y - p2.y);
1474
1475 orientationArrows.moveTo(p2.x + cosPHI * sx - sinPHI * sy, p2.y + sinPHI * sx + cosPHI * sy);
1476 orientationArrows.lineTo(p2.x, p2.y);
1477 orientationArrows.lineTo(p2.x + cosPHI * sx + sinPHI * sy, p2.y - sinPHI * sx + cosPHI * sy);
1478 }
1479 }
1480 if (showOneway) {
1481 final double segmentLength = p1.distance(p2);
1482 if (segmentLength != 0) {
1483 final double nx = (p2.x - p1.x) / segmentLength;
1484 final double ny = (p2.y - p1.y) / segmentLength;
1485
1486 final double interval = 60;
1487 // distance from p1
1488 double dist = interval - (wayLength % interval);
1489
1490 while (dist < segmentLength) {
1491 for (int i = 0; i < 2; ++i) {
1492 double onewaySize = i == 0 ? 3d : 2d;
1493 GeneralPath onewayPath = i == 0 ? onewayArrowsCasing : onewayArrows;
1494
1495 // scale such that border is 1 px
1496 final double fac = -(onewayReversed ? -1 : 1) * onewaySize * (1 + sinPHI) / (sinPHI * cosPHI);
1497 final double sx = nx * fac;
1498 final double sy = ny * fac;
1499
1500 // Attach the triangle at the incenter and not at the tip.
1501 // Makes the border even at all sides.
1502 final double x = p1.x + nx * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI));
1503 final double y = p1.y + ny * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI));
1504
1505 onewayPath.moveTo(x, y);
1506 onewayPath.lineTo(x + cosPHI * sx - sinPHI * sy, y + sinPHI * sx + cosPHI * sy);
1507 onewayPath.lineTo(x + cosPHI * sx + sinPHI * sy, y - sinPHI * sx + cosPHI * sy);
1508 onewayPath.lineTo(x, y);
1509 }
1510 dist += interval;
1511 }
1512 }
1513 wayLength += segmentLength;
1514 }
1515 }
1516 }
1517 lastPoint = p;
1518 }
1519 if (way.isHighlighted()) {
1520 drawPathHighlight(path, line);
1521 }
1522 displaySegments(path, orientationArrows, onewayArrows, onewayArrowsCasing, color, line, dashes, dashedColor);
1523 }
1524
1525 /**
1526 * Gets the "circum". This is the distance on the map in meters that 100 screen pixels represent.
1527 * @return The "circum"
1528 */
1529 public double getCircum() {
1530 return circum;
1531 }
1532
1533 @Override
1534 public void getColors() {
1535 super.getColors();
1536 this.highlightColorTransparent = new Color(highlightColor.getRed(), highlightColor.getGreen(), highlightColor.getBlue(), 100);
1537 this.backgroundColor = PaintColors.getBackgroundColor();
1538 }
1539
1540 @Override
1541 public void getSettings(boolean virtual) {
1542 super.getSettings(virtual);
1543 paintSettings = MapPaintSettings.INSTANCE;
1544
1545 circum = nc.getDist100Pixel();
1546 scale = nc.getScale();
1547
1548 leftHandTraffic = Main.pref.getBoolean("mappaint.lefthandtraffic", false);
1549
1550 useStrokes = paintSettings.getUseStrokesDistance() > circum;
1551 showNames = paintSettings.getShowNamesDistance() > circum;
1552 showIcons = paintSettings.getShowIconsDistance() > circum;
1553 isOutlineOnly = paintSettings.isOutlineOnly();
1554 orderFont = new Font(Main.pref.get("mappaint.font", "Droid Sans"), Font.PLAIN, Main.pref.getInteger("mappaint.fontsize", 8));
1555
1556 antialiasing = Main.pref.getBoolean("mappaint.use-antialiasing", true) ?
1557 RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF;
1558 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing);
1559
1560 Object textAntialiasing;
1561 switch (Main.pref.get("mappaint.text-antialiasing", "default")) {
1562 case "on":
1563 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_ON;
1564 break;
1565 case "off":
1566 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_OFF;
1567 break;
1568 case "gasp":
1569 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_GASP;
1570 break;
1571 case "lcd-hrgb":
1572 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB;
1573 break;
1574 case "lcd-hbgr":
1575 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HBGR;
1576 break;
1577 case "lcd-vrgb":
1578 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VRGB;
1579 break;
1580 case "lcd-vbgr":
1581 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VBGR;
1582 break;
1583 default:
1584 textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT;
1585 }
1586 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, textAntialiasing);
1587
1588 highlightLineWidth = Main.pref.getInteger("mappaint.highlight.width", 4);
1589 highlightPointRadius = Main.pref.getInteger("mappaint.highlight.radius", 7);
1590 widerHighlight = Main.pref.getInteger("mappaint.highlight.bigger-increment", 5);
1591 highlightStep = Main.pref.getInteger("mappaint.highlight.step", 4);
1592 }
1593
1594 private static Path2D.Double getPath(Way w) {
1595 Path2D.Double path = new Path2D.Double();
1596 boolean initial = true;
1597 for (Node n : w.getNodes()) {
1598 EastNorth p = n.getEastNorth();
1599 if (p != null) {
1600 if (initial) {
1601 path.moveTo(p.getX(), p.getY());
1602 initial = false;
1603 } else {
1604 path.lineTo(p.getX(), p.getY());
1605 }
1606 }
1607 }
1608 if (w.isClosed()) {
1609 path.closePath();
1610 }
1611 return path;
1612 }
1613
1614 private static Path2D.Double getPFClip(Way w, double extent) {
1615 Path2D.Double clip = new Path2D.Double();
1616 buildPFClip(clip, w.getNodes(), extent);
1617 return clip;
1618 }
1619
1620 private static Path2D.Double getPFClip(PolyData pd, double extent) {
1621 Path2D.Double clip = new Path2D.Double();
1622 clip.setWindingRule(Path2D.WIND_EVEN_ODD);
1623 buildPFClip(clip, pd.getNodes(), extent);
1624 for (PolyData pdInner : pd.getInners()) {
1625 buildPFClip(clip, pdInner.getNodes(), extent);
1626 }
1627 return clip;
1628 }
1629
1630 /**
1631 * Fix the clipping area of unclosed polygons for partial fill.
1632 *
1633 * The current algorithm for partial fill simply strokes the polygon with a
1634 * large stroke width after masking the outside with a clipping area.
1635 * This works, but for unclosed polygons, the mask can crop the corners at
1636 * both ends (see #12104).
1637 *
1638 * This method fixes the clipping area by sort of adding the corners to the
1639 * clip outline.
1640 *
1641 * @param clip the clipping area to modify (initially empty)
1642 * @param nodes nodes of the polygon
1643 * @param extent the extent
1644 */
1645 private static void buildPFClip(Path2D.Double clip, List<Node> nodes, double extent) {
1646 boolean initial = true;
1647 for (Node n : nodes) {
1648 EastNorth p = n.getEastNorth();
1649 if (p != null) {
1650 if (initial) {
1651 clip.moveTo(p.getX(), p.getY());
1652 initial = false;
1653 } else {
1654 clip.lineTo(p.getX(), p.getY());
1655 }
1656 }
1657 }
1658 if (nodes.size() >= 3) {
1659 EastNorth fst = nodes.get(0).getEastNorth();
1660 EastNorth snd = nodes.get(1).getEastNorth();
1661 EastNorth lst = nodes.get(nodes.size() - 1).getEastNorth();
1662 EastNorth lbo = nodes.get(nodes.size() - 2).getEastNorth();
1663
1664 EastNorth cLst = getPFDisplacedEndPoint(lbo, lst, fst, extent);
1665 EastNorth cFst = getPFDisplacedEndPoint(snd, fst, cLst != null ? cLst : lst, extent);
1666 if (cLst == null && cFst != null) {
1667 cLst = getPFDisplacedEndPoint(lbo, lst, cFst, extent);
1668 }
1669 if (cLst != null) {
1670 clip.lineTo(cLst.getX(), cLst.getY());
1671 }
1672 if (cFst != null) {
1673 clip.lineTo(cFst.getX(), cFst.getY());
1674 }
1675 }
1676 }
1677
1678 /**
1679 * Get the point to add to the clipping area for partial fill of unclosed polygons.
1680 *
1681 * <code>(p1,p2)</code> is the first or last way segment and <code>p3</code> the
1682 * opposite endpoint.
1683 *
1684 * @param p1 1st point
1685 * @param p2 2nd point
1686 * @param p3 3rd point
1687 * @param extent the extent
1688 * @return a point q, such that p1,p2,q form a right angle
1689 * and the distance of q to p2 is <code>extent</code>. The point q lies on
1690 * the same side of the line p1,p2 as the point p3.
1691 * Returns null if p1,p2,p3 forms an angle greater 90 degrees. (In this case
1692 * the corner of the partial fill would not be cut off by the mask, so an
1693 * additional point is not necessary.)
1694 */
1695 private static EastNorth getPFDisplacedEndPoint(EastNorth p1, EastNorth p2, EastNorth p3, double extent) {
1696 double dx1 = p2.getX() - p1.getX();
1697 double dy1 = p2.getY() - p1.getY();
1698 double dx2 = p3.getX() - p2.getX();
1699 double dy2 = p3.getY() - p2.getY();
1700 if (dx1 * dx2 + dy1 * dy2 < 0) {
1701 double len = Math.sqrt(dx1 * dx1 + dy1 * dy1);
1702 if (len == 0) return null;
1703 double dxm = -dy1 * extent / len;
1704 double dym = dx1 * extent / len;
1705 if (dx1 * dy2 - dx2 * dy1 < 0) {
1706 dxm = -dxm;
1707 dym = -dym;
1708 }
1709 return new EastNorth(p2.getX() + dxm, p2.getY() + dym);
1710 }
1711 return null;
1712 }
1713
1714 private boolean isAreaVisible(Path2D.Double area) {
1715 Rectangle2D bounds = area.getBounds2D();
1716 if (bounds.isEmpty()) return false;
1717 Point2D p = nc.getPoint2D(new EastNorth(bounds.getX(), bounds.getY()));
1718 if (p.getX() > nc.getWidth()) return false;
1719 if (p.getY() < 0) return false;
1720 p = nc.getPoint2D(new EastNorth(bounds.getX() + bounds.getWidth(), bounds.getY() + bounds.getHeight()));
1721 if (p.getX() < 0) return false;
1722 if (p.getY() > nc.getHeight()) return false;
1723 return true;
1724 }
1725
1726 public boolean isInactiveMode() {
1727 return isInactiveMode;
1728 }
1729
1730 public boolean isShowIcons() {
1731 return showIcons;
1732 }
1733
1734 public boolean isShowNames() {
1735 return showNames;
1736 }
1737
1738 private static double[] pointAt(double t, Polygon poly, double pathLength) {
1739 double totalLen = t * pathLength;
1740 double curLen = 0;
1741 long dx, dy;
1742 double segLen;
1743
1744 // Yes, it is inefficient to iterate from the beginning for each glyph.
1745 // Can be optimized if it turns out to be slow.
1746 for (int i = 1; i < poly.npoints; ++i) {
1747 dx = (long) poly.xpoints[i] - poly.xpoints[i-1];
1748 dy = (long) poly.ypoints[i] - poly.ypoints[i-1];
1749 segLen = Math.sqrt(dx*dx + dy*dy);
1750 if (totalLen > curLen + segLen) {
1751 curLen += segLen;
1752 continue;
1753 }
1754 return new double[] {
1755 poly.xpoints[i-1]+(totalLen - curLen)/segLen*dx,
1756 poly.ypoints[i-1]+(totalLen - curLen)/segLen*dy,
1757 Math.atan2(dy, dx)};
1758 }
1759 return null;
1760 }
1761
1762 /**
1763 * Computes the flags for a given OSM primitive.
1764 * @param primitive The primititve to compute the flags for.
1765 * @param checkOuterMember <code>true</code> if we should also add {@link #FLAG_OUTERMEMBER_OF_SELECTED}
1766 * @return The flag.
1767 */
1768 public static int computeFlags(OsmPrimitive primitive, boolean checkOuterMember) {
1769 if (primitive.isDisabled()) {
1770 return FLAG_DISABLED;
1771 } else if (primitive.isSelected()) {
1772 return FLAG_SELECTED;
1773 } else if (checkOuterMember && primitive.isOuterMemberOfSelected()) {
1774 return FLAG_OUTERMEMBER_OF_SELECTED;
1775 } else if (primitive.isMemberOfSelected()) {
1776 return FLAG_MEMBER_OF_SELECTED;
1777 } else {
1778 return FLAG_NORMAL;
1779 }
1780 }
1781
1782 private class ComputeStyleListWorker extends RecursiveTask<List<StyleRecord>> implements Visitor {
1783 private final transient List<? extends OsmPrimitive> input;
1784 private final transient List<StyleRecord> output;
1785
1786 private final transient ElemStyles styles = MapPaintStyles.getStyles();
1787 private final int directExecutionTaskSize;
1788
1789 private final boolean drawArea = circum <= Main.pref.getInteger("mappaint.fillareas", 10000000);
1790 private final boolean drawMultipolygon = drawArea && Main.pref.getBoolean("mappaint.multipolygon", true);
1791 private final boolean drawRestriction = Main.pref.getBoolean("mappaint.restriction", true);
1792
1793 /**
1794 * Constructs a new {@code ComputeStyleListWorker}.
1795 * @param input the primitives to process
1796 * @param output the list of styles to which styles will be added
1797 * @param directExecutionTaskSize the threshold deciding whether to subdivide the tasks
1798 */
1799 ComputeStyleListWorker(final List<? extends OsmPrimitive> input, List<StyleRecord> output, int directExecutionTaskSize) {
1800 this.input = input;
1801 this.output = output;
1802 this.directExecutionTaskSize = directExecutionTaskSize;
1803 this.styles.setDrawMultipolygon(drawMultipolygon);
1804 }
1805
1806 @Override
1807 protected List<StyleRecord> compute() {
1808 if (input.size() <= directExecutionTaskSize) {
1809 return computeDirectly();
1810 } else {
1811 final Collection<ForkJoinTask<List<StyleRecord>>> tasks = new ArrayList<>();
1812 for (int fromIndex = 0; fromIndex < input.size(); fromIndex += directExecutionTaskSize) {
1813 final int toIndex = Math.min(fromIndex + directExecutionTaskSize, input.size());
1814 final List<StyleRecord> output = new ArrayList<>(directExecutionTaskSize);
1815 tasks.add(new ComputeStyleListWorker(input.subList(fromIndex, toIndex), output, directExecutionTaskSize).fork());
1816 }
1817 for (ForkJoinTask<List<StyleRecord>> task : tasks) {
1818 output.addAll(task.join());
1819 }
1820 return output;
1821 }
1822 }
1823
1824 public List<StyleRecord> computeDirectly() {
1825 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
1826 try {
1827 for (final OsmPrimitive osm : input) {
1828 if (osm.isDrawable()) {
1829 osm.accept(this);
1830 }
1831 }
1832 return output;
1833 } finally {
1834 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
1835 }
1836 }
1837
1838 @Override
1839 public void visit(Node n) {
1840 add(n, computeFlags(n, false));
1841 }
1842
1843 @Override
1844 public void visit(Way w) {
1845 add(w, computeFlags(w, true));
1846 }
1847
1848 @Override
1849 public void visit(Relation r) {
1850 add(r, computeFlags(r, true));
1851 }
1852
1853 @Override
1854 public void visit(Changeset cs) {
1855 throw new UnsupportedOperationException();
1856 }
1857
1858 public void add(Node osm, int flags) {
1859 StyleElementList sl = styles.get(osm, circum, nc);
1860 for (StyleElement s : sl) {
1861 output.add(new StyleRecord(s, osm, flags));
1862 }
1863 }
1864
1865 public void add(Relation osm, int flags) {
1866 StyleElementList sl = styles.get(osm, circum, nc);
1867 for (StyleElement s : sl) {
1868 if (drawMultipolygon && drawArea && s instanceof AreaElement && (flags & FLAG_DISABLED) == 0) {
1869 output.add(new StyleRecord(s, osm, flags));
1870 } else if (drawRestriction && s instanceof NodeElement) {
1871 output.add(new StyleRecord(s, osm, flags));
1872 }
1873 }
1874 }
1875
1876 public void add(Way osm, int flags) {
1877 StyleElementList sl = styles.get(osm, circum, nc);
1878 for (StyleElement s : sl) {
1879 if (!(drawArea && (flags & FLAG_DISABLED) == 0) && s instanceof AreaElement) {
1880 continue;
1881 }
1882 output.add(new StyleRecord(s, osm, flags));
1883 }
1884 }
1885 }
1886
1887 /**
1888 * Sets the factory that creates the benchmark data receivers.
1889 * @param benchmarkFactory The factory.
1890 * @since 10697
1891 */
1892 public void setBenchmarkFactory(Supplier<RenderBenchmarkCollector> benchmarkFactory) {
1893 this.benchmarkFactory = benchmarkFactory;
1894 }
1895
1896 @Override
1897 public void render(final DataSet data, boolean renderVirtualNodes, Bounds bounds) {
1898 RenderBenchmarkCollector benchmark = benchmarkFactory.get();
1899 BBox bbox = bounds.toBBox();
1900 getSettings(renderVirtualNodes);
1901
1902 data.getReadLock().lock();
1903 try {
1904 highlightWaySegments = data.getHighlightedWaySegments();
1905
1906 benchmark.renderStart(circum);
1907
1908 List<Node> nodes = data.searchNodes(bbox);
1909 List<Way> ways = data.searchWays(bbox);
1910 List<Relation> relations = data.searchRelations(bbox);
1911
1912 final List<StyleRecord> allStyleElems = new ArrayList<>(nodes.size()+ways.size()+relations.size());
1913
1914 // Need to process all relations first.
1915 // Reason: Make sure, ElemStyles.getStyleCacheWithRange is
1916 // not called for the same primitive in parallel threads.
1917 // (Could be synchronized, but try to avoid this for
1918 // performance reasons.)
1919 THREAD_POOL.invoke(new ComputeStyleListWorker(relations, allStyleElems,
1920 Math.max(20, relations.size() / THREAD_POOL.getParallelism() / 3)));
1921 THREAD_POOL.invoke(new ComputeStyleListWorker(new CompositeList<>(nodes, ways), allStyleElems,
1922 Math.max(100, (nodes.size() + ways.size()) / THREAD_POOL.getParallelism() / 3)));
1923
1924 if (!benchmark.renderSort()) {
1925 return;
1926 }
1927
1928 Collections.sort(allStyleElems); // TODO: try parallel sort when switching to Java 8
1929
1930 if (!benchmark.renderDraw(allStyleElems)) {
1931 return;
1932 }
1933
1934 for (StyleRecord r : allStyleElems) {
1935 r.style.paintPrimitive(
1936 r.osm,
1937 paintSettings,
1938 this,
1939 (r.flags & FLAG_SELECTED) != 0,
1940 (r.flags & FLAG_OUTERMEMBER_OF_SELECTED) != 0,
1941 (r.flags & FLAG_MEMBER_OF_SELECTED) != 0
1942 );
1943 }
1944
1945 drawVirtualNodes(data, bbox);
1946
1947 benchmark.renderDone();
1948 } finally {
1949 data.getReadLock().unlock();
1950 }
1951 }
1952}
Note: See TracBrowser for help on using the repository browser.