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