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                    // CHECKSTYLE.OFF: SingleSpaceSeparator
593                    final int x1 = pb.x + (int)   (w/4.0);
594                    final int x3 = pb.x + (int) (3*w/4.0);
595                    final int y1 = pb.y + (int)   (h/4.0);
596                    final int y3 = pb.y + (int) (3*h/4.0);
597                    // CHECKSTYLE.ON: SingleSpaceSeparator
598                    // +-----------+
599                    // |  5  1  6  |
600                    // |  4  C  2  |
601                    // |  8  3  7  |
602                    // +-----------+
603                    Rectangle[] candidates = new Rectangle[] {
604                            new Rectangle(x2, y1, nbw, nbh),
605                            new Rectangle(x3, y2, nbw, nbh),
606                            new Rectangle(x2, y3, nbw, nbh),
607                            new Rectangle(x1, y2, nbw, nbh),
608                            new Rectangle(x1, y1, nbw, nbh),
609                            new Rectangle(x3, y1, nbw, nbh),
610                            new Rectangle(x3, y3, nbw, nbh),
611                            new Rectangle(x1, y3, nbw, nbh)
612                    };
613                    // Dumb algorithm to find a better placement. We could surely find a smarter one but it should
614                    // solve most of building issues with only few calculations (8 at most)
615                    for (int i = 0; i < candidates.length && !labelOK; i++) {
616                        centeredNBounds = candidates[i];
617                        labelOK = area.contains(centeredNBounds);
618                    }
619                }
620                if (labelOK) {
621                    Font defaultFont = g.getFont();
622                    int x = (int) (centeredNBounds.getMinX() - nb.getMinX());
623                    int y = (int) (centeredNBounds.getMinY() - nb.getMinY());
624                    displayText(null, name, x, y, osm.isDisabled(), text);
625                    g.setFont(defaultFont);
626                } else if (Main.isTraceEnabled()) {
627                    Main.trace("Couldn't find a correct label placement for "+osm+" / "+name);
628                }
629            }
630        }
631    }
632
633    /**
634     * Draws a multipolygon area.
635     * @param r The multipolygon relation
636     * @param color The color to fill the area with.
637     * @param fillImage The image to fill the area with. Overrides color.
638     * @param extent if not null, area will be filled partially; specifies, how
639     * far to fill from the boundary towards the center of the area;
640     * if null, area will be filled completely
641     * @param extentThreshold if not null, determines if the partial filled should
642     * be replaced by plain fill, when it covers a certain fraction of the total area
643     * @param disabled If this should be drawn with a special disabled style.
644     * @param text The text to write on the area.
645     */
646    public void drawArea(Relation r, Color color, MapImage fillImage, Float extent, Float extentThreshold, boolean disabled, TextLabel text) {
647        Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, r);
648        if (!r.isDisabled() && !multipolygon.getOuterWays().isEmpty()) {
649            for (PolyData pd : multipolygon.getCombinedPolygons()) {
650                Path2D.Double p = pd.get();
651                Path2D.Double pfClip = null;
652                if (!isAreaVisible(p)) {
653                    continue;
654                }
655                if (extent != null) {
656                    if (!usePartialFill(pd.getAreaAndPerimeter(null), extent, extentThreshold)) {
657                        extent = null;
658                    } else if (!pd.isClosed()) {
659                        pfClip = getPFClip(pd, extent * scale);
660                    }
661                }
662                drawArea(r, p,
663                        pd.isSelected() ? paintSettings.getRelationSelectedColor(color.getAlpha()) : color,
664                        fillImage, extent, pfClip, disabled, text);
665            }
666        }
667    }
668
669    /**
670     * Draws an area defined by a way. They way does not need to be closed, but it should.
671     * @param w The way.
672     * @param color The color to fill the area with.
673     * @param fillImage The image to fill the area with. Overrides color.
674     * @param extent if not null, area will be filled partially; specifies, how
675     * far to fill from the boundary towards the center of the area;
676     * if null, area will be filled completely
677     * @param extentThreshold if not null, determines if the partial filled should
678     * be replaced by plain fill, when it covers a certain fraction of the total area
679     * @param disabled If this should be drawn with a special disabled style.
680     * @param text The text to write on the area.
681     */
682    public void drawArea(Way w, Color color, MapImage fillImage, Float extent, Float extentThreshold, boolean disabled, TextLabel text) {
683        Path2D.Double pfClip = null;
684        if (extent != null) {
685            if (!usePartialFill(Geometry.getAreaAndPerimeter(w.getNodes()), extent, extentThreshold)) {
686                extent = null;
687            } else if (!w.isClosed()) {
688                pfClip = getPFClip(w, extent * scale);
689            }
690        }
691        drawArea(w, getPath(w), color, fillImage, extent, pfClip, disabled, text);
692    }
693
694    /**
695     * Determine, if partial fill should be turned off for this object, because
696     * only a small unfilled gap in the center of the area would be left.
697     *
698     * This is used to get a cleaner look for urban regions with many small
699     * areas like buildings, etc.
700     * @param ap the area and the perimeter of the object
701     * @param extent the "width" of partial fill
702     * @param threshold when the partial fill covers that much of the total
703     * area, the partial fill is turned off; can be greater than 100% as the
704     * covered area is estimated as <code>perimeter * extent</code>
705     * @return true, if the partial fill should be used, false otherwise
706     */
707    private boolean usePartialFill(AreaAndPerimeter ap, float extent, Float threshold) {
708        if (threshold == null) return true;
709        return ap.getPerimeter() * extent * scale < threshold * ap.getArea();
710    }
711
712    public void drawBoxText(Node n, BoxTextElement bs) {
713        if (!isShowNames() || bs == null)
714            return;
715
716        Point p = nc.getPoint(n);
717        TextLabel text = bs.text;
718        String s = text.labelCompositionStrategy.compose(n);
719        if (s == null) return;
720
721        Font defaultFont = g.getFont();
722        g.setFont(text.font);
723
724        int x = p.x + text.xOffset;
725        int y = p.y + text.yOffset;
726        /**
727         *
728         *       left-above __center-above___ right-above
729         *         left-top|                 |right-top
730         *                 |                 |
731         *      left-center|  center-center  |right-center
732         *                 |                 |
733         *      left-bottom|_________________|right-bottom
734         *       left-below   center-below    right-below
735         *
736         */
737        Rectangle box = bs.getBox();
738        if (bs.hAlign == HorizontalTextAlignment.RIGHT) {
739            x += box.x + box.width + 2;
740        } else {
741            FontRenderContext frc = g.getFontRenderContext();
742            Rectangle2D bounds = text.font.getStringBounds(s, frc);
743            int textWidth = (int) bounds.getWidth();
744            if (bs.hAlign == HorizontalTextAlignment.CENTER) {
745                x -= textWidth / 2;
746            } else if (bs.hAlign == HorizontalTextAlignment.LEFT) {
747                x -= -box.x + 4 + textWidth;
748            } else throw new AssertionError();
749        }
750
751        if (bs.vAlign == VerticalTextAlignment.BOTTOM) {
752            y += box.y + box.height;
753        } else {
754            FontRenderContext frc = g.getFontRenderContext();
755            LineMetrics metrics = text.font.getLineMetrics(s, frc);
756            if (bs.vAlign == VerticalTextAlignment.ABOVE) {
757                y -= -box.y + metrics.getDescent();
758            } else if (bs.vAlign == VerticalTextAlignment.TOP) {
759                y -= -box.y - metrics.getAscent();
760            } else if (bs.vAlign == VerticalTextAlignment.CENTER) {
761                y += (metrics.getAscent() - metrics.getDescent()) / 2;
762            } else if (bs.vAlign == VerticalTextAlignment.BELOW) {
763                y += box.y + box.height + metrics.getAscent() + 2;
764            } else throw new AssertionError();
765        }
766        displayText(null, s, x, y, n.isDisabled(), text);
767        g.setFont(defaultFont);
768    }
769
770    /**
771     * Draw an image along a way repeatedly.
772     *
773     * @param way the way
774     * @param pattern the image
775     * @param disabled If this should be drawn with a special disabled style.
776     * @param offset offset from the way
777     * @param spacing spacing between two images
778     * @param phase initial spacing
779     * @param align alignment of the image. The top, center or bottom edge can be aligned with the way.
780     */
781    public void drawRepeatImage(Way way, MapImage pattern, boolean disabled, double offset, double spacing, double phase,
782            LineImageAlignment align) {
783        final int imgWidth = pattern.getWidth();
784        final double repeat = imgWidth + spacing;
785        final int imgHeight = pattern.getHeight();
786
787        Point lastP = null;
788        double currentWayLength = phase % repeat;
789        if (currentWayLength < 0) {
790            currentWayLength += repeat;
791        }
792
793        int dy1, dy2;
794        switch (align) {
795            case TOP:
796                dy1 = 0;
797                dy2 = imgHeight;
798                break;
799            case CENTER:
800                dy1 = -imgHeight / 2;
801                dy2 = imgHeight + dy1;
802                break;
803            case BOTTOM:
804                dy1 = -imgHeight;
805                dy2 = 0;
806                break;
807            default:
808                throw new AssertionError();
809        }
810
811        OffsetIterator it = new OffsetIterator(way.getNodes(), offset);
812        while (it.hasNext()) {
813            Point thisP = it.next();
814
815            if (lastP != null) {
816                final double segmentLength = thisP.distance(lastP);
817
818                final double dx = (double) thisP.x - lastP.x;
819                final double dy = (double) thisP.y - lastP.y;
820
821                // pos is the position from the beginning of the current segment
822                // where an image should be painted
823                double pos = repeat - (currentWayLength % repeat);
824
825                AffineTransform saveTransform = g.getTransform();
826                g.translate(lastP.x, lastP.y);
827                g.rotate(Math.atan2(dy, dx));
828
829                // draw the rest of the image from the last segment in case it
830                // is cut off
831                if (pos > spacing) {
832                    // segment is too short for a complete image
833                    if (pos > segmentLength + spacing) {
834                        g.drawImage(pattern.getImage(disabled), 0, dy1, (int) segmentLength, dy2,
835                                (int) (repeat - pos), 0,
836                                (int) (repeat - pos + segmentLength), imgHeight, null);
837                    } else {
838                        // rest of the image fits fully on the current segment
839                        g.drawImage(pattern.getImage(disabled), 0, dy1, (int) (pos - spacing), dy2,
840                                (int) (repeat - pos), 0, imgWidth, imgHeight, null);
841                    }
842                }
843                // draw remaining images for this segment
844                while (pos < segmentLength) {
845                    // cut off at the end?
846                    if (pos + imgWidth > segmentLength) {
847                        g.drawImage(pattern.getImage(disabled), (int) pos, dy1, (int) segmentLength, dy2,
848                                0, 0, (int) segmentLength - (int) pos, imgHeight, null);
849                    } else {
850                        g.drawImage(pattern.getImage(disabled), (int) pos, dy1, nc);
851                    }
852                    pos += repeat;
853                }
854                g.setTransform(saveTransform);
855
856                currentWayLength += segmentLength;
857            }
858            lastP = thisP;
859        }
860    }
861
862    @Override
863    public void drawNode(Node n, Color color, int size, boolean fill) {
864        if (size <= 0 && !n.isHighlighted())
865            return;
866
867        Point p = nc.getPoint(n);
868
869        if (n.isHighlighted()) {
870            drawPointHighlight(p, size);
871        }
872
873        if (size > 1) {
874            if ((p.x < 0) || (p.y < 0) || (p.x > nc.getWidth()) || (p.y > nc.getHeight())) return;
875            int radius = size / 2;
876
877            if (isInactiveMode || n.isDisabled()) {
878                g.setColor(inactiveColor);
879            } else {
880                g.setColor(color);
881            }
882            if (fill) {
883                g.fillRect(p.x-radius-1, p.y-radius-1, size + 1, size + 1);
884            } else {
885                g.drawRect(p.x-radius-1, p.y-radius-1, size, size);
886            }
887        }
888    }
889
890    public void drawNodeIcon(Node n, MapImage img, boolean disabled, boolean selected, boolean member, double theta) {
891        Point p = nc.getPoint(n);
892
893        final int w = img.getWidth(), h = img.getHeight();
894        if (n.isHighlighted()) {
895            drawPointHighlight(p, Math.max(w, h));
896        }
897
898        float alpha = img.getAlphaFloat();
899
900        if (!Utils.equalsEpsilon(alpha, 1f)) {
901            g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
902        }
903        g.rotate(theta, p.x, p.y);
904        g.drawImage(img.getImage(disabled), p.x - w/2 + img.offsetX, p.y - h/2 + img.offsetY, nc);
905        g.rotate(-theta, p.x, p.y);
906        g.setPaintMode();
907        if (selected || member) {
908            Color color;
909            if (disabled) {
910                color = inactiveColor;
911            } else if (selected) {
912                color = selectedColor;
913            } else {
914                color = relationSelectedColor;
915            }
916            g.setColor(color);
917            g.drawRect(p.x - w/2 + img.offsetX - 2, p.y - h/2 + img.offsetY - 2, w + 4, h + 4);
918        }
919    }
920
921    public void drawNodeSymbol(Node n, Symbol s, Color fillColor, Color strokeColor) {
922        Point p = nc.getPoint(n);
923        int radius = s.size / 2;
924
925        if (n.isHighlighted()) {
926            drawPointHighlight(p, s.size);
927        }
928
929        if (fillColor != null) {
930            g.setColor(fillColor);
931            switch (s.symbol) {
932            case SQUARE:
933                g.fillRect(p.x - radius, p.y - radius, s.size, s.size);
934                break;
935            case CIRCLE:
936                g.fillOval(p.x - radius, p.y - radius, s.size, s.size);
937                break;
938            case TRIANGLE:
939                g.fillPolygon(buildPolygon(p, radius, 3, Math.PI / 2));
940                break;
941            case PENTAGON:
942                g.fillPolygon(buildPolygon(p, radius, 5, Math.PI / 2));
943                break;
944            case HEXAGON:
945                g.fillPolygon(buildPolygon(p, radius, 6));
946                break;
947            case HEPTAGON:
948                g.fillPolygon(buildPolygon(p, radius, 7, Math.PI / 2));
949                break;
950            case OCTAGON:
951                g.fillPolygon(buildPolygon(p, radius, 8, Math.PI / 8));
952                break;
953            case NONAGON:
954                g.fillPolygon(buildPolygon(p, radius, 9, Math.PI / 2));
955                break;
956            case DECAGON:
957                g.fillPolygon(buildPolygon(p, radius, 10));
958                break;
959            default:
960                throw new AssertionError();
961            }
962        }
963        if (s.stroke != null) {
964            g.setStroke(s.stroke);
965            g.setColor(strokeColor);
966            switch (s.symbol) {
967            case SQUARE:
968                g.drawRect(p.x - radius, p.y - radius, s.size - 1, s.size - 1);
969                break;
970            case CIRCLE:
971                g.drawOval(p.x - radius, p.y - radius, s.size - 1, s.size - 1);
972                break;
973            case TRIANGLE:
974                g.drawPolygon(buildPolygon(p, radius, 3, Math.PI / 2));
975                break;
976            case PENTAGON:
977                g.drawPolygon(buildPolygon(p, radius, 5, Math.PI / 2));
978                break;
979            case HEXAGON:
980                g.drawPolygon(buildPolygon(p, radius, 6));
981                break;
982            case HEPTAGON:
983                g.drawPolygon(buildPolygon(p, radius, 7, Math.PI / 2));
984                break;
985            case OCTAGON:
986                g.drawPolygon(buildPolygon(p, radius, 8, Math.PI / 8));
987                break;
988            case NONAGON:
989                g.drawPolygon(buildPolygon(p, radius, 9, Math.PI / 2));
990                break;
991            case DECAGON:
992                g.drawPolygon(buildPolygon(p, radius, 10));
993                break;
994            default:
995                throw new AssertionError();
996            }
997            g.setStroke(new BasicStroke());
998        }
999    }
1000
1001    /**
1002     * Draw a number of the order of the two consecutive nodes within the
1003     * parents way
1004     *
1005     * @param n1 First node of the way segment.
1006     * @param n2 Second node of the way segment.
1007     * @param orderNumber The number of the segment in the way.
1008     * @param clr The color to use for drawing the text.
1009     */
1010    public void drawOrderNumber(Node n1, Node n2, int orderNumber, Color clr) {
1011        Point p1 = nc.getPoint(n1);
1012        Point p2 = nc.getPoint(n2);
1013        drawOrderNumber(p1, p2, orderNumber, clr);
1014    }
1015
1016    /**
1017     * highlights a given GeneralPath using the settings from BasicStroke to match the line's
1018     * style. Width of the highlight is hard coded.
1019     * @param path path to draw
1020     * @param line line style
1021     */
1022    private void drawPathHighlight(GeneralPath path, BasicStroke line) {
1023        if (path == null)
1024            return;
1025        g.setColor(highlightColorTransparent);
1026        float w = line.getLineWidth() + highlightLineWidth;
1027        if (useWiderHighlight) w += widerHighlight;
1028        while (w >= line.getLineWidth()) {
1029            g.setStroke(new BasicStroke(w, line.getEndCap(), line.getLineJoin(), line.getMiterLimit()));
1030            g.draw(path);
1031            w -= highlightStep;
1032        }
1033    }
1034
1035    /**
1036     * highlights a given point by drawing a rounded rectangle around it. Give the
1037     * size of the object you want to be highlighted, width is added automatically.
1038     * @param p point
1039     * @param size highlight size
1040     */
1041    private void drawPointHighlight(Point p, int size) {
1042        g.setColor(highlightColorTransparent);
1043        int s = size + highlightPointRadius;
1044        if (useWiderHighlight) s += widerHighlight;
1045        while (s >= size) {
1046            int r = (int) Math.floor(s/2d);
1047            g.fillRoundRect(p.x-r, p.y-r, s, s, r, r);
1048            s -= highlightStep;
1049        }
1050    }
1051
1052    public void drawRestriction(Image img, Point pVia, double vx, double vx2, double vy, double vy2, double angle, boolean selected) {
1053        // rotate image with direction last node in from to, and scale down image to 16*16 pixels
1054        Image smallImg = ImageProvider.createRotatedImage(img, angle, new Dimension(16, 16));
1055        int w = smallImg.getWidth(null), h = smallImg.getHeight(null);
1056        g.drawImage(smallImg, (int) (pVia.x+vx+vx2)-w/2, (int) (pVia.y+vy+vy2)-h/2, nc);
1057
1058        if (selected) {
1059            g.setColor(isInactiveMode ? inactiveColor : relationSelectedColor);
1060            g.drawRect((int) (pVia.x+vx+vx2)-w/2-2, (int) (pVia.y+vy+vy2)-h/2-2, w+4, h+4);
1061        }
1062    }
1063
1064    public void drawRestriction(Relation r, MapImage icon, boolean disabled) {
1065        Way fromWay = null;
1066        Way toWay = null;
1067        OsmPrimitive via = null;
1068
1069        /* find the "from", "via" and "to" elements */
1070        for (RelationMember m : r.getMembers()) {
1071            if (m.getMember().isIncomplete())
1072                return;
1073            else {
1074                if (m.isWay()) {
1075                    Way w = m.getWay();
1076                    if (w.getNodesCount() < 2) {
1077                        continue;
1078                    }
1079
1080                    switch(m.getRole()) {
1081                    case "from":
1082                        if (fromWay == null) {
1083                            fromWay = w;
1084                        }
1085                        break;
1086                    case "to":
1087                        if (toWay == null) {
1088                            toWay = w;
1089                        }
1090                        break;
1091                    case "via":
1092                        if (via == null) {
1093                            via = w;
1094                        }
1095                        break;
1096                    default: // Do nothing
1097                    }
1098                } else if (m.isNode()) {
1099                    Node n = m.getNode();
1100                    if ("via".equals(m.getRole()) && via == null) {
1101                        via = n;
1102                    }
1103                }
1104            }
1105        }
1106
1107        if (fromWay == null || toWay == null || via == null)
1108            return;
1109
1110        Node viaNode;
1111        if (via instanceof Node) {
1112            viaNode = (Node) via;
1113            if (!fromWay.isFirstLastNode(viaNode))
1114                return;
1115        } else {
1116            Way viaWay = (Way) via;
1117            Node firstNode = viaWay.firstNode();
1118            Node lastNode = viaWay.lastNode();
1119            Boolean onewayvia = Boolean.FALSE;
1120
1121            String onewayviastr = viaWay.get("oneway");
1122            if (onewayviastr != null) {
1123                if ("-1".equals(onewayviastr)) {
1124                    onewayvia = Boolean.TRUE;
1125                    Node tmp = firstNode;
1126                    firstNode = lastNode;
1127                    lastNode = tmp;
1128                } else {
1129                    onewayvia = OsmUtils.getOsmBoolean(onewayviastr);
1130                    if (onewayvia == null) {
1131                        onewayvia = Boolean.FALSE;
1132                    }
1133                }
1134            }
1135
1136            if (fromWay.isFirstLastNode(firstNode)) {
1137                viaNode = firstNode;
1138            } else if (!onewayvia && fromWay.isFirstLastNode(lastNode)) {
1139                viaNode = lastNode;
1140            } else
1141                return;
1142        }
1143
1144        /* find the "direct" nodes before the via node */
1145        Node fromNode;
1146        if (fromWay.firstNode() == via) {
1147            fromNode = fromWay.getNode(1);
1148        } else {
1149            fromNode = fromWay.getNode(fromWay.getNodesCount()-2);
1150        }
1151
1152        Point pFrom = nc.getPoint(fromNode);
1153        Point pVia = nc.getPoint(viaNode);
1154
1155        /* starting from via, go back the "from" way a few pixels
1156           (calculate the vector vx/vy with the specified length and the direction
1157           away from the "via" node along the first segment of the "from" way)
1158         */
1159        double distanceFromVia = 14;
1160        double dx = pFrom.x >= pVia.x ? pFrom.x - pVia.x : pVia.x - pFrom.x;
1161        double dy = pFrom.y >= pVia.y ? pFrom.y - pVia.y : pVia.y - pFrom.y;
1162
1163        double fromAngle;
1164        if (dx == 0) {
1165            fromAngle = Math.PI/2;
1166        } else {
1167            fromAngle = Math.atan(dy / dx);
1168        }
1169        double fromAngleDeg = Math.toDegrees(fromAngle);
1170
1171        double vx = distanceFromVia * Math.cos(fromAngle);
1172        double vy = distanceFromVia * Math.sin(fromAngle);
1173
1174        if (pFrom.x < pVia.x) {
1175            vx = -vx;
1176        }
1177        if (pFrom.y < pVia.y) {
1178            vy = -vy;
1179        }
1180
1181        /* go a few pixels away from the way (in a right angle)
1182           (calculate the vx2/vy2 vector with the specified length and the direction
1183           90degrees away from the first segment of the "from" way)
1184         */
1185        double distanceFromWay = 10;
1186        double vx2 = 0;
1187        double vy2 = 0;
1188        double iconAngle = 0;
1189
1190        if (pFrom.x >= pVia.x && pFrom.y >= pVia.y) {
1191            if (!leftHandTraffic) {
1192                vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
1193                vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
1194            } else {
1195                vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
1196                vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
1197            }
1198            iconAngle = 270+fromAngleDeg;
1199        }
1200        if (pFrom.x < pVia.x && pFrom.y >= pVia.y) {
1201            if (!leftHandTraffic) {
1202                vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
1203                vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
1204            } else {
1205                vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
1206                vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
1207            }
1208            iconAngle = 90-fromAngleDeg;
1209        }
1210        if (pFrom.x < pVia.x && pFrom.y < pVia.y) {
1211            if (!leftHandTraffic) {
1212                vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
1213                vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
1214            } else {
1215                vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
1216                vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
1217            }
1218            iconAngle = 90+fromAngleDeg;
1219        }
1220        if (pFrom.x >= pVia.x && pFrom.y < pVia.y) {
1221            if (!leftHandTraffic) {
1222                vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
1223                vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
1224            } else {
1225                vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
1226                vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
1227            }
1228            iconAngle = 270-fromAngleDeg;
1229        }
1230
1231        drawRestriction(icon.getImage(disabled),
1232                pVia, vx, vx2, vy, vy2, iconAngle, r.isSelected());
1233    }
1234
1235    /**
1236     * Draws a text along a given way.
1237     * @param way The way to draw the text on.
1238     * @param text The text definition (font/.../text content) to draw.
1239     */
1240    public void drawTextOnPath(Way way, TextLabel text) {
1241        if (way == null || text == null)
1242            return;
1243        String name = text.getString(way);
1244        if (name == null || name.isEmpty())
1245            return;
1246
1247        FontMetrics fontMetrics = g.getFontMetrics(text.font);
1248        Rectangle2D rec = fontMetrics.getStringBounds(name, g);
1249
1250        Rectangle bounds = g.getClipBounds();
1251
1252        Polygon poly = new Polygon();
1253        Point lastPoint = null;
1254        Iterator<Node> it = way.getNodes().iterator();
1255        double pathLength = 0;
1256        long dx, dy;
1257
1258        // 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)
1259        List<Double> longHalfSegmentStart = new ArrayList<>(); // start point of half segment (as length along the way)
1260        List<Double> longHalfSegmentEnd = new ArrayList<>(); // end point of half segment (as length along the way)
1261        List<Double> longHalfsegmentQuality = new ArrayList<>(); // quality factor (off screen / partly on screen / fully on screen)
1262
1263        while (it.hasNext()) {
1264            Node n = it.next();
1265            Point p = nc.getPoint(n);
1266            poly.addPoint(p.x, p.y);
1267
1268            if (lastPoint != null) {
1269                dx = (long) p.x - lastPoint.x;
1270                dy = (long) p.y - lastPoint.y;
1271                double segmentLength = Math.sqrt(dx*dx + dy*dy);
1272                if (segmentLength > 2*(rec.getWidth()+4)) {
1273                    Point center = new Point((lastPoint.x + p.x)/2, (lastPoint.y + p.y)/2);
1274                    double q = 0;
1275                    if (bounds != null) {
1276                        if (bounds.contains(lastPoint) && bounds.contains(center)) {
1277                            q = 2;
1278                        } else if (bounds.contains(lastPoint) || bounds.contains(center)) {
1279                            q = 1;
1280                        }
1281                    }
1282                    longHalfSegmentStart.add(pathLength);
1283                    longHalfSegmentEnd.add(pathLength + segmentLength / 2);
1284                    longHalfsegmentQuality.add(q);
1285
1286                    q = 0;
1287                    if (bounds != null) {
1288                        if (bounds.contains(center) && bounds.contains(p)) {
1289                            q = 2;
1290                        } else if (bounds.contains(center) || bounds.contains(p)) {
1291                            q = 1;
1292                        }
1293                    }
1294                    longHalfSegmentStart.add(pathLength + segmentLength / 2);
1295                    longHalfSegmentEnd.add(pathLength + segmentLength);
1296                    longHalfsegmentQuality.add(q);
1297                }
1298                pathLength += segmentLength;
1299            }
1300            lastPoint = p;
1301        }
1302
1303        if (rec.getWidth() > pathLength)
1304            return;
1305
1306        double t1, t2;
1307
1308        if (!longHalfSegmentStart.isEmpty()) {
1309            if (way.getNodesCount() == 2) {
1310                // For 2 node ways, the two half segments are exactly the same size and distance from the center.
1311                // Prefer the first one for consistency.
1312                longHalfsegmentQuality.set(0, longHalfsegmentQuality.get(0) + 0.5);
1313            }
1314
1315            // find the long half segment that is closest to the center of the way
1316            // candidates with higher quality value are preferred
1317            double bestStart = Double.NaN;
1318            double bestEnd = Double.NaN;
1319            double bestDistanceToCenter = Double.MAX_VALUE;
1320            double bestQuality = -1;
1321            for (int i = 0; i < longHalfSegmentStart.size(); i++) {
1322                double start = longHalfSegmentStart.get(i);
1323                double end = longHalfSegmentEnd.get(i);
1324                double dist = Math.abs(0.5 * (end + start) - 0.5 * pathLength);
1325                if (longHalfsegmentQuality.get(i) > bestQuality
1326                        || (dist < bestDistanceToCenter && Utils.equalsEpsilon(longHalfsegmentQuality.get(i), bestQuality))) {
1327                    bestStart = start;
1328                    bestEnd = end;
1329                    bestDistanceToCenter = dist;
1330                    bestQuality = longHalfsegmentQuality.get(i);
1331                }
1332            }
1333            double remaining = bestEnd - bestStart - rec.getWidth(); // total space left and right from the text
1334            // The space left and right of the text should be distributed 20% - 80% (towards the center),
1335            // but the smaller space should not be less than 7 px.
1336            // However, if the total remaining space is less than 14 px, then distribute it evenly.
1337            double smallerSpace = Math.min(Math.max(0.2 * remaining, 7), 0.5 * remaining);
1338            if ((bestEnd + bestStart)/2 < pathLength/2) {
1339                t2 = bestEnd - smallerSpace;
1340                t1 = t2 - rec.getWidth();
1341            } else {
1342                t1 = bestStart + smallerSpace;
1343                t2 = t1 + rec.getWidth();
1344            }
1345        } else {
1346            // doesn't fit into one half-segment -> just put it in the center of the way
1347            t1 = pathLength/2 - rec.getWidth()/2;
1348            t2 = pathLength/2 + rec.getWidth()/2;
1349        }
1350        t1 /= pathLength;
1351        t2 /= pathLength;
1352
1353        double[] p1 = pointAt(t1, poly, pathLength);
1354        double[] p2 = pointAt(t2, poly, pathLength);
1355
1356        if (p1 == null || p2 == null)
1357            return;
1358
1359        double angleOffset;
1360        double offsetSign;
1361        double tStart;
1362
1363        if (p1[0] < p2[0] &&
1364                p1[2] < Math.PI/2 &&
1365                p1[2] > -Math.PI/2) {
1366            angleOffset = 0;
1367            offsetSign = 1;
1368            tStart = t1;
1369        } else {
1370            angleOffset = Math.PI;
1371            offsetSign = -1;
1372            tStart = t2;
1373        }
1374
1375        List<GlyphVector> gvs = Utils.getGlyphVectorsBidi(name, text.font, g.getFontRenderContext());
1376        double gvOffset = 0;
1377        for (GlyphVector gv : gvs) {
1378            double gvWidth = gv.getLogicalBounds().getBounds2D().getWidth();
1379            for (int i = 0; i < gv.getNumGlyphs(); ++i) {
1380                Rectangle2D rect = gv.getGlyphLogicalBounds(i).getBounds2D();
1381                double t = tStart + offsetSign * (gvOffset + rect.getX() + rect.getWidth()/2) / pathLength;
1382                double[] p = pointAt(t, poly, pathLength);
1383                if (p != null) {
1384                    AffineTransform trfm = AffineTransform.getTranslateInstance(p[0] - rect.getX(), p[1]);
1385                    trfm.rotate(p[2]+angleOffset);
1386                    double off = -rect.getY() - rect.getHeight()/2 + text.yOffset;
1387                    trfm.translate(-rect.getWidth()/2, off);
1388                    if (isGlyphVectorDoubleTranslationBug(text.font)) {
1389                        // scale the translation components by one half
1390                        AffineTransform tmp = AffineTransform.getTranslateInstance(-0.5 * trfm.getTranslateX(), -0.5 * trfm.getTranslateY());
1391                        tmp.concatenate(trfm);
1392                        trfm = tmp;
1393                    }
1394                    gv.setGlyphTransform(i, trfm);
1395                }
1396            }
1397            displayText(gv, null, 0, 0, way.isDisabled(), text);
1398            gvOffset += gvWidth;
1399        }
1400    }
1401
1402    /**
1403     * draw way. This method allows for two draw styles (line using color, dashes using dashedColor) to be passed.
1404     * @param way The way to draw
1405     * @param color The base color to draw the way in
1406     * @param line The line style to use. This is drawn using color.
1407     * @param dashes The dash style to use. This is drawn using dashedColor. <code>null</code> if unused.
1408     * @param dashedColor The color of the dashes.
1409     * @param offset The offset
1410     * @param showOrientation show arrows that indicate the technical orientation of
1411     *              the way (defined by order of nodes)
1412     * @param showHeadArrowOnly True if only the arrow at the end of the line but not those on the segments should be displayed.
1413     * @param showOneway show symbols that indicate the direction of the feature,
1414     *              e.g. oneway street or waterway
1415     * @param onewayReversed for oneway=-1 and similar
1416     */
1417    public void drawWay(Way way, Color color, BasicStroke line, BasicStroke dashes, Color dashedColor, float offset,
1418            boolean showOrientation, boolean showHeadArrowOnly,
1419            boolean showOneway, boolean onewayReversed) {
1420
1421        GeneralPath path = new GeneralPath();
1422        GeneralPath orientationArrows = showOrientation ? new GeneralPath() : null;
1423        GeneralPath onewayArrows = showOneway ? new GeneralPath() : null;
1424        GeneralPath onewayArrowsCasing = showOneway ? new GeneralPath() : null;
1425        Rectangle bounds = g.getClipBounds();
1426        if (bounds != null) {
1427            // avoid arrow heads at the border
1428            bounds.grow(100, 100);
1429        }
1430
1431        double wayLength = 0;
1432        Point lastPoint = null;
1433        boolean initialMoveToNeeded = true;
1434        List<Node> wayNodes = way.getNodes();
1435        if (wayNodes.size() < 2) return;
1436
1437        // only highlight the segment if the way itself is not highlighted
1438        if (!way.isHighlighted() && highlightWaySegments != null) {
1439            GeneralPath highlightSegs = null;
1440            for (WaySegment ws : highlightWaySegments) {
1441                if (ws.way != way || ws.lowerIndex < offset) {
1442                    continue;
1443                }
1444                if (highlightSegs == null) {
1445                    highlightSegs = new GeneralPath();
1446                }
1447
1448                Point p1 = nc.getPoint(ws.getFirstNode());
1449                Point p2 = nc.getPoint(ws.getSecondNode());
1450                highlightSegs.moveTo(p1.x, p1.y);
1451                highlightSegs.lineTo(p2.x, p2.y);
1452            }
1453
1454            drawPathHighlight(highlightSegs, line);
1455        }
1456
1457        Iterator<Point> it = new OffsetIterator(wayNodes, offset);
1458        while (it.hasNext()) {
1459            Point p = it.next();
1460            if (lastPoint != null) {
1461                Point p1 = lastPoint;
1462                Point p2 = p;
1463
1464                /**
1465                 * Do custom clipping to work around openjdk bug. It leads to
1466                 * drawing artefacts when zooming in a lot. (#4289, #4424)
1467                 * (Looks like int overflow.)
1468                 */
1469                LineClip clip = new LineClip(p1, p2, bounds);
1470                if (clip.execute()) {
1471                    if (!p1.equals(clip.getP1())) {
1472                        p1 = clip.getP1();
1473                        path.moveTo(p1.x, p1.y);
1474                    } else if (initialMoveToNeeded) {
1475                        initialMoveToNeeded = false;
1476                        path.moveTo(p1.x, p1.y);
1477                    }
1478                    p2 = clip.getP2();
1479                    path.lineTo(p2.x, p2.y);
1480
1481                    /* draw arrow */
1482                    if (showHeadArrowOnly ? !it.hasNext() : showOrientation) {
1483                        final double segmentLength = p1.distance(p2);
1484                        if (segmentLength != 0) {
1485                            final double l = (10. + line.getLineWidth()) / segmentLength;
1486
1487                            final double sx = l * (p1.x - p2.x);
1488                            final double sy = l * (p1.y - p2.y);
1489
1490                            orientationArrows.moveTo(p2.x + cosPHI * sx - sinPHI * sy, p2.y + sinPHI * sx + cosPHI * sy);
1491                            orientationArrows.lineTo(p2.x, p2.y);
1492                            orientationArrows.lineTo(p2.x + cosPHI * sx + sinPHI * sy, p2.y - sinPHI * sx + cosPHI * sy);
1493                        }
1494                    }
1495                    if (showOneway) {
1496                        final double segmentLength = p1.distance(p2);
1497                        if (segmentLength != 0) {
1498                            final double nx = (p2.x - p1.x) / segmentLength;
1499                            final double ny = (p2.y - p1.y) / segmentLength;
1500
1501                            final double interval = 60;
1502                            // distance from p1
1503                            double dist = interval - (wayLength % interval);
1504
1505                            while (dist < segmentLength) {
1506                                for (int i = 0; i < 2; ++i) {
1507                                    double onewaySize = i == 0 ? 3d : 2d;
1508                                    GeneralPath onewayPath = i == 0 ? onewayArrowsCasing : onewayArrows;
1509
1510                                    // scale such that border is 1 px
1511                                    final double fac = -(onewayReversed ? -1 : 1) * onewaySize * (1 + sinPHI) / (sinPHI * cosPHI);
1512                                    final double sx = nx * fac;
1513                                    final double sy = ny * fac;
1514
1515                                    // Attach the triangle at the incenter and not at the tip.
1516                                    // Makes the border even at all sides.
1517                                    final double x = p1.x + nx * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI));
1518                                    final double y = p1.y + ny * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI));
1519
1520                                    onewayPath.moveTo(x, y);
1521                                    onewayPath.lineTo(x + cosPHI * sx - sinPHI * sy, y + sinPHI * sx + cosPHI * sy);
1522                                    onewayPath.lineTo(x + cosPHI * sx + sinPHI * sy, y - sinPHI * sx + cosPHI * sy);
1523                                    onewayPath.lineTo(x, y);
1524                                }
1525                                dist += interval;
1526                            }
1527                        }
1528                        wayLength += segmentLength;
1529                    }
1530                }
1531            }
1532            lastPoint = p;
1533        }
1534        if (way.isHighlighted()) {
1535            drawPathHighlight(path, line);
1536        }
1537        displaySegments(path, orientationArrows, onewayArrows, onewayArrowsCasing, color, line, dashes, dashedColor);
1538    }
1539
1540    /**
1541     * Gets the "circum". This is the distance on the map in meters that 100 screen pixels represent.
1542     * @return The "circum"
1543     */
1544    public double getCircum() {
1545        return circum;
1546    }
1547
1548    @Override
1549    public void getColors() {
1550        super.getColors();
1551        this.highlightColorTransparent = new Color(highlightColor.getRed(), highlightColor.getGreen(), highlightColor.getBlue(), 100);
1552        this.backgroundColor = PaintColors.getBackgroundColor();
1553    }
1554
1555    @Override
1556    public void getSettings(boolean virtual) {
1557        super.getSettings(virtual);
1558        paintSettings = MapPaintSettings.INSTANCE;
1559
1560        circum = nc.getDist100Pixel();
1561        scale = nc.getScale();
1562
1563        leftHandTraffic = Main.pref.getBoolean("mappaint.lefthandtraffic", false);
1564
1565        useStrokes = paintSettings.getUseStrokesDistance() > circum;
1566        showNames = paintSettings.getShowNamesDistance() > circum;
1567        showIcons = paintSettings.getShowIconsDistance() > circum;
1568        isOutlineOnly = paintSettings.isOutlineOnly();
1569        orderFont = new Font(Main.pref.get("mappaint.font", "Droid Sans"), Font.PLAIN, Main.pref.getInteger("mappaint.fontsize", 8));
1570
1571        antialiasing = Main.pref.getBoolean("mappaint.use-antialiasing", true) ?
1572                        RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF;
1573        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing);
1574
1575        Object textAntialiasing;
1576        switch (Main.pref.get("mappaint.text-antialiasing", "default")) {
1577            case "on":
1578                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_ON;
1579                break;
1580            case "off":
1581                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_OFF;
1582                break;
1583            case "gasp":
1584                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_GASP;
1585                break;
1586            case "lcd-hrgb":
1587                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB;
1588                break;
1589            case "lcd-hbgr":
1590                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HBGR;
1591                break;
1592            case "lcd-vrgb":
1593                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VRGB;
1594                break;
1595            case "lcd-vbgr":
1596                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VBGR;
1597                break;
1598            default:
1599                textAntialiasing = RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT;
1600        }
1601        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, textAntialiasing);
1602
1603        highlightLineWidth = Main.pref.getInteger("mappaint.highlight.width", 4);
1604        highlightPointRadius = Main.pref.getInteger("mappaint.highlight.radius", 7);
1605        widerHighlight = Main.pref.getInteger("mappaint.highlight.bigger-increment", 5);
1606        highlightStep = Main.pref.getInteger("mappaint.highlight.step", 4);
1607    }
1608
1609    private static Path2D.Double getPath(Way w) {
1610        Path2D.Double path = new Path2D.Double();
1611        boolean initial = true;
1612        for (Node n : w.getNodes()) {
1613            EastNorth p = n.getEastNorth();
1614            if (p != null) {
1615                if (initial) {
1616                    path.moveTo(p.getX(), p.getY());
1617                    initial = false;
1618                } else {
1619                    path.lineTo(p.getX(), p.getY());
1620                }
1621            }
1622        }
1623        if (w.isClosed()) {
1624            path.closePath();
1625        }
1626        return path;
1627    }
1628
1629    private static Path2D.Double getPFClip(Way w, double extent) {
1630        Path2D.Double clip = new Path2D.Double();
1631        buildPFClip(clip, w.getNodes(), extent);
1632        return clip;
1633    }
1634
1635    private static Path2D.Double getPFClip(PolyData pd, double extent) {
1636        Path2D.Double clip = new Path2D.Double();
1637        clip.setWindingRule(Path2D.WIND_EVEN_ODD);
1638        buildPFClip(clip, pd.getNodes(), extent);
1639        for (PolyData pdInner : pd.getInners()) {
1640            buildPFClip(clip, pdInner.getNodes(), extent);
1641        }
1642        return clip;
1643    }
1644
1645    /**
1646     * Fix the clipping area of unclosed polygons for partial fill.
1647     *
1648     * The current algorithm for partial fill simply strokes the polygon with a
1649     * large stroke width after masking the outside with a clipping area.
1650     * This works, but for unclosed polygons, the mask can crop the corners at
1651     * both ends (see #12104).
1652     *
1653     * This method fixes the clipping area by sort of adding the corners to the
1654     * clip outline.
1655     *
1656     * @param clip the clipping area to modify (initially empty)
1657     * @param nodes nodes of the polygon
1658     * @param extent the extent
1659     */
1660    private static void buildPFClip(Path2D.Double clip, List<Node> nodes, double extent) {
1661        boolean initial = true;
1662        for (Node n : nodes) {
1663            EastNorth p = n.getEastNorth();
1664            if (p != null) {
1665                if (initial) {
1666                    clip.moveTo(p.getX(), p.getY());
1667                    initial = false;
1668                } else {
1669                    clip.lineTo(p.getX(), p.getY());
1670                }
1671            }
1672        }
1673        if (nodes.size() >= 3) {
1674            EastNorth fst = nodes.get(0).getEastNorth();
1675            EastNorth snd = nodes.get(1).getEastNorth();
1676            EastNorth lst = nodes.get(nodes.size() - 1).getEastNorth();
1677            EastNorth lbo = nodes.get(nodes.size() - 2).getEastNorth();
1678
1679            EastNorth cLst = getPFDisplacedEndPoint(lbo, lst, fst, extent);
1680            EastNorth cFst = getPFDisplacedEndPoint(snd, fst, cLst != null ? cLst : lst, extent);
1681            if (cLst == null && cFst != null) {
1682                cLst = getPFDisplacedEndPoint(lbo, lst, cFst, extent);
1683            }
1684            if (cLst != null) {
1685                clip.lineTo(cLst.getX(), cLst.getY());
1686            }
1687            if (cFst != null) {
1688                clip.lineTo(cFst.getX(), cFst.getY());
1689            }
1690        }
1691    }
1692
1693    /**
1694     * Get the point to add to the clipping area for partial fill of unclosed polygons.
1695     *
1696     * <code>(p1,p2)</code> is the first or last way segment and <code>p3</code> the
1697     * opposite endpoint.
1698     *
1699     * @param p1 1st point
1700     * @param p2 2nd point
1701     * @param p3 3rd point
1702     * @param extent the extent
1703     * @return a point q, such that p1,p2,q form a right angle
1704     * and the distance of q to p2 is <code>extent</code>. The point q lies on
1705     * the same side of the line p1,p2 as the point p3.
1706     * Returns null if p1,p2,p3 forms an angle greater 90 degrees. (In this case
1707     * the corner of the partial fill would not be cut off by the mask, so an
1708     * additional point is not necessary.)
1709     */
1710    private static EastNorth getPFDisplacedEndPoint(EastNorth p1, EastNorth p2, EastNorth p3, double extent) {
1711        double dx1 = p2.getX() - p1.getX();
1712        double dy1 = p2.getY() - p1.getY();
1713        double dx2 = p3.getX() - p2.getX();
1714        double dy2 = p3.getY() - p2.getY();
1715        if (dx1 * dx2 + dy1 * dy2 < 0) {
1716            double len = Math.sqrt(dx1 * dx1 + dy1 * dy1);
1717            if (len == 0) return null;
1718            double dxm = -dy1 * extent / len;
1719            double dym = dx1 * extent / len;
1720            if (dx1 * dy2 - dx2 * dy1 < 0) {
1721                dxm = -dxm;
1722                dym = -dym;
1723            }
1724            return new EastNorth(p2.getX() + dxm, p2.getY() + dym);
1725        }
1726        return null;
1727    }
1728
1729    private boolean isAreaVisible(Path2D.Double area) {
1730        Rectangle2D bounds = area.getBounds2D();
1731        if (bounds.isEmpty()) return false;
1732        Point2D p = nc.getPoint2D(new EastNorth(bounds.getX(), bounds.getY()));
1733        if (p.getX() > nc.getWidth()) return false;
1734        if (p.getY() < 0) return false;
1735        p = nc.getPoint2D(new EastNorth(bounds.getX() + bounds.getWidth(), bounds.getY() + bounds.getHeight()));
1736        if (p.getX() < 0) return false;
1737        if (p.getY() > nc.getHeight()) return false;
1738        return true;
1739    }
1740
1741    public boolean isInactiveMode() {
1742        return isInactiveMode;
1743    }
1744
1745    public boolean isShowIcons() {
1746        return showIcons;
1747    }
1748
1749    public boolean isShowNames() {
1750        return showNames;
1751    }
1752
1753    private static double[] pointAt(double t, Polygon poly, double pathLength) {
1754        double totalLen = t * pathLength;
1755        double curLen = 0;
1756        long dx, dy;
1757        double segLen;
1758
1759        // Yes, it is inefficient to iterate from the beginning for each glyph.
1760        // Can be optimized if it turns out to be slow.
1761        for (int i = 1; i < poly.npoints; ++i) {
1762            dx = (long) poly.xpoints[i] - poly.xpoints[i-1];
1763            dy = (long) poly.ypoints[i] - poly.ypoints[i-1];
1764            segLen = Math.sqrt(dx*dx + dy*dy);
1765            if (totalLen > curLen + segLen) {
1766                curLen += segLen;
1767                continue;
1768            }
1769            return new double[] {
1770                    poly.xpoints[i-1]+(totalLen - curLen)/segLen*dx,
1771                    poly.ypoints[i-1]+(totalLen - curLen)/segLen*dy,
1772                    Math.atan2(dy, dx)};
1773        }
1774        return null;
1775    }
1776
1777    /**
1778     * Computes the flags for a given OSM primitive.
1779     * @param primitive The primititve to compute the flags for.
1780     * @param checkOuterMember <code>true</code> if we should also add {@link #FLAG_OUTERMEMBER_OF_SELECTED}
1781     * @return The flag.
1782     */
1783    public static int computeFlags(OsmPrimitive primitive, boolean checkOuterMember) {
1784        if (primitive.isDisabled()) {
1785            return FLAG_DISABLED;
1786        } else if (primitive.isSelected()) {
1787            return FLAG_SELECTED;
1788        } else if (checkOuterMember && primitive.isOuterMemberOfSelected()) {
1789            return FLAG_OUTERMEMBER_OF_SELECTED;
1790        } else if (primitive.isMemberOfSelected()) {
1791            return FLAG_MEMBER_OF_SELECTED;
1792        } else {
1793            return FLAG_NORMAL;
1794        }
1795    }
1796
1797    private class ComputeStyleListWorker extends RecursiveTask<List<StyleRecord>> implements Visitor {
1798        private final transient List<? extends OsmPrimitive> input;
1799        private final transient List<StyleRecord> output;
1800
1801        private final transient ElemStyles styles = MapPaintStyles.getStyles();
1802        private final int directExecutionTaskSize;
1803
1804        private final boolean drawArea = circum <= Main.pref.getInteger("mappaint.fillareas", 10000000);
1805        private final boolean drawMultipolygon = drawArea && Main.pref.getBoolean("mappaint.multipolygon", true);
1806        private final boolean drawRestriction = Main.pref.getBoolean("mappaint.restriction", true);
1807
1808        /**
1809         * Constructs a new {@code ComputeStyleListWorker}.
1810         * @param input the primitives to process
1811         * @param output the list of styles to which styles will be added
1812         * @param directExecutionTaskSize the threshold deciding whether to subdivide the tasks
1813         */
1814        ComputeStyleListWorker(final List<? extends OsmPrimitive> input, List<StyleRecord> output, int directExecutionTaskSize) {
1815            this.input = input;
1816            this.output = output;
1817            this.directExecutionTaskSize = directExecutionTaskSize;
1818            this.styles.setDrawMultipolygon(drawMultipolygon);
1819        }
1820
1821        @Override
1822        protected List<StyleRecord> compute() {
1823            if (input.size() <= directExecutionTaskSize) {
1824                return computeDirectly();
1825            } else {
1826                final Collection<ForkJoinTask<List<StyleRecord>>> tasks = new ArrayList<>();
1827                for (int fromIndex = 0; fromIndex < input.size(); fromIndex += directExecutionTaskSize) {
1828                    final int toIndex = Math.min(fromIndex + directExecutionTaskSize, input.size());
1829                    final List<StyleRecord> output = new ArrayList<>(directExecutionTaskSize);
1830                    tasks.add(new ComputeStyleListWorker(input.subList(fromIndex, toIndex), output, directExecutionTaskSize).fork());
1831                }
1832                for (ForkJoinTask<List<StyleRecord>> task : tasks) {
1833                    output.addAll(task.join());
1834                }
1835                return output;
1836            }
1837        }
1838
1839        public List<StyleRecord> computeDirectly() {
1840            MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
1841            try {
1842                for (final OsmPrimitive osm : input) {
1843                    if (osm.isDrawable()) {
1844                        osm.accept(this);
1845                    }
1846                }
1847                return output;
1848            } finally {
1849                MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
1850            }
1851        }
1852
1853        @Override
1854        public void visit(Node n) {
1855            add(n, computeFlags(n, false));
1856        }
1857
1858        @Override
1859        public void visit(Way w) {
1860            add(w, computeFlags(w, true));
1861        }
1862
1863        @Override
1864        public void visit(Relation r) {
1865            add(r, computeFlags(r, true));
1866        }
1867
1868        @Override
1869        public void visit(Changeset cs) {
1870            throw new UnsupportedOperationException();
1871        }
1872
1873        public void add(Node osm, int flags) {
1874            StyleElementList sl = styles.get(osm, circum, nc);
1875            for (StyleElement s : sl) {
1876                output.add(new StyleRecord(s, osm, flags));
1877            }
1878        }
1879
1880        public void add(Relation osm, int flags) {
1881            StyleElementList sl = styles.get(osm, circum, nc);
1882            for (StyleElement s : sl) {
1883                if (drawMultipolygon && drawArea && s instanceof AreaElement && (flags & FLAG_DISABLED) == 0) {
1884                    output.add(new StyleRecord(s, osm, flags));
1885                } else if (drawRestriction && s instanceof NodeElement) {
1886                    output.add(new StyleRecord(s, osm, flags));
1887                }
1888            }
1889        }
1890
1891        public void add(Way osm, int flags) {
1892            StyleElementList sl = styles.get(osm, circum, nc);
1893            for (StyleElement s : sl) {
1894                if (!(drawArea && (flags & FLAG_DISABLED) == 0) && s instanceof AreaElement) {
1895                    continue;
1896                }
1897                output.add(new StyleRecord(s, osm, flags));
1898            }
1899        }
1900    }
1901
1902    @Override
1903    public void render(final DataSet data, boolean renderVirtualNodes, Bounds bounds) {
1904        BBox bbox = bounds.toBBox();
1905        getSettings(renderVirtualNodes);
1906        boolean benchmarkOutput = Main.isTraceEnabled() || Main.pref.getBoolean("mappaint.render.benchmark", false);
1907        boolean benchmark = benchmarkOutput || benchmarkData != null;
1908
1909        data.getReadLock().lock();
1910        try {
1911            highlightWaySegments = data.getHighlightedWaySegments();
1912
1913            long timeStart = 0, timeGenerateDone = 0, timeSortingDone = 0, timeFinished;
1914            if (benchmark) {
1915                timeStart = System.currentTimeMillis();
1916                if (benchmarkOutput) {
1917                    System.err.print("BENCHMARK: rendering ");
1918                }
1919            }
1920
1921            List<Node> nodes = data.searchNodes(bbox);
1922            List<Way> ways = data.searchWays(bbox);
1923            List<Relation> relations = data.searchRelations(bbox);
1924
1925            final List<StyleRecord> allStyleElems = new ArrayList<>(nodes.size()+ways.size()+relations.size());
1926
1927            // Need to process all relations first.
1928            // Reason: Make sure, ElemStyles.getStyleCacheWithRange is
1929            // not called for the same primitive in parallel threads.
1930            // (Could be synchronized, but try to avoid this for
1931            // performance reasons.)
1932            THREAD_POOL.invoke(new ComputeStyleListWorker(relations, allStyleElems,
1933                    Math.max(20, relations.size() / THREAD_POOL.getParallelism() / 3)));
1934            THREAD_POOL.invoke(new ComputeStyleListWorker(new CompositeList<>(nodes, ways), allStyleElems,
1935                    Math.max(100, (nodes.size() + ways.size()) / THREAD_POOL.getParallelism() / 3)));
1936
1937            if (benchmark) {
1938                timeGenerateDone = System.currentTimeMillis();
1939                if (benchmarkOutput) {
1940                    System.err.print("phase 1 (calculate styles): " + Utils.getDurationString(timeGenerateDone - timeStart));
1941                }
1942                if (benchmarkData != null) {
1943                    benchmarkData.generateTime = timeGenerateDone - timeStart;
1944                }
1945            }
1946
1947            Collections.sort(allStyleElems); // TODO: try parallel sort when switching to Java 8
1948
1949            if (benchmarkData != null) {
1950                timeSortingDone = System.currentTimeMillis();
1951                benchmarkData.sortTime = timeSortingDone - timeGenerateDone;
1952                if (benchmarkData.skipDraw) {
1953                    benchmarkData.recordElementStats(allStyleElems);
1954                    return;
1955                }
1956            }
1957
1958            for (StyleRecord r : allStyleElems) {
1959                r.style.paintPrimitive(
1960                        r.osm,
1961                        paintSettings,
1962                        this,
1963                        (r.flags & FLAG_SELECTED) != 0,
1964                        (r.flags & FLAG_OUTERMEMBER_OF_SELECTED) != 0,
1965                        (r.flags & FLAG_MEMBER_OF_SELECTED) != 0
1966                );
1967            }
1968
1969            if (benchmark) {
1970                timeFinished = System.currentTimeMillis();
1971                if (benchmarkData != null) {
1972                    benchmarkData.drawTime = timeFinished - timeGenerateDone;
1973                    benchmarkData.recordElementStats(allStyleElems);
1974                }
1975                if (benchmarkOutput) {
1976                    System.err.println("; phase 2 (draw): " + Utils.getDurationString(timeFinished - timeGenerateDone) +
1977                        "; total: " + Utils.getDurationString(timeFinished - timeStart) +
1978                        " (scale: " + circum + " zoom level: " + Selector.GeneralSelector.scale2level(circum) + ')');
1979                }
1980            }
1981
1982            drawVirtualNodes(data, bbox);
1983        } finally {
1984            data.getReadLock().unlock();
1985        }
1986    }
1987}