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