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