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.geom.AffineTransform;
023import java.awt.geom.GeneralPath;
024import java.awt.geom.Path2D;
025import java.awt.geom.Point2D;
026import java.awt.geom.Rectangle2D;
027import java.util.ArrayList;
028import java.util.Collection;
029import java.util.Collections;
030import java.util.Iterator;
031import java.util.List;
032import java.util.concurrent.Callable;
033import java.util.concurrent.ExecutionException;
034import java.util.concurrent.ExecutorService;
035import java.util.concurrent.Executors;
036import java.util.concurrent.Future;
037
038import javax.swing.AbstractButton;
039import javax.swing.FocusManager;
040
041import org.openstreetmap.josm.Main;
042import org.openstreetmap.josm.data.Bounds;
043import org.openstreetmap.josm.data.coor.EastNorth;
044import org.openstreetmap.josm.data.osm.BBox;
045import org.openstreetmap.josm.data.osm.Changeset;
046import org.openstreetmap.josm.data.osm.DataSet;
047import org.openstreetmap.josm.data.osm.Node;
048import org.openstreetmap.josm.data.osm.OsmPrimitive;
049import org.openstreetmap.josm.data.osm.OsmUtils;
050import org.openstreetmap.josm.data.osm.Relation;
051import org.openstreetmap.josm.data.osm.RelationMember;
052import org.openstreetmap.josm.data.osm.Way;
053import org.openstreetmap.josm.data.osm.WaySegment;
054import org.openstreetmap.josm.data.osm.visitor.Visitor;
055import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
056import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData;
057import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
058import org.openstreetmap.josm.gui.NavigatableComponent;
059import org.openstreetmap.josm.gui.mappaint.AreaElemStyle;
060import org.openstreetmap.josm.gui.mappaint.BoxTextElemStyle;
061import org.openstreetmap.josm.gui.mappaint.BoxTextElemStyle.HorizontalTextAlignment;
062import org.openstreetmap.josm.gui.mappaint.BoxTextElemStyle.VerticalTextAlignment;
063import org.openstreetmap.josm.gui.mappaint.ElemStyle;
064import org.openstreetmap.josm.gui.mappaint.ElemStyles;
065import org.openstreetmap.josm.gui.mappaint.MapImage;
066import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
067import org.openstreetmap.josm.gui.mappaint.NodeElemStyle;
068import org.openstreetmap.josm.gui.mappaint.NodeElemStyle.Symbol;
069import org.openstreetmap.josm.gui.mappaint.RepeatImageElemStyle.LineImageAlignment;
070import org.openstreetmap.josm.gui.mappaint.StyleCache.StyleList;
071import org.openstreetmap.josm.gui.mappaint.TextElement;
072import org.openstreetmap.josm.tools.CompositeList;
073import org.openstreetmap.josm.tools.ImageProvider;
074import org.openstreetmap.josm.tools.Utils;
075
076/**
077 * <p>A map renderer which renders a map according to style rules in a set of style sheets.</p>
078 *
079 */
080public class StyledMapRenderer extends AbstractMapRenderer {
081
082    final public static int noThreads;
083    final public static ExecutorService styleCreatorPool;
084
085    static {
086        noThreads = Main.pref.getInteger(
087                "mappaint.StyledMapRenderer.style_creation.numberOfThreads",
088                Runtime.getRuntime().availableProcessors());
089        styleCreatorPool = noThreads <= 1 ? null : Executors.newFixedThreadPool(noThreads);
090    }
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 List<Node> nodes;
103        private float offset;
104        private int idx;
105
106        private Point prev = null;
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 x_prev0, y_prev0;
112
113        public OffsetIterator(List<Node> nodes, float 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.1f) 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(x_prev0 + current.x - prev.x, y_prev0 + current.y - prev.y);
134                } else {
135                    return current;
136                }
137            }
138
139            Point next = nc.getPoint(nodes.get(idx+1));
140
141            int dx_next = next.x - current.x;
142            int dy_next = next.y - current.y;
143            double len_next = Math.sqrt(dx_next*dx_next + dy_next*dy_next);
144
145            if (len_next == 0) {
146                len_next = 1; // value does not matter, because dy_next and dx_next is 0
147            }
148
149            int x_current0 = current.x + (int) Math.round(offset * dy_next / len_next);
150            int y_current0 = current.y - (int) Math.round(offset * dx_next / len_next);
151
152            if (idx==0) {
153                ++idx;
154                prev = current;
155                x_prev0 = x_current0;
156                y_prev0 = y_current0;
157                return new Point(x_current0, y_current0);
158            } else {
159                int dx_prev = current.x - prev.x;
160                int dy_prev = current.y - prev.y;
161
162                // determine intersection of the lines parallel to the two
163                // segments
164                int det = dx_next*dy_prev - dx_prev*dy_next;
165
166                if (det == 0) {
167                    ++idx;
168                    prev = current;
169                    x_prev0 = x_current0;
170                    y_prev0 = y_current0;
171                    return new Point(x_current0, y_current0);
172                }
173
174                int m = dx_next*(y_current0 - y_prev0) - dy_next*(x_current0 - x_prev0);
175
176                int cx_ = x_prev0 + Math.round((float)m * dx_prev / det);
177                int cy_ = y_prev0 + Math.round((float)m * dy_prev / det);
178                ++idx;
179                prev = current;
180                x_prev0 = x_current0;
181                y_prev0 = y_current0;
182                return new Point(cx_, cy_);
183            }
184        }
185
186        @Override
187        public void remove() {
188            throw new UnsupportedOperationException();
189        }
190    }
191
192    private static class StyleRecord implements Comparable<StyleRecord> {
193        final ElemStyle style;
194        final OsmPrimitive osm;
195        final int flags;
196
197        public StyleRecord(ElemStyle style, OsmPrimitive osm, int flags) {
198            this.style = style;
199            this.osm = osm;
200            this.flags = flags;
201        }
202
203        @Override
204        public int compareTo(StyleRecord other) {
205            if ((this.flags & FLAG_DISABLED) != 0 && (other.flags & FLAG_DISABLED) == 0)
206                return -1;
207            if ((this.flags & FLAG_DISABLED) == 0 && (other.flags & FLAG_DISABLED) != 0)
208                return 1;
209
210            int d0 = Float.compare(this.style.major_z_index, other.style.major_z_index);
211            if (d0 != 0)
212                return d0;
213
214            // selected on top of member of selected on top of unselected
215            // FLAG_DISABLED bit is the same at this point
216            if (this.flags > other.flags)
217                return 1;
218            if (this.flags < other.flags)
219                return -1;
220
221            int dz = Float.compare(this.style.z_index, other.style.z_index);
222            if (dz != 0)
223                return dz;
224
225            // simple node on top of icons and shapes
226            if (this.style == NodeElemStyle.SIMPLE_NODE_ELEMSTYLE && other.style != NodeElemStyle.SIMPLE_NODE_ELEMSTYLE)
227                return 1;
228            if (this.style != NodeElemStyle.SIMPLE_NODE_ELEMSTYLE && other.style == NodeElemStyle.SIMPLE_NODE_ELEMSTYLE)
229                return -1;
230
231            // newer primitives to the front
232            long id = this.osm.getUniqueId() - other.osm.getUniqueId();
233            if (id > 0)
234                return 1;
235            if (id < 0)
236                return -1;
237
238            return Float.compare(this.style.object_z_index, other.style.object_z_index);
239        }
240    }
241
242    private static Boolean IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG = null;
243
244    /**
245     * Check, if this System has the GlyphVector double translation bug.
246     *
247     * With this bug, <code>gv.setGlyphTransform(i, trfm)</code> has a different
248     * effect than on most other systems, namely the translation components
249     * ("m02" &amp; "m12", {@link AffineTransform}) appear to be twice as large, as
250     * they actually are. The rotation is unaffected (scale &amp; shear not tested
251     * so far).
252     *
253     * This bug has only been observed on Mac OS X, see #7841.
254     *
255     * @return true, if the GlyphVector double translation bug is present on
256     * this System
257     */
258    public static boolean isGlyphVectorDoubleTranslationBug() {
259        if (IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG != null)
260            return IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG;
261        FontRenderContext frc = new FontRenderContext(null, false, false);
262        Font font = new Font("Dialog", Font.PLAIN, 12);
263        GlyphVector gv = font.createGlyphVector(frc, "x");
264        gv.setGlyphTransform(0, AffineTransform.getTranslateInstance(1000, 1000));
265        Shape shape = gv.getGlyphOutline(0);
266        // x is about 1000 on normal stystems and about 2000 when the bug occurs
267        int x = shape.getBounds().x;
268        IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG = x > 1500;
269        return IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG;
270    }
271
272    private double circum;
273
274    private MapPaintSettings paintSettings;
275
276    private Color relationSelectedColor;
277    private Color highlightColorTransparent;
278
279    private static final int FLAG_NORMAL = 0;
280    private static final int FLAG_DISABLED = 1;
281    private static final int FLAG_MEMBER_OF_SELECTED = 2;
282    private static final int FLAG_SELECTED = 4;
283
284    private static final double PHI = Math.toRadians(20);
285    private static final double cosPHI = Math.cos(PHI);
286    private static final double sinPHI = Math.sin(PHI);
287
288    private Collection<WaySegment> highlightWaySegments;
289
290    // highlight customization fields
291    private int highlightLineWidth;
292    private int highlightPointRadius;
293    private int widerHighlight;
294    private int highlightStep;
295
296    //flag that activate wider highlight mode
297    private boolean useWiderHighlight;
298
299    private boolean useStrokes;
300    private boolean showNames;
301    private boolean showIcons;
302    private boolean  isOutlineOnly;
303
304    private Font orderFont;
305
306    private boolean leftHandTraffic;
307
308    /**
309     * Constructs a new {@code StyledMapRenderer}.
310     *
311     * @param g the graphics context. Must not be null.
312     * @param nc the map viewport. Must not be null.
313     * @param isInactiveMode if true, the paint visitor shall render OSM objects such that they
314     * look inactive. Example: rendering of data in an inactive layer using light gray as color only.
315     * @throws IllegalArgumentException thrown if {@code g} is null
316     * @throws IllegalArgumentException thrown if {@code nc} is null
317     */
318    public StyledMapRenderer(Graphics2D g, NavigatableComponent nc, boolean isInactiveMode) {
319        super(g, nc, isInactiveMode);
320
321        if (nc!=null) {
322            Component focusOwner = FocusManager.getCurrentManager().getFocusOwner();
323            useWiderHighlight = !(focusOwner instanceof AbstractButton || focusOwner == nc);
324        }
325    }
326
327    private Polygon buildPolygon(Point center, int radius, int sides) {
328        return buildPolygon(center, radius, sides, 0.0);
329    }
330
331    private Polygon buildPolygon(Point center, int radius, int sides, double rotation) {
332        Polygon polygon = new Polygon();
333        for (int i = 0; i < sides; i++) {
334            double angle = ((2 * Math.PI / sides) * i) - rotation;
335            int x = (int) Math.round(center.x + radius * Math.cos(angle));
336            int y = (int) Math.round(center.y + radius * Math.sin(angle));
337            polygon.addPoint(x, y);
338        }
339        return polygon;
340    }
341
342    private void displaySegments(GeneralPath path, GeneralPath orientationArrows, GeneralPath onewayArrows, GeneralPath onewayArrowsCasing,
343            Color color, BasicStroke line, BasicStroke dashes, Color dashedColor) {
344        g.setColor(isInactiveMode ? inactiveColor : color);
345        if (useStrokes) {
346            g.setStroke(line);
347        }
348        g.draw(path);
349
350        if(!isInactiveMode && useStrokes && dashes != null) {
351            g.setColor(dashedColor);
352            g.setStroke(dashes);
353            g.draw(path);
354        }
355
356        if (orientationArrows != null) {
357            g.setColor(isInactiveMode ? inactiveColor : color);
358            g.setStroke(new BasicStroke(line.getLineWidth(), line.getEndCap(), BasicStroke.JOIN_MITER, line.getMiterLimit()));
359            g.draw(orientationArrows);
360        }
361
362        if (onewayArrows != null) {
363            g.setStroke(new BasicStroke(1, line.getEndCap(), BasicStroke.JOIN_MITER, line.getMiterLimit()));
364            g.fill(onewayArrowsCasing);
365            g.setColor(isInactiveMode ? inactiveColor : backgroundColor);
366            g.fill(onewayArrows);
367        }
368
369        if (useStrokes) {
370            g.setStroke(new BasicStroke());
371        }
372    }
373
374    /**
375     * Displays text at specified position including its halo, if applicable.
376     *
377     * @param gv Text's glyphs to display. If {@code null}, use text from {@code s} instead.
378     * @param s text to display if {@code gv} is {@code null}
379     * @param x X position
380     * @param y Y position
381     * @param disabled {@code true} if element is disabled (filtered out)
382     * @param text text style to use
383     */
384    private void displayText(GlyphVector gv, String s, int x, int y, boolean disabled, TextElement text) {
385        if (isInactiveMode || disabled) {
386            g.setColor(inactiveColor);
387            if (gv != null) {
388                g.drawGlyphVector(gv, x, y);
389            } else {
390                g.setFont(text.font);
391                g.drawString(s, x, y);
392            }
393        } else if (text.haloRadius != null) {
394            g.setStroke(new BasicStroke(2*text.haloRadius, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND));
395            g.setColor(text.haloColor);
396            if (gv == null) {
397                FontRenderContext frc = g.getFontRenderContext();
398                gv = text.font.createGlyphVector(frc, s);
399            }
400            Shape textOutline = gv.getOutline(x, y);
401            g.draw(textOutline);
402            g.setStroke(new BasicStroke());
403            g.setColor(text.color);
404            g.fill(textOutline);
405        } else {
406            g.setColor(text.color);
407            if (gv != null) {
408                g.drawGlyphVector(gv, x, y);
409            } else {
410                g.setFont(text.font);
411                g.drawString(s, x, y);
412            }
413        }
414    }
415
416    protected void drawArea(OsmPrimitive osm, Path2D.Double path, Color color, MapImage fillImage, TextElement text) {
417
418        Shape area = path.createTransformedShape(nc.getAffineTransform());
419
420        if (!isOutlineOnly) {
421            if (fillImage == null) {
422                if (isInactiveMode) {
423                    g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.33f));
424                }
425                g.setColor(color);
426                g.fill(area);
427            } else {
428                TexturePaint texture = new TexturePaint(fillImage.getImage(),
429                        new Rectangle(0, 0, fillImage.getWidth(), fillImage.getHeight()));
430                g.setPaint(texture);
431                Float alpha = Utils.color_int2float(fillImage.alpha);
432                if (alpha != 1f) {
433                    g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
434                }
435                g.fill(area);
436                g.setPaintMode();
437            }
438        }
439
440        if (text != null && isShowNames()) {
441            // abort if we can't compose the label to be rendered
442            if (text.labelCompositionStrategy == null) return;
443            String name = text.labelCompositionStrategy.compose(osm);
444            if (name == null) return;
445
446            Rectangle pb = area.getBounds();
447            FontMetrics fontMetrics = g.getFontMetrics(orderFont); // if slow, use cache
448            Rectangle2D nb = fontMetrics.getStringBounds(name, g); // if slow, approximate by strlen()*maxcharbounds(font)
449
450            // Using the Centroid is Nicer for buildings like: +--------+
451            // but this needs to be fast.  As most houses are  |   42   |
452            // boxes anyway, the center of the bounding box    +---++---+
453            // will have to do.                                    ++
454            // Centroids are not optimal either, just imagine a U-shaped house.
455
456            Rectangle centeredNBounds = new Rectangle(pb.x + (int)((pb.width - nb.getWidth())/2.0),
457                    pb.y + (int)((pb.height - nb.getHeight())/2.0),
458                    (int)nb.getWidth(),
459                    (int)nb.getHeight());
460
461            if ((pb.width >= nb.getWidth() && pb.height >= nb.getHeight()) && // quick check
462                    area.contains(centeredNBounds) // slow but nice
463            ) {
464                Font defaultFont = g.getFont();
465                int x = (int)(centeredNBounds.getMinX() - nb.getMinX());
466                int y = (int)(centeredNBounds.getMinY() - nb.getMinY());
467                displayText(null, name, x, y, osm.isDisabled(), text);
468                g.setFont(defaultFont);
469            }
470        }
471    }
472
473    public void drawArea(Relation r, Color color, MapImage fillImage, TextElement text) {
474        Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, r);
475        if (!r.isDisabled() && !multipolygon.getOuterWays().isEmpty()) {
476            for (PolyData pd : multipolygon.getCombinedPolygons()) {
477                Path2D.Double p = pd.get();
478                if (!isAreaVisible(p)) {
479                    continue;
480                }
481                drawArea(r, p,
482                        pd.selected ? paintSettings.getRelationSelectedColor(color.getAlpha()) : color,
483                                fillImage, text);
484            }
485        }
486    }
487
488    public void drawArea(Way w, Color color, MapImage fillImage, TextElement text) {
489        drawArea(w, getPath(w), color, fillImage, text);
490    }
491
492    public void drawBoxText(Node n, BoxTextElemStyle bs) {
493        if (!isShowNames() || bs == null)
494            return;
495
496        Point p = nc.getPoint(n);
497        TextElement text = bs.text;
498        String s = text.labelCompositionStrategy.compose(n);
499        if (s == null) return;
500
501        Font defaultFont = g.getFont();
502        g.setFont(text.font);
503
504        int x = p.x + text.xOffset;
505        int y = p.y + text.yOffset;
506        /**
507         *
508         *       left-above __center-above___ right-above
509         *         left-top|                 |right-top
510         *                 |                 |
511         *      left-center|  center-center  |right-center
512         *                 |                 |
513         *      left-bottom|_________________|right-bottom
514         *       left-below   center-below    right-below
515         *
516         */
517        Rectangle box = bs.getBox();
518        if (bs.hAlign == HorizontalTextAlignment.RIGHT) {
519            x += box.x + box.width + 2;
520        } else {
521            FontRenderContext frc = g.getFontRenderContext();
522            Rectangle2D bounds = text.font.getStringBounds(s, frc);
523            int textWidth = (int) bounds.getWidth();
524            if (bs.hAlign == HorizontalTextAlignment.CENTER) {
525                x -= textWidth / 2;
526            } else if (bs.hAlign == HorizontalTextAlignment.LEFT) {
527                x -= - box.x + 4 + textWidth;
528            } else throw new AssertionError();
529        }
530
531        if (bs.vAlign == VerticalTextAlignment.BOTTOM) {
532            y += box.y + box.height;
533        } else {
534            FontRenderContext frc = g.getFontRenderContext();
535            LineMetrics metrics = text.font.getLineMetrics(s, frc);
536            if (bs.vAlign == VerticalTextAlignment.ABOVE) {
537                y -= - box.y + metrics.getDescent();
538            } else if (bs.vAlign == VerticalTextAlignment.TOP) {
539                y -= - box.y - metrics.getAscent();
540            } else if (bs.vAlign == VerticalTextAlignment.CENTER) {
541                y += (metrics.getAscent() - metrics.getDescent()) / 2;
542            } else if (bs.vAlign == VerticalTextAlignment.BELOW) {
543                y += box.y + box.height + metrics.getAscent() + 2;
544            } else throw new AssertionError();
545        }
546        displayText(null, s, x, y, n.isDisabled(), text);
547        g.setFont(defaultFont);
548    }
549
550    /**
551     * Draw an image along a way repeatedly.
552     *
553     * @param way the way
554     * @param pattern the image
555     * @param offset offset from the way
556     * @param spacing spacing between two images
557     * @param phase initial spacing
558     * @param align alignment of the image. The top, center or bottom edge
559     * can be aligned with the way.
560     */
561    public void drawRepeatImage(Way way, Image pattern, float offset, float spacing, float phase, LineImageAlignment align) {
562        final int imgWidth = pattern.getWidth(null);
563        final double repeat = imgWidth + spacing;
564        final int imgHeight = pattern.getHeight(null);
565
566        Point lastP = null;
567        double currentWayLength = phase % repeat;
568        if (currentWayLength < 0) {
569            currentWayLength += repeat;
570        }
571
572        int dy1, dy2;
573        switch (align) {
574            case TOP:
575                dy1 = 0;
576                dy2 = imgHeight;
577                break;
578            case CENTER:
579                dy1 = - imgHeight / 2;
580                dy2 = imgHeight + dy1;
581                break;
582            case BOTTOM:
583                dy1 = -imgHeight;
584                dy2 = 0;
585                break;
586            default:
587                throw new AssertionError();
588        }
589
590        OffsetIterator it = new OffsetIterator(way.getNodes(), offset);
591        while (it.hasNext()) {
592            Point thisP = it.next();
593
594            if (lastP != null) {
595                final double segmentLength = thisP.distance(lastP);
596
597                final double dx = thisP.x - lastP.x;
598                final double dy = thisP.y - lastP.y;
599
600                // pos is the position from the beginning of the current segment
601                // where an image should be painted
602                double pos = repeat - (currentWayLength % repeat);
603
604                AffineTransform saveTransform = g.getTransform();
605                g.translate(lastP.x, lastP.y);
606                g.rotate(Math.atan2(dy, dx));
607
608                // draw the rest of the image from the last segment in case it
609                // is cut off
610                if (pos > spacing) {
611                    // segment is too short for a complete image
612                    if (pos > segmentLength + spacing) {
613                        g.drawImage(pattern, 0, dy1, (int) segmentLength, dy2,
614                                (int) (repeat - pos), 0,
615                                (int) (repeat - pos + segmentLength), imgHeight, null);
616                    // rest of the image fits fully on the current segment
617                    } else {
618                        g.drawImage(pattern, 0, dy1, (int) (pos - spacing), dy2,
619                                (int) (repeat - pos), 0, imgWidth, imgHeight, null);
620                    }
621                }
622                // draw remaining images for this segment
623                while (pos < segmentLength) {
624                    // cut off at the end?
625                    if (pos + imgWidth > segmentLength) {
626                        g.drawImage(pattern, (int) pos, dy1, (int) segmentLength, dy2,
627                                0, 0, (int) segmentLength - (int) pos, imgHeight, null);
628                    } else {
629                        g.drawImage(pattern, (int) pos, dy1, nc);
630                    }
631                    pos += repeat;
632                }
633                g.setTransform(saveTransform);
634
635                currentWayLength += segmentLength;
636            }
637            lastP = thisP;
638        }
639    }
640
641    @Override
642    public void drawNode(Node n, Color color, int size, boolean fill) {
643        if(size <= 0 && !n.isHighlighted())
644            return;
645
646        Point p = nc.getPoint(n);
647
648        if(n.isHighlighted()) {
649            drawPointHighlight(p, size);
650        }
651
652        if (size > 1) {
653            if ((p.x < 0) || (p.y < 0) || (p.x > nc.getWidth()) || (p.y > nc.getHeight())) return;
654            int radius = size / 2;
655
656            if (isInactiveMode || n.isDisabled()) {
657                g.setColor(inactiveColor);
658            } else {
659                g.setColor(color);
660            }
661            if (fill) {
662                g.fillRect(p.x-radius-1, p.y-radius-1, size + 1, size + 1);
663            } else {
664                g.drawRect(p.x-radius-1, p.y-radius-1, size, size);
665            }
666        }
667    }
668
669    public void drawNodeIcon(Node n, Image img, float alpha, boolean selected, boolean member) {
670        Point p = nc.getPoint(n);
671
672        final int w = img.getWidth(null), h=img.getHeight(null);
673        if(n.isHighlighted()) {
674            drawPointHighlight(p, Math.max(w, h));
675        }
676
677        if (alpha != 1f) {
678            g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
679        }
680        g.drawImage(img, p.x-w/2, p.y-h/2, nc);
681        g.setPaintMode();
682        if (selected || member)
683        {
684            Color color;
685            if (isInactiveMode || n.isDisabled()) {
686                color = inactiveColor;
687            } else if (selected) {
688                color = selectedColor;
689            } else {
690                color = relationSelectedColor;
691            }
692            g.setColor(color);
693            g.drawRect(p.x-w/2-2, p.y-h/2-2, w+4, h+4);
694        }
695    }
696
697    public void drawNodeSymbol(Node n, Symbol s, Color fillColor, Color strokeColor) {
698        Point p = nc.getPoint(n);
699        int radius = s.size / 2;
700
701        if(n.isHighlighted()) {
702            drawPointHighlight(p, s.size);
703        }
704
705        if (fillColor != null) {
706            g.setColor(fillColor);
707            switch (s.symbol) {
708            case SQUARE:
709                g.fillRect(p.x - radius, p.y - radius, s.size, s.size);
710                break;
711            case CIRCLE:
712                g.fillOval(p.x - radius, p.y - radius, s.size, s.size);
713                break;
714            case TRIANGLE:
715                g.fillPolygon(buildPolygon(p, radius, 3, Math.PI / 2));
716                break;
717            case PENTAGON:
718                g.fillPolygon(buildPolygon(p, radius, 5, Math.PI / 2));
719                break;
720            case HEXAGON:
721                g.fillPolygon(buildPolygon(p, radius, 6));
722                break;
723            case HEPTAGON:
724                g.fillPolygon(buildPolygon(p, radius, 7, Math.PI / 2));
725                break;
726            case OCTAGON:
727                g.fillPolygon(buildPolygon(p, radius, 8, Math.PI / 8));
728                break;
729            case NONAGON:
730                g.fillPolygon(buildPolygon(p, radius, 9, Math.PI / 2));
731                break;
732            case DECAGON:
733                g.fillPolygon(buildPolygon(p, radius, 10));
734                break;
735            default:
736                throw new AssertionError();
737            }
738        }
739        if (s.stroke != null) {
740            g.setStroke(s.stroke);
741            g.setColor(strokeColor);
742            switch (s.symbol) {
743            case SQUARE:
744                g.drawRect(p.x - radius, p.y - radius, s.size - 1, s.size - 1);
745                break;
746            case CIRCLE:
747                g.drawOval(p.x - radius, p.y - radius, s.size - 1, s.size - 1);
748                break;
749            case TRIANGLE:
750                g.drawPolygon(buildPolygon(p, radius, 3, Math.PI / 2));
751                break;
752            case PENTAGON:
753                g.drawPolygon(buildPolygon(p, radius, 5, Math.PI / 2));
754                break;
755            case HEXAGON:
756                g.drawPolygon(buildPolygon(p, radius, 6));
757                break;
758            case HEPTAGON:
759                g.drawPolygon(buildPolygon(p, radius, 7, Math.PI / 2));
760                break;
761            case OCTAGON:
762                g.drawPolygon(buildPolygon(p, radius, 8, Math.PI / 8));
763                break;
764            case NONAGON:
765                g.drawPolygon(buildPolygon(p, radius, 9, Math.PI / 2));
766                break;
767            case DECAGON:
768                g.drawPolygon(buildPolygon(p, radius, 10));
769                break;
770            default:
771                throw new AssertionError();
772            }
773            g.setStroke(new BasicStroke());
774        }
775    }
776
777    /**
778     * Draw a number of the order of the two consecutive nodes within the
779     * parents way
780     */
781    public void drawOrderNumber(Node n1, Node n2, int orderNumber, Color clr) {
782        Point p1 = nc.getPoint(n1);
783        Point p2 = nc.getPoint(n2);
784        StyledMapRenderer.this.drawOrderNumber(p1, p2, orderNumber, clr);
785    }
786
787    /**
788     * highlights a given GeneralPath using the settings from BasicStroke to match the line's
789     * style. Width of the highlight is hard coded.
790     * @param path
791     * @param line
792     */
793    private void drawPathHighlight(GeneralPath path, BasicStroke line) {
794        if(path == null)
795            return;
796        g.setColor(highlightColorTransparent);
797        float w = (line.getLineWidth() + highlightLineWidth);
798        if (useWiderHighlight) w+=widerHighlight;
799        while(w >= line.getLineWidth()) {
800            g.setStroke(new BasicStroke(w, line.getEndCap(), line.getLineJoin(), line.getMiterLimit()));
801            g.draw(path);
802            w -= highlightStep;
803        }
804    }
805    /**
806     * highlights a given point by drawing a rounded rectangle around it. Give the
807     * size of the object you want to be highlighted, width is added automatically.
808     */
809    private void drawPointHighlight(Point p, int size) {
810        g.setColor(highlightColorTransparent);
811        int s = size + highlightPointRadius;
812        if (useWiderHighlight) s+=widerHighlight;
813        while(s >= size) {
814            int r = (int) Math.floor(s/2);
815            g.fillRoundRect(p.x-r, p.y-r, s, s, r, r);
816            s -= highlightStep;
817        }
818    }
819
820    public void drawRestriction(Image img, Point pVia, double vx, double vx2, double vy, double vy2, double angle, boolean selected) {
821        // rotate image with direction last node in from to, and scale down image to 16*16 pixels
822        Image smallImg = ImageProvider.createRotatedImage(img, angle, new Dimension(16, 16));
823        int w = smallImg.getWidth(null), h=smallImg.getHeight(null);
824        g.drawImage(smallImg, (int)(pVia.x+vx+vx2)-w/2, (int)(pVia.y+vy+vy2)-h/2, nc);
825
826        if (selected) {
827            g.setColor(isInactiveMode ? inactiveColor : relationSelectedColor);
828            g.drawRect((int)(pVia.x+vx+vx2)-w/2-2,(int)(pVia.y+vy+vy2)-h/2-2, w+4, h+4);
829        }
830    }
831
832    public void drawRestriction(Relation r, MapImage icon) {
833        Way fromWay = null;
834        Way toWay = null;
835        OsmPrimitive via = null;
836
837        /* find the "from", "via" and "to" elements */
838        for (RelationMember m : r.getMembers()) {
839            if(m.getMember().isIncomplete())
840                return;
841            else {
842                if(m.isWay()) {
843                    Way w = m.getWay();
844                    if(w.getNodesCount() < 2) {
845                        continue;
846                    }
847
848                    switch(m.getRole()) {
849                    case "from":
850                        if(fromWay == null) {
851                            fromWay = w;
852                        }
853                        break;
854                    case "to":
855                        if(toWay == null) {
856                            toWay = w;
857                        }
858                        break;
859                    case "via":
860                        if(via == null) {
861                            via = w;
862                        }
863                    }
864                } else if(m.isNode()) {
865                    Node n = m.getNode();
866                    if("via".equals(m.getRole()) && via == null) {
867                        via = n;
868                    }
869                }
870            }
871        }
872
873        if (fromWay == null || toWay == null || via == null)
874            return;
875
876        Node viaNode;
877        if(via instanceof Node)
878        {
879            viaNode = (Node) via;
880            if(!fromWay.isFirstLastNode(viaNode))
881                return;
882        }
883        else
884        {
885            Way viaWay = (Way) via;
886            Node firstNode = viaWay.firstNode();
887            Node lastNode = viaWay.lastNode();
888            Boolean onewayvia = false;
889
890            String onewayviastr = viaWay.get("oneway");
891            if(onewayviastr != null)
892            {
893                if("-1".equals(onewayviastr)) {
894                    onewayvia = true;
895                    Node tmp = firstNode;
896                    firstNode = lastNode;
897                    lastNode = tmp;
898                } else {
899                    onewayvia = OsmUtils.getOsmBoolean(onewayviastr);
900                    if (onewayvia == null) {
901                        onewayvia = false;
902                    }
903                }
904            }
905
906            if(fromWay.isFirstLastNode(firstNode)) {
907                viaNode = firstNode;
908            } else if (!onewayvia && fromWay.isFirstLastNode(lastNode)) {
909                viaNode = lastNode;
910            } else
911                return;
912        }
913
914        /* find the "direct" nodes before the via node */
915        Node fromNode;
916        if(fromWay.firstNode() == via) {
917            fromNode = fromWay.getNode(1);
918        } else {
919            fromNode = fromWay.getNode(fromWay.getNodesCount()-2);
920        }
921
922        Point pFrom = nc.getPoint(fromNode);
923        Point pVia = nc.getPoint(viaNode);
924
925        /* starting from via, go back the "from" way a few pixels
926           (calculate the vector vx/vy with the specified length and the direction
927           away from the "via" node along the first segment of the "from" way)
928         */
929        double distanceFromVia=14;
930        double dx = (pFrom.x >= pVia.x) ? (pFrom.x - pVia.x) : (pVia.x - pFrom.x);
931        double dy = (pFrom.y >= pVia.y) ? (pFrom.y - pVia.y) : (pVia.y - pFrom.y);
932
933        double fromAngle;
934        if(dx == 0.0) {
935            fromAngle = Math.PI/2;
936        } else {
937            fromAngle = Math.atan(dy / dx);
938        }
939        double fromAngleDeg = Math.toDegrees(fromAngle);
940
941        double vx = distanceFromVia * Math.cos(fromAngle);
942        double vy = distanceFromVia * Math.sin(fromAngle);
943
944        if(pFrom.x < pVia.x) {
945            vx = -vx;
946        }
947        if(pFrom.y < pVia.y) {
948            vy = -vy;
949        }
950
951        /* go a few pixels away from the way (in a right angle)
952           (calculate the vx2/vy2 vector with the specified length and the direction
953           90degrees away from the first segment of the "from" way)
954         */
955        double distanceFromWay=10;
956        double vx2 = 0;
957        double vy2 = 0;
958        double iconAngle = 0;
959
960        if(pFrom.x >= pVia.x && pFrom.y >= pVia.y) {
961            if(!leftHandTraffic) {
962                vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
963                vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
964            } else {
965                vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
966                vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
967            }
968            iconAngle = 270+fromAngleDeg;
969        }
970        if(pFrom.x < pVia.x && pFrom.y >= pVia.y) {
971            if(!leftHandTraffic) {
972                vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
973                vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
974            } else {
975                vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
976                vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
977            }
978            iconAngle = 90-fromAngleDeg;
979        }
980        if(pFrom.x < pVia.x && pFrom.y < pVia.y) {
981            if(!leftHandTraffic) {
982                vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
983                vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
984            } else {
985                vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
986                vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
987            }
988            iconAngle = 90+fromAngleDeg;
989        }
990        if(pFrom.x >= pVia.x && pFrom.y < pVia.y) {
991            if(!leftHandTraffic) {
992                vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
993                vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
994            } else {
995                vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
996                vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
997            }
998            iconAngle = 270-fromAngleDeg;
999        }
1000
1001        drawRestriction(isInactiveMode || r.isDisabled() ? icon.getDisabled() : icon.getImage(),
1002                pVia, vx, vx2, vy, vy2, iconAngle, r.isSelected());
1003    }
1004
1005    public void drawTextOnPath(Way way, TextElement text) {
1006        if (way == null || text == null)
1007            return;
1008        String name = text.getString(way);
1009        if (name == null || name.isEmpty())
1010            return;
1011
1012        Polygon poly = new Polygon();
1013        Point lastPoint = null;
1014        Iterator<Node> it = way.getNodes().iterator();
1015        double pathLength = 0;
1016        long dx, dy;
1017        while (it.hasNext()) {
1018            Node n = it.next();
1019            Point p = nc.getPoint(n);
1020            poly.addPoint(p.x, p.y);
1021
1022            if(lastPoint != null) {
1023                dx = p.x - lastPoint.x;
1024                dy = p.y - lastPoint.y;
1025                pathLength += Math.sqrt(dx*dx + dy*dy);
1026            }
1027            lastPoint = p;
1028        }
1029
1030        FontMetrics fontMetrics = g.getFontMetrics(text.font); // if slow, use cache
1031        Rectangle2D rec = fontMetrics.getStringBounds(name, g); // if slow, approximate by strlen()*maxcharbounds(font)
1032
1033        if (rec.getWidth() > pathLength)
1034            return;
1035
1036        double t1 = (pathLength/2 - rec.getWidth()/2) / pathLength;
1037        double t2 = (pathLength/2 + rec.getWidth()/2) / pathLength;
1038
1039        double[] p1 = pointAt(t1, poly, pathLength);
1040        double[] p2 = pointAt(t2, poly, pathLength);
1041
1042        if (p1 == null || p2 == null)
1043            return;
1044
1045        double angleOffset;
1046        double offsetSign;
1047        double tStart;
1048
1049        if (p1[0] < p2[0] &&
1050                p1[2] < Math.PI/2 &&
1051                p1[2] > -Math.PI/2) {
1052            angleOffset = 0;
1053            offsetSign = 1;
1054            tStart = t1;
1055        } else {
1056            angleOffset = Math.PI;
1057            offsetSign = -1;
1058            tStart = t2;
1059        }
1060
1061        FontRenderContext frc = g.getFontRenderContext();
1062        GlyphVector gv = text.font.createGlyphVector(frc, name);
1063
1064        for (int i=0; i<gv.getNumGlyphs(); ++i) {
1065            Rectangle2D rect = gv.getGlyphLogicalBounds(i).getBounds2D();
1066            double t = tStart + offsetSign * (rect.getX() + rect.getWidth()/2) / pathLength;
1067            double[] p = pointAt(t, poly, pathLength);
1068            if (p != null) {
1069                AffineTransform trfm = AffineTransform.getTranslateInstance(p[0] - rect.getX(), p[1]);
1070                trfm.rotate(p[2]+angleOffset);
1071                double off = -rect.getY() - rect.getHeight()/2 + text.yOffset;
1072                trfm.translate(-rect.getWidth()/2, off);
1073                if (isGlyphVectorDoubleTranslationBug()) {
1074                    // scale the translation components by one half
1075                    AffineTransform tmp = AffineTransform.getTranslateInstance(-0.5 * trfm.getTranslateX(), -0.5 * trfm.getTranslateY());
1076                    tmp.concatenate(trfm);
1077                    trfm = tmp;
1078                }
1079                gv.setGlyphTransform(i, trfm);
1080            }
1081        }
1082        displayText(gv, null, 0, 0, way.isDisabled(), text);
1083    }
1084
1085    /**
1086     * draw way
1087     * @param showOrientation show arrows that indicate the technical orientation of
1088     *              the way (defined by order of nodes)
1089     * @param showOneway show symbols that indicate the direction of the feature,
1090     *              e.g. oneway street or waterway
1091     * @param onewayReversed for oneway=-1 and similar
1092     */
1093    public void drawWay(Way way, Color color, BasicStroke line, BasicStroke dashes, Color dashedColor, float offset,
1094            boolean showOrientation, boolean showHeadArrowOnly,
1095            boolean showOneway, boolean onewayReversed) {
1096
1097        GeneralPath path = new GeneralPath();
1098        GeneralPath orientationArrows = showOrientation ? new GeneralPath() : null;
1099        GeneralPath onewayArrows = showOneway ? new GeneralPath() : null;
1100        GeneralPath onewayArrowsCasing = showOneway ? new GeneralPath() : null;
1101        Rectangle bounds = g.getClipBounds();
1102        if (bounds != null) {
1103            // avoid arrow heads at the border
1104            bounds.grow(100, 100);
1105        }
1106
1107        double wayLength = 0;
1108        Point lastPoint = null;
1109        boolean initialMoveToNeeded = true;
1110        List<Node> wayNodes = way.getNodes();
1111        if (wayNodes.size() < 2) return;
1112
1113        // only highlight the segment if the way itself is not highlighted
1114        if (!way.isHighlighted()) {
1115            GeneralPath highlightSegs = null;
1116            for (WaySegment ws : highlightWaySegments) {
1117                if (ws.way != way || ws.lowerIndex < offset) {
1118                    continue;
1119                }
1120                if(highlightSegs == null) {
1121                    highlightSegs = new GeneralPath();
1122                }
1123
1124                Point p1 = nc.getPoint(ws.getFirstNode());
1125                Point p2 = nc.getPoint(ws.getSecondNode());
1126                highlightSegs.moveTo(p1.x, p1.y);
1127                highlightSegs.lineTo(p2.x, p2.y);
1128            }
1129
1130            drawPathHighlight(highlightSegs, line);
1131        }
1132
1133        Iterator<Point> it = new OffsetIterator(wayNodes, offset);
1134        while (it.hasNext()) {
1135            Point p = it.next();
1136            if (lastPoint != null) {
1137                Point p1 = lastPoint;
1138                Point p2 = p;
1139
1140                /**
1141                 * Do custom clipping to work around openjdk bug. It leads to
1142                 * drawing artefacts when zooming in a lot. (#4289, #4424)
1143                 * (Looks like int overflow.)
1144                 */
1145                LineClip clip = new LineClip(p1, p2, bounds);
1146                if (clip.execute()) {
1147                    if (!p1.equals(clip.getP1())) {
1148                        p1 = clip.getP1();
1149                        path.moveTo(p1.x, p1.y);
1150                    } else if (initialMoveToNeeded) {
1151                        initialMoveToNeeded = false;
1152                        path.moveTo(p1.x, p1.y);
1153                    }
1154                    p2 = clip.getP2();
1155                    path.lineTo(p2.x, p2.y);
1156
1157                    /* draw arrow */
1158                    if (showHeadArrowOnly ? !it.hasNext() : showOrientation) {
1159                        final double segmentLength = p1.distance(p2);
1160                        if (segmentLength != 0.0) {
1161                            final double l =  (10. + line.getLineWidth()) / segmentLength;
1162
1163                            final double sx = l * (p1.x - p2.x);
1164                            final double sy = l * (p1.y - p2.y);
1165
1166                            orientationArrows.moveTo (p2.x + cosPHI * sx - sinPHI * sy, p2.y + sinPHI * sx + cosPHI * sy);
1167                            orientationArrows.lineTo(p2.x, p2.y);
1168                            orientationArrows.lineTo (p2.x + cosPHI * sx + sinPHI * sy, p2.y - sinPHI * sx + cosPHI * sy);
1169                        }
1170                    }
1171                    if (showOneway) {
1172                        final double segmentLength = p1.distance(p2);
1173                        if (segmentLength != 0.0) {
1174                            final double nx = (p2.x - p1.x) / segmentLength;
1175                            final double ny = (p2.y - p1.y) / segmentLength;
1176
1177                            final double interval = 60;
1178                            // distance from p1
1179                            double dist = interval - (wayLength % interval);
1180
1181                            while (dist < segmentLength) {
1182                                for (int i=0; i<2; ++i) {
1183                                    float onewaySize = i == 0 ? 3f : 2f;
1184                                    GeneralPath onewayPath = i == 0 ? onewayArrowsCasing : onewayArrows;
1185
1186                                    // scale such that border is 1 px
1187                                    final double fac = - (onewayReversed ? -1 : 1) * onewaySize * (1 + sinPHI) / (sinPHI * cosPHI);
1188                                    final double sx = nx * fac;
1189                                    final double sy = ny * fac;
1190
1191                                    // Attach the triangle at the incenter and not at the tip.
1192                                    // Makes the border even at all sides.
1193                                    final double x = p1.x + nx * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI));
1194                                    final double y = p1.y + ny * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI));
1195
1196                                    onewayPath.moveTo(x, y);
1197                                    onewayPath.lineTo (x + cosPHI * sx - sinPHI * sy, y + sinPHI * sx + cosPHI * sy);
1198                                    onewayPath.lineTo (x + cosPHI * sx + sinPHI * sy, y - sinPHI * sx + cosPHI * sy);
1199                                    onewayPath.lineTo(x, y);
1200                                }
1201                                dist += interval;
1202                            }
1203                        }
1204                        wayLength += segmentLength;
1205                    }
1206                }
1207            }
1208            lastPoint = p;
1209        }
1210        if(way.isHighlighted()) {
1211            drawPathHighlight(path, line);
1212        }
1213        displaySegments(path, orientationArrows, onewayArrows, onewayArrowsCasing, color, line, dashes, dashedColor);
1214    }
1215
1216    public double getCircum() {
1217        return circum;
1218    }
1219
1220    @Override
1221    public void getColors() {
1222        super.getColors();
1223        this.relationSelectedColor = PaintColors.RELATIONSELECTED.get();
1224        this.highlightColorTransparent = new Color(highlightColor.getRed(), highlightColor.getGreen(), highlightColor.getBlue(), 100);
1225        this.backgroundColor = PaintColors.getBackgroundColor();
1226    }
1227
1228    @Override
1229    protected void getSettings(boolean virtual) {
1230        super.getSettings(virtual);
1231        paintSettings = MapPaintSettings.INSTANCE;
1232
1233        circum = nc.getDist100Pixel();
1234
1235        leftHandTraffic = Main.pref.getBoolean("mappaint.lefthandtraffic", false);
1236
1237        useStrokes = paintSettings.getUseStrokesDistance() > circum;
1238        showNames = paintSettings.getShowNamesDistance() > circum;
1239        showIcons = paintSettings.getShowIconsDistance() > circum;
1240        isOutlineOnly = paintSettings.isOutlineOnly();
1241        orderFont = new Font(Main.pref.get("mappaint.font", "Helvetica"), Font.PLAIN, Main.pref.getInteger("mappaint.fontsize", 8));
1242
1243        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1244                Main.pref.getBoolean("mappaint.use-antialiasing", true) ?
1245                        RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
1246
1247        highlightLineWidth = Main.pref.getInteger("mappaint.highlight.width", 4);
1248        highlightPointRadius = Main.pref.getInteger("mappaint.highlight.radius", 7);
1249        widerHighlight = Main.pref.getInteger("mappaint.highlight.bigger-increment", 5);
1250        highlightStep = Main.pref.getInteger("mappaint.highlight.step", 4);
1251    }
1252
1253    private Path2D.Double getPath(Way w) {
1254        Path2D.Double path = new Path2D.Double();
1255        boolean initial = true;
1256        for (Node n : w.getNodes()) {
1257            EastNorth p = n.getEastNorth();
1258            if (p != null) {
1259                if (initial) {
1260                    path.moveTo(p.getX(), p.getY());
1261                    initial = false;
1262                } else {
1263                    path.lineTo(p.getX(), p.getY());
1264                }
1265            }
1266        }
1267        return path;
1268    }
1269
1270    private boolean isAreaVisible(Path2D.Double area) {
1271        Rectangle2D bounds = area.getBounds2D();
1272        if (bounds.isEmpty()) return false;
1273        Point2D p = nc.getPoint2D(new EastNorth(bounds.getX(), bounds.getY()));
1274        if (p.getX() > nc.getWidth()) return false;
1275        if (p.getY() < 0) return false;
1276        p = nc.getPoint2D(new EastNorth(bounds.getX() + bounds.getWidth(), bounds.getY() + bounds.getHeight()));
1277        if (p.getX() < 0) return false;
1278        if (p.getY() > nc.getHeight()) return false;
1279        return true;
1280    }
1281
1282    public boolean isInactiveMode() {
1283        return isInactiveMode;
1284    }
1285
1286    public boolean isShowIcons() {
1287        return showIcons;
1288    }
1289
1290    public boolean isShowNames() {
1291        return showNames;
1292    }
1293
1294    private double[] pointAt(double t, Polygon poly, double pathLength) {
1295        double totalLen = t * pathLength;
1296        double curLen = 0;
1297        long dx, dy;
1298        double segLen;
1299
1300        // Yes, it is inefficient to iterate from the beginning for each glyph.
1301        // Can be optimized if it turns out to be slow.
1302        for (int i = 1; i < poly.npoints; ++i) {
1303            dx = poly.xpoints[i] - poly.xpoints[i-1];
1304            dy = poly.ypoints[i] - poly.ypoints[i-1];
1305            segLen = Math.sqrt(dx*dx + dy*dy);
1306            if (totalLen > curLen + segLen) {
1307                curLen += segLen;
1308                continue;
1309            }
1310            return new double[] {
1311                    poly.xpoints[i-1]+(totalLen - curLen)/segLen*dx,
1312                    poly.ypoints[i-1]+(totalLen - curLen)/segLen*dy,
1313                    Math.atan2(dy, dx)};
1314        }
1315        return null;
1316    }
1317
1318    private class ComputeStyleListWorker implements Callable<List<StyleRecord>>, Visitor {
1319        private final List<? extends OsmPrimitive> input;
1320        private final int from;
1321        private final int to;
1322        private final List<StyleRecord> output;
1323        private final DataSet data;
1324
1325        private final ElemStyles styles = MapPaintStyles.getStyles();
1326
1327        private final boolean drawArea = circum <= Main.pref.getInteger("mappaint.fillareas", 10000000);
1328        private final boolean drawMultipolygon = drawArea && Main.pref.getBoolean("mappaint.multipolygon", true);
1329        private final boolean drawRestriction = Main.pref.getBoolean("mappaint.restriction", true);
1330
1331        /**
1332         * Constructs a new {@code ComputeStyleListWorker}.
1333         * @param input the primitives to process
1334         * @param from first index of <code>input</code> to use
1335         * @param to last index + 1
1336         * @param output the list of styles to which styles will be added
1337         * @param data the data set
1338         */
1339        public ComputeStyleListWorker(final List<? extends OsmPrimitive> input, int from, int to, List<StyleRecord> output, DataSet data) {
1340            this.input = input;
1341            this.from = from;
1342            this.to = to;
1343            this.output = output;
1344            this.data = data;
1345            this.styles.setDrawMultipolygon(drawMultipolygon);
1346        }
1347
1348        @Override
1349        public List<StyleRecord> call() throws Exception {
1350            for (int i = from; i<to; i++) {
1351                OsmPrimitive osm = input.get(i);
1352                if (osm.isDrawable()) {
1353                    osm.accept(this);
1354                }
1355            }
1356            return output;
1357        }
1358
1359        @Override
1360        public void visit(Node n) {
1361            if (n.isDisabled()) {
1362                add(n, FLAG_DISABLED);
1363            } else if (data.isSelected(n)) {
1364                add(n, FLAG_SELECTED);
1365            } else if (n.isMemberOfSelected()) {
1366                add(n, FLAG_MEMBER_OF_SELECTED);
1367            } else {
1368                add(n, FLAG_NORMAL);
1369            }
1370        }
1371
1372        @Override
1373        public void visit(Way w) {
1374            if (w.isDisabled()) {
1375                add(w, FLAG_DISABLED);
1376            } else if (data.isSelected(w)) {
1377                add(w, FLAG_SELECTED);
1378            } else if (w.isMemberOfSelected()) {
1379                add(w, FLAG_MEMBER_OF_SELECTED);
1380            } else {
1381                add(w, FLAG_NORMAL);
1382            }
1383        }
1384
1385        @Override
1386        public void visit(Relation r) {
1387            if (r.isDisabled()) {
1388                add(r, FLAG_DISABLED);
1389            } else if (data.isSelected(r)) {
1390                add(r, FLAG_SELECTED);
1391            } else {
1392                add(r, FLAG_NORMAL);
1393            }
1394        }
1395
1396        @Override
1397        public void visit(Changeset cs) {
1398            throw new UnsupportedOperationException();
1399        }
1400
1401        public void add(Node osm, int flags) {
1402            StyleList sl = styles.get(osm, circum, nc);
1403            for (ElemStyle s : sl) {
1404                output.add(new StyleRecord(s, osm, flags));
1405            }
1406        }
1407
1408        public void add(Relation osm, int flags) {
1409            StyleList sl = styles.get(osm, circum, nc);
1410            for (ElemStyle s : sl) {
1411                if (drawMultipolygon && drawArea && s instanceof AreaElemStyle && (flags & FLAG_DISABLED) == 0) {
1412                    output.add(new StyleRecord(s, osm, flags));
1413                } else if (drawRestriction && s instanceof NodeElemStyle) {
1414                    output.add(new StyleRecord(s, osm, flags));
1415                }
1416            }
1417        }
1418
1419        public void add(Way osm, int flags) {
1420            StyleList sl = styles.get(osm, circum, nc);
1421            for (ElemStyle s : sl) {
1422                if (!(drawArea && (flags & FLAG_DISABLED) == 0) && s instanceof AreaElemStyle) {
1423                    continue;
1424                }
1425                output.add(new StyleRecord(s, osm, flags));
1426            }
1427        }
1428    }
1429
1430    private class ConcurrentTasksHelper {
1431
1432        private final List<StyleRecord> allStyleElems;
1433        private final DataSet data;
1434
1435        public ConcurrentTasksHelper(List<StyleRecord> allStyleElems, DataSet data) {
1436            this.allStyleElems = allStyleElems;
1437            this.data = data;
1438        }
1439
1440        void process(List<? extends OsmPrimitive> prims) {
1441            final List<ComputeStyleListWorker> tasks = new ArrayList<>();
1442            final int bucketsize = Math.max(100, prims.size()/noThreads/3);
1443            final int noBuckets = (prims.size() + bucketsize - 1) / bucketsize;
1444            final boolean singleThread = noThreads == 1 || noBuckets == 1;
1445            for (int i=0; i<noBuckets; i++) {
1446                int from = i*bucketsize;
1447                int to = Math.min((i+1)*bucketsize, prims.size());
1448                List<StyleRecord> target = singleThread ? allStyleElems : new ArrayList<StyleRecord>(to - from);
1449                tasks.add(new ComputeStyleListWorker(prims, from, to, target, data));
1450            }
1451            if (singleThread) {
1452                try {
1453                    for (ComputeStyleListWorker task : tasks) {
1454                        task.call();
1455                    }
1456                } catch (Exception ex) {
1457                    throw new RuntimeException(ex);
1458                }
1459            } else if (tasks.size() > 1) {
1460                try {
1461                    for (Future<List<StyleRecord>> future : styleCreatorPool.invokeAll(tasks)) {
1462                            allStyleElems.addAll(future.get());
1463                    }
1464                } catch (InterruptedException | ExecutionException ex) {
1465                    throw new RuntimeException(ex);
1466                }
1467            }
1468        }
1469    }
1470
1471    @Override
1472    public void render(final DataSet data, boolean renderVirtualNodes, Bounds bounds) {
1473        BBox bbox = bounds.toBBox();
1474        getSettings(renderVirtualNodes);
1475
1476        data.getReadLock().lock();
1477        try {
1478            highlightWaySegments = data.getHighlightedWaySegments();
1479
1480            long timeStart=0, timePhase1=0, timeFinished;
1481            if (Main.isTraceEnabled()) {
1482                timeStart = System.currentTimeMillis();
1483                System.err.print("BENCHMARK: rendering ");
1484                Main.debug(null);
1485            }
1486
1487            List<Node> nodes = data.searchNodes(bbox);
1488            List<Way> ways = data.searchWays(bbox);
1489            List<Relation> relations = data.searchRelations(bbox);
1490    
1491            final List<StyleRecord> allStyleElems = new ArrayList<>(nodes.size()+ways.size()+relations.size());
1492    
1493            ConcurrentTasksHelper helper = new ConcurrentTasksHelper(allStyleElems, data);
1494
1495            // Need to process all relations first.
1496            // Reason: Make sure, ElemStyles.getStyleCacheWithRange is
1497            // not called for the same primitive in parallel threads.
1498            // (Could be synchronized, but try to avoid this for
1499            // performance reasons.)
1500            helper.process(relations);
1501            helper.process(new CompositeList<>(nodes, ways));
1502
1503            if (Main.isTraceEnabled()) {
1504                timePhase1 = System.currentTimeMillis();
1505                System.err.print("phase 1 (calculate styles): " + (timePhase1 - timeStart) + " ms");
1506            }
1507
1508            Collections.sort(allStyleElems);
1509
1510            for (StyleRecord r : allStyleElems) {
1511                r.style.paintPrimitive(
1512                        r.osm,
1513                        paintSettings,
1514                        StyledMapRenderer.this,
1515                        (r.flags & FLAG_SELECTED) != 0,
1516                        (r.flags & FLAG_MEMBER_OF_SELECTED) != 0
1517                );
1518            }
1519    
1520            if (Main.isTraceEnabled()) {
1521                timeFinished = System.currentTimeMillis();
1522                System.err.println("; phase 2 (draw): " + (timeFinished - timePhase1) + " ms; total: " + (timeFinished - timeStart) + " ms");
1523            }
1524    
1525            drawVirtualNodes(data, bbox);
1526        } finally {
1527            data.getReadLock().unlock();
1528        }
1529    }
1530}