001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.mappaint.styleelement;
003
004import java.awt.BasicStroke;
005import java.awt.Color;
006import java.util.Arrays;
007import java.util.Objects;
008import java.util.Optional;
009
010import org.openstreetmap.josm.data.osm.Node;
011import org.openstreetmap.josm.data.osm.OsmPrimitive;
012import org.openstreetmap.josm.data.osm.Way;
013import org.openstreetmap.josm.data.osm.visitor.paint.MapPaintSettings;
014import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
015import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer;
016import org.openstreetmap.josm.gui.mappaint.Cascade;
017import org.openstreetmap.josm.gui.mappaint.Environment;
018import org.openstreetmap.josm.gui.mappaint.Keyword;
019import org.openstreetmap.josm.gui.mappaint.MultiCascade;
020import org.openstreetmap.josm.gui.mappaint.mapcss.Instruction.RelativeFloat;
021import org.openstreetmap.josm.tools.Logging;
022import org.openstreetmap.josm.tools.Utils;
023
024/**
025 * This is the style definition for a simple line.
026 */
027public class LineElement extends StyleElement {
028    /**
029     * The default style for any untagged way.
030     */
031    public static final LineElement UNTAGGED_WAY = createSimpleLineStyle(null, false);
032
033    /**
034     * The stroke used to paint the line
035     */
036    private final BasicStroke line;
037    /**
038     * The color of the line. Should not be accessed directly
039     */
040    public Color color;
041
042    /**
043     * The stroke used to paint the gaps between the dashes
044     */
045    private final BasicStroke dashesLine;
046    /**
047     * The secondary color of the line that is used for the gaps in dashed lines. Should not be accessed directly
048     */
049    public Color dashesBackground;
050    /**
051     * The dash offset. Should not be accessed directly
052     */
053    public float offset;
054    /**
055     * the real width of this line in meter. Should not be accessed directly
056     */
057    public float realWidth;
058    /**
059     * A flag indicating if the direction arrwos should be painted. Should not be accessed directly
060     */
061    public boolean wayDirectionArrows;
062
063    /**
064     * The type of this line
065     */
066    public enum LineType {
067        /**
068         * A normal line
069         */
070        NORMAL("", 3f),
071        /**
072         * A casing (line behind normal line, extended to the right/left)
073         */
074        CASING("casing-", 2f),
075        /**
076         * A casing, but only to the left
077         */
078        LEFT_CASING("left-casing-", 2.1f),
079        /**
080         * A casing, but only to the right
081         */
082        RIGHT_CASING("right-casing-", 2.1f);
083
084        /**
085         * The MapCSS line prefix used
086         */
087        public final String prefix;
088        /**
089         * The major z index to use during painting
090         */
091        public final float defaultMajorZIndex;
092
093        LineType(String prefix, float defaultMajorZindex) {
094            this.prefix = prefix;
095            this.defaultMajorZIndex = defaultMajorZindex;
096        }
097    }
098
099    protected LineElement(Cascade c, float defaultMajorZindex, BasicStroke line, Color color, BasicStroke dashesLine,
100            Color dashesBackground, float offset, float realWidth, boolean wayDirectionArrows) {
101        super(c, defaultMajorZindex);
102        this.line = line;
103        this.color = color;
104        this.dashesLine = dashesLine;
105        this.dashesBackground = dashesBackground;
106        this.offset = offset;
107        this.realWidth = realWidth;
108        this.wayDirectionArrows = wayDirectionArrows;
109    }
110
111    @Override
112    public void paintPrimitive(OsmPrimitive primitive, MapPaintSettings paintSettings, StyledMapRenderer painter,
113            boolean selected, boolean outermember, boolean member) {
114        /* show direction arrows, if draw.segment.relevant_directions_only is not set,
115        the way is tagged with a direction key
116        (even if the tag is negated as in oneway=false) or the way is selected */
117        boolean showOrientation;
118        if (defaultSelectedHandling) {
119            showOrientation = !isModifier && (selected || paintSettings.isShowDirectionArrow()) && !paintSettings.isUseRealWidth();
120        } else {
121            showOrientation = wayDirectionArrows;
122        }
123        boolean showOneway = !isModifier && !selected &&
124                !paintSettings.isUseRealWidth() &&
125                paintSettings.isShowOnewayArrow() && primitive.hasDirectionKeys();
126        boolean onewayReversed = primitive.reversedDirection();
127        /* head only takes over control if the option is true,
128        the direction should be shown at all and not only because it's selected */
129        boolean showOnlyHeadArrowOnly = showOrientation && !selected && paintSettings.isShowHeadArrowOnly();
130        Node lastN;
131
132        Color myDashedColor = dashesBackground;
133        BasicStroke myLine = line, myDashLine = dashesLine;
134        if (realWidth > 0 && paintSettings.isUseRealWidth() && !showOrientation) {
135            float myWidth = (int) (100 / (float) (painter.getCircum() / realWidth));
136            if (myWidth < line.getLineWidth()) {
137                myWidth = line.getLineWidth();
138            }
139            myLine = new BasicStroke(myWidth, line.getEndCap(), line.getLineJoin(),
140                    line.getMiterLimit(), line.getDashArray(), line.getDashPhase());
141            if (dashesLine != null) {
142                myDashLine = new BasicStroke(myWidth, dashesLine.getEndCap(), dashesLine.getLineJoin(),
143                        dashesLine.getMiterLimit(), dashesLine.getDashArray(), dashesLine.getDashPhase());
144            }
145        }
146
147        Color myColor = color;
148        if (defaultSelectedHandling && selected) {
149            myColor = paintSettings.getSelectedColor(color.getAlpha());
150        } else if (member || outermember) {
151            myColor = paintSettings.getRelationSelectedColor(color.getAlpha());
152        } else if (primitive.isDisabled()) {
153            myColor = paintSettings.getInactiveColor();
154            myDashedColor = paintSettings.getInactiveColor();
155        }
156
157        if (primitive instanceof Way) {
158            Way w = (Way) primitive;
159            painter.drawWay(w, myColor, myLine, myDashLine, myDashedColor, offset, showOrientation,
160                    showOnlyHeadArrowOnly, showOneway, onewayReversed);
161
162            if ((paintSettings.isShowOrderNumber() || (paintSettings.isShowOrderNumberOnSelectedWay() && selected))
163                    && !painter.isInactiveMode()) {
164                int orderNumber = 0;
165                lastN = null;
166                for (Node n : w.getNodes()) {
167                    if (lastN != null) {
168                        orderNumber++;
169                        painter.drawOrderNumber(lastN, n, orderNumber, myColor);
170                    }
171                    lastN = n;
172                }
173            }
174        }
175    }
176
177    @Override
178    public boolean isProperLineStyle() {
179        return !isModifier;
180    }
181
182    /**
183     * Converts a linejoin of a {@link BasicStroke} to a MapCSS string
184     * @param linejoin The linejoin
185     * @return The MapCSS string or <code>null</code> on error.
186     * @see BasicStroke#getLineJoin()
187     */
188    public String linejoinToString(int linejoin) {
189        switch (linejoin) {
190            case BasicStroke.JOIN_BEVEL: return "bevel";
191            case BasicStroke.JOIN_ROUND: return "round";
192            case BasicStroke.JOIN_MITER: return "miter";
193            default: return null;
194        }
195    }
196
197    /**
198     * Converts a linecap of a {@link BasicStroke} to a MapCSS string
199     * @param linecap The linecap
200     * @return The MapCSS string or <code>null</code> on error.
201     * @see BasicStroke#getEndCap()
202     */
203    public String linecapToString(int linecap) {
204        switch (linecap) {
205            case BasicStroke.CAP_BUTT: return "none";
206            case BasicStroke.CAP_ROUND: return "round";
207            case BasicStroke.CAP_SQUARE: return "square";
208            default: return null;
209        }
210    }
211
212    @Override
213    public boolean equals(Object obj) {
214        if (obj == null || getClass() != obj.getClass())
215            return false;
216        if (!super.equals(obj))
217            return false;
218        final LineElement other = (LineElement) obj;
219        return offset == other.offset &&
220               realWidth == other.realWidth &&
221               wayDirectionArrows == other.wayDirectionArrows &&
222               Objects.equals(line, other.line) &&
223               Objects.equals(color, other.color) &&
224               Objects.equals(dashesLine, other.dashesLine) &&
225               Objects.equals(dashesBackground, other.dashesBackground);
226    }
227
228    @Override
229    public int hashCode() {
230        return Objects.hash(super.hashCode(), line, color, dashesBackground, offset, realWidth, wayDirectionArrows, dashesLine);
231    }
232
233    @Override
234    public String toString() {
235        return "LineElemStyle{" + super.toString() + "width=" + line.getLineWidth() +
236            " realWidth=" + realWidth + " color=" + Utils.toString(color) +
237            " dashed=" + Arrays.toString(line.getDashArray()) +
238            (line.getDashPhase() == 0 ? "" : " dashesOffses=" + line.getDashPhase()) +
239            " dashedColor=" + Utils.toString(dashesBackground) +
240            " linejoin=" + linejoinToString(line.getLineJoin()) +
241            " linecap=" + linecapToString(line.getEndCap()) +
242            (offset == 0 ? "" : " offset=" + offset) +
243            '}';
244    }
245
246    /**
247     * Creates a simple line with default widt.
248     * @param color The color to use
249     * @param isAreaEdge If this is an edge for an area. Edges are drawn at lower Z-Index.
250     * @return The line style.
251     */
252    public static LineElement createSimpleLineStyle(Color color, boolean isAreaEdge) {
253        MultiCascade mc = new MultiCascade();
254        Cascade c = mc.getOrCreateCascade("default");
255        c.put(WIDTH, Keyword.DEFAULT);
256        c.put(COLOR, color != null ? color : PaintColors.UNTAGGED.get());
257        c.put(OPACITY, 1f);
258        if (isAreaEdge) {
259            c.put(Z_INDEX, -3f);
260        }
261        Way w = new Way();
262        return createLine(new Environment(w, mc, "default", null));
263    }
264
265    /**
266     * Create a line element from the given MapCSS environment
267     * @param env The environment
268     * @return The line element describing the line that should be painted, or <code>null</code> if none should be painted.
269     */
270    public static LineElement createLine(Environment env) {
271        return createImpl(env, LineType.NORMAL);
272    }
273
274    /**
275     * Create a line element for the left casing from the given MapCSS environment
276     * @param env The environment
277     * @return The line element describing the line that should be painted, or <code>null</code> if none should be painted.
278     */
279    public static LineElement createLeftCasing(Environment env) {
280        LineElement leftCasing = createImpl(env, LineType.LEFT_CASING);
281        if (leftCasing != null) {
282            leftCasing.isModifier = true;
283        }
284        return leftCasing;
285    }
286
287    /**
288     * Create a line element for the right casing from the given MapCSS environment
289     * @param env The environment
290     * @return The line element describing the line that should be painted, or <code>null</code> if none should be painted.
291     */
292    public static LineElement createRightCasing(Environment env) {
293        LineElement rightCasing = createImpl(env, LineType.RIGHT_CASING);
294        if (rightCasing != null) {
295            rightCasing.isModifier = true;
296        }
297        return rightCasing;
298    }
299
300    /**
301     * Create a line element for the casing from the given MapCSS environment
302     * @param env The environment
303     * @return The line element describing the line that should be painted, or <code>null</code> if none should be painted.
304     */
305    public static LineElement createCasing(Environment env) {
306        LineElement casing = createImpl(env, LineType.CASING);
307        if (casing != null) {
308            casing.isModifier = true;
309        }
310        return casing;
311    }
312
313    private static LineElement createImpl(Environment env, LineType type) {
314        Cascade c = env.mc.getCascade(env.layer);
315        Cascade cDef = env.mc.getCascade("default");
316        Float width = computeWidth(type, c, cDef);
317        if (width == null)
318            return null;
319
320        float realWidth = computeRealWidth(env, type, c);
321
322        Float offset = computeOffset(type, c, cDef, width);
323
324        int alpha = 255;
325        Color color = c.get(type.prefix + COLOR, null, Color.class);
326        if (color != null) {
327            alpha = color.getAlpha();
328        }
329        if (type == LineType.NORMAL && color == null) {
330            color = c.get(FILL_COLOR, null, Color.class);
331        }
332        if (color == null) {
333            color = PaintColors.UNTAGGED.get();
334        }
335
336        Integer pAlpha = Utils.colorFloat2int(c.get(type.prefix + OPACITY, null, Float.class));
337        if (pAlpha != null) {
338            alpha = pAlpha;
339        }
340        color = new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha);
341
342        float[] dashes = c.get(type.prefix + DASHES, null, float[].class, true);
343        if (dashes != null) {
344            boolean hasPositive = false;
345            for (float f : dashes) {
346                if (f > 0) {
347                    hasPositive = true;
348                }
349                if (f < 0) {
350                    dashes = null;
351                    break;
352                }
353            }
354            if (!hasPositive || (dashes != null && dashes.length == 0)) {
355                dashes = null;
356            }
357        }
358        float dashesOffset = c.get(type.prefix + DASHES_OFFSET, 0f, Float.class);
359        if (dashesOffset < 0f) {
360            Logging.warn("Found negative " + DASHES_OFFSET + ": " + dashesOffset);
361            dashesOffset = 0f;
362        }
363        Color dashesBackground = c.get(type.prefix + DASHES_BACKGROUND_COLOR, null, Color.class);
364        if (dashesBackground != null) {
365            pAlpha = Utils.colorFloat2int(c.get(type.prefix + DASHES_BACKGROUND_OPACITY, null, Float.class));
366            if (pAlpha != null) {
367                alpha = pAlpha;
368            }
369            dashesBackground = new Color(dashesBackground.getRed(), dashesBackground.getGreen(),
370                    dashesBackground.getBlue(), alpha);
371        }
372
373        Integer cap = null;
374        Keyword capKW = c.get(type.prefix + LINECAP, null, Keyword.class);
375        if (capKW != null) {
376            if ("none".equals(capKW.val)) {
377                cap = BasicStroke.CAP_BUTT;
378            } else if ("round".equals(capKW.val)) {
379                cap = BasicStroke.CAP_ROUND;
380            } else if ("square".equals(capKW.val)) {
381                cap = BasicStroke.CAP_SQUARE;
382            }
383        }
384        if (cap == null) {
385            cap = dashes != null ? BasicStroke.CAP_BUTT : BasicStroke.CAP_ROUND;
386        }
387
388        Integer join = null;
389        Keyword joinKW = c.get(type.prefix + LINEJOIN, null, Keyword.class);
390        if (joinKW != null) {
391            if ("round".equals(joinKW.val)) {
392                join = BasicStroke.JOIN_ROUND;
393            } else if ("miter".equals(joinKW.val)) {
394                join = BasicStroke.JOIN_MITER;
395            } else if ("bevel".equals(joinKW.val)) {
396                join = BasicStroke.JOIN_BEVEL;
397            }
398        }
399        if (join == null) {
400            join = BasicStroke.JOIN_ROUND;
401        }
402
403        float miterlimit = c.get(type.prefix + MITERLIMIT, 10f, Float.class);
404        if (miterlimit < 1f) {
405            miterlimit = 10f;
406        }
407
408        BasicStroke line = new BasicStroke(width, cap, join, miterlimit, dashes, dashesOffset);
409        BasicStroke dashesLine = null;
410
411        if (dashes != null && dashesBackground != null) {
412            float[] dashes2 = new float[dashes.length];
413            System.arraycopy(dashes, 0, dashes2, 1, dashes.length - 1);
414            dashes2[0] = dashes[dashes.length-1];
415            dashesLine = new BasicStroke(width, cap, join, miterlimit, dashes2, dashes2[0] + dashesOffset);
416        }
417
418        boolean wayDirectionArrows = c.get(type.prefix + WAY_DIRECTION_ARROWS, env.osm.isSelected(), Boolean.class);
419
420        return new LineElement(c, type.defaultMajorZIndex, line, color, dashesLine, dashesBackground,
421                offset, realWidth, wayDirectionArrows);
422    }
423
424    private static Float computeWidth(LineType type, Cascade c, Cascade cDef) {
425        Float width;
426        switch (type) {
427            case NORMAL:
428                width = getWidth(c, WIDTH, getWidth(cDef, WIDTH, null));
429                break;
430            case CASING:
431                Float casingWidth = c.get(type.prefix + WIDTH, null, Float.class, true);
432                if (casingWidth == null) {
433                    RelativeFloat relCasingWidth = c.get(type.prefix + WIDTH, null, RelativeFloat.class, true);
434                    if (relCasingWidth != null) {
435                        casingWidth = relCasingWidth.val / 2;
436                    }
437                }
438                if (casingWidth == null)
439                    return null;
440                width = Optional.ofNullable(getWidth(c, WIDTH, getWidth(cDef, WIDTH, null))).orElse(0f) + 2 * casingWidth;
441                break;
442            case LEFT_CASING:
443            case RIGHT_CASING:
444                width = getWidth(c, type.prefix + WIDTH, null);
445                break;
446            default:
447                throw new AssertionError();
448        }
449        return width;
450    }
451
452    private static float computeRealWidth(Environment env, LineType type, Cascade c) {
453        float realWidth = c.get(type.prefix + REAL_WIDTH, 0f, Float.class);
454        if (realWidth > 0 && MapPaintSettings.INSTANCE.isUseRealWidth()) {
455
456            /* if we have a "width" tag, try use it */
457            String widthTag = Optional.ofNullable(env.osm.get("width")).orElseGet(() -> env.osm.get("est_width"));
458            if (widthTag != null) {
459                try {
460                    realWidth = Float.parseFloat(widthTag);
461                } catch (NumberFormatException nfe) {
462                    Logging.warn(nfe);
463                }
464            }
465        }
466        return realWidth;
467    }
468
469    private static Float computeOffset(LineType type, Cascade c, Cascade cDef, Float width) {
470        Float offset = c.get(OFFSET, 0f, Float.class);
471        switch (type) {
472            case NORMAL:
473                break;
474            case CASING:
475                offset += c.get(type.prefix + OFFSET, 0f, Float.class);
476                break;
477            case LEFT_CASING:
478            case RIGHT_CASING:
479                Float baseWidthOnDefault = getWidth(cDef, WIDTH, null);
480                Float baseWidth = getWidth(c, WIDTH, baseWidthOnDefault);
481                if (baseWidth == null || baseWidth < 2f) {
482                    baseWidth = 2f;
483                }
484                float casingOffset = c.get(type.prefix + OFFSET, 0f, Float.class);
485                casingOffset += baseWidth / 2 + width / 2;
486                /* flip sign for the right-casing-offset */
487                if (type == LineType.RIGHT_CASING) {
488                    casingOffset *= -1f;
489                }
490                offset += casingOffset;
491                break;
492        }
493        return offset;
494    }
495}