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