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() || (paintSettings.isShowOrderNumberOnSelectedWay() && selected))
119                && !painter.isInactiveMode()) {
120            int orderNumber = 0;
121            lastN = null;
122            for (Node n : w.getNodes()) {
123                if (lastN != null) {
124                    orderNumber++;
125                    painter.drawOrderNumber(lastN, n, orderNumber, myColor);
126                }
127                lastN = n;
128            }
129        }
130    }
131
132    @Override
133    public boolean isProperLineStyle() {
134        return !isModifier;
135    }
136
137    public String linejoinToString(int linejoin) {
138        switch (linejoin) {
139            case BasicStroke.JOIN_BEVEL: return "bevel";
140            case BasicStroke.JOIN_ROUND: return "round";
141            case BasicStroke.JOIN_MITER: return "miter";
142            default: return null;
143        }
144    }
145
146    public String linecapToString(int linecap) {
147        switch (linecap) {
148            case BasicStroke.CAP_BUTT: return "none";
149            case BasicStroke.CAP_ROUND: return "round";
150            case BasicStroke.CAP_SQUARE: return "square";
151            default: return null;
152        }
153    }
154
155    @Override
156    public boolean equals(Object obj) {
157        if (obj == null || getClass() != obj.getClass())
158            return false;
159        if (!super.equals(obj))
160            return false;
161        final LineElement other = (LineElement) obj;
162        return Objects.equals(line, other.line) &&
163            Objects.equals(color, other.color) &&
164            Objects.equals(dashesLine, other.dashesLine) &&
165            Objects.equals(dashesBackground, other.dashesBackground) &&
166            offset == other.offset &&
167            realWidth == other.realWidth &&
168            wayDirectionArrows == other.wayDirectionArrows;
169    }
170
171    @Override
172    public int hashCode() {
173        return Objects.hash(super.hashCode(), line, color, dashesBackground, offset, realWidth, wayDirectionArrows, dashesLine);
174    }
175
176    @Override
177    public String toString() {
178        return "LineElemStyle{" + super.toString() + "width=" + line.getLineWidth() +
179            " realWidth=" + realWidth + " color=" + Utils.toString(color) +
180            " dashed=" + Arrays.toString(line.getDashArray()) +
181            (line.getDashPhase() == 0 ? "" : " dashesOffses=" + line.getDashPhase()) +
182            " dashedColor=" + Utils.toString(dashesBackground) +
183            " linejoin=" + linejoinToString(line.getLineJoin()) +
184            " linecap=" + linecapToString(line.getEndCap()) +
185            (offset == 0 ? "" : " offset=" + offset) +
186            '}';
187    }
188
189    /**
190     * Creates a simple line with default widt.
191     * @param color The color to use
192     * @param isAreaEdge If this is an edge for an area. Edges are drawn at lower Z-Index.
193     * @return The line style.
194     */
195    public static LineElement createSimpleLineStyle(Color color, boolean isAreaEdge) {
196        MultiCascade mc = new MultiCascade();
197        Cascade c = mc.getOrCreateCascade("default");
198        c.put(WIDTH, Keyword.DEFAULT);
199        c.put(COLOR, color != null ? color : PaintColors.UNTAGGED.get());
200        c.put(OPACITY, 1f);
201        if (isAreaEdge) {
202            c.put(Z_INDEX, -3f);
203        }
204        Way w = new Way();
205        return createLine(new Environment(w, mc, "default", null));
206    }
207
208    public static LineElement createLine(Environment env) {
209        return createImpl(env, LineType.NORMAL);
210    }
211
212    public static LineElement createLeftCasing(Environment env) {
213        LineElement leftCasing = createImpl(env, LineType.LEFT_CASING);
214        if (leftCasing != null) {
215            leftCasing.isModifier = true;
216        }
217        return leftCasing;
218    }
219
220    public static LineElement createRightCasing(Environment env) {
221        LineElement rightCasing = createImpl(env, LineType.RIGHT_CASING);
222        if (rightCasing != null) {
223            rightCasing.isModifier = true;
224        }
225        return rightCasing;
226    }
227
228    public static LineElement createCasing(Environment env) {
229        LineElement casing = createImpl(env, LineType.CASING);
230        if (casing != null) {
231            casing.isModifier = true;
232        }
233        return casing;
234    }
235
236    private static LineElement createImpl(Environment env, LineType type) {
237        Cascade c = env.mc.getCascade(env.layer);
238        Cascade cDef = env.mc.getCascade("default");
239        Float width = computeWidth(type, c, cDef);
240        if (width == null)
241            return null;
242
243        float realWidth = computeRealWidth(env, type, c);
244
245        Float offset = computeOffset(type, c, cDef, width);
246
247        int alpha = 255;
248        Color color = c.get(type.prefix + COLOR, null, Color.class);
249        if (color != null) {
250            alpha = color.getAlpha();
251        }
252        if (type == LineType.NORMAL && color == null) {
253            color = c.get(FILL_COLOR, null, Color.class);
254        }
255        if (color == null) {
256            color = PaintColors.UNTAGGED.get();
257        }
258
259        Integer pAlpha = Utils.colorFloat2int(c.get(type.prefix + OPACITY, null, Float.class));
260        if (pAlpha != null) {
261            alpha = pAlpha;
262        }
263        color = new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha);
264
265        float[] dashes = c.get(type.prefix + DASHES, null, float[].class, true);
266        if (dashes != null) {
267            boolean hasPositive = false;
268            for (float f : dashes) {
269                if (f > 0) {
270                    hasPositive = true;
271                }
272                if (f < 0) {
273                    dashes = null;
274                    break;
275                }
276            }
277            if (!hasPositive || (dashes != null && dashes.length == 0)) {
278                dashes = null;
279            }
280        }
281        float dashesOffset = c.get(type.prefix + DASHES_OFFSET, 0f, Float.class);
282        Color dashesBackground = c.get(type.prefix + DASHES_BACKGROUND_COLOR, null, Color.class);
283        if (dashesBackground != null) {
284            pAlpha = Utils.colorFloat2int(c.get(type.prefix + DASHES_BACKGROUND_OPACITY, null, Float.class));
285            if (pAlpha != null) {
286                alpha = pAlpha;
287            }
288            dashesBackground = new Color(dashesBackground.getRed(), dashesBackground.getGreen(),
289                    dashesBackground.getBlue(), alpha);
290        }
291
292        Integer cap = null;
293        Keyword capKW = c.get(type.prefix + LINECAP, null, Keyword.class);
294        if (capKW != null) {
295            if ("none".equals(capKW.val)) {
296                cap = BasicStroke.CAP_BUTT;
297            } else if ("round".equals(capKW.val)) {
298                cap = BasicStroke.CAP_ROUND;
299            } else if ("square".equals(capKW.val)) {
300                cap = BasicStroke.CAP_SQUARE;
301            }
302        }
303        if (cap == null) {
304            cap = dashes != null ? BasicStroke.CAP_BUTT : BasicStroke.CAP_ROUND;
305        }
306
307        Integer join = null;
308        Keyword joinKW = c.get(type.prefix + LINEJOIN, null, Keyword.class);
309        if (joinKW != null) {
310            if ("round".equals(joinKW.val)) {
311                join = BasicStroke.JOIN_ROUND;
312            } else if ("miter".equals(joinKW.val)) {
313                join = BasicStroke.JOIN_MITER;
314            } else if ("bevel".equals(joinKW.val)) {
315                join = BasicStroke.JOIN_BEVEL;
316            }
317        }
318        if (join == null) {
319            join = BasicStroke.JOIN_ROUND;
320        }
321
322        float miterlimit = c.get(type.prefix + MITERLIMIT, 10f, Float.class);
323        if (miterlimit < 1f) {
324            miterlimit = 10f;
325        }
326
327        BasicStroke line = new BasicStroke(width, cap, join, miterlimit, dashes, dashesOffset);
328        BasicStroke dashesLine = null;
329
330        if (dashes != null && dashesBackground != null) {
331            float[] dashes2 = new float[dashes.length];
332            System.arraycopy(dashes, 0, dashes2, 1, dashes.length - 1);
333            dashes2[0] = dashes[dashes.length-1];
334            dashesLine = new BasicStroke(width, cap, join, miterlimit, dashes2, dashes2[0] + dashesOffset);
335        }
336
337        boolean wayDirectionArrows = c.get(type.prefix + WAY_DIRECTION_ARROWS, env.osm.isSelected(), Boolean.class);
338
339        return new LineElement(c, type.defaultMajorZIndex, line, color, dashesLine, dashesBackground,
340                offset, realWidth, wayDirectionArrows);
341    }
342
343    private static Float computeWidth(LineType type, Cascade c, Cascade cDef) {
344        Float width;
345        switch (type) {
346            case NORMAL:
347                width = getWidth(c, WIDTH, getWidth(cDef, WIDTH, null));
348                break;
349            case CASING:
350                Float casingWidth = c.get(type.prefix + WIDTH, null, Float.class, true);
351                if (casingWidth == null) {
352                    RelativeFloat relCasingWidth = c.get(type.prefix + WIDTH, null, RelativeFloat.class, true);
353                    if (relCasingWidth != null) {
354                        casingWidth = relCasingWidth.val / 2;
355                    }
356                }
357                if (casingWidth == null)
358                    return null;
359                width = getWidth(c, WIDTH, getWidth(cDef, WIDTH, null));
360                if (width == null) {
361                    width = 0f;
362                }
363                width += 2 * casingWidth;
364                break;
365            case LEFT_CASING:
366            case RIGHT_CASING:
367                width = getWidth(c, type.prefix + WIDTH, null);
368                break;
369            default:
370                throw new AssertionError();
371        }
372        return width;
373    }
374
375    private static float computeRealWidth(Environment env, LineType type, Cascade c) {
376        float realWidth = c.get(type.prefix + REAL_WIDTH, 0f, Float.class);
377        if (realWidth > 0 && MapPaintSettings.INSTANCE.isUseRealWidth()) {
378
379            /* if we have a "width" tag, try use it */
380            String widthTag = env.osm.get("width");
381            if (widthTag == null) {
382                widthTag = env.osm.get("est_width");
383            }
384            if (widthTag != null) {
385                try {
386                    realWidth = Float.parseFloat(widthTag);
387                } catch (NumberFormatException nfe) {
388                    Main.warn(nfe);
389                }
390            }
391        }
392        return realWidth;
393    }
394
395    private static Float computeOffset(LineType type, Cascade c, Cascade cDef, Float width) {
396        Float offset = c.get(OFFSET, 0f, Float.class);
397        switch (type) {
398            case NORMAL:
399                break;
400            case CASING:
401                offset += c.get(type.prefix + OFFSET, 0f, Float.class);
402                break;
403            case LEFT_CASING:
404            case RIGHT_CASING:
405                Float baseWidthOnDefault = getWidth(cDef, WIDTH, null);
406                Float baseWidth = getWidth(c, WIDTH, baseWidthOnDefault);
407                if (baseWidth == null || baseWidth < 2f) {
408                    baseWidth = 2f;
409                }
410                float casingOffset = c.get(type.prefix + OFFSET, 0f, Float.class);
411                casingOffset += baseWidth / 2 + width / 2;
412                /* flip sign for the right-casing-offset */
413                if (type == LineType.RIGHT_CASING) {
414                    casingOffset *= -1f;
415                }
416                offset += casingOffset;
417                break;
418        }
419        return offset;
420    }
421}