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, 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(),
443                        new Rectangle(0, 0, fillImage.getWidth(), fillImage.getHeight()));
444                g.setPaint(texture);
445                Float alpha = Utils.color_int2float(fillImage.alpha);
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, 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, text);
542            }
543        }
544    }
545
546    public void drawArea(Way w, Color color, MapImage fillImage, TextElement text) {
547        drawArea(w, getPath(w), color, fillImage, 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, Image pattern, float offset, float spacing, float phase, LineImageAlignment align) {
620        final int imgWidth = pattern.getWidth(null);
621        final double repeat = imgWidth + spacing;
622        final int imgHeight = pattern.getHeight(null);
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, 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, 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, (int) pos, dy1, (int) segmentLength, dy2,
685                                0, 0, (int) segmentLength - (int) pos, imgHeight, null);
686                    } else {
687                        g.drawImage(pattern, (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, Image img, float alpha, boolean selected, boolean member) {
728        Point p = nc.getPoint(n);
729
730        final int w = img.getWidth(null), h=img.getHeight(null);
731        if(n.isHighlighted()) {
732            drawPointHighlight(p, Math.max(w, h));
733        }
734
735        if (alpha != 1f) {
736            g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
737        }
738        g.drawImage(img, p.x-w/2, p.y-h/2, nc);
739        g.setPaintMode();
740        if (selected || member)
741        {
742            Color color;
743            if (isInactiveMode || n.isDisabled()) {
744                color = inactiveColor;
745            } else if (selected) {
746                color = selectedColor;
747            } else {
748                color = relationSelectedColor;
749            }
750            g.setColor(color);
751            g.drawRect(p.x-w/2-2, p.y-h/2-2, w+4, h+4);
752        }
753    }
754
755    public void drawNodeSymbol(Node n, Symbol s, Color fillColor, Color strokeColor) {
756        Point p = nc.getPoint(n);
757        int radius = s.size / 2;
758
759        if(n.isHighlighted()) {
760            drawPointHighlight(p, s.size);
761        }
762
763        if (fillColor != null) {
764            g.setColor(fillColor);
765            switch (s.symbol) {
766            case SQUARE:
767                g.fillRect(p.x - radius, p.y - radius, s.size, s.size);
768                break;
769            case CIRCLE:
770                g.fillOval(p.x - radius, p.y - radius, s.size, s.size);
771                break;
772            case TRIANGLE:
773                g.fillPolygon(buildPolygon(p, radius, 3, Math.PI / 2));
774                break;
775            case PENTAGON:
776                g.fillPolygon(buildPolygon(p, radius, 5, Math.PI / 2));
777                break;
778            case HEXAGON:
779                g.fillPolygon(buildPolygon(p, radius, 6));
780                break;
781            case HEPTAGON:
782                g.fillPolygon(buildPolygon(p, radius, 7, Math.PI / 2));
783                break;
784            case OCTAGON:
785                g.fillPolygon(buildPolygon(p, radius, 8, Math.PI / 8));
786                break;
787            case NONAGON:
788                g.fillPolygon(buildPolygon(p, radius, 9, Math.PI / 2));
789                break;
790            case DECAGON:
791                g.fillPolygon(buildPolygon(p, radius, 10));
792                break;
793            default:
794                throw new AssertionError();
795            }
796        }
797        if (s.stroke != null) {
798            g.setStroke(s.stroke);
799            g.setColor(strokeColor);
800            switch (s.symbol) {
801            case SQUARE:
802                g.drawRect(p.x - radius, p.y - radius, s.size - 1, s.size - 1);
803                break;
804            case CIRCLE:
805                g.drawOval(p.x - radius, p.y - radius, s.size - 1, s.size - 1);
806                break;
807            case TRIANGLE:
808                g.drawPolygon(buildPolygon(p, radius, 3, Math.PI / 2));
809                break;
810            case PENTAGON:
811                g.drawPolygon(buildPolygon(p, radius, 5, Math.PI / 2));
812                break;
813            case HEXAGON:
814                g.drawPolygon(buildPolygon(p, radius, 6));
815                break;
816            case HEPTAGON:
817                g.drawPolygon(buildPolygon(p, radius, 7, Math.PI / 2));
818                break;
819            case OCTAGON:
820                g.drawPolygon(buildPolygon(p, radius, 8, Math.PI / 8));
821                break;
822            case NONAGON:
823                g.drawPolygon(buildPolygon(p, radius, 9, Math.PI / 2));
824                break;
825            case DECAGON:
826                g.drawPolygon(buildPolygon(p, radius, 10));
827                break;
828            default:
829                throw new AssertionError();
830            }
831            g.setStroke(new BasicStroke());
832        }
833    }
834
835    /**
836     * Draw a number of the order of the two consecutive nodes within the
837     * parents way
838     */
839    public void drawOrderNumber(Node n1, Node n2, int orderNumber, Color clr) {
840        Point p1 = nc.getPoint(n1);
841        Point p2 = nc.getPoint(n2);
842        StyledMapRenderer.this.drawOrderNumber(p1, p2, orderNumber, clr);
843    }
844
845    /**
846     * highlights a given GeneralPath using the settings from BasicStroke to match the line's
847     * style. Width of the highlight is hard coded.
848     * @param path
849     * @param line
850     */
851    private void drawPathHighlight(GeneralPath path, BasicStroke line) {
852        if(path == null)
853            return;
854        g.setColor(highlightColorTransparent);
855        float w = (line.getLineWidth() + highlightLineWidth);
856        if (useWiderHighlight) w+=widerHighlight;
857        while(w >= line.getLineWidth()) {
858            g.setStroke(new BasicStroke(w, line.getEndCap(), line.getLineJoin(), line.getMiterLimit()));
859            g.draw(path);
860            w -= highlightStep;
861        }
862    }
863    /**
864     * highlights a given point by drawing a rounded rectangle around it. Give the
865     * size of the object you want to be highlighted, width is added automatically.
866     */
867    private void drawPointHighlight(Point p, int size) {
868        g.setColor(highlightColorTransparent);
869        int s = size + highlightPointRadius;
870        if (useWiderHighlight) s+=widerHighlight;
871        while(s >= size) {
872            int r = (int) Math.floor(s/2);
873            g.fillRoundRect(p.x-r, p.y-r, s, s, r, r);
874            s -= highlightStep;
875        }
876    }
877
878    public void drawRestriction(Image img, Point pVia, double vx, double vx2, double vy, double vy2, double angle, boolean selected) {
879        // rotate image with direction last node in from to, and scale down image to 16*16 pixels
880        Image smallImg = ImageProvider.createRotatedImage(img, angle, new Dimension(16, 16));
881        int w = smallImg.getWidth(null), h=smallImg.getHeight(null);
882        g.drawImage(smallImg, (int)(pVia.x+vx+vx2)-w/2, (int)(pVia.y+vy+vy2)-h/2, nc);
883
884        if (selected) {
885            g.setColor(isInactiveMode ? inactiveColor : relationSelectedColor);
886            g.drawRect((int)(pVia.x+vx+vx2)-w/2-2,(int)(pVia.y+vy+vy2)-h/2-2, w+4, h+4);
887        }
888    }
889
890    public void drawRestriction(Relation r, MapImage icon) {
891        Way fromWay = null;
892        Way toWay = null;
893        OsmPrimitive via = null;
894
895        /* find the "from", "via" and "to" elements */
896        for (RelationMember m : r.getMembers()) {
897            if(m.getMember().isIncomplete())
898                return;
899            else {
900                if(m.isWay()) {
901                    Way w = m.getWay();
902                    if(w.getNodesCount() < 2) {
903                        continue;
904                    }
905
906                    switch(m.getRole()) {
907                    case "from":
908                        if(fromWay == null) {
909                            fromWay = w;
910                        }
911                        break;
912                    case "to":
913                        if(toWay == null) {
914                            toWay = w;
915                        }
916                        break;
917                    case "via":
918                        if(via == null) {
919                            via = w;
920                        }
921                    }
922                } else if(m.isNode()) {
923                    Node n = m.getNode();
924                    if("via".equals(m.getRole()) && via == null) {
925                        via = n;
926                    }
927                }
928            }
929        }
930
931        if (fromWay == null || toWay == null || via == null)
932            return;
933
934        Node viaNode;
935        if(via instanceof Node)
936        {
937            viaNode = (Node) via;
938            if(!fromWay.isFirstLastNode(viaNode))
939                return;
940        }
941        else
942        {
943            Way viaWay = (Way) via;
944            Node firstNode = viaWay.firstNode();
945            Node lastNode = viaWay.lastNode();
946            Boolean onewayvia = false;
947
948            String onewayviastr = viaWay.get("oneway");
949            if(onewayviastr != null)
950            {
951                if("-1".equals(onewayviastr)) {
952                    onewayvia = true;
953                    Node tmp = firstNode;
954                    firstNode = lastNode;
955                    lastNode = tmp;
956                } else {
957                    onewayvia = OsmUtils.getOsmBoolean(onewayviastr);
958                    if (onewayvia == null) {
959                        onewayvia = false;
960                    }
961                }
962            }
963
964            if(fromWay.isFirstLastNode(firstNode)) {
965                viaNode = firstNode;
966            } else if (!onewayvia && fromWay.isFirstLastNode(lastNode)) {
967                viaNode = lastNode;
968            } else
969                return;
970        }
971
972        /* find the "direct" nodes before the via node */
973        Node fromNode;
974        if(fromWay.firstNode() == via) {
975            fromNode = fromWay.getNode(1);
976        } else {
977            fromNode = fromWay.getNode(fromWay.getNodesCount()-2);
978        }
979
980        Point pFrom = nc.getPoint(fromNode);
981        Point pVia = nc.getPoint(viaNode);
982
983        /* starting from via, go back the "from" way a few pixels
984           (calculate the vector vx/vy with the specified length and the direction
985           away from the "via" node along the first segment of the "from" way)
986         */
987        double distanceFromVia=14;
988        double dx = (pFrom.x >= pVia.x) ? (pFrom.x - pVia.x) : (pVia.x - pFrom.x);
989        double dy = (pFrom.y >= pVia.y) ? (pFrom.y - pVia.y) : (pVia.y - pFrom.y);
990
991        double fromAngle;
992        if(dx == 0.0) {
993            fromAngle = Math.PI/2;
994        } else {
995            fromAngle = Math.atan(dy / dx);
996        }
997        double fromAngleDeg = Math.toDegrees(fromAngle);
998
999        double vx = distanceFromVia * Math.cos(fromAngle);
1000        double vy = distanceFromVia * Math.sin(fromAngle);
1001
1002        if(pFrom.x < pVia.x) {
1003            vx = -vx;
1004        }
1005        if(pFrom.y < pVia.y) {
1006            vy = -vy;
1007        }
1008
1009        /* go a few pixels away from the way (in a right angle)
1010           (calculate the vx2/vy2 vector with the specified length and the direction
1011           90degrees away from the first segment of the "from" way)
1012         */
1013        double distanceFromWay=10;
1014        double vx2 = 0;
1015        double vy2 = 0;
1016        double iconAngle = 0;
1017
1018        if(pFrom.x >= pVia.x && pFrom.y >= pVia.y) {
1019            if(!leftHandTraffic) {
1020                vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
1021                vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
1022            } else {
1023                vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
1024                vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
1025            }
1026            iconAngle = 270+fromAngleDeg;
1027        }
1028        if(pFrom.x < pVia.x && pFrom.y >= pVia.y) {
1029            if(!leftHandTraffic) {
1030                vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
1031                vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
1032            } else {
1033                vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
1034                vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
1035            }
1036            iconAngle = 90-fromAngleDeg;
1037        }
1038        if(pFrom.x < pVia.x && pFrom.y < pVia.y) {
1039            if(!leftHandTraffic) {
1040                vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
1041                vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
1042            } else {
1043                vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
1044                vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
1045            }
1046            iconAngle = 90+fromAngleDeg;
1047        }
1048        if(pFrom.x >= pVia.x && pFrom.y < pVia.y) {
1049            if(!leftHandTraffic) {
1050                vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
1051                vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
1052            } else {
1053                vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
1054                vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
1055            }
1056            iconAngle = 270-fromAngleDeg;
1057        }
1058
1059        drawRestriction(isInactiveMode || r.isDisabled() ? icon.getDisabled() : icon.getImage(),
1060                pVia, vx, vx2, vy, vy2, iconAngle, r.isSelected());
1061    }
1062
1063    public void drawTextOnPath(Way way, TextElement text) {
1064        if (way == null || text == null)
1065            return;
1066        String name = text.getString(way);
1067        if (name == null || name.isEmpty())
1068            return;
1069
1070        FontMetrics fontMetrics = g.getFontMetrics(text.font);
1071        Rectangle2D rec = fontMetrics.getStringBounds(name, g);
1072
1073        Rectangle bounds = g.getClipBounds();
1074
1075        Polygon poly = new Polygon();
1076        Point lastPoint = null;
1077        Iterator<Node> it = way.getNodes().iterator();
1078        double pathLength = 0;
1079        long dx, dy;
1080
1081        // find half segments that are long enough to draw text on
1082        // (don't draw text over the cross hair in the center of each segment)
1083        List<Double> longHalfSegmentStart = new ArrayList<>(); // start point of half segment (as length along the way)
1084        List<Double> longHalfSegmentEnd = new ArrayList<>(); // end point of half segment (as length along the way)
1085        List<Double> longHalfsegmentQuality = new ArrayList<>(); // quality factor (off screen / partly on screen / fully on screen)
1086
1087        while (it.hasNext()) {
1088            Node n = it.next();
1089            Point p = nc.getPoint(n);
1090            poly.addPoint(p.x, p.y);
1091
1092            if(lastPoint != null) {
1093                dx = p.x - lastPoint.x;
1094                dy = p.y - lastPoint.y;
1095                double segmentLength = Math.sqrt(dx*dx + dy*dy);
1096                if (segmentLength > 2*(rec.getWidth()+4)) {
1097                    Point center = new Point((lastPoint.x + p.x)/2, (lastPoint.y + p.y)/2);
1098                    double q = 0;
1099                    if (bounds != null) {
1100                        if (bounds.contains(lastPoint) && bounds.contains(center)) {
1101                            q = 2;
1102                        } else if (bounds.contains(lastPoint) || bounds.contains(center)) {
1103                            q = 1;
1104                        }
1105                    }
1106                    longHalfSegmentStart.add(pathLength);
1107                    longHalfSegmentEnd.add(pathLength + segmentLength / 2);
1108                    longHalfsegmentQuality.add(q);
1109
1110                    q = 0;
1111                    if (bounds != null) {
1112                        if (bounds.contains(center) && bounds.contains(p)) {
1113                            q = 2;
1114                        } else if (bounds.contains(center) || bounds.contains(p)) {
1115                            q = 1;
1116                        }
1117                    }
1118                    longHalfSegmentStart.add(pathLength + segmentLength / 2);
1119                    longHalfSegmentEnd.add(pathLength + segmentLength);
1120                    longHalfsegmentQuality.add(q);
1121                }
1122                pathLength += segmentLength;
1123            }
1124            lastPoint = p;
1125        }
1126
1127        if (rec.getWidth() > pathLength)
1128            return;
1129
1130        double t1, t2;
1131
1132        if (!longHalfSegmentStart.isEmpty()) {
1133            if (way.getNodesCount() == 2) {
1134                // For 2 node ways, the two half segments are exactly
1135                // the same size and distance from the center.
1136                // Prefer the first one for consistency.
1137                longHalfsegmentQuality.set(0, longHalfsegmentQuality.get(0) + 0.5);
1138            }
1139
1140            // find the long half segment that is closest to the center of the way
1141            // candidates with higher quality value are preferred
1142            double bestStart = Double.NaN;
1143            double bestEnd = Double.NaN;
1144            double bestDistanceToCenter = Double.MAX_VALUE;
1145            double bestQuality = -1;
1146            for (int i=0; i<longHalfSegmentStart.size(); i++) {
1147                double start = longHalfSegmentStart.get(i);
1148                double end = longHalfSegmentEnd.get(i);
1149                double dist = Math.abs(0.5 * (end + start) - 0.5 * pathLength);
1150                if (longHalfsegmentQuality.get(i) > bestQuality || (dist < bestDistanceToCenter && longHalfsegmentQuality.get(i) == bestQuality)) {
1151                    bestStart = start;
1152                    bestEnd = end;
1153                    bestDistanceToCenter = dist;
1154                    bestQuality = longHalfsegmentQuality.get(i);
1155                }
1156            }
1157            double remaining = bestEnd - bestStart - rec.getWidth(); // total space left and right from the text
1158            // The space left and right of the text should be distributed 20% - 80% (towards the center),
1159            // but the smaller space should not be less than 7 px.
1160            // However, if the total remaining space is less than 14 px, then distribute it evenly.
1161            double smallerSpace = Math.min(Math.max(0.2 * remaining, 7), 0.5 * remaining);
1162            if ((bestEnd + bestStart)/2 < pathLength/2) {
1163                t2 = bestEnd - smallerSpace;
1164                t1 = t2 - rec.getWidth();
1165            } else {
1166                t1 = bestStart + smallerSpace;
1167                t2 = t1 + rec.getWidth();
1168            }
1169        } else {
1170            // doesn't fit into one half-segment -> just put it in the center of the way
1171            t1 = pathLength/2 - rec.getWidth()/2;
1172            t2 = pathLength/2 + rec.getWidth()/2;
1173        }
1174        t1 /= pathLength;
1175        t2 /= pathLength;
1176
1177        double[] p1 = pointAt(t1, poly, pathLength);
1178        double[] p2 = pointAt(t2, poly, pathLength);
1179
1180        if (p1 == null || p2 == null)
1181            return;
1182
1183        double angleOffset;
1184        double offsetSign;
1185        double tStart;
1186
1187        if (p1[0] < p2[0] &&
1188                p1[2] < Math.PI/2 &&
1189                p1[2] > -Math.PI/2) {
1190            angleOffset = 0;
1191            offsetSign = 1;
1192            tStart = t1;
1193        } else {
1194            angleOffset = Math.PI;
1195            offsetSign = -1;
1196            tStart = t2;
1197        }
1198
1199        FontRenderContext frc = g.getFontRenderContext();
1200        GlyphVector gv = text.font.createGlyphVector(frc, name);
1201
1202        for (int i=0; i<gv.getNumGlyphs(); ++i) {
1203            Rectangle2D rect = gv.getGlyphLogicalBounds(i).getBounds2D();
1204            double t = tStart + offsetSign * (rect.getX() + rect.getWidth()/2) / pathLength;
1205            double[] p = pointAt(t, poly, pathLength);
1206            if (p != null) {
1207                AffineTransform trfm = AffineTransform.getTranslateInstance(p[0] - rect.getX(), p[1]);
1208                trfm.rotate(p[2]+angleOffset);
1209                double off = -rect.getY() - rect.getHeight()/2 + text.yOffset;
1210                trfm.translate(-rect.getWidth()/2, off);
1211                if (isGlyphVectorDoubleTranslationBug(text.font)) {
1212                    // scale the translation components by one half
1213                    AffineTransform tmp = AffineTransform.getTranslateInstance(-0.5 * trfm.getTranslateX(), -0.5 * trfm.getTranslateY());
1214                    tmp.concatenate(trfm);
1215                    trfm = tmp;
1216                }
1217                gv.setGlyphTransform(i, trfm);
1218            }
1219        }
1220        displayText(gv, null, 0, 0, way.isDisabled(), text);
1221    }
1222
1223    /**
1224     * draw way
1225     * @param showOrientation show arrows that indicate the technical orientation of
1226     *              the way (defined by order of nodes)
1227     * @param showOneway show symbols that indicate the direction of the feature,
1228     *              e.g. oneway street or waterway
1229     * @param onewayReversed for oneway=-1 and similar
1230     */
1231    public void drawWay(Way way, Color color, BasicStroke line, BasicStroke dashes, Color dashedColor, float offset,
1232            boolean showOrientation, boolean showHeadArrowOnly,
1233            boolean showOneway, boolean onewayReversed) {
1234
1235        GeneralPath path = new GeneralPath();
1236        GeneralPath orientationArrows = showOrientation ? new GeneralPath() : null;
1237        GeneralPath onewayArrows = showOneway ? new GeneralPath() : null;
1238        GeneralPath onewayArrowsCasing = showOneway ? new GeneralPath() : null;
1239        Rectangle bounds = g.getClipBounds();
1240        if (bounds != null) {
1241            // avoid arrow heads at the border
1242            bounds.grow(100, 100);
1243        }
1244
1245        double wayLength = 0;
1246        Point lastPoint = null;
1247        boolean initialMoveToNeeded = true;
1248        List<Node> wayNodes = way.getNodes();
1249        if (wayNodes.size() < 2) return;
1250
1251        // only highlight the segment if the way itself is not highlighted
1252        if (!way.isHighlighted() && highlightWaySegments != null) {
1253            GeneralPath highlightSegs = null;
1254            for (WaySegment ws : highlightWaySegments) {
1255                if (ws.way != way || ws.lowerIndex < offset) {
1256                    continue;
1257                }
1258                if(highlightSegs == null) {
1259                    highlightSegs = new GeneralPath();
1260                }
1261
1262                Point p1 = nc.getPoint(ws.getFirstNode());
1263                Point p2 = nc.getPoint(ws.getSecondNode());
1264                highlightSegs.moveTo(p1.x, p1.y);
1265                highlightSegs.lineTo(p2.x, p2.y);
1266            }
1267
1268            drawPathHighlight(highlightSegs, line);
1269        }
1270
1271        Iterator<Point> it = new OffsetIterator(wayNodes, offset);
1272        while (it.hasNext()) {
1273            Point p = it.next();
1274            if (lastPoint != null) {
1275                Point p1 = lastPoint;
1276                Point p2 = p;
1277
1278                /**
1279                 * Do custom clipping to work around openjdk bug. It leads to
1280                 * drawing artefacts when zooming in a lot. (#4289, #4424)
1281                 * (Looks like int overflow.)
1282                 */
1283                LineClip clip = new LineClip(p1, p2, bounds);
1284                if (clip.execute()) {
1285                    if (!p1.equals(clip.getP1())) {
1286                        p1 = clip.getP1();
1287                        path.moveTo(p1.x, p1.y);
1288                    } else if (initialMoveToNeeded) {
1289                        initialMoveToNeeded = false;
1290                        path.moveTo(p1.x, p1.y);
1291                    }
1292                    p2 = clip.getP2();
1293                    path.lineTo(p2.x, p2.y);
1294
1295                    /* draw arrow */
1296                    if (showHeadArrowOnly ? !it.hasNext() : showOrientation) {
1297                        final double segmentLength = p1.distance(p2);
1298                        if (segmentLength != 0.0) {
1299                            final double l =  (10. + line.getLineWidth()) / segmentLength;
1300
1301                            final double sx = l * (p1.x - p2.x);
1302                            final double sy = l * (p1.y - p2.y);
1303
1304                            orientationArrows.moveTo (p2.x + cosPHI * sx - sinPHI * sy, p2.y + sinPHI * sx + cosPHI * sy);
1305                            orientationArrows.lineTo(p2.x, p2.y);
1306                            orientationArrows.lineTo (p2.x + cosPHI * sx + sinPHI * sy, p2.y - sinPHI * sx + cosPHI * sy);
1307                        }
1308                    }
1309                    if (showOneway) {
1310                        final double segmentLength = p1.distance(p2);
1311                        if (segmentLength != 0.0) {
1312                            final double nx = (p2.x - p1.x) / segmentLength;
1313                            final double ny = (p2.y - p1.y) / segmentLength;
1314
1315                            final double interval = 60;
1316                            // distance from p1
1317                            double dist = interval - (wayLength % interval);
1318
1319                            while (dist < segmentLength) {
1320                                for (int i=0; i<2; ++i) {
1321                                    float onewaySize = i == 0 ? 3f : 2f;
1322                                    GeneralPath onewayPath = i == 0 ? onewayArrowsCasing : onewayArrows;
1323
1324                                    // scale such that border is 1 px
1325                                    final double fac = - (onewayReversed ? -1 : 1) * onewaySize * (1 + sinPHI) / (sinPHI * cosPHI);
1326                                    final double sx = nx * fac;
1327                                    final double sy = ny * fac;
1328
1329                                    // Attach the triangle at the incenter and not at the tip.
1330                                    // Makes the border even at all sides.
1331                                    final double x = p1.x + nx * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI));
1332                                    final double y = p1.y + ny * (dist + (onewayReversed ? -1 : 1) * (onewaySize / sinPHI));
1333
1334                                    onewayPath.moveTo(x, y);
1335                                    onewayPath.lineTo (x + cosPHI * sx - sinPHI * sy, y + sinPHI * sx + cosPHI * sy);
1336                                    onewayPath.lineTo (x + cosPHI * sx + sinPHI * sy, y - sinPHI * sx + cosPHI * sy);
1337                                    onewayPath.lineTo(x, y);
1338                                }
1339                                dist += interval;
1340                            }
1341                        }
1342                        wayLength += segmentLength;
1343                    }
1344                }
1345            }
1346            lastPoint = p;
1347        }
1348        if(way.isHighlighted()) {
1349            drawPathHighlight(path, line);
1350        }
1351        displaySegments(path, orientationArrows, onewayArrows, onewayArrowsCasing, color, line, dashes, dashedColor);
1352    }
1353
1354    public double getCircum() {
1355        return circum;
1356    }
1357
1358    @Override
1359    public void getColors() {
1360        super.getColors();
1361        this.highlightColorTransparent = new Color(highlightColor.getRed(), highlightColor.getGreen(), highlightColor.getBlue(), 100);
1362        this.backgroundColor = PaintColors.getBackgroundColor();
1363    }
1364
1365    @Override
1366    public void getSettings(boolean virtual) {
1367        super.getSettings(virtual);
1368        paintSettings = MapPaintSettings.INSTANCE;
1369
1370        circum = nc.getDist100Pixel();
1371
1372        leftHandTraffic = Main.pref.getBoolean("mappaint.lefthandtraffic", false);
1373
1374        useStrokes = paintSettings.getUseStrokesDistance() > circum;
1375        showNames = paintSettings.getShowNamesDistance() > circum;
1376        showIcons = paintSettings.getShowIconsDistance() > circum;
1377        isOutlineOnly = paintSettings.isOutlineOnly();
1378        orderFont = new Font(Main.pref.get("mappaint.font", "Droid Sans"), Font.PLAIN, Main.pref.getInteger("mappaint.fontsize", 8));
1379
1380        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
1381                Main.pref.getBoolean("mappaint.use-antialiasing", true) ?
1382                        RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
1383
1384        highlightLineWidth = Main.pref.getInteger("mappaint.highlight.width", 4);
1385        highlightPointRadius = Main.pref.getInteger("mappaint.highlight.radius", 7);
1386        widerHighlight = Main.pref.getInteger("mappaint.highlight.bigger-increment", 5);
1387        highlightStep = Main.pref.getInteger("mappaint.highlight.step", 4);
1388    }
1389
1390    private Path2D.Double getPath(Way w) {
1391        Path2D.Double path = new Path2D.Double();
1392        boolean initial = true;
1393        for (Node n : w.getNodes()) {
1394            EastNorth p = n.getEastNorth();
1395            if (p != null) {
1396                if (initial) {
1397                    path.moveTo(p.getX(), p.getY());
1398                    initial = false;
1399                } else {
1400                    path.lineTo(p.getX(), p.getY());
1401                }
1402            }
1403        }
1404        return path;
1405    }
1406
1407    private boolean isAreaVisible(Path2D.Double area) {
1408        Rectangle2D bounds = area.getBounds2D();
1409        if (bounds.isEmpty()) return false;
1410        Point2D p = nc.getPoint2D(new EastNorth(bounds.getX(), bounds.getY()));
1411        if (p.getX() > nc.getWidth()) return false;
1412        if (p.getY() < 0) return false;
1413        p = nc.getPoint2D(new EastNorth(bounds.getX() + bounds.getWidth(), bounds.getY() + bounds.getHeight()));
1414        if (p.getX() < 0) return false;
1415        if (p.getY() > nc.getHeight()) return false;
1416        return true;
1417    }
1418
1419    public boolean isInactiveMode() {
1420        return isInactiveMode;
1421    }
1422
1423    public boolean isShowIcons() {
1424        return showIcons;
1425    }
1426
1427    public boolean isShowNames() {
1428        return showNames;
1429    }
1430
1431    private double[] pointAt(double t, Polygon poly, double pathLength) {
1432        double totalLen = t * pathLength;
1433        double curLen = 0;
1434        long dx, dy;
1435        double segLen;
1436
1437        // Yes, it is inefficient to iterate from the beginning for each glyph.
1438        // Can be optimized if it turns out to be slow.
1439        for (int i = 1; i < poly.npoints; ++i) {
1440            dx = poly.xpoints[i] - poly.xpoints[i-1];
1441            dy = poly.ypoints[i] - poly.ypoints[i-1];
1442            segLen = Math.sqrt(dx*dx + dy*dy);
1443            if (totalLen > curLen + segLen) {
1444                curLen += segLen;
1445                continue;
1446            }
1447            return new double[] {
1448                    poly.xpoints[i-1]+(totalLen - curLen)/segLen*dx,
1449                    poly.ypoints[i-1]+(totalLen - curLen)/segLen*dy,
1450                    Math.atan2(dy, dx)};
1451        }
1452        return null;
1453    }
1454
1455    private class ComputeStyleListWorker implements Callable<List<StyleRecord>>, Visitor {
1456        private final List<? extends OsmPrimitive> input;
1457        private final int from;
1458        private final int to;
1459        private final List<StyleRecord> output;
1460
1461        private final ElemStyles styles = MapPaintStyles.getStyles();
1462
1463        private final boolean drawArea = circum <= Main.pref.getInteger("mappaint.fillareas", 10000000);
1464        private final boolean drawMultipolygon = drawArea && Main.pref.getBoolean("mappaint.multipolygon", true);
1465        private final boolean drawRestriction = Main.pref.getBoolean("mappaint.restriction", true);
1466
1467        /**
1468         * Constructs a new {@code ComputeStyleListWorker}.
1469         * @param input the primitives to process
1470         * @param from first index of <code>input</code> to use
1471         * @param to last index + 1
1472         * @param output the list of styles to which styles will be added
1473         */
1474        public ComputeStyleListWorker(final List<? extends OsmPrimitive> input, int from, int to, List<StyleRecord> output) {
1475            this.input = input;
1476            this.from = from;
1477            this.to = to;
1478            this.output = output;
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));
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}