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