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