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