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