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