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((double) dxNext*dxNext + (double) dyNext*dyNext);
147
148            if (lenNext == 0) {
149                lenNext = 1; // value does not matter, because dy_next and dx_next is 0
150            }
151
152            int xCurrent0 = current.x + (int) Math.round(offset * dyNext / lenNext);
153            int yCurrent0 = current.y - (int) Math.round(offset * dxNext / lenNext);
154
155            if (idx == 0) {
156                ++idx;
157                prev = current;
158                xPrev0 = xCurrent0;
159                yPrev0 = yCurrent0;
160                return new Point(xCurrent0, yCurrent0);
161            } else {
162                int dxPrev = current.x - prev.x;
163                int dyPrev = current.y - prev.y;
164
165                // determine intersection of the lines parallel to the two segments
166                int det = dxNext*dyPrev - dxPrev*dyNext;
167
168                if (det == 0) {
169                    ++idx;
170                    prev = current;
171                    xPrev0 = xCurrent0;
172                    yPrev0 = yCurrent0;
173                    return new Point(xCurrent0, yCurrent0);
174                }
175
176                int m = dxNext*(yCurrent0 - yPrev0) - dyNext*(xCurrent0 - xPrev0);
177
178                int cx = xPrev0 + (int) Math.round((double) m * dxPrev / det);
179                int cy = yPrev0 + (int) Math.round((double) m * dyPrev / det);
180                ++idx;
181                prev = current;
182                xPrev0 = xCurrent0;
183                yPrev0 = yCurrent0;
184                return new Point(cx, cy);
185            }
186        }
187
188        @Override
189        public void remove() {
190            throw new UnsupportedOperationException();
191        }
192    }
193
194    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 static 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.isTraceEnabled()) {
625                    Main.trace("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.isSelected() ? 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 = (double) thisP.x - lastP.x;
817                final double dy = (double) 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                        break;
1094                    default: // Do nothing
1095                    }
1096                } else if (m.isNode()) {
1097                    Node n = m.getNode();
1098                    if ("via".equals(m.getRole()) && via == null) {
1099                        via = n;
1100                    }
1101                }
1102            }
1103        }
1104
1105        if (fromWay == null || toWay == null || via == null)
1106            return;
1107
1108        Node viaNode;
1109        if (via instanceof Node) {
1110            viaNode = (Node) via;
1111            if (!fromWay.isFirstLastNode(viaNode))
1112                return;
1113        } else {
1114            Way viaWay = (Way) via;
1115            Node firstNode = viaWay.firstNode();
1116            Node lastNode = viaWay.lastNode();
1117            Boolean onewayvia = Boolean.FALSE;
1118
1119            String onewayviastr = viaWay.get("oneway");
1120            if (onewayviastr != null) {
1121                if ("-1".equals(onewayviastr)) {
1122                    onewayvia = Boolean.TRUE;
1123                    Node tmp = firstNode;
1124                    firstNode = lastNode;
1125                    lastNode = tmp;
1126                } else {
1127                    onewayvia = OsmUtils.getOsmBoolean(onewayviastr);
1128                    if (onewayvia == null) {
1129                        onewayvia = Boolean.FALSE;
1130                    }
1131                }
1132            }
1133
1134            if (fromWay.isFirstLastNode(firstNode)) {
1135                viaNode = firstNode;
1136            } else if (!onewayvia && fromWay.isFirstLastNode(lastNode)) {
1137                viaNode = lastNode;
1138            } else
1139                return;
1140        }
1141
1142        /* find the "direct" nodes before the via node */
1143        Node fromNode;
1144        if (fromWay.firstNode() == via) {
1145            fromNode = fromWay.getNode(1);
1146        } else {
1147            fromNode = fromWay.getNode(fromWay.getNodesCount()-2);
1148        }
1149
1150        Point pFrom = nc.getPoint(fromNode);
1151        Point pVia = nc.getPoint(viaNode);
1152
1153        /* starting from via, go back the "from" way a few pixels
1154           (calculate the vector vx/vy with the specified length and the direction
1155           away from the "via" node along the first segment of the "from" way)
1156         */
1157        double distanceFromVia = 14;
1158        double dx = pFrom.x >= pVia.x ? pFrom.x - pVia.x : pVia.x - pFrom.x;
1159        double dy = pFrom.y >= pVia.y ? pFrom.y - pVia.y : pVia.y - pFrom.y;
1160
1161        double fromAngle;
1162        if (dx == 0) {
1163            fromAngle = Math.PI/2;
1164        } else {
1165            fromAngle = Math.atan(dy / dx);
1166        }
1167        double fromAngleDeg = Math.toDegrees(fromAngle);
1168
1169        double vx = distanceFromVia * Math.cos(fromAngle);
1170        double vy = distanceFromVia * Math.sin(fromAngle);
1171
1172        if (pFrom.x < pVia.x) {
1173            vx = -vx;
1174        }
1175        if (pFrom.y < pVia.y) {
1176            vy = -vy;
1177        }
1178
1179        /* go a few pixels away from the way (in a right angle)
1180           (calculate the vx2/vy2 vector with the specified length and the direction
1181           90degrees away from the first segment of the "from" way)
1182         */
1183        double distanceFromWay = 10;
1184        double vx2 = 0;
1185        double vy2 = 0;
1186        double iconAngle = 0;
1187
1188        if (pFrom.x >= pVia.x && pFrom.y >= pVia.y) {
1189            if (!leftHandTraffic) {
1190                vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
1191                vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
1192            } else {
1193                vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
1194                vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
1195            }
1196            iconAngle = 270+fromAngleDeg;
1197        }
1198        if (pFrom.x < pVia.x && pFrom.y >= pVia.y) {
1199            if (!leftHandTraffic) {
1200                vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
1201                vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
1202            } else {
1203                vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
1204                vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
1205            }
1206            iconAngle = 90-fromAngleDeg;
1207        }
1208        if (pFrom.x < pVia.x && pFrom.y < pVia.y) {
1209            if (!leftHandTraffic) {
1210                vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
1211                vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
1212            } else {
1213                vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
1214                vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
1215            }
1216            iconAngle = 90+fromAngleDeg;
1217        }
1218        if (pFrom.x >= pVia.x && pFrom.y < pVia.y) {
1219            if (!leftHandTraffic) {
1220                vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
1221                vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
1222            } else {
1223                vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
1224                vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
1225            }
1226            iconAngle = 270-fromAngleDeg;
1227        }
1228
1229        drawRestriction(icon.getImage(disabled),
1230                pVia, vx, vx2, vy, vy2, iconAngle, r.isSelected());
1231    }
1232
1233    /**
1234     * Draws a text along a given way.
1235     * @param way The way to draw the text on.
1236     * @param text The text definition (font/.../text content) to draw.
1237     */
1238    public void drawTextOnPath(Way way, TextLabel text) {
1239        if (way == null || text == null)
1240            return;
1241        String name = text.getString(way);
1242        if (name == null || name.isEmpty())
1243            return;
1244
1245        FontMetrics fontMetrics = g.getFontMetrics(text.font);
1246        Rectangle2D rec = fontMetrics.getStringBounds(name, g);
1247
1248        Rectangle bounds = g.getClipBounds();
1249
1250        Polygon poly = new Polygon();
1251        Point lastPoint = null;
1252        Iterator<Node> it = way.getNodes().iterator();
1253        double pathLength = 0;
1254        long dx, dy;
1255
1256        // 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)
1257        List<Double> longHalfSegmentStart = new ArrayList<>(); // start point of half segment (as length along the way)
1258        List<Double> longHalfSegmentEnd = new ArrayList<>(); // end point of half segment (as length along the way)
1259        List<Double> longHalfsegmentQuality = new ArrayList<>(); // quality factor (off screen / partly on screen / fully on screen)
1260
1261        while (it.hasNext()) {
1262            Node n = it.next();
1263            Point p = nc.getPoint(n);
1264            poly.addPoint(p.x, p.y);
1265
1266            if (lastPoint != null) {
1267                dx = (long) p.x - lastPoint.x;
1268                dy = (long) p.y - lastPoint.y;
1269                double segmentLength = Math.sqrt(dx*dx + dy*dy);
1270                if (segmentLength > 2*(rec.getWidth()+4)) {
1271                    Point center = new Point((lastPoint.x + p.x)/2, (lastPoint.y + p.y)/2);
1272                    double q = 0;
1273                    if (bounds != null) {
1274                        if (bounds.contains(lastPoint) && bounds.contains(center)) {
1275                            q = 2;
1276                        } else if (bounds.contains(lastPoint) || bounds.contains(center)) {
1277                            q = 1;
1278                        }
1279                    }
1280                    longHalfSegmentStart.add(pathLength);
1281                    longHalfSegmentEnd.add(pathLength + segmentLength / 2);
1282                    longHalfsegmentQuality.add(q);
1283
1284                    q = 0;
1285                    if (bounds != null) {
1286                        if (bounds.contains(center) && bounds.contains(p)) {
1287                            q = 2;
1288                        } else if (bounds.contains(center) || bounds.contains(p)) {
1289                            q = 1;
1290                        }
1291                    }
1292                    longHalfSegmentStart.add(pathLength + segmentLength / 2);
1293                    longHalfSegmentEnd.add(pathLength + segmentLength);
1294                    longHalfsegmentQuality.add(q);
1295                }
1296                pathLength += segmentLength;
1297            }
1298            lastPoint = p;
1299        }
1300
1301        if (rec.getWidth() > pathLength)
1302            return;
1303
1304        double t1, t2;
1305
1306        if (!longHalfSegmentStart.isEmpty()) {
1307            if (way.getNodesCount() == 2) {
1308                // For 2 node ways, the two half segments are exactly the same size and distance from the center.
1309                // Prefer the first one for consistency.
1310                longHalfsegmentQuality.set(0, longHalfsegmentQuality.get(0) + 0.5);
1311            }
1312
1313            // find the long half segment that is closest to the center of the way
1314            // candidates with higher quality value are preferred
1315            double bestStart = Double.NaN;
1316            double bestEnd = Double.NaN;
1317            double bestDistanceToCenter = Double.MAX_VALUE;
1318            double bestQuality = -1;
1319            for (int i = 0; i < longHalfSegmentStart.size(); i++) {
1320                double start = longHalfSegmentStart.get(i);
1321                double end = longHalfSegmentEnd.get(i);
1322                double dist = Math.abs(0.5 * (end + start) - 0.5 * pathLength);
1323                if (longHalfsegmentQuality.get(i) > bestQuality
1324                        || (dist < bestDistanceToCenter && Utils.equalsEpsilon(longHalfsegmentQuality.get(i), bestQuality))) {
1325                    bestStart = start;
1326                    bestEnd = end;
1327                    bestDistanceToCenter = dist;
1328                    bestQuality = longHalfsegmentQuality.get(i);
1329                }
1330            }
1331            double remaining = bestEnd - bestStart - rec.getWidth(); // total space left and right from the text
1332            // The space left and right of the text should be distributed 20% - 80% (towards the center),
1333            // but the smaller space should not be less than 7 px.
1334            // However, if the total remaining space is less than 14 px, then distribute it evenly.
1335            double smallerSpace = Math.min(Math.max(0.2 * remaining, 7), 0.5 * remaining);
1336            if ((bestEnd + bestStart)/2 < pathLength/2) {
1337                t2 = bestEnd - smallerSpace;
1338                t1 = t2 - rec.getWidth();
1339            } else {
1340                t1 = bestStart + smallerSpace;
1341                t2 = t1 + rec.getWidth();
1342            }
1343        } else {
1344            // doesn't fit into one half-segment -> just put it in the center of the way
1345            t1 = pathLength/2 - rec.getWidth()/2;
1346            t2 = pathLength/2 + rec.getWidth()/2;
1347        }
1348        t1 /= pathLength;
1349        t2 /= pathLength;
1350
1351        double[] p1 = pointAt(t1, poly, pathLength);
1352        double[] p2 = pointAt(t2, poly, pathLength);
1353
1354        if (p1 == null || p2 == null)
1355            return;
1356
1357        double angleOffset;
1358        double offsetSign;
1359        double tStart;
1360
1361        if (p1[0] < p2[0] &&
1362                p1[2] < Math.PI/2 &&
1363                p1[2] > -Math.PI/2) {
1364            angleOffset = 0;
1365            offsetSign = 1;
1366            tStart = t1;
1367        } else {
1368            angleOffset = Math.PI;
1369            offsetSign = -1;
1370            tStart = t2;
1371        }
1372
1373        List<GlyphVector> gvs = Utils.getGlyphVectorsBidi(name, text.font, g.getFontRenderContext());
1374        double gvOffset = 0;
1375        for (GlyphVector gv : gvs) {
1376            double gvWidth = gv.getLogicalBounds().getBounds2D().getWidth();
1377            for (int i = 0; i < gv.getNumGlyphs(); ++i) {
1378                Rectangle2D rect = gv.getGlyphLogicalBounds(i).getBounds2D();
1379                double t = tStart + offsetSign * (gvOffset + rect.getX() + rect.getWidth()/2) / pathLength;
1380                double[] p = pointAt(t, poly, pathLength);
1381                if (p != null) {
1382                    AffineTransform trfm = AffineTransform.getTranslateInstance(p[0] - rect.getX(), p[1]);
1383                    trfm.rotate(p[2]+angleOffset);
1384                    double off = -rect.getY() - rect.getHeight()/2 + text.yOffset;
1385                    trfm.translate(-rect.getWidth()/2, off);
1386                    if (isGlyphVectorDoubleTranslationBug(text.font)) {
1387                        // scale the translation components by one half
1388                        AffineTransform tmp = AffineTransform.getTranslateInstance(-0.5 * trfm.getTranslateX(), -0.5 * trfm.getTranslateY());
1389                        tmp.concatenate(trfm);
1390                        trfm = tmp;
1391                    }
1392                    gv.setGlyphTransform(i, trfm);
1393                }
1394            }
1395            displayText(gv, null, 0, 0, way.isDisabled(), text);
1396            gvOffset += gvWidth;
1397        }
1398    }
1399
1400    /**
1401     * draw way. This method allows for two draw styles (line using color, dashes using dashedColor) to be passed.
1402     * @param way The way to draw
1403     * @param color The base color to draw the way in
1404     * @param line The line style to use. This is drawn using color.
1405     * @param dashes The dash style to use. This is drawn using dashedColor. <code>null</code> if unused.
1406     * @param dashedColor The color of the dashes.
1407     * @param offset The offset
1408     * @param showOrientation show arrows that indicate the technical orientation of
1409     *              the way (defined by order of nodes)
1410     * @param showHeadArrowOnly True if only the arrow at the end of the line but not those on the segments should be displayed.
1411     * @param showOneway show symbols that indicate the direction of the feature,
1412     *              e.g. oneway street or waterway
1413     * @param onewayReversed for oneway=-1 and similar
1414     */
1415    public void drawWay(Way way, Color color, BasicStroke line, BasicStroke dashes, Color dashedColor, float offset,
1416            boolean showOrientation, boolean showHeadArrowOnly,
1417            boolean showOneway, boolean onewayReversed) {
1418
1419        GeneralPath path = new GeneralPath();
1420        GeneralPath orientationArrows = showOrientation ? new GeneralPath() : null;
1421        GeneralPath onewayArrows = showOneway ? new GeneralPath() : null;
1422        GeneralPath onewayArrowsCasing = showOneway ? new GeneralPath() : null;
1423        Rectangle bounds = g.getClipBounds();
1424        if (bounds != null) {
1425            // avoid arrow heads at the border
1426            bounds.grow(100, 100);
1427        }
1428
1429        double wayLength = 0;
1430        Point lastPoint = null;
1431        boolean initialMoveToNeeded = true;
1432        List<Node> wayNodes = way.getNodes();
1433        if (wayNodes.size() < 2) return;
1434
1435        // only highlight the segment if the way itself is not highlighted
1436        if (!way.isHighlighted() && highlightWaySegments != null) {
1437            GeneralPath highlightSegs = null;
1438            for (WaySegment ws : highlightWaySegments) {
1439                if (ws.way != way || ws.lowerIndex < offset) {
1440                    continue;
1441                }
1442                if (highlightSegs == null) {
1443                    highlightSegs = new GeneralPath();
1444                }
1445
1446                Point p1 = nc.getPoint(ws.getFirstNode());
1447                Point p2 = nc.getPoint(ws.getSecondNode());
1448                highlightSegs.moveTo(p1.x, p1.y);
1449                highlightSegs.lineTo(p2.x, p2.y);
1450            }
1451
1452            drawPathHighlight(highlightSegs, line);
1453        }
1454
1455        Iterator<Point> it = new OffsetIterator(wayNodes, offset);
1456        while (it.hasNext()) {
1457            Point p = it.next();
1458            if (lastPoint != null) {
1459                Point p1 = lastPoint;
1460                Point p2 = p;
1461
1462                /**
1463                 * Do custom clipping to work around openjdk bug. It leads to
1464                 * drawing artefacts when zooming in a lot. (#4289, #4424)
1465                 * (Looks like int overflow.)
1466                 */
1467                LineClip clip = new LineClip(p1, p2, bounds);
1468                if (clip.execute()) {
1469                    if (!p1.equals(clip.getP1())) {
1470                        p1 = clip.getP1();
1471                        path.moveTo(p1.x, p1.y);
1472                    } else if (initialMoveToNeeded) {
1473                        initialMoveToNeeded = false;
1474                        path.moveTo(p1.x, p1.y);
1475                    }
1476                    p2 = clip.getP2();
1477                    path.lineTo(p2.x, p2.y);
1478
1479                    /* draw arrow */
1480                    if (showHeadArrowOnly ? !it.hasNext() : showOrientation) {
1481                        final double segmentLength = p1.distance(p2);
1482                        if (segmentLength != 0) {
1483                            final double l =  (10. + line.getLineWidth()) / segmentLength;
1484
1485                            final double sx = l * (p1.x - p2.x);
1486                            final double sy = l * (p1.y - p2.y);
1487
1488                            orientationArrows.moveTo(p2.x + cosPHI * sx - sinPHI * sy, p2.y + sinPHI * sx + cosPHI * sy);
1489                            orientationArrows.lineTo(p2.x, p2.y);
1490                            orientationArrows.lineTo(p2.x + cosPHI * sx + sinPHI * sy, p2.y - sinPHI * sx + cosPHI * sy);
1491                        }
1492                    }
1493                    if (showOneway) {
1494                        final double segmentLength = p1.distance(p2);
1495                        if (segmentLength != 0) {
1496                            final double nx = (p2.x - p1.x) / segmentLength;
1497                            final double ny = (p2.y - p1.y) / segmentLength;
1498
1499                            final double interval = 60;
1500                            // distance from p1
1501                            double dist = interval - (wayLength % interval);
1502
1503                            while (dist < segmentLength) {
1504                                for (int i = 0; i < 2; ++i) {
1505                                    double onewaySize = i == 0 ? 3d : 2d;
1506                                    GeneralPath onewayPath = i == 0 ? onewayArrowsCasing : onewayArrows;
1507
1508                                    // scale such that border is 1 px
1509                                    final double fac = -(onewayReversed ? -1 : 1) * onewaySize * (1 + sinPHI) / (sinPHI * cosPHI);
1510                                    final double sx = nx * fac;
1511                                    final double sy = ny * fac;
1512
1513                                    // Attach the triangle at the incenter and not at the tip.
1514                                    // Makes the border even at all sides.
1515                                    final double x = p1.x + nx * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI));
1516                                    final double y = p1.y + ny * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI));
1517
1518                                    onewayPath.moveTo(x, y);
1519                                    onewayPath.lineTo(x + cosPHI * sx - sinPHI * sy, y + sinPHI * sx + cosPHI * sy);
1520                                    onewayPath.lineTo(x + cosPHI * sx + sinPHI * sy, y - sinPHI * sx + cosPHI * sy);
1521                                    onewayPath.lineTo(x, y);
1522                                }
1523                                dist += interval;
1524                            }
1525                        }
1526                        wayLength += segmentLength;
1527                    }
1528                }
1529            }
1530            lastPoint = p;
1531        }
1532        if (way.isHighlighted()) {
1533            drawPathHighlight(path, line);
1534        }
1535        displaySegments(path, orientationArrows, onewayArrows, onewayArrowsCasing, color, line, dashes, dashedColor);
1536    }
1537
1538    /**
1539     * Gets the "circum". This is the distance on the map in meters that 100 screen pixels represent.
1540     * @return The "circum"
1541     */
1542    public double getCircum() {
1543        return circum;
1544    }
1545
1546    @Override
1547    public void getColors() {
1548        super.getColors();
1549        this.highlightColorTransparent = new Color(highlightColor.getRed(), highlightColor.getGreen(), highlightColor.getBlue(), 100);
1550        this.backgroundColor = PaintColors.getBackgroundColor();
1551    }
1552
1553    @Override
1554    public void getSettings(boolean virtual) {
1555        super.getSettings(virtual);
1556        paintSettings = MapPaintSettings.INSTANCE;
1557
1558        circum = nc.getDist100Pixel();
1559        scale = nc.getScale();
1560
1561        leftHandTraffic = Main.pref.getBoolean("mappaint.lefthandtraffic", false);
1562
1563        useStrokes = paintSettings.getUseStrokesDistance() > circum;
1564        showNames = paintSettings.getShowNamesDistance() > circum;
1565        showIcons = paintSettings.getShowIconsDistance() > circum;
1566        isOutlineOnly = paintSettings.isOutlineOnly();
1567        orderFont = new Font(Main.pref.get("mappaint.font", "Droid Sans"), Font.PLAIN, Main.pref.getInteger("mappaint.fontsize", 8));
1568
1569        antialiasing = Main.pref.getBoolean("mappaint.use-antialiasing", true) ?
1570                        RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF;
1571        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing);
1572
1573        Object textAntialiasing;
1574        switch (Main.pref.get("mappaint.text-antialiasing", "default")) {
1575            case "on":
1576                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_ON;
1577                break;
1578            case "off":
1579                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_OFF;
1580                break;
1581            case "gasp":
1582                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_GASP;
1583                break;
1584            case "lcd-hrgb":
1585                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB;
1586                break;
1587            case "lcd-hbgr":
1588                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HBGR;
1589                break;
1590            case "lcd-vrgb":
1591                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VRGB;
1592                break;
1593            case "lcd-vbgr":
1594                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VBGR;
1595                break;
1596            default:
1597                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT;
1598        }
1599        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, textAntialiasing);
1600
1601        highlightLineWidth = Main.pref.getInteger("mappaint.highlight.width", 4);
1602        highlightPointRadius = Main.pref.getInteger("mappaint.highlight.radius", 7);
1603        widerHighlight = Main.pref.getInteger("mappaint.highlight.bigger-increment", 5);
1604        highlightStep = Main.pref.getInteger("mappaint.highlight.step", 4);
1605    }
1606
1607    private static Path2D.Double getPath(Way w) {
1608        Path2D.Double path = new Path2D.Double();
1609        boolean initial = true;
1610        for (Node n : w.getNodes()) {
1611            EastNorth p = n.getEastNorth();
1612            if (p != null) {
1613                if (initial) {
1614                    path.moveTo(p.getX(), p.getY());
1615                    initial = false;
1616                } else {
1617                    path.lineTo(p.getX(), p.getY());
1618                }
1619            }
1620        }
1621        if (w.isClosed()) {
1622            path.closePath();
1623        }
1624        return path;
1625    }
1626
1627    private static Path2D.Double getPFClip(Way w, double extent) {
1628        Path2D.Double clip = new Path2D.Double();
1629        buildPFClip(clip, w.getNodes(), extent);
1630        return clip;
1631    }
1632
1633    private static Path2D.Double getPFClip(PolyData pd, double extent) {
1634        Path2D.Double clip = new Path2D.Double();
1635        clip.setWindingRule(Path2D.WIND_EVEN_ODD);
1636        buildPFClip(clip, pd.getNodes(), extent);
1637        for (PolyData pdInner : pd.getInners()) {
1638            buildPFClip(clip, pdInner.getNodes(), extent);
1639        }
1640        return clip;
1641    }
1642
1643    /**
1644     * Fix the clipping area of unclosed polygons for partial fill.
1645     *
1646     * The current algorithm for partial fill simply strokes the polygon with a
1647     * large stroke width after masking the outside with a clipping area.
1648     * This works, but for unclosed polygons, the mask can crop the corners at
1649     * both ends (see #12104).
1650     *
1651     * This method fixes the clipping area by sort of adding the corners to the
1652     * clip outline.
1653     *
1654     * @param clip the clipping area to modify (initially empty)
1655     * @param nodes nodes of the polygon
1656     * @param extent the extent
1657     */
1658    private static void buildPFClip(Path2D.Double clip, List<Node> nodes, double extent) {
1659        boolean initial = true;
1660        for (Node n : nodes) {
1661            EastNorth p = n.getEastNorth();
1662            if (p != null) {
1663                if (initial) {
1664                    clip.moveTo(p.getX(), p.getY());
1665                    initial = false;
1666                } else {
1667                    clip.lineTo(p.getX(), p.getY());
1668                }
1669            }
1670        }
1671        if (nodes.size() >= 3) {
1672            EastNorth fst = nodes.get(0).getEastNorth();
1673            EastNorth snd = nodes.get(1).getEastNorth();
1674            EastNorth lst = nodes.get(nodes.size() - 1).getEastNorth();
1675            EastNorth lbo = nodes.get(nodes.size() - 2).getEastNorth();
1676
1677            EastNorth cLst = getPFDisplacedEndPoint(lbo, lst, fst, extent);
1678            EastNorth cFst = getPFDisplacedEndPoint(snd, fst, cLst != null ? cLst : lst, extent);
1679            if (cLst == null && cFst != null) {
1680                cLst = getPFDisplacedEndPoint(lbo, lst, cFst, extent);
1681            }
1682            if (cLst != null) {
1683                clip.lineTo(cLst.getX(), cLst.getY());
1684            }
1685            if (cFst != null) {
1686                clip.lineTo(cFst.getX(), cFst.getY());
1687            }
1688        }
1689    }
1690
1691    /**
1692     * Get the point to add to the clipping area for partial fill of unclosed polygons.
1693     *
1694     * <code>(p1,p2)</code> is the first or last way segment and <code>p3</code> the
1695     * opposite endpoint.
1696     *
1697     * @param p1 1st point
1698     * @param p2 2nd point
1699     * @param p3 3rd point
1700     * @param extent the extent
1701     * @return a point q, such that p1,p2,q form a right angle
1702     * and the distance of q to p2 is <code>extent</code>. The point q lies on
1703     * the same side of the line p1,p2 as the point p3.
1704     * Returns null if p1,p2,p3 forms an angle greater 90 degrees. (In this case
1705     * the corner of the partial fill would not be cut off by the mask, so an
1706     * additional point is not necessary.)
1707     */
1708    private static EastNorth getPFDisplacedEndPoint(EastNorth p1, EastNorth p2, EastNorth p3, double extent) {
1709        double dx1 = p2.getX() - p1.getX();
1710        double dy1 = p2.getY() - p1.getY();
1711        double dx2 = p3.getX() - p2.getX();
1712        double dy2 = p3.getY() - p2.getY();
1713        if (dx1 * dx2 + dy1 * dy2 < 0) {
1714            double len = Math.sqrt(dx1 * dx1 + dy1 * dy1);
1715            if (len == 0) return null;
1716            double dxm = -dy1 * extent / len;
1717            double dym = dx1 * extent / len;
1718            if (dx1 * dy2 - dx2 * dy1 < 0) {
1719                dxm = -dxm;
1720                dym = -dym;
1721            }
1722            return new EastNorth(p2.getX() + dxm, p2.getY() + dym);
1723        }
1724        return null;
1725    }
1726
1727    private boolean isAreaVisible(Path2D.Double area) {
1728        Rectangle2D bounds = area.getBounds2D();
1729        if (bounds.isEmpty()) return false;
1730        Point2D p = nc.getPoint2D(new EastNorth(bounds.getX(), bounds.getY()));
1731        if (p.getX() > nc.getWidth()) return false;
1732        if (p.getY() < 0) return false;
1733        p = nc.getPoint2D(new EastNorth(bounds.getX() + bounds.getWidth(), bounds.getY() + bounds.getHeight()));
1734        if (p.getX() < 0) return false;
1735        if (p.getY() > nc.getHeight()) return false;
1736        return true;
1737    }
1738
1739    public boolean isInactiveMode() {
1740        return isInactiveMode;
1741    }
1742
1743    public boolean isShowIcons() {
1744        return showIcons;
1745    }
1746
1747    public boolean isShowNames() {
1748        return showNames;
1749    }
1750
1751    private static double[] pointAt(double t, Polygon poly, double pathLength) {
1752        double totalLen = t * pathLength;
1753        double curLen = 0;
1754        long dx, dy;
1755        double segLen;
1756
1757        // Yes, it is inefficient to iterate from the beginning for each glyph.
1758        // Can be optimized if it turns out to be slow.
1759        for (int i = 1; i < poly.npoints; ++i) {
1760            dx = (long) poly.xpoints[i] - poly.xpoints[i-1];
1761            dy = (long) poly.ypoints[i] - poly.ypoints[i-1];
1762            segLen = Math.sqrt(dx*dx + dy*dy);
1763            if (totalLen > curLen + segLen) {
1764                curLen += segLen;
1765                continue;
1766            }
1767            return new double[] {
1768                    poly.xpoints[i-1]+(totalLen - curLen)/segLen*dx,
1769                    poly.ypoints[i-1]+(totalLen - curLen)/segLen*dy,
1770                    Math.atan2(dy, dx)};
1771        }
1772        return null;
1773    }
1774
1775    /**
1776     * Computes the flags for a given OSM primitive.
1777     * @param primitive The primititve to compute the flags for.
1778     * @param checkOuterMember <code>true</code> if we should also add {@link #FLAG_OUTERMEMBER_OF_SELECTED}
1779     * @return The flag.
1780     */
1781    public static int computeFlags(OsmPrimitive primitive, boolean checkOuterMember) {
1782        if (primitive.isDisabled()) {
1783            return FLAG_DISABLED;
1784        } else if (primitive.isSelected()) {
1785            return FLAG_SELECTED;
1786        } else if (checkOuterMember && primitive.isOuterMemberOfSelected()) {
1787            return FLAG_OUTERMEMBER_OF_SELECTED;
1788        } else if (primitive.isMemberOfSelected()) {
1789            return FLAG_MEMBER_OF_SELECTED;
1790        } else {
1791            return FLAG_NORMAL;
1792        }
1793    }
1794
1795    private class ComputeStyleListWorker extends RecursiveTask<List<StyleRecord>> implements Visitor {
1796        private final transient List<? extends OsmPrimitive> input;
1797        private final transient List<StyleRecord> output;
1798
1799        private final transient ElemStyles styles = MapPaintStyles.getStyles();
1800        private final int directExecutionTaskSize;
1801
1802        private final boolean drawArea = circum <= Main.pref.getInteger("mappaint.fillareas", 10000000);
1803        private final boolean drawMultipolygon = drawArea && Main.pref.getBoolean("mappaint.multipolygon", true);
1804        private final boolean drawRestriction = Main.pref.getBoolean("mappaint.restriction", true);
1805
1806        /**
1807         * Constructs a new {@code ComputeStyleListWorker}.
1808         * @param input the primitives to process
1809         * @param output the list of styles to which styles will be added
1810         * @param directExecutionTaskSize the threshold deciding whether to subdivide the tasks
1811         */
1812        ComputeStyleListWorker(final List<? extends OsmPrimitive> input, List<StyleRecord> output, int directExecutionTaskSize) {
1813            this.input = input;
1814            this.output = output;
1815            this.directExecutionTaskSize = directExecutionTaskSize;
1816            this.styles.setDrawMultipolygon(drawMultipolygon);
1817        }
1818
1819        @Override
1820        protected List<StyleRecord> compute() {
1821            if (input.size() <= directExecutionTaskSize) {
1822                return computeDirectly();
1823            } else {
1824                final Collection<ForkJoinTask<List<StyleRecord>>> tasks = new ArrayList<>();
1825                for (int fromIndex = 0; fromIndex < input.size(); fromIndex += directExecutionTaskSize) {
1826                    final int toIndex = Math.min(fromIndex + directExecutionTaskSize, input.size());
1827                    final List<StyleRecord> output = new ArrayList<>(directExecutionTaskSize);
1828                    tasks.add(new ComputeStyleListWorker(input.subList(fromIndex, toIndex), output, directExecutionTaskSize).fork());
1829                }
1830                for (ForkJoinTask<List<StyleRecord>> task : tasks) {
1831                    output.addAll(task.join());
1832                }
1833                return output;
1834            }
1835        }
1836
1837        public List<StyleRecord> computeDirectly() {
1838            MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
1839            try {
1840                for (final OsmPrimitive osm : input) {
1841                    if (osm.isDrawable()) {
1842                        osm.accept(this);
1843                    }
1844                }
1845                return output;
1846            } finally {
1847                MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
1848            }
1849        }
1850
1851        @Override
1852        public void visit(Node n) {
1853            add(n, computeFlags(n, false));
1854        }
1855
1856        @Override
1857        public void visit(Way w) {
1858            add(w, computeFlags(w, true));
1859        }
1860
1861        @Override
1862        public void visit(Relation r) {
1863            add(r, computeFlags(r, true));
1864        }
1865
1866        @Override
1867        public void visit(Changeset cs) {
1868            throw new UnsupportedOperationException();
1869        }
1870
1871        public void add(Node osm, int flags) {
1872            StyleElementList sl = styles.get(osm, circum, nc);
1873            for (StyleElement s : sl) {
1874                output.add(new StyleRecord(s, osm, flags));
1875            }
1876        }
1877
1878        public void add(Relation osm, int flags) {
1879            StyleElementList sl = styles.get(osm, circum, nc);
1880            for (StyleElement s : sl) {
1881                if (drawMultipolygon && drawArea && s instanceof AreaElement && (flags & FLAG_DISABLED) == 0) {
1882                    output.add(new StyleRecord(s, osm, flags));
1883                } else if (drawRestriction && s instanceof NodeElement) {
1884                    output.add(new StyleRecord(s, osm, flags));
1885                }
1886            }
1887        }
1888
1889        public void add(Way osm, int flags) {
1890            StyleElementList sl = styles.get(osm, circum, nc);
1891            for (StyleElement s : sl) {
1892                if (!(drawArea && (flags & FLAG_DISABLED) == 0) && s instanceof AreaElement) {
1893                    continue;
1894                }
1895                output.add(new StyleRecord(s, osm, flags));
1896            }
1897        }
1898    }
1899
1900    @Override
1901    public void render(final DataSet data, boolean renderVirtualNodes, Bounds bounds) {
1902        BBox bbox = bounds.toBBox();
1903        getSettings(renderVirtualNodes);
1904        boolean benchmarkOutput = Main.isTraceEnabled() || Main.pref.getBoolean("mappaint.render.benchmark", false);
1905        boolean benchmark = benchmarkOutput || benchmarkData != null;
1906
1907        data.getReadLock().lock();
1908        try {
1909            highlightWaySegments = data.getHighlightedWaySegments();
1910
1911            long timeStart = 0, timeGenerateDone = 0, timeSortingDone = 0, timeFinished;
1912            if (benchmark) {
1913                timeStart = System.currentTimeMillis();
1914                if (benchmarkOutput) {
1915                    System.err.print("BENCHMARK: rendering ");
1916                }
1917            }
1918
1919            List<Node> nodes = data.searchNodes(bbox);
1920            List<Way> ways = data.searchWays(bbox);
1921            List<Relation> relations = data.searchRelations(bbox);
1922
1923            final List<StyleRecord> allStyleElems = new ArrayList<>(nodes.size()+ways.size()+relations.size());
1924
1925            // Need to process all relations first.
1926            // Reason: Make sure, ElemStyles.getStyleCacheWithRange is
1927            // not called for the same primitive in parallel threads.
1928            // (Could be synchronized, but try to avoid this for
1929            // performance reasons.)
1930            THREAD_POOL.invoke(new ComputeStyleListWorker(relations, allStyleElems,
1931                    Math.max(20, relations.size() / THREAD_POOL.getParallelism() / 3)));
1932            THREAD_POOL.invoke(new ComputeStyleListWorker(new CompositeList<>(nodes, ways), allStyleElems,
1933                    Math.max(100, (nodes.size() + ways.size()) / THREAD_POOL.getParallelism() / 3)));
1934
1935            if (benchmark) {
1936                timeGenerateDone = System.currentTimeMillis();
1937                if (benchmarkOutput) {
1938                    System.err.print("phase 1 (calculate styles): " + Utils.getDurationString(timeGenerateDone - timeStart));
1939                }
1940                if (benchmarkData != null) {
1941                    benchmarkData.generateTime = timeGenerateDone - timeStart;
1942                }
1943            }
1944
1945            Collections.sort(allStyleElems); // TODO: try parallel sort when switching to Java 8
1946
1947            if (benchmarkData != null) {
1948                timeSortingDone = System.currentTimeMillis();
1949                benchmarkData.sortTime = timeSortingDone - timeGenerateDone;
1950                if (benchmarkData.skipDraw) {
1951                    benchmarkData.recordElementStats(allStyleElems);
1952                    return;
1953                }
1954            }
1955
1956            for (StyleRecord r : allStyleElems) {
1957                r.style.paintPrimitive(
1958                        r.osm,
1959                        paintSettings,
1960                        this,
1961                        (r.flags & FLAG_SELECTED) != 0,
1962                        (r.flags & FLAG_OUTERMEMBER_OF_SELECTED) != 0,
1963                        (r.flags & FLAG_MEMBER_OF_SELECTED) != 0
1964                );
1965            }
1966
1967            if (benchmark) {
1968                timeFinished = System.currentTimeMillis();
1969                if (benchmarkData != null) {
1970                    benchmarkData.drawTime = timeFinished - timeGenerateDone;
1971                    benchmarkData.recordElementStats(allStyleElems);
1972                }
1973                if (benchmarkOutput) {
1974                    System.err.println("; phase 2 (draw): " + Utils.getDurationString(timeFinished - timeGenerateDone) +
1975                        "; total: " + Utils.getDurationString(timeFinished - timeStart) +
1976                        " (scale: " + circum + " zoom level: " + Selector.GeneralSelector.scale2level(circum) + ')');
1977                }
1978            }
1979
1980            drawVirtualNodes(data, bbox);
1981        } finally {
1982            data.getReadLock().unlock();
1983        }
1984    }
1985}