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