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