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