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    public void drawBoxText(Node n, BoxTextElement bs) {
759        if (!isShowNames() || bs == null)
760            return;
761
762        MapViewPoint p = mapState.getPointFor(n);
763        TextLabel text = bs.text;
764        String s = text.labelCompositionStrategy.compose(n);
765        if (s == null) return;
766
767        Font defaultFont = g.getFont();
768        g.setFont(text.font);
769
770        int x = (int) (Math.round(p.getInViewX()) + text.xOffset);
771        int y = (int) (Math.round(p.getInViewY()) + text.yOffset);
772        /**
773         *
774         *       left-above __center-above___ right-above
775         *         left-top|                 |right-top
776         *                 |                 |
777         *      left-center|  center-center  |right-center
778         *                 |                 |
779         *      left-bottom|_________________|right-bottom
780         *       left-below   center-below    right-below
781         *
782         */
783        Rectangle box = bs.getBox();
784        if (bs.hAlign == HorizontalTextAlignment.RIGHT) {
785            x += box.x + box.width + 2;
786        } else {
787            FontRenderContext frc = g.getFontRenderContext();
788            Rectangle2D bounds = text.font.getStringBounds(s, frc);
789            int textWidth = (int) bounds.getWidth();
790            if (bs.hAlign == HorizontalTextAlignment.CENTER) {
791                x -= textWidth / 2;
792            } else if (bs.hAlign == HorizontalTextAlignment.LEFT) {
793                x -= -box.x + 4 + textWidth;
794            } else throw new AssertionError();
795        }
796
797        if (bs.vAlign == VerticalTextAlignment.BOTTOM) {
798            y += box.y + box.height;
799        } else {
800            FontRenderContext frc = g.getFontRenderContext();
801            LineMetrics metrics = text.font.getLineMetrics(s, frc);
802            if (bs.vAlign == VerticalTextAlignment.ABOVE) {
803                y -= -box.y + metrics.getDescent();
804            } else if (bs.vAlign == VerticalTextAlignment.TOP) {
805                y -= -box.y - metrics.getAscent();
806            } else if (bs.vAlign == VerticalTextAlignment.CENTER) {
807                y += (metrics.getAscent() - metrics.getDescent()) / 2;
808            } else if (bs.vAlign == VerticalTextAlignment.BELOW) {
809                y += box.y + box.height + metrics.getAscent() + 2;
810            } else throw new AssertionError();
811        }
812        displayText(null, s, x, y, n.isDisabled(), text);
813        g.setFont(defaultFont);
814    }
815
816    /**
817     * Draw an image along a way repeatedly.
818     *
819     * @param way the way
820     * @param pattern the image
821     * @param disabled If this should be drawn with a special disabled style.
822     * @param offset offset from the way
823     * @param spacing spacing between two images
824     * @param phase initial spacing
825     * @param align alignment of the image. The top, center or bottom edge can be aligned with the way.
826     */
827    public void drawRepeatImage(Way way, MapImage pattern, boolean disabled, double offset, double spacing, double phase,
828            LineImageAlignment align) {
829        final int imgWidth = pattern.getWidth();
830        final double repeat = imgWidth + spacing;
831        final int imgHeight = pattern.getHeight();
832
833        int dy1 = (int) ((align.getAlignmentOffset() - .5) * imgHeight);
834        int dy2 = dy1 + imgHeight;
835
836        OffsetIterator it = new OffsetIterator(way.getNodes(), offset);
837        MapViewPath path = new MapViewPath(mapState);
838        if (it.hasNext()) {
839            path.moveTo(it.next());
840        }
841        while (it.hasNext()) {
842            path.lineTo(it.next());
843        }
844
845        double startOffset = phase % repeat;
846        if (startOffset < 0) {
847            startOffset += repeat;
848        }
849
850        BufferedImage image = pattern.getImage(disabled);
851
852        path.visitClippedLine(startOffset, repeat, (inLineOffset, start, end, startIsOldEnd) -> {
853            final double segmentLength = start.distanceToInView(end);
854            if (segmentLength < 0.1) {
855                // avoid odd patterns when zoomed out.
856                return;
857            }
858            if (segmentLength > repeat * 500) {
859                // simply skip drawing so many images - something must be wrong.
860                return;
861            }
862            AffineTransform saveTransform = g.getTransform();
863            g.translate(start.getInViewX(), start.getInViewY());
864            double dx = end.getInViewX() - start.getInViewX();
865            double dy = end.getInViewY() - start.getInViewY();
866            g.rotate(Math.atan2(dy, dx));
867
868            // The start of the next image
869            double imageStart = -(inLineOffset % repeat);
870
871            while (imageStart < segmentLength) {
872                int x = (int) imageStart;
873                int sx1 = Math.max(0, -x);
874                int sx2 = imgWidth - Math.max(0, x + imgWidth - (int) Math.ceil(segmentLength));
875                g.drawImage(image, x + sx1, dy1, x + sx2, dy2, sx1, 0, sx2, imgHeight, null);
876                imageStart += repeat;
877            }
878
879            g.setTransform(saveTransform);
880        });
881    }
882
883    @Override
884    public void drawNode(Node n, Color color, int size, boolean fill) {
885        if (size <= 0 && !n.isHighlighted())
886            return;
887
888        MapViewPoint p = mapState.getPointFor(n);
889
890        if (n.isHighlighted()) {
891            drawPointHighlight(p.getInView(), size);
892        }
893
894        if (size > 1 && p.isInView()) {
895            int radius = size / 2;
896
897            if (isInactiveMode || n.isDisabled()) {
898                g.setColor(inactiveColor);
899            } else {
900                g.setColor(color);
901            }
902            Rectangle2D rect = new Rectangle2D.Double(p.getInViewX()-radius-1, p.getInViewY()-radius-1, size + 1, size + 1);
903            if (fill) {
904                g.fill(rect);
905            } else {
906                g.draw(rect);
907            }
908        }
909    }
910
911    /**
912     * Draw the icon for a given node.
913     * @param n The node
914     * @param img The icon to draw at the node position
915     * @param disabled {@code} true to render disabled version, {@code false} for the standard version
916     * @param selected {@code} true to render it as selected, {@code false} otherwise
917     * @param member {@code} true to render it as a relation member, {@code false} otherwise
918     * @param theta the angle of rotation in radians
919     */
920    public void drawNodeIcon(Node n, MapImage img, boolean disabled, boolean selected, boolean member, double theta) {
921        MapViewPoint p = mapState.getPointFor(n);
922
923        int w = img.getWidth();
924        int h = img.getHeight();
925        if (n.isHighlighted()) {
926            drawPointHighlight(p.getInView(), Math.max(w, h));
927        }
928
929        float alpha = img.getAlphaFloat();
930
931        Graphics2D temporaryGraphics = (Graphics2D) g.create();
932        if (!Utils.equalsEpsilon(alpha, 1f)) {
933            temporaryGraphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
934        }
935
936        double x = Math.round(p.getInViewX());
937        double y = Math.round(p.getInViewY());
938        temporaryGraphics.translate(x, y);
939        temporaryGraphics.rotate(theta);
940        int drawX = -w/2 + img.offsetX;
941        int drawY = -h/2 + img.offsetY;
942        temporaryGraphics.drawImage(img.getImage(disabled), drawX, drawY, nc);
943        if (selected || member) {
944            Color color;
945            if (disabled) {
946                color = inactiveColor;
947            } else if (selected) {
948                color = selectedColor;
949            } else {
950                color = relationSelectedColor;
951            }
952            temporaryGraphics.setColor(color);
953            temporaryGraphics.draw(new Rectangle2D.Double(drawX - 2, drawY - 2, w + 4, h + 4));
954        }
955    }
956
957    /**
958     * Draw the symbol and possibly a highlight marking on a given node.
959     * @param n The position to draw the symbol on
960     * @param s The symbol to draw
961     * @param fillColor The color to fill the symbol with
962     * @param strokeColor The color to use for the outer corner of the symbol
963     */
964    public void drawNodeSymbol(Node n, Symbol s, Color fillColor, Color strokeColor) {
965        MapViewPoint p = mapState.getPointFor(n);
966
967        if (n.isHighlighted()) {
968            drawPointHighlight(p.getInView(), s.size);
969        }
970
971        if (fillColor != null || strokeColor != null) {
972            Shape shape = s.buildShapeAround(p.getInViewX(), p.getInViewY());
973
974            if (fillColor != null) {
975                g.setColor(fillColor);
976                g.fill(shape);
977            }
978            if (s.stroke != null) {
979                g.setStroke(s.stroke);
980                g.setColor(strokeColor);
981                g.draw(shape);
982                g.setStroke(new BasicStroke());
983            }
984        }
985    }
986
987    /**
988     * Draw a number of the order of the two consecutive nodes within the
989     * parents way
990     *
991     * @param n1 First node of the way segment.
992     * @param n2 Second node of the way segment.
993     * @param orderNumber The number of the segment in the way.
994     * @param clr The color to use for drawing the text.
995     */
996    public void drawOrderNumber(Node n1, Node n2, int orderNumber, Color clr) {
997        MapViewPoint p1 = mapState.getPointFor(n1);
998        MapViewPoint p2 = mapState.getPointFor(n2);
999        drawOrderNumber(p1, p2, orderNumber, clr);
1000    }
1001
1002    /**
1003     * highlights a given GeneralPath using the settings from BasicStroke to match the line's
1004     * style. Width of the highlight is hard coded.
1005     * @param path path to draw
1006     * @param line line style
1007     */
1008    private void drawPathHighlight(MapViewPath path, BasicStroke line) {
1009        if (path == null)
1010            return;
1011        g.setColor(highlightColorTransparent);
1012        float w = line.getLineWidth() + highlightLineWidth;
1013        if (useWiderHighlight) w += widerHighlight;
1014        while (w >= line.getLineWidth()) {
1015            g.setStroke(new BasicStroke(w, line.getEndCap(), line.getLineJoin(), line.getMiterLimit()));
1016            g.draw(path);
1017            w -= highlightStep;
1018        }
1019    }
1020
1021    /**
1022     * highlights a given point by drawing a rounded rectangle around it. Give the
1023     * size of the object you want to be highlighted, width is added automatically.
1024     * @param p point
1025     * @param size highlight size
1026     */
1027    private void drawPointHighlight(Point2D p, int size) {
1028        g.setColor(highlightColorTransparent);
1029        int s = size + highlightPointRadius;
1030        if (useWiderHighlight) s += widerHighlight;
1031        while (s >= size) {
1032            int r = (int) Math.floor(s/2d);
1033            g.fill(new RoundRectangle2D.Double(p.getX()-r, p.getY()-r, s, s, r, r));
1034            s -= highlightStep;
1035        }
1036    }
1037
1038    public void drawRestriction(Image img, Point pVia, double vx, double vx2, double vy, double vy2, double angle, boolean selected) {
1039        // rotate image with direction last node in from to, and scale down image to 16*16 pixels
1040        Image smallImg = ImageProvider.createRotatedImage(img, angle, new Dimension(16, 16));
1041        int w = smallImg.getWidth(null), h = smallImg.getHeight(null);
1042        g.drawImage(smallImg, (int) (pVia.x+vx+vx2)-w/2, (int) (pVia.y+vy+vy2)-h/2, nc);
1043
1044        if (selected) {
1045            g.setColor(isInactiveMode ? inactiveColor : relationSelectedColor);
1046            g.drawRect((int) (pVia.x+vx+vx2)-w/2-2, (int) (pVia.y+vy+vy2)-h/2-2, w+4, h+4);
1047        }
1048    }
1049
1050    public void drawRestriction(Relation r, MapImage icon, boolean disabled) {
1051        Way fromWay = null;
1052        Way toWay = null;
1053        OsmPrimitive via = null;
1054
1055        /* find the "from", "via" and "to" elements */
1056        for (RelationMember m : r.getMembers()) {
1057            if (m.getMember().isIncomplete())
1058                return;
1059            else {
1060                if (m.isWay()) {
1061                    Way w = m.getWay();
1062                    if (w.getNodesCount() < 2) {
1063                        continue;
1064                    }
1065
1066                    switch(m.getRole()) {
1067                    case "from":
1068                        if (fromWay == null) {
1069                            fromWay = w;
1070                        }
1071                        break;
1072                    case "to":
1073                        if (toWay == null) {
1074                            toWay = w;
1075                        }
1076                        break;
1077                    case "via":
1078                        if (via == null) {
1079                            via = w;
1080                        }
1081                        break;
1082                    default: // Do nothing
1083                    }
1084                } else if (m.isNode()) {
1085                    Node n = m.getNode();
1086                    if ("via".equals(m.getRole()) && via == null) {
1087                        via = n;
1088                    }
1089                }
1090            }
1091        }
1092
1093        if (fromWay == null || toWay == null || via == null)
1094            return;
1095
1096        Node viaNode;
1097        if (via instanceof Node) {
1098            viaNode = (Node) via;
1099            if (!fromWay.isFirstLastNode(viaNode))
1100                return;
1101        } else {
1102            Way viaWay = (Way) via;
1103            Node firstNode = viaWay.firstNode();
1104            Node lastNode = viaWay.lastNode();
1105            Boolean onewayvia = Boolean.FALSE;
1106
1107            String onewayviastr = viaWay.get("oneway");
1108            if (onewayviastr != null) {
1109                if ("-1".equals(onewayviastr)) {
1110                    onewayvia = Boolean.TRUE;
1111                    Node tmp = firstNode;
1112                    firstNode = lastNode;
1113                    lastNode = tmp;
1114                } else {
1115                    onewayvia = OsmUtils.getOsmBoolean(onewayviastr);
1116                    if (onewayvia == null) {
1117                        onewayvia = Boolean.FALSE;
1118                    }
1119                }
1120            }
1121
1122            if (fromWay.isFirstLastNode(firstNode)) {
1123                viaNode = firstNode;
1124            } else if (!onewayvia && fromWay.isFirstLastNode(lastNode)) {
1125                viaNode = lastNode;
1126            } else
1127                return;
1128        }
1129
1130        /* find the "direct" nodes before the via node */
1131        Node fromNode;
1132        if (fromWay.firstNode() == via) {
1133            fromNode = fromWay.getNode(1);
1134        } else {
1135            fromNode = fromWay.getNode(fromWay.getNodesCount()-2);
1136        }
1137
1138        Point pFrom = nc.getPoint(fromNode);
1139        Point pVia = nc.getPoint(viaNode);
1140
1141        /* starting from via, go back the "from" way a few pixels
1142           (calculate the vector vx/vy with the specified length and the direction
1143           away from the "via" node along the first segment of the "from" way)
1144         */
1145        double distanceFromVia = 14;
1146        double dx = pFrom.x >= pVia.x ? pFrom.x - pVia.x : pVia.x - pFrom.x;
1147        double dy = pFrom.y >= pVia.y ? pFrom.y - pVia.y : pVia.y - pFrom.y;
1148
1149        double fromAngle;
1150        if (dx == 0) {
1151            fromAngle = Math.PI/2;
1152        } else {
1153            fromAngle = Math.atan(dy / dx);
1154        }
1155        double fromAngleDeg = Math.toDegrees(fromAngle);
1156
1157        double vx = distanceFromVia * Math.cos(fromAngle);
1158        double vy = distanceFromVia * Math.sin(fromAngle);
1159
1160        if (pFrom.x < pVia.x) {
1161            vx = -vx;
1162        }
1163        if (pFrom.y < pVia.y) {
1164            vy = -vy;
1165        }
1166
1167        /* go a few pixels away from the way (in a right angle)
1168           (calculate the vx2/vy2 vector with the specified length and the direction
1169           90degrees away from the first segment of the "from" way)
1170         */
1171        double distanceFromWay = 10;
1172        double vx2 = 0;
1173        double vy2 = 0;
1174        double iconAngle = 0;
1175
1176        if (pFrom.x >= pVia.x && pFrom.y >= pVia.y) {
1177            if (!leftHandTraffic) {
1178                vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
1179                vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
1180            } else {
1181                vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
1182                vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
1183            }
1184            iconAngle = 270+fromAngleDeg;
1185        }
1186        if (pFrom.x < pVia.x && pFrom.y >= pVia.y) {
1187            if (!leftHandTraffic) {
1188                vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
1189                vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
1190            } else {
1191                vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
1192                vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
1193            }
1194            iconAngle = 90-fromAngleDeg;
1195        }
1196        if (pFrom.x < pVia.x && pFrom.y < pVia.y) {
1197            if (!leftHandTraffic) {
1198                vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
1199                vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
1200            } else {
1201                vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
1202                vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
1203            }
1204            iconAngle = 90+fromAngleDeg;
1205        }
1206        if (pFrom.x >= pVia.x && pFrom.y < pVia.y) {
1207            if (!leftHandTraffic) {
1208                vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
1209                vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
1210            } else {
1211                vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
1212                vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
1213            }
1214            iconAngle = 270-fromAngleDeg;
1215        }
1216
1217        drawRestriction(icon.getImage(disabled),
1218                pVia, vx, vx2, vy, vy2, iconAngle, r.isSelected());
1219    }
1220
1221    /**
1222     * A half segment that can be used to place text on it. Used in the drawTextOnPath algorithm.
1223     * @author Michael Zangl
1224     */
1225    private static class HalfSegment {
1226        /**
1227         * start point of half segment (as length along the way)
1228         */
1229        final double start;
1230        /**
1231         * end point of half segment (as length along the way)
1232         */
1233        final double end;
1234        /**
1235         * quality factor (off screen / partly on screen / fully on screen)
1236         */
1237        final double quality;
1238
1239        /**
1240         * Create a new half segment
1241         * @param start The start along the way
1242         * @param end The end of the segment
1243         * @param quality A quality factor.
1244         */
1245        HalfSegment(double start, double end, double quality) {
1246            super();
1247            this.start = start;
1248            this.end = end;
1249            this.quality = quality;
1250        }
1251
1252        @Override
1253        public String toString() {
1254            return "HalfSegment [start=" + start + ", end=" + end + ", quality=" + quality + "]";
1255        }
1256    }
1257
1258    /**
1259     * Draws a text along a given way.
1260     * @param way The way to draw the text on.
1261     * @param text The text definition (font/.../text content) to draw.
1262     */
1263    public void drawTextOnPath(Way way, TextLabel text) {
1264        if (way == null || text == null)
1265            return;
1266        String name = text.getString(way);
1267        if (name == null || name.isEmpty())
1268            return;
1269
1270        FontMetrics fontMetrics = g.getFontMetrics(text.font);
1271        Rectangle2D rec = fontMetrics.getStringBounds(name, g);
1272
1273        Rectangle bounds = g.getClipBounds();
1274
1275        List<MapViewPoint> points = way.getNodes().stream().map(mapState::getPointFor).collect(Collectors.toList());
1276
1277        // 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)
1278        List<HalfSegment> longHalfSegment = new ArrayList<>();
1279
1280        double pathLength = computePath(2 * (rec.getWidth() + 4), bounds, points, longHalfSegment);
1281
1282        if (rec.getWidth() > pathLength)
1283            return;
1284
1285        double t1, t2;
1286
1287        if (!longHalfSegment.isEmpty()) {
1288            // find the segment with the best quality. If there are several with best quality, the one close to the center is prefered.
1289            Optional<HalfSegment> besto = longHalfSegment.stream().max(
1290                    Comparator.comparingDouble(segment ->
1291                        segment.quality - 1e-5 * Math.abs(0.5 * (segment.end + segment.start) - 0.5 * pathLength)
1292                    ));
1293            if (!besto.isPresent())
1294                throw new IllegalStateException("Unable to find the segment with the best quality for " + way);
1295            HalfSegment best = besto.get();
1296            double remaining = best.end - best.start - rec.getWidth(); // total space left and right from the text
1297            // The space left and right of the text should be distributed 20% - 80% (towards the center),
1298            // but the smaller space should not be less than 7 px.
1299            // However, if the total remaining space is less than 14 px, then distribute it evenly.
1300            double smallerSpace = Math.min(Math.max(0.2 * remaining, 7), 0.5 * remaining);
1301            if ((best.end + best.start)/2 < pathLength/2) {
1302                t2 = best.end - smallerSpace;
1303                t1 = t2 - rec.getWidth();
1304            } else {
1305                t1 = best.start + smallerSpace;
1306                t2 = t1 + rec.getWidth();
1307            }
1308        } else {
1309            // doesn't fit into one half-segment -> just put it in the center of the way
1310            t1 = pathLength/2 - rec.getWidth()/2;
1311            t2 = pathLength/2 + rec.getWidth()/2;
1312        }
1313        t1 /= pathLength;
1314        t2 /= pathLength;
1315
1316        double[] p1 = pointAt(t1, points, pathLength);
1317        double[] p2 = pointAt(t2, points, pathLength);
1318
1319        if (p1 == null || p2 == null)
1320            return;
1321
1322        double angleOffset;
1323        double offsetSign;
1324        double tStart;
1325
1326        if (p1[0] < p2[0] &&
1327                p1[2] < Math.PI/2 &&
1328                p1[2] > -Math.PI/2) {
1329            angleOffset = 0;
1330            offsetSign = 1;
1331            tStart = t1;
1332        } else {
1333            angleOffset = Math.PI;
1334            offsetSign = -1;
1335            tStart = t2;
1336        }
1337
1338        List<GlyphVector> gvs = Utils.getGlyphVectorsBidi(name, text.font, g.getFontRenderContext());
1339        double gvOffset = 0;
1340        for (GlyphVector gv : gvs) {
1341            double gvWidth = gv.getLogicalBounds().getBounds2D().getWidth();
1342            for (int i = 0; i < gv.getNumGlyphs(); ++i) {
1343                Rectangle2D rect = gv.getGlyphLogicalBounds(i).getBounds2D();
1344                double t = tStart + offsetSign * (gvOffset + rect.getX() + rect.getWidth()/2) / pathLength;
1345                double[] p = pointAt(t, points, pathLength);
1346                if (p != null) {
1347                    AffineTransform trfm = AffineTransform.getTranslateInstance(p[0] - rect.getX(), p[1]);
1348                    trfm.rotate(p[2]+angleOffset);
1349                    double off = -rect.getY() - rect.getHeight()/2 + text.yOffset;
1350                    trfm.translate(-rect.getWidth()/2, off);
1351                    if (isGlyphVectorDoubleTranslationBug(text.font)) {
1352                        // scale the translation components by one half
1353                        AffineTransform tmp = AffineTransform.getTranslateInstance(-0.5 * trfm.getTranslateX(), -0.5 * trfm.getTranslateY());
1354                        tmp.concatenate(trfm);
1355                        trfm = tmp;
1356                    }
1357                    gv.setGlyphTransform(i, trfm);
1358                }
1359            }
1360            displayText(gv, null, 0, 0, way.isDisabled(), text);
1361            gvOffset += gvWidth;
1362        }
1363    }
1364
1365    private static double computePath(double minSegmentLength, Rectangle bounds, List<MapViewPoint> points,
1366            List<HalfSegment> longHalfSegment) {
1367        MapViewPoint lastPoint = points.get(0);
1368        double pathLength = 0;
1369        for (MapViewPoint p : points.subList(1, points.size())) {
1370            double segmentLength = p.distanceToInView(lastPoint);
1371            if (segmentLength > minSegmentLength) {
1372                Point2D center = new Point2D.Double((lastPoint.getInViewX() + p.getInViewX())/2, (lastPoint.getInViewY() + p.getInViewY())/2);
1373                double q = computeQuality(bounds, lastPoint, center);
1374                // prefer the first one for quality equality.
1375                longHalfSegment.add(new HalfSegment(pathLength, pathLength + segmentLength / 2, q));
1376
1377                q = 0;
1378                if (bounds != null) {
1379                    if (bounds.contains(center) && bounds.contains(p.getInView())) {
1380                        q = 2;
1381                    } else if (bounds.contains(center) || bounds.contains(p.getInView())) {
1382                        q = 1;
1383                    }
1384                }
1385                longHalfSegment.add(new HalfSegment(pathLength + segmentLength / 2, pathLength + segmentLength, q));
1386            }
1387            pathLength += segmentLength;
1388            lastPoint = p;
1389        }
1390        return pathLength;
1391    }
1392
1393    private static double computeQuality(Rectangle bounds, MapViewPoint p1, Point2D p2) {
1394        double q = 0;
1395        if (bounds != null) {
1396            if (bounds.contains(p1.getInView())) {
1397                q += 1;
1398            }
1399            if (bounds.contains(p2)) {
1400                q += 1;
1401            }
1402        }
1403        return q;
1404    }
1405
1406    /**
1407     * draw way. This method allows for two draw styles (line using color, dashes using dashedColor) to be passed.
1408     * @param way The way to draw
1409     * @param color The base color to draw the way in
1410     * @param line The line style to use. This is drawn using color.
1411     * @param dashes The dash style to use. This is drawn using dashedColor. <code>null</code> if unused.
1412     * @param dashedColor The color of the dashes.
1413     * @param offset The offset
1414     * @param showOrientation show arrows that indicate the technical orientation of
1415     *              the way (defined by order of nodes)
1416     * @param showHeadArrowOnly True if only the arrow at the end of the line but not those on the segments should be displayed.
1417     * @param showOneway show symbols that indicate the direction of the feature,
1418     *              e.g. oneway street or waterway
1419     * @param onewayReversed for oneway=-1 and similar
1420     */
1421    public void drawWay(Way way, Color color, BasicStroke line, BasicStroke dashes, Color dashedColor, float offset,
1422            boolean showOrientation, boolean showHeadArrowOnly,
1423            boolean showOneway, boolean onewayReversed) {
1424
1425        MapViewPath path = new MapViewPath(mapState);
1426        MapViewPath orientationArrows = showOrientation ? new MapViewPath(mapState) : null;
1427        MapViewPath onewayArrows;
1428        MapViewPath onewayArrowsCasing;
1429        Rectangle bounds = g.getClipBounds();
1430        if (bounds != null) {
1431            // avoid arrow heads at the border
1432            bounds.grow(100, 100);
1433        }
1434
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        Iterator<MapViewPoint> it = new OffsetIterator(wayNodes, offset);
1458        boolean initialMoveToNeeded = true;
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            }
1478            lastPoint = p;
1479        }
1480        if (showOneway) {
1481            onewayArrows = new MapViewPath(mapState);
1482            onewayArrowsCasing = new MapViewPath(mapState);
1483            double interval = 60;
1484
1485            path.visitClippedLine(0, 60, (inLineOffset, start, end, startIsOldEnd) -> {
1486                double segmentLength = start.distanceToInView(end);
1487                if (segmentLength > 0.001) {
1488                    final double nx = (end.getInViewX() - start.getInViewX()) / segmentLength;
1489                    final double ny = (end.getInViewY() - start.getInViewY()) / segmentLength;
1490
1491                    // distance from p1
1492                    double dist = interval - (inLineOffset % interval);
1493
1494                    while (dist < segmentLength) {
1495                        appenOnewayPath(onewayReversed, start, nx, ny, dist, 3d, onewayArrowsCasing);
1496                        appenOnewayPath(onewayReversed, start, nx, ny, dist, 2d, onewayArrows);
1497                        dist += interval;
1498                    }
1499                }
1500            });
1501        } else {
1502            onewayArrows = null;
1503            onewayArrowsCasing = null;
1504        }
1505
1506        if (way.isHighlighted()) {
1507            drawPathHighlight(path, line);
1508        }
1509        displaySegments(path, orientationArrows, onewayArrows, onewayArrowsCasing, color, line, dashes, dashedColor);
1510    }
1511
1512    private static void appenOnewayPath(boolean onewayReversed, MapViewPoint p1, double nx, double ny, double dist,
1513            double onewaySize, Path2D onewayPath) {
1514        // scale such that border is 1 px
1515        final double fac = -(onewayReversed ? -1 : 1) * onewaySize * (1 + sinPHI) / (sinPHI * cosPHI);
1516        final double sx = nx * fac;
1517        final double sy = ny * fac;
1518
1519        // Attach the triangle at the incenter and not at the tip.
1520        // Makes the border even at all sides.
1521        final double x = p1.getInViewX() + nx * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI));
1522        final double y = p1.getInViewY() + ny * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI));
1523
1524        onewayPath.moveTo(x, y);
1525        onewayPath.lineTo(x + cosPHI * sx - sinPHI * sy, y + sinPHI * sx + cosPHI * sy);
1526        onewayPath.lineTo(x + cosPHI * sx + sinPHI * sy, y - sinPHI * sx + cosPHI * sy);
1527        onewayPath.lineTo(x, y);
1528    }
1529
1530    /**
1531     * Gets the "circum". This is the distance on the map in meters that 100 screen pixels represent.
1532     * @return The "circum"
1533     */
1534    public double getCircum() {
1535        return circum;
1536    }
1537
1538    @Override
1539    public void getColors() {
1540        super.getColors();
1541        this.highlightColorTransparent = new Color(highlightColor.getRed(), highlightColor.getGreen(), highlightColor.getBlue(), 100);
1542        this.backgroundColor = PaintColors.getBackgroundColor();
1543    }
1544
1545    @Override
1546    public void getSettings(boolean virtual) {
1547        super.getSettings(virtual);
1548        paintSettings = MapPaintSettings.INSTANCE;
1549
1550        circum = nc.getDist100Pixel();
1551        scale = nc.getScale();
1552
1553        leftHandTraffic = Main.pref.getBoolean("mappaint.lefthandtraffic", false);
1554
1555        useStrokes = paintSettings.getUseStrokesDistance() > circum;
1556        showNames = paintSettings.getShowNamesDistance() > circum;
1557        showIcons = paintSettings.getShowIconsDistance() > circum;
1558        isOutlineOnly = paintSettings.isOutlineOnly();
1559        orderFont = new Font(Main.pref.get("mappaint.font", "Droid Sans"), Font.PLAIN, Main.pref.getInteger("mappaint.fontsize", 8));
1560
1561        antialiasing = Main.pref.getBoolean("mappaint.use-antialiasing", true) ?
1562                        RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF;
1563        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing);
1564
1565        Object textAntialiasing;
1566        switch (Main.pref.get("mappaint.text-antialiasing", "default")) {
1567            case "on":
1568                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_ON;
1569                break;
1570            case "off":
1571                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_OFF;
1572                break;
1573            case "gasp":
1574                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_GASP;
1575                break;
1576            case "lcd-hrgb":
1577                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB;
1578                break;
1579            case "lcd-hbgr":
1580                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HBGR;
1581                break;
1582            case "lcd-vrgb":
1583                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VRGB;
1584                break;
1585            case "lcd-vbgr":
1586                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VBGR;
1587                break;
1588            default:
1589                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT;
1590        }
1591        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, textAntialiasing);
1592
1593        highlightLineWidth = Main.pref.getInteger("mappaint.highlight.width", 4);
1594        highlightPointRadius = Main.pref.getInteger("mappaint.highlight.radius", 7);
1595        widerHighlight = Main.pref.getInteger("mappaint.highlight.bigger-increment", 5);
1596        highlightStep = Main.pref.getInteger("mappaint.highlight.step", 4);
1597    }
1598
1599    private static Path2D.Double getPath(Way w) {
1600        Path2D.Double path = new Path2D.Double();
1601        boolean initial = true;
1602        for (Node n : w.getNodes()) {
1603            EastNorth p = n.getEastNorth();
1604            if (p != null) {
1605                if (initial) {
1606                    path.moveTo(p.getX(), p.getY());
1607                    initial = false;
1608                } else {
1609                    path.lineTo(p.getX(), p.getY());
1610                }
1611            }
1612        }
1613        if (w.isClosed()) {
1614            path.closePath();
1615        }
1616        return path;
1617    }
1618
1619    private static Path2D.Double getPFClip(Way w, double extent) {
1620        Path2D.Double clip = new Path2D.Double();
1621        buildPFClip(clip, w.getNodes(), extent);
1622        return clip;
1623    }
1624
1625    private static Path2D.Double getPFClip(PolyData pd, double extent) {
1626        Path2D.Double clip = new Path2D.Double();
1627        clip.setWindingRule(Path2D.WIND_EVEN_ODD);
1628        buildPFClip(clip, pd.getNodes(), extent);
1629        for (PolyData pdInner : pd.getInners()) {
1630            buildPFClip(clip, pdInner.getNodes(), extent);
1631        }
1632        return clip;
1633    }
1634
1635    /**
1636     * Fix the clipping area of unclosed polygons for partial fill.
1637     *
1638     * The current algorithm for partial fill simply strokes the polygon with a
1639     * large stroke width after masking the outside with a clipping area.
1640     * This works, but for unclosed polygons, the mask can crop the corners at
1641     * both ends (see #12104).
1642     *
1643     * This method fixes the clipping area by sort of adding the corners to the
1644     * clip outline.
1645     *
1646     * @param clip the clipping area to modify (initially empty)
1647     * @param nodes nodes of the polygon
1648     * @param extent the extent
1649     */
1650    private static void buildPFClip(Path2D.Double clip, List<Node> nodes, double extent) {
1651        boolean initial = true;
1652        for (Node n : nodes) {
1653            EastNorth p = n.getEastNorth();
1654            if (p != null) {
1655                if (initial) {
1656                    clip.moveTo(p.getX(), p.getY());
1657                    initial = false;
1658                } else {
1659                    clip.lineTo(p.getX(), p.getY());
1660                }
1661            }
1662        }
1663        if (nodes.size() >= 3) {
1664            EastNorth fst = nodes.get(0).getEastNorth();
1665            EastNorth snd = nodes.get(1).getEastNorth();
1666            EastNorth lst = nodes.get(nodes.size() - 1).getEastNorth();
1667            EastNorth lbo = nodes.get(nodes.size() - 2).getEastNorth();
1668
1669            EastNorth cLst = getPFDisplacedEndPoint(lbo, lst, fst, extent);
1670            EastNorth cFst = getPFDisplacedEndPoint(snd, fst, cLst != null ? cLst : lst, extent);
1671            if (cLst == null && cFst != null) {
1672                cLst = getPFDisplacedEndPoint(lbo, lst, cFst, extent);
1673            }
1674            if (cLst != null) {
1675                clip.lineTo(cLst.getX(), cLst.getY());
1676            }
1677            if (cFst != null) {
1678                clip.lineTo(cFst.getX(), cFst.getY());
1679            }
1680        }
1681    }
1682
1683    /**
1684     * Get the point to add to the clipping area for partial fill of unclosed polygons.
1685     *
1686     * <code>(p1,p2)</code> is the first or last way segment and <code>p3</code> the
1687     * opposite endpoint.
1688     *
1689     * @param p1 1st point
1690     * @param p2 2nd point
1691     * @param p3 3rd point
1692     * @param extent the extent
1693     * @return a point q, such that p1,p2,q form a right angle
1694     * and the distance of q to p2 is <code>extent</code>. The point q lies on
1695     * the same side of the line p1,p2 as the point p3.
1696     * Returns null if p1,p2,p3 forms an angle greater 90 degrees. (In this case
1697     * the corner of the partial fill would not be cut off by the mask, so an
1698     * additional point is not necessary.)
1699     */
1700    private static EastNorth getPFDisplacedEndPoint(EastNorth p1, EastNorth p2, EastNorth p3, double extent) {
1701        double dx1 = p2.getX() - p1.getX();
1702        double dy1 = p2.getY() - p1.getY();
1703        double dx2 = p3.getX() - p2.getX();
1704        double dy2 = p3.getY() - p2.getY();
1705        if (dx1 * dx2 + dy1 * dy2 < 0) {
1706            double len = Math.sqrt(dx1 * dx1 + dy1 * dy1);
1707            if (len == 0) return null;
1708            double dxm = -dy1 * extent / len;
1709            double dym = dx1 * extent / len;
1710            if (dx1 * dy2 - dx2 * dy1 < 0) {
1711                dxm = -dxm;
1712                dym = -dym;
1713            }
1714            return new EastNorth(p2.getX() + dxm, p2.getY() + dym);
1715        }
1716        return null;
1717    }
1718
1719    /**
1720     * Test if the area is visible
1721     * @param area The area, interpreted in east/north space.
1722     * @return true if it is visible.
1723     */
1724    private boolean isAreaVisible(Path2D.Double area) {
1725        Rectangle2D bounds = area.getBounds2D();
1726        if (bounds.isEmpty()) return false;
1727        MapViewPoint p = mapState.getPointFor(new EastNorth(bounds.getX(), bounds.getY()));
1728        if (p.getInViewX() > mapState.getViewWidth()) return false;
1729        if (p.getInViewY() < 0) return false;
1730        p = mapState.getPointFor(new EastNorth(bounds.getX() + bounds.getWidth(), bounds.getY() + bounds.getHeight()));
1731        if (p.getInViewX() < 0) return false;
1732        if (p.getInViewY() > mapState.getViewHeight()) return false;
1733        return true;
1734    }
1735
1736    public boolean isInactiveMode() {
1737        return isInactiveMode;
1738    }
1739
1740    public boolean isShowIcons() {
1741        return showIcons;
1742    }
1743
1744    public boolean isShowNames() {
1745        return showNames;
1746    }
1747
1748    private static double[] pointAt(double t, List<MapViewPoint> poly, double pathLength) {
1749        double totalLen = t * pathLength;
1750        double curLen = 0;
1751        double dx, dy;
1752        double segLen;
1753
1754        // Yes, it is inefficient to iterate from the beginning for each glyph.
1755        // Can be optimized if it turns out to be slow.
1756        for (int i = 1; i < poly.size(); ++i) {
1757            dx = poly.get(i).getInViewX() - poly.get(i - 1).getInViewX();
1758            dy = poly.get(i).getInViewY() - poly.get(i - 1).getInViewY();
1759            segLen = Math.sqrt(dx*dx + dy*dy);
1760            if (totalLen > curLen + segLen) {
1761                curLen += segLen;
1762                continue;
1763            }
1764            return new double[] {
1765                    poly.get(i - 1).getInViewX() + (totalLen - curLen) / segLen * dx,
1766                    poly.get(i - 1).getInViewY() + (totalLen - curLen) / segLen * dy,
1767                    Math.atan2(dy, dx)};
1768        }
1769        return null;
1770    }
1771
1772    /**
1773     * Computes the flags for a given OSM primitive.
1774     * @param primitive The primititve to compute the flags for.
1775     * @param checkOuterMember <code>true</code> if we should also add {@link #FLAG_OUTERMEMBER_OF_SELECTED}
1776     * @return The flag.
1777     */
1778    public static int computeFlags(OsmPrimitive primitive, boolean checkOuterMember) {
1779        if (primitive.isDisabled()) {
1780            return FLAG_DISABLED;
1781        } else if (primitive.isSelected()) {
1782            return FLAG_SELECTED;
1783        } else if (checkOuterMember && primitive.isOuterMemberOfSelected()) {
1784            return FLAG_OUTERMEMBER_OF_SELECTED;
1785        } else if (primitive.isMemberOfSelected()) {
1786            return FLAG_MEMBER_OF_SELECTED;
1787        } else {
1788            return FLAG_NORMAL;
1789        }
1790    }
1791
1792    private class ComputeStyleListWorker extends RecursiveTask<List<StyleRecord>> implements Visitor {
1793        private final transient List<? extends OsmPrimitive> input;
1794        private final transient List<StyleRecord> output;
1795
1796        private final transient ElemStyles styles = MapPaintStyles.getStyles();
1797        private final int directExecutionTaskSize;
1798
1799        private final boolean drawArea = circum <= Main.pref.getInteger("mappaint.fillareas", 10_000_000);
1800        private final boolean drawMultipolygon = drawArea && Main.pref.getBoolean("mappaint.multipolygon", true);
1801        private final boolean drawRestriction = Main.pref.getBoolean("mappaint.restriction", true);
1802
1803        /**
1804         * Constructs a new {@code ComputeStyleListWorker}.
1805         * @param input the primitives to process
1806         * @param output the list of styles to which styles will be added
1807         * @param directExecutionTaskSize the threshold deciding whether to subdivide the tasks
1808         */
1809        ComputeStyleListWorker(final List<? extends OsmPrimitive> input, List<StyleRecord> output, int directExecutionTaskSize) {
1810            this.input = input;
1811            this.output = output;
1812            this.directExecutionTaskSize = directExecutionTaskSize;
1813            this.styles.setDrawMultipolygon(drawMultipolygon);
1814        }
1815
1816        @Override
1817        protected List<StyleRecord> compute() {
1818            if (input.size() <= directExecutionTaskSize) {
1819                return computeDirectly();
1820            } else {
1821                final Collection<ForkJoinTask<List<StyleRecord>>> tasks = new ArrayList<>();
1822                for (int fromIndex = 0; fromIndex < input.size(); fromIndex += directExecutionTaskSize) {
1823                    final int toIndex = Math.min(fromIndex + directExecutionTaskSize, input.size());
1824                    final List<StyleRecord> output = new ArrayList<>(directExecutionTaskSize);
1825                    tasks.add(new ComputeStyleListWorker(input.subList(fromIndex, toIndex), output, directExecutionTaskSize).fork());
1826                }
1827                for (ForkJoinTask<List<StyleRecord>> task : tasks) {
1828                    output.addAll(task.join());
1829                }
1830                return output;
1831            }
1832        }
1833
1834        public List<StyleRecord> computeDirectly() {
1835            MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
1836            try {
1837                for (final OsmPrimitive osm : input) {
1838                    acceptDrawable(osm);
1839                }
1840                return output;
1841            } catch (RuntimeException e) {
1842                throw BugReport.intercept(e).put("input-size", input.size()).put("output-size", output.size());
1843            } finally {
1844                MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
1845            }
1846        }
1847
1848        private void acceptDrawable(final OsmPrimitive osm) {
1849            try {
1850                if (osm.isDrawable()) {
1851                    osm.accept(this);
1852                }
1853            } catch (RuntimeException e) {
1854                throw BugReport.intercept(e).put("osm", osm);
1855            }
1856        }
1857
1858        @Override
1859        public void visit(Node n) {
1860            add(n, computeFlags(n, false));
1861        }
1862
1863        @Override
1864        public void visit(Way w) {
1865            add(w, computeFlags(w, true));
1866        }
1867
1868        @Override
1869        public void visit(Relation r) {
1870            add(r, computeFlags(r, true));
1871        }
1872
1873        @Override
1874        public void visit(Changeset cs) {
1875            throw new UnsupportedOperationException();
1876        }
1877
1878        public void add(Node osm, int flags) {
1879            StyleElementList sl = styles.get(osm, circum, nc);
1880            for (StyleElement s : sl) {
1881                output.add(new StyleRecord(s, osm, flags));
1882            }
1883        }
1884
1885        public void add(Relation osm, int flags) {
1886            StyleElementList sl = styles.get(osm, circum, nc);
1887            for (StyleElement s : sl) {
1888                if (drawMultipolygon && drawArea && s instanceof AreaElement && (flags & FLAG_DISABLED) == 0) {
1889                    output.add(new StyleRecord(s, osm, flags));
1890                } else if (drawRestriction && s instanceof NodeElement) {
1891                    output.add(new StyleRecord(s, osm, flags));
1892                }
1893            }
1894        }
1895
1896        public void add(Way osm, int flags) {
1897            StyleElementList sl = styles.get(osm, circum, nc);
1898            for (StyleElement s : sl) {
1899                if (!(drawArea && (flags & FLAG_DISABLED) == 0) && s instanceof AreaElement) {
1900                    continue;
1901                }
1902                output.add(new StyleRecord(s, osm, flags));
1903            }
1904        }
1905    }
1906
1907    /**
1908     * Sets the factory that creates the benchmark data receivers.
1909     * @param benchmarkFactory The factory.
1910     * @since 10697
1911     */
1912    public void setBenchmarkFactory(Supplier<RenderBenchmarkCollector> benchmarkFactory) {
1913        this.benchmarkFactory = benchmarkFactory;
1914    }
1915
1916    @Override
1917    public void render(final DataSet data, boolean renderVirtualNodes, Bounds bounds) {
1918        RenderBenchmarkCollector benchmark = benchmarkFactory.get();
1919        BBox bbox = bounds.toBBox();
1920        getSettings(renderVirtualNodes);
1921
1922        data.getReadLock().lock();
1923        try {
1924            highlightWaySegments = data.getHighlightedWaySegments();
1925
1926            benchmark.renderStart(circum);
1927
1928            List<Node> nodes = data.searchNodes(bbox);
1929            List<Way> ways = data.searchWays(bbox);
1930            List<Relation> relations = data.searchRelations(bbox);
1931
1932            final List<StyleRecord> allStyleElems = new ArrayList<>(nodes.size()+ways.size()+relations.size());
1933
1934            // Need to process all relations first.
1935            // Reason: Make sure, ElemStyles.getStyleCacheWithRange is
1936            // not called for the same primitive in parallel threads.
1937            // (Could be synchronized, but try to avoid this for
1938            // performance reasons.)
1939            THREAD_POOL.invoke(new ComputeStyleListWorker(relations, allStyleElems,
1940                    Math.max(20, relations.size() / THREAD_POOL.getParallelism() / 3)));
1941            THREAD_POOL.invoke(new ComputeStyleListWorker(new CompositeList<>(nodes, ways), allStyleElems,
1942                    Math.max(100, (nodes.size() + ways.size()) / THREAD_POOL.getParallelism() / 3)));
1943
1944            if (!benchmark.renderSort()) {
1945                return;
1946            }
1947
1948            Collections.sort(allStyleElems); // TODO: try parallel sort when switching to Java 8
1949
1950            if (!benchmark.renderDraw(allStyleElems)) {
1951                return;
1952            }
1953
1954            for (StyleRecord record : allStyleElems) {
1955                paintRecord(record);
1956            }
1957
1958            drawVirtualNodes(data, bbox);
1959
1960            benchmark.renderDone();
1961        } catch (RuntimeException e) {
1962            throw BugReport.intercept(e)
1963                    .put("data", data)
1964                    .put("circum", circum)
1965                    .put("scale", scale)
1966                    .put("paintSettings", paintSettings)
1967                    .put("renderVirtualNodes", renderVirtualNodes);
1968        } finally {
1969            data.getReadLock().unlock();
1970        }
1971    }
1972
1973    private void paintRecord(StyleRecord record) {
1974        try {
1975            record.paintPrimitive(paintSettings, this);
1976        } catch (RuntimeException e) {
1977            throw BugReport.intercept(e).put("record", record);
1978        }
1979    }
1980}