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