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