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.INode; 011import org.openstreetmap.josm.data.osm.IPrimitive; 012import org.openstreetmap.josm.data.osm.IWay; 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.Cascade; 018import org.openstreetmap.josm.gui.mappaint.Environment; 019import org.openstreetmap.josm.gui.mappaint.Keyword; 020import org.openstreetmap.josm.gui.mappaint.MultiCascade; 021import org.openstreetmap.josm.gui.mappaint.mapcss.Instruction.RelativeFloat; 022import org.openstreetmap.josm.tools.Logging; 023import org.openstreetmap.josm.tools.Utils; 024 025/** 026 * This is the style definition for a simple line. 027 */ 028public class LineElement extends StyleElement { 029 /** 030 * The default style for any untagged way. 031 */ 032 public static final LineElement UNTAGGED_WAY = createSimpleLineStyle(null, false); 033 034 /** 035 * The stroke used to paint the line 036 */ 037 private final BasicStroke line; 038 /** 039 * The color of the line. Should not be accessed directly 040 */ 041 public Color color; 042 043 /** 044 * The stroke used to paint the gaps between the dashes 045 */ 046 private final BasicStroke dashesLine; 047 /** 048 * The secondary color of the line that is used for the gaps in dashed lines. Should not be accessed directly 049 */ 050 public Color dashesBackground; 051 /** 052 * The dash offset. Should not be accessed directly 053 */ 054 public float offset; 055 /** 056 * the real width of this line in meter. Should not be accessed directly 057 */ 058 public float realWidth; 059 /** 060 * A flag indicating if the direction arrwos should be painted. Should not be accessed directly 061 */ 062 public boolean wayDirectionArrows; 063 064 /** 065 * The type of this line 066 */ 067 public enum LineType { 068 /** 069 * A normal line 070 */ 071 NORMAL("", 3f), 072 /** 073 * A casing (line behind normal line, extended to the right/left) 074 */ 075 CASING("casing-", 2f), 076 /** 077 * A casing, but only to the left 078 */ 079 LEFT_CASING("left-casing-", 2.1f), 080 /** 081 * A casing, but only to the right 082 */ 083 RIGHT_CASING("right-casing-", 2.1f); 084 085 /** 086 * The MapCSS line prefix used 087 */ 088 public final String prefix; 089 /** 090 * The major z index to use during painting 091 */ 092 public final float defaultMajorZIndex; 093 094 LineType(String prefix, float defaultMajorZindex) { 095 this.prefix = prefix; 096 this.defaultMajorZIndex = defaultMajorZindex; 097 } 098 } 099 100 protected LineElement(Cascade c, float defaultMajorZindex, BasicStroke line, Color color, BasicStroke dashesLine, 101 Color dashesBackground, float offset, float realWidth, boolean wayDirectionArrows) { 102 super(c, defaultMajorZindex); 103 this.line = line; 104 this.color = color; 105 this.dashesLine = dashesLine; 106 this.dashesBackground = dashesBackground; 107 this.offset = offset; 108 this.realWidth = realWidth; 109 this.wayDirectionArrows = wayDirectionArrows; 110 } 111 112 @Override 113 public void paintPrimitive(IPrimitive primitive, MapPaintSettings paintSettings, StyledMapRenderer painter, 114 boolean selected, boolean outermember, boolean member) { 115 /* show direction arrows, if draw.segment.relevant_directions_only is not set, 116 the way is tagged with a direction key 117 (even if the tag is negated as in oneway=false) or the way is selected */ 118 boolean showOrientation; 119 if (defaultSelectedHandling) { 120 showOrientation = !isModifier && (selected || paintSettings.isShowDirectionArrow()) && !paintSettings.isUseRealWidth(); 121 } else { 122 showOrientation = wayDirectionArrows; 123 } 124 boolean showOneway = !isModifier && !selected && 125 !paintSettings.isUseRealWidth() && 126 paintSettings.isShowOnewayArrow() && primitive.hasDirectionKeys(); 127 boolean onewayReversed = primitive.reversedDirection(); 128 /* head only takes over control if the option is true, 129 the direction should be shown at all and not only because it's selected */ 130 boolean showOnlyHeadArrowOnly = showOrientation && !selected && paintSettings.isShowHeadArrowOnly(); 131 INode lastN; 132 133 Color myDashedColor = dashesBackground; 134 BasicStroke myLine = line, myDashLine = dashesLine; 135 if (realWidth > 0 && paintSettings.isUseRealWidth() && !showOrientation) { 136 float myWidth = (int) (100 / (float) (painter.getCircum() / realWidth)); 137 if (myWidth < line.getLineWidth()) { 138 myWidth = line.getLineWidth(); 139 } 140 myLine = new BasicStroke(myWidth, line.getEndCap(), line.getLineJoin(), 141 line.getMiterLimit(), line.getDashArray(), line.getDashPhase()); 142 if (dashesLine != null) { 143 myDashLine = new BasicStroke(myWidth, dashesLine.getEndCap(), dashesLine.getLineJoin(), 144 dashesLine.getMiterLimit(), dashesLine.getDashArray(), dashesLine.getDashPhase()); 145 } 146 } 147 148 Color myColor = color; 149 if (defaultSelectedHandling && selected) { 150 myColor = paintSettings.getSelectedColor(color.getAlpha()); 151 } else if (member || outermember) { 152 myColor = paintSettings.getRelationSelectedColor(color.getAlpha()); 153 } else if (primitive.isDisabled()) { 154 myColor = paintSettings.getInactiveColor(); 155 myDashedColor = paintSettings.getInactiveColor(); 156 } 157 158 if (primitive instanceof IWay) { 159 IWay<?> w = (IWay<?>) primitive; 160 painter.drawWay(w, myColor, myLine, myDashLine, myDashedColor, offset, showOrientation, 161 showOnlyHeadArrowOnly, showOneway, onewayReversed); 162 163 if ((paintSettings.isShowOrderNumber() || (paintSettings.isShowOrderNumberOnSelectedWay() && selected)) 164 && !painter.isInactiveMode()) { 165 int orderNumber = 0; 166 lastN = null; 167 for (INode n : w.getNodes()) { 168 if (lastN != null) { 169 orderNumber++; 170 painter.drawOrderNumber(lastN, n, orderNumber, myColor); 171 } 172 lastN = n; 173 } 174 } 175 } 176 } 177 178 @Override 179 public boolean isProperLineStyle() { 180 return !isModifier; 181 } 182 183 /** 184 * Converts a linejoin of a {@link BasicStroke} to a MapCSS string 185 * @param linejoin The linejoin 186 * @return The MapCSS string or <code>null</code> on error. 187 * @see BasicStroke#getLineJoin() 188 */ 189 public String linejoinToString(int linejoin) { 190 switch (linejoin) { 191 case BasicStroke.JOIN_BEVEL: return "bevel"; 192 case BasicStroke.JOIN_ROUND: return "round"; 193 case BasicStroke.JOIN_MITER: return "miter"; 194 default: return null; 195 } 196 } 197 198 /** 199 * Converts a linecap of a {@link BasicStroke} to a MapCSS string 200 * @param linecap The linecap 201 * @return The MapCSS string or <code>null</code> on error. 202 * @see BasicStroke#getEndCap() 203 */ 204 public String linecapToString(int linecap) { 205 switch (linecap) { 206 case BasicStroke.CAP_BUTT: return "none"; 207 case BasicStroke.CAP_ROUND: return "round"; 208 case BasicStroke.CAP_SQUARE: return "square"; 209 default: return null; 210 } 211 } 212 213 @Override 214 public boolean equals(Object obj) { 215 if (obj == null || getClass() != obj.getClass()) 216 return false; 217 if (!super.equals(obj)) 218 return false; 219 final LineElement other = (LineElement) obj; 220 return offset == other.offset && 221 realWidth == other.realWidth && 222 wayDirectionArrows == other.wayDirectionArrows && 223 Objects.equals(line, other.line) && 224 Objects.equals(color, other.color) && 225 Objects.equals(dashesLine, other.dashesLine) && 226 Objects.equals(dashesBackground, other.dashesBackground); 227 } 228 229 @Override 230 public int hashCode() { 231 return Objects.hash(super.hashCode(), line, color, dashesBackground, offset, realWidth, wayDirectionArrows, dashesLine); 232 } 233 234 @Override 235 public String toString() { 236 return "LineElemStyle{" + super.toString() + "width=" + line.getLineWidth() + 237 " realWidth=" + realWidth + " color=" + Utils.toString(color) + 238 " dashed=" + Arrays.toString(line.getDashArray()) + 239 (line.getDashPhase() == 0 ? "" : " dashesOffses=" + line.getDashPhase()) + 240 " dashedColor=" + Utils.toString(dashesBackground) + 241 " linejoin=" + linejoinToString(line.getLineJoin()) + 242 " linecap=" + linecapToString(line.getEndCap()) + 243 (offset == 0 ? "" : " offset=" + offset) + 244 '}'; 245 } 246 247 /** 248 * Creates a simple line with default width. 249 * @param color The color to use 250 * @param isAreaEdge If this is an edge for an area. Edges are drawn at lower Z-Index. 251 * @return The line style. 252 */ 253 public static LineElement createSimpleLineStyle(Color color, boolean isAreaEdge) { 254 MultiCascade mc = new MultiCascade(); 255 Cascade c = mc.getOrCreateCascade("default"); 256 c.put(WIDTH, Keyword.DEFAULT); 257 c.put(COLOR, color != null ? color : PaintColors.UNTAGGED.get()); 258 c.put(OPACITY, 1f); 259 if (isAreaEdge) { 260 c.put(Z_INDEX, -3f); 261 } 262 return createLine(new Environment(new Way(), 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}