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; 008import java.util.Optional; 009 010import org.openstreetmap.josm.data.osm.IPrimitive; 011import org.openstreetmap.josm.data.osm.Node; 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.Logging; 022import org.openstreetmap.josm.tools.Utils; 023 024/** 025 * This is the style definition for a simple line. 026 */ 027public class LineElement extends StyleElement { 028 /** 029 * The default style for any untagged way. 030 */ 031 public static final LineElement UNTAGGED_WAY = createSimpleLineStyle(null, false); 032 033 /** 034 * The stroke used to paint the line 035 */ 036 private final BasicStroke line; 037 /** 038 * The color of the line. Should not be accessed directly 039 */ 040 public Color color; 041 042 /** 043 * The stroke used to paint the gaps between the dashes 044 */ 045 private final BasicStroke dashesLine; 046 /** 047 * The secondary color of the line that is used for the gaps in dashed lines. Should not be accessed directly 048 */ 049 public Color dashesBackground; 050 /** 051 * The dash offset. Should not be accessed directly 052 */ 053 public float offset; 054 /** 055 * the real width of this line in meter. Should not be accessed directly 056 */ 057 public float realWidth; 058 /** 059 * A flag indicating if the direction arrwos should be painted. Should not be accessed directly 060 */ 061 public boolean wayDirectionArrows; 062 063 /** 064 * The type of this line 065 */ 066 public enum LineType { 067 /** 068 * A normal line 069 */ 070 NORMAL("", 3f), 071 /** 072 * A casing (line behind normal line, extended to the right/left) 073 */ 074 CASING("casing-", 2f), 075 /** 076 * A casing, but only to the left 077 */ 078 LEFT_CASING("left-casing-", 2.1f), 079 /** 080 * A casing, but only to the right 081 */ 082 RIGHT_CASING("right-casing-", 2.1f); 083 084 /** 085 * The MapCSS line prefix used 086 */ 087 public final String prefix; 088 /** 089 * The major z index to use during painting 090 */ 091 public final float defaultMajorZIndex; 092 093 LineType(String prefix, float defaultMajorZindex) { 094 this.prefix = prefix; 095 this.defaultMajorZIndex = defaultMajorZindex; 096 } 097 } 098 099 protected LineElement(Cascade c, float defaultMajorZindex, BasicStroke line, Color color, BasicStroke dashesLine, 100 Color dashesBackground, float offset, float realWidth, boolean wayDirectionArrows) { 101 super(c, defaultMajorZindex); 102 this.line = line; 103 this.color = color; 104 this.dashesLine = dashesLine; 105 this.dashesBackground = dashesBackground; 106 this.offset = offset; 107 this.realWidth = realWidth; 108 this.wayDirectionArrows = wayDirectionArrows; 109 } 110 111 @Override 112 public void paintPrimitive(IPrimitive primitive, MapPaintSettings paintSettings, StyledMapRenderer painter, 113 boolean selected, boolean outermember, boolean member) { 114 /* show direction arrows, if draw.segment.relevant_directions_only is not set, 115 the way is tagged with a direction key 116 (even if the tag is negated as in oneway=false) or the way is selected */ 117 boolean showOrientation; 118 if (defaultSelectedHandling) { 119 showOrientation = !isModifier && (selected || paintSettings.isShowDirectionArrow()) && !paintSettings.isUseRealWidth(); 120 } else { 121 showOrientation = wayDirectionArrows; 122 } 123 boolean showOneway = !isModifier && !selected && 124 !paintSettings.isUseRealWidth() && 125 paintSettings.isShowOnewayArrow() && primitive.hasDirectionKeys(); 126 boolean onewayReversed = primitive.reversedDirection(); 127 /* head only takes over control if the option is true, 128 the direction should be shown at all and not only because it's selected */ 129 boolean showOnlyHeadArrowOnly = showOrientation && !selected && paintSettings.isShowHeadArrowOnly(); 130 Node lastN; 131 132 Color myDashedColor = dashesBackground; 133 BasicStroke myLine = line, myDashLine = dashesLine; 134 if (realWidth > 0 && paintSettings.isUseRealWidth() && !showOrientation) { 135 float myWidth = (int) (100 / (float) (painter.getCircum() / realWidth)); 136 if (myWidth < line.getLineWidth()) { 137 myWidth = line.getLineWidth(); 138 } 139 myLine = new BasicStroke(myWidth, line.getEndCap(), line.getLineJoin(), 140 line.getMiterLimit(), line.getDashArray(), line.getDashPhase()); 141 if (dashesLine != null) { 142 myDashLine = new BasicStroke(myWidth, dashesLine.getEndCap(), dashesLine.getLineJoin(), 143 dashesLine.getMiterLimit(), dashesLine.getDashArray(), dashesLine.getDashPhase()); 144 } 145 } 146 147 Color myColor = color; 148 if (defaultSelectedHandling && selected) { 149 myColor = paintSettings.getSelectedColor(color.getAlpha()); 150 } else if (member || outermember) { 151 myColor = paintSettings.getRelationSelectedColor(color.getAlpha()); 152 } else if (primitive.isDisabled()) { 153 myColor = paintSettings.getInactiveColor(); 154 myDashedColor = paintSettings.getInactiveColor(); 155 } 156 157 if (primitive instanceof Way) { 158 Way w = (Way) primitive; 159 painter.drawWay(w, myColor, myLine, myDashLine, myDashedColor, offset, showOrientation, 160 showOnlyHeadArrowOnly, showOneway, onewayReversed); 161 162 if ((paintSettings.isShowOrderNumber() || (paintSettings.isShowOrderNumberOnSelectedWay() && selected)) 163 && !painter.isInactiveMode()) { 164 int orderNumber = 0; 165 lastN = null; 166 for (Node n : w.getNodes()) { 167 if (lastN != null) { 168 orderNumber++; 169 painter.drawOrderNumber(lastN, n, orderNumber, myColor); 170 } 171 lastN = n; 172 } 173 } 174 } 175 } 176 177 @Override 178 public boolean isProperLineStyle() { 179 return !isModifier; 180 } 181 182 /** 183 * Converts a linejoin of a {@link BasicStroke} to a MapCSS string 184 * @param linejoin The linejoin 185 * @return The MapCSS string or <code>null</code> on error. 186 * @see BasicStroke#getLineJoin() 187 */ 188 public String linejoinToString(int linejoin) { 189 switch (linejoin) { 190 case BasicStroke.JOIN_BEVEL: return "bevel"; 191 case BasicStroke.JOIN_ROUND: return "round"; 192 case BasicStroke.JOIN_MITER: return "miter"; 193 default: return null; 194 } 195 } 196 197 /** 198 * Converts a linecap of a {@link BasicStroke} to a MapCSS string 199 * @param linecap The linecap 200 * @return The MapCSS string or <code>null</code> on error. 201 * @see BasicStroke#getEndCap() 202 */ 203 public String linecapToString(int linecap) { 204 switch (linecap) { 205 case BasicStroke.CAP_BUTT: return "none"; 206 case BasicStroke.CAP_ROUND: return "round"; 207 case BasicStroke.CAP_SQUARE: return "square"; 208 default: return null; 209 } 210 } 211 212 @Override 213 public boolean equals(Object obj) { 214 if (obj == null || getClass() != obj.getClass()) 215 return false; 216 if (!super.equals(obj)) 217 return false; 218 final LineElement other = (LineElement) obj; 219 return offset == other.offset && 220 realWidth == other.realWidth && 221 wayDirectionArrows == other.wayDirectionArrows && 222 Objects.equals(line, other.line) && 223 Objects.equals(color, other.color) && 224 Objects.equals(dashesLine, other.dashesLine) && 225 Objects.equals(dashesBackground, other.dashesBackground); 226 } 227 228 @Override 229 public int hashCode() { 230 return Objects.hash(super.hashCode(), line, color, dashesBackground, offset, realWidth, wayDirectionArrows, dashesLine); 231 } 232 233 @Override 234 public String toString() { 235 return "LineElemStyle{" + super.toString() + "width=" + line.getLineWidth() + 236 " realWidth=" + realWidth + " color=" + Utils.toString(color) + 237 " dashed=" + Arrays.toString(line.getDashArray()) + 238 (line.getDashPhase() == 0 ? "" : " dashesOffses=" + line.getDashPhase()) + 239 " dashedColor=" + Utils.toString(dashesBackground) + 240 " linejoin=" + linejoinToString(line.getLineJoin()) + 241 " linecap=" + linecapToString(line.getEndCap()) + 242 (offset == 0 ? "" : " offset=" + offset) + 243 '}'; 244 } 245 246 /** 247 * Creates a simple line with default widt. 248 * @param color The color to use 249 * @param isAreaEdge If this is an edge for an area. Edges are drawn at lower Z-Index. 250 * @return The line style. 251 */ 252 public static LineElement createSimpleLineStyle(Color color, boolean isAreaEdge) { 253 MultiCascade mc = new MultiCascade(); 254 Cascade c = mc.getOrCreateCascade("default"); 255 c.put(WIDTH, Keyword.DEFAULT); 256 c.put(COLOR, color != null ? color : PaintColors.UNTAGGED.get()); 257 c.put(OPACITY, 1f); 258 if (isAreaEdge) { 259 c.put(Z_INDEX, -3f); 260 } 261 Way w = new Way(); 262 return createLine(new Environment(w, mc, "default", null)); 263 } 264 265 /** 266 * Create a line element from the given MapCSS environment 267 * @param env The environment 268 * @return The line element describing the line that should be painted, or <code>null</code> if none should be painted. 269 */ 270 public static LineElement createLine(Environment env) { 271 return createImpl(env, LineType.NORMAL); 272 } 273 274 /** 275 * Create a line element for the left casing from the given MapCSS environment 276 * @param env The environment 277 * @return The line element describing the line that should be painted, or <code>null</code> if none should be painted. 278 */ 279 public static LineElement createLeftCasing(Environment env) { 280 LineElement leftCasing = createImpl(env, LineType.LEFT_CASING); 281 if (leftCasing != null) { 282 leftCasing.isModifier = true; 283 } 284 return leftCasing; 285 } 286 287 /** 288 * Create a line element for the right casing from the given MapCSS environment 289 * @param env The environment 290 * @return The line element describing the line that should be painted, or <code>null</code> if none should be painted. 291 */ 292 public static LineElement createRightCasing(Environment env) { 293 LineElement rightCasing = createImpl(env, LineType.RIGHT_CASING); 294 if (rightCasing != null) { 295 rightCasing.isModifier = true; 296 } 297 return rightCasing; 298 } 299 300 /** 301 * Create a line element for the casing from the given MapCSS environment 302 * @param env The environment 303 * @return The line element describing the line that should be painted, or <code>null</code> if none should be painted. 304 */ 305 public static LineElement createCasing(Environment env) { 306 LineElement casing = createImpl(env, LineType.CASING); 307 if (casing != null) { 308 casing.isModifier = true; 309 } 310 return casing; 311 } 312 313 private static LineElement createImpl(Environment env, LineType type) { 314 Cascade c = env.mc.getCascade(env.layer); 315 Cascade cDef = env.mc.getCascade("default"); 316 Float width = computeWidth(type, c, cDef); 317 if (width == null) 318 return null; 319 320 float realWidth = computeRealWidth(env, type, c); 321 322 Float offset = computeOffset(type, c, cDef, width); 323 324 int alpha = 255; 325 Color color = c.get(type.prefix + COLOR, null, Color.class); 326 if (color != null) { 327 alpha = color.getAlpha(); 328 } 329 if (type == LineType.NORMAL && color == null) { 330 color = c.get(FILL_COLOR, null, Color.class); 331 } 332 if (color == null) { 333 color = PaintColors.UNTAGGED.get(); 334 } 335 336 Integer pAlpha = Utils.colorFloat2int(c.get(type.prefix + OPACITY, null, Float.class)); 337 if (pAlpha != null) { 338 alpha = pAlpha; 339 } 340 color = new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha); 341 342 float[] dashes = c.get(type.prefix + DASHES, null, float[].class, true); 343 if (dashes != null) { 344 boolean hasPositive = false; 345 for (float f : dashes) { 346 if (f > 0) { 347 hasPositive = true; 348 } 349 if (f < 0) { 350 dashes = null; 351 break; 352 } 353 } 354 if (!hasPositive || (dashes != null && dashes.length == 0)) { 355 dashes = null; 356 } 357 } 358 float dashesOffset = c.get(type.prefix + DASHES_OFFSET, 0f, Float.class); 359 if (dashesOffset < 0f) { 360 Logging.warn("Found negative " + DASHES_OFFSET + ": " + dashesOffset); 361 dashesOffset = 0f; 362 } 363 Color dashesBackground = c.get(type.prefix + DASHES_BACKGROUND_COLOR, null, Color.class); 364 if (dashesBackground != null) { 365 pAlpha = Utils.colorFloat2int(c.get(type.prefix + DASHES_BACKGROUND_OPACITY, null, Float.class)); 366 if (pAlpha != null) { 367 alpha = pAlpha; 368 } 369 dashesBackground = new Color(dashesBackground.getRed(), dashesBackground.getGreen(), 370 dashesBackground.getBlue(), alpha); 371 } 372 373 Integer cap = null; 374 Keyword capKW = c.get(type.prefix + LINECAP, null, Keyword.class); 375 if (capKW != null) { 376 if ("none".equals(capKW.val)) { 377 cap = BasicStroke.CAP_BUTT; 378 } else if ("round".equals(capKW.val)) { 379 cap = BasicStroke.CAP_ROUND; 380 } else if ("square".equals(capKW.val)) { 381 cap = BasicStroke.CAP_SQUARE; 382 } 383 } 384 if (cap == null) { 385 cap = dashes != null ? BasicStroke.CAP_BUTT : BasicStroke.CAP_ROUND; 386 } 387 388 Integer join = null; 389 Keyword joinKW = c.get(type.prefix + LINEJOIN, null, Keyword.class); 390 if (joinKW != null) { 391 if ("round".equals(joinKW.val)) { 392 join = BasicStroke.JOIN_ROUND; 393 } else if ("miter".equals(joinKW.val)) { 394 join = BasicStroke.JOIN_MITER; 395 } else if ("bevel".equals(joinKW.val)) { 396 join = BasicStroke.JOIN_BEVEL; 397 } 398 } 399 if (join == null) { 400 join = BasicStroke.JOIN_ROUND; 401 } 402 403 float miterlimit = c.get(type.prefix + MITERLIMIT, 10f, Float.class); 404 if (miterlimit < 1f) { 405 miterlimit = 10f; 406 } 407 408 BasicStroke line = new BasicStroke(width, cap, join, miterlimit, dashes, dashesOffset); 409 BasicStroke dashesLine = null; 410 411 if (dashes != null && dashesBackground != null) { 412 float[] dashes2 = new float[dashes.length]; 413 System.arraycopy(dashes, 0, dashes2, 1, dashes.length - 1); 414 dashes2[0] = dashes[dashes.length-1]; 415 dashesLine = new BasicStroke(width, cap, join, miterlimit, dashes2, dashes2[0] + dashesOffset); 416 } 417 418 boolean wayDirectionArrows = c.get(type.prefix + WAY_DIRECTION_ARROWS, env.osm.isSelected(), Boolean.class); 419 420 return new LineElement(c, type.defaultMajorZIndex, line, color, dashesLine, dashesBackground, 421 offset, realWidth, wayDirectionArrows); 422 } 423 424 private static Float computeWidth(LineType type, Cascade c, Cascade cDef) { 425 Float width; 426 switch (type) { 427 case NORMAL: 428 width = getWidth(c, WIDTH, getWidth(cDef, WIDTH, null)); 429 break; 430 case CASING: 431 Float casingWidth = c.get(type.prefix + WIDTH, null, Float.class, true); 432 if (casingWidth == null) { 433 RelativeFloat relCasingWidth = c.get(type.prefix + WIDTH, null, RelativeFloat.class, true); 434 if (relCasingWidth != null) { 435 casingWidth = relCasingWidth.val / 2; 436 } 437 } 438 if (casingWidth == null) 439 return null; 440 width = Optional.ofNullable(getWidth(c, WIDTH, getWidth(cDef, WIDTH, null))).orElse(0f) + 2 * casingWidth; 441 break; 442 case LEFT_CASING: 443 case RIGHT_CASING: 444 width = getWidth(c, type.prefix + WIDTH, null); 445 break; 446 default: 447 throw new AssertionError(); 448 } 449 return width; 450 } 451 452 private static float computeRealWidth(Environment env, LineType type, Cascade c) { 453 float realWidth = c.get(type.prefix + REAL_WIDTH, 0f, Float.class); 454 if (realWidth > 0 && MapPaintSettings.INSTANCE.isUseRealWidth()) { 455 456 /* if we have a "width" tag, try use it */ 457 String widthTag = Optional.ofNullable(env.osm.get("width")).orElseGet(() -> env.osm.get("est_width")); 458 if (widthTag != null) { 459 try { 460 realWidth = Float.parseFloat(widthTag); 461 } catch (NumberFormatException nfe) { 462 Logging.warn(nfe); 463 } 464 } 465 } 466 return realWidth; 467 } 468 469 private static Float computeOffset(LineType type, Cascade c, Cascade cDef, Float width) { 470 Float offset = c.get(OFFSET, 0f, Float.class); 471 switch (type) { 472 case NORMAL: 473 break; 474 case CASING: 475 offset += c.get(type.prefix + OFFSET, 0f, Float.class); 476 break; 477 case LEFT_CASING: 478 case RIGHT_CASING: 479 Float baseWidthOnDefault = getWidth(cDef, WIDTH, null); 480 Float baseWidth = getWidth(c, WIDTH, baseWidthOnDefault); 481 if (baseWidth == null || baseWidth < 2f) { 482 baseWidth = 2f; 483 } 484 float casingOffset = c.get(type.prefix + OFFSET, 0f, Float.class); 485 casingOffset += baseWidth / 2 + width / 2; 486 /* flip sign for the right-casing-offset */ 487 if (type == LineType.RIGHT_CASING) { 488 casingOffset *= -1f; 489 } 490 offset += casingOffset; 491 break; 492 } 493 return offset; 494 } 495}