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