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