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

Last change on this file since 11027 was 11027, checked in by michael2402, 8 years ago

See #13636: Speed up map paint by clipping the way segments.

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