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