001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.mappaint; 003 004import java.awt.BasicStroke; 005import java.awt.Color; 006import java.awt.Rectangle; 007import java.awt.Stroke; 008import java.util.Objects; 009 010import org.openstreetmap.josm.Main; 011import org.openstreetmap.josm.data.osm.Node; 012import org.openstreetmap.josm.data.osm.OsmPrimitive; 013import org.openstreetmap.josm.data.osm.Relation; 014import org.openstreetmap.josm.data.osm.visitor.paint.MapPaintSettings; 015import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer; 016import org.openstreetmap.josm.gui.mappaint.BoxTextElemStyle.BoxProvider; 017import org.openstreetmap.josm.gui.mappaint.BoxTextElemStyle.SimpleBoxProvider; 018import org.openstreetmap.josm.gui.mappaint.MapPaintStyles.IconReference; 019import org.openstreetmap.josm.gui.mappaint.StyleCache.StyleList; 020import org.openstreetmap.josm.gui.util.RotationAngle; 021import org.openstreetmap.josm.tools.Utils; 022 023/** 024 * applies for Nodes and turn restriction relations 025 */ 026public class NodeElemStyle extends ElemStyle implements StyleKeys { 027 public final MapImage mapImage; 028 public final RotationAngle mapImageAngle; 029 public final Symbol symbol; 030 031 public enum SymbolShape { SQUARE, CIRCLE, TRIANGLE, PENTAGON, HEXAGON, HEPTAGON, OCTAGON, NONAGON, DECAGON } 032 033 public static class Symbol { 034 public SymbolShape symbol; 035 public int size; 036 public Stroke stroke; 037 public Color strokeColor; 038 public Color fillColor; 039 040 public Symbol(SymbolShape symbol, int size, Stroke stroke, Color strokeColor, Color fillColor) { 041 if (stroke != null && strokeColor == null) 042 throw new IllegalArgumentException("Stroke given without color"); 043 if (stroke == null && fillColor == null) 044 throw new IllegalArgumentException("Either a stroke or a fill color must be given"); 045 this.symbol = symbol; 046 this.size = size; 047 this.stroke = stroke; 048 this.strokeColor = strokeColor; 049 this.fillColor = fillColor; 050 } 051 052 @Override 053 public boolean equals(Object obj) { 054 if (obj == null || getClass() != obj.getClass()) 055 return false; 056 final Symbol other = (Symbol) obj; 057 return symbol == other.symbol && 058 size == other.size && 059 Objects.equals(stroke, other.stroke) && 060 Objects.equals(strokeColor, other.strokeColor) && 061 Objects.equals(fillColor, other.fillColor); 062 } 063 064 @Override 065 public int hashCode() { 066 int hash = 7; 067 hash = 67 * hash + symbol.hashCode(); 068 hash = 67 * hash + size; 069 hash = 67 * hash + (stroke != null ? stroke.hashCode() : 0); 070 hash = 67 * hash + (strokeColor != null ? strokeColor.hashCode() : 0); 071 hash = 67 * hash + (fillColor != null ? fillColor.hashCode() : 0); 072 return hash; 073 } 074 075 @Override 076 public String toString() { 077 return "symbol=" + symbol + " size=" + size + 078 (stroke != null ? " stroke=" + stroke + " strokeColor=" + strokeColor : "") + 079 (fillColor != null ? " fillColor=" + fillColor : ""); 080 } 081 } 082 083 public static final NodeElemStyle SIMPLE_NODE_ELEMSTYLE; 084 public static final BoxProvider SIMPLE_NODE_ELEMSTYLE_BOXPROVIDER; 085 static { 086 MultiCascade mc = new MultiCascade(); 087 mc.getOrCreateCascade("default"); 088 SIMPLE_NODE_ELEMSTYLE = create(new Environment(null, mc, "default", null), 4.1f, true); 089 if (SIMPLE_NODE_ELEMSTYLE == null) throw new AssertionError(); 090 SIMPLE_NODE_ELEMSTYLE_BOXPROVIDER = SIMPLE_NODE_ELEMSTYLE.getBoxProvider(); 091 } 092 093 public static final StyleList DEFAULT_NODE_STYLELIST = new StyleList(NodeElemStyle.SIMPLE_NODE_ELEMSTYLE); 094 public static final StyleList DEFAULT_NODE_STYLELIST_TEXT = new StyleList(NodeElemStyle.SIMPLE_NODE_ELEMSTYLE, 095 BoxTextElemStyle.SIMPLE_NODE_TEXT_ELEMSTYLE); 096 097 protected NodeElemStyle(Cascade c, MapImage mapImage, Symbol symbol, float default_major_z_index, RotationAngle rotationAngle) { 098 super(c, default_major_z_index); 099 this.mapImage = mapImage; 100 this.symbol = symbol; 101 this.mapImageAngle = rotationAngle; 102 } 103 104 public static NodeElemStyle create(Environment env) { 105 return create(env, 4f, false); 106 } 107 108 private static NodeElemStyle create(Environment env, float default_major_z_index, boolean allowDefault) { 109 Cascade c = env.mc.getCascade(env.layer); 110 111 MapImage mapImage = createIcon(env, ICON_KEYS); 112 Symbol symbol = null; 113 if (mapImage == null) { 114 symbol = createSymbol(env); 115 } 116 RotationAngle rotationAngle = null; 117 final Float angle = c.get(ICON_ROTATION, null, Float.class, true); 118 if (angle != null) { 119 rotationAngle = RotationAngle.buildStaticRotation(angle); 120 } else { 121 final Keyword rotationKW = c.get(ICON_ROTATION, null, Keyword.class); 122 if (rotationKW != null) { 123 if ("way".equals(rotationKW.val)) { 124 rotationAngle = RotationAngle.buildWayDirectionRotation(); 125 } else { 126 try { 127 rotationAngle = RotationAngle.buildStaticRotation(rotationKW.val); 128 } catch (IllegalArgumentException ignore) { 129 if (Main.isTraceEnabled()) { 130 Main.trace(ignore.getMessage()); 131 } 132 } 133 } 134 } 135 } 136 137 // optimization: if we neither have a symbol, nor a mapImage 138 // we don't have to check for the remaining style properties and we don't 139 // have to allocate a node element style. 140 if (!allowDefault && symbol == null && mapImage == null) return null; 141 142 return new NodeElemStyle(c, mapImage, symbol, default_major_z_index, rotationAngle); 143 } 144 145 public static MapImage createIcon(final Environment env, final String[] keys) { 146 Cascade c = env.mc.getCascade(env.layer); 147 148 final IconReference iconRef = c.get(keys[ICON_IMAGE_IDX], null, IconReference.class, true); 149 if (iconRef == null) 150 return null; 151 152 Cascade c_def = env.mc.getCascade("default"); 153 154 Float widthOnDefault = c_def.get(keys[ICON_WIDTH_IDX], null, Float.class); 155 if (widthOnDefault != null && widthOnDefault <= 0) { 156 widthOnDefault = null; 157 } 158 Float widthF = getWidth(c, keys[ICON_WIDTH_IDX], widthOnDefault); 159 160 Float heightOnDefault = c_def.get(keys[ICON_HEIGHT_IDX], null, Float.class); 161 if (heightOnDefault != null && heightOnDefault <= 0) { 162 heightOnDefault = null; 163 } 164 Float heightF = getWidth(c, keys[ICON_HEIGHT_IDX], heightOnDefault); 165 166 int width = widthF == null ? -1 : Math.round(widthF); 167 int height = heightF == null ? -1 : Math.round(heightF); 168 169 float offsetXF = 0f; 170 float offsetYF = 0f; 171 if (keys[ICON_OFFSET_X_IDX] != null) { 172 offsetXF = c.get(keys[ICON_OFFSET_X_IDX], 0f, Float.class); 173 offsetYF = c.get(keys[ICON_OFFSET_Y_IDX], 0f, Float.class); 174 } 175 176 final MapImage mapImage = new MapImage(iconRef.iconName, iconRef.source); 177 178 mapImage.width = width; 179 mapImage.height = height; 180 mapImage.offsetX = Math.round(offsetXF); 181 mapImage.offsetY = Math.round(offsetYF); 182 183 mapImage.alpha = Math.min(255, Math.max(0, Integer.valueOf(Main.pref.getInteger("mappaint.icon-image-alpha", 255)))); 184 Integer pAlpha = Utils.color_float2int(c.get(keys[ICON_OPACITY_IDX], null, float.class)); 185 if (pAlpha != null) { 186 mapImage.alpha = pAlpha; 187 } 188 return mapImage; 189 } 190 191 private static Symbol createSymbol(Environment env) { 192 Cascade c = env.mc.getCascade(env.layer); 193 Cascade c_def = env.mc.getCascade("default"); 194 195 SymbolShape shape; 196 Keyword shapeKW = c.get("symbol-shape", null, Keyword.class); 197 if (shapeKW == null) 198 return null; 199 if ("square".equals(shapeKW.val)) { 200 shape = SymbolShape.SQUARE; 201 } else if ("circle".equals(shapeKW.val)) { 202 shape = SymbolShape.CIRCLE; 203 } else if ("triangle".equals(shapeKW.val)) { 204 shape = SymbolShape.TRIANGLE; 205 } else if ("pentagon".equals(shapeKW.val)) { 206 shape = SymbolShape.PENTAGON; 207 } else if ("hexagon".equals(shapeKW.val)) { 208 shape = SymbolShape.HEXAGON; 209 } else if ("heptagon".equals(shapeKW.val)) { 210 shape = SymbolShape.HEPTAGON; 211 } else if ("octagon".equals(shapeKW.val)) { 212 shape = SymbolShape.OCTAGON; 213 } else if ("nonagon".equals(shapeKW.val)) { 214 shape = SymbolShape.NONAGON; 215 } else if ("decagon".equals(shapeKW.val)) { 216 shape = SymbolShape.DECAGON; 217 } else 218 return null; 219 220 Float sizeOnDefault = c_def.get("symbol-size", null, Float.class); 221 if (sizeOnDefault != null && sizeOnDefault <= 0) { 222 sizeOnDefault = null; 223 } 224 Float size = getWidth(c, "symbol-size", sizeOnDefault); 225 226 if (size == null) { 227 size = 10f; 228 } 229 230 if (size <= 0) 231 return null; 232 233 Float strokeWidthOnDefault = getWidth(c_def, "symbol-stroke-width", null); 234 Float strokeWidth = getWidth(c, "symbol-stroke-width", strokeWidthOnDefault); 235 236 Color strokeColor = c.get("symbol-stroke-color", null, Color.class); 237 238 if (strokeWidth == null && strokeColor != null) { 239 strokeWidth = 1f; 240 } else if (strokeWidth != null && strokeColor == null) { 241 strokeColor = Color.ORANGE; 242 } 243 244 Stroke stroke = null; 245 if (strokeColor != null) { 246 Integer strokeAlpha = Utils.color_float2int(c.get("symbol-stroke-opacity", null, Float.class)); 247 if (strokeAlpha != null) { 248 strokeColor = new Color(strokeColor.getRed(), strokeColor.getGreen(), 249 strokeColor.getBlue(), strokeAlpha); 250 } 251 stroke = new BasicStroke(strokeWidth); 252 } 253 254 Color fillColor = c.get("symbol-fill-color", null, Color.class); 255 if (stroke == null && fillColor == null) { 256 fillColor = Color.BLUE; 257 } 258 259 if (fillColor != null) { 260 Integer fillAlpha = Utils.color_float2int(c.get("symbol-fill-opacity", null, Float.class)); 261 if (fillAlpha != null) { 262 fillColor = new Color(fillColor.getRed(), fillColor.getGreen(), 263 fillColor.getBlue(), fillAlpha); 264 } 265 } 266 267 return new Symbol(shape, Math.round(size), stroke, strokeColor, fillColor); 268 } 269 270 @Override 271 public void paintPrimitive(OsmPrimitive primitive, MapPaintSettings settings, StyledMapRenderer painter, 272 boolean selected, boolean outermember, boolean member) { 273 if (primitive instanceof Node) { 274 Node n = (Node) primitive; 275 if (mapImage != null && painter.isShowIcons()) { 276 painter.drawNodeIcon(n, mapImage, painter.isInactiveMode() || n.isDisabled(), selected, member, 277 mapImageAngle == null ? 0.0 : mapImageAngle.getRotationAngle(primitive)); 278 } else if (symbol != null) { 279 Color fillColor = symbol.fillColor; 280 if (fillColor != null) { 281 if (painter.isInactiveMode() || n.isDisabled()) { 282 fillColor = settings.getInactiveColor(); 283 } else if (selected) { 284 fillColor = settings.getSelectedColor(fillColor.getAlpha()); 285 } else if (member) { 286 fillColor = settings.getRelationSelectedColor(fillColor.getAlpha()); 287 } 288 } 289 Color strokeColor = symbol.strokeColor; 290 if (strokeColor != null) { 291 if (painter.isInactiveMode() || n.isDisabled()) { 292 strokeColor = settings.getInactiveColor(); 293 } else if (selected) { 294 strokeColor = settings.getSelectedColor(strokeColor.getAlpha()); 295 } else if (member) { 296 strokeColor = settings.getRelationSelectedColor(strokeColor.getAlpha()); 297 } 298 } 299 painter.drawNodeSymbol(n, symbol, fillColor, strokeColor); 300 } else { 301 Color color; 302 boolean isConnection = n.isConnectionNode(); 303 304 if (painter.isInactiveMode() || n.isDisabled()) { 305 color = settings.getInactiveColor(); 306 } else if (selected) { 307 color = settings.getSelectedColor(); 308 } else if (member) { 309 color = settings.getRelationSelectedColor(); 310 } else if (isConnection) { 311 if (n.isTagged()) { 312 color = settings.getTaggedConnectionColor(); 313 } else { 314 color = settings.getConnectionColor(); 315 } 316 } else { 317 if (n.isTagged()) { 318 color = settings.getTaggedColor(); 319 } else { 320 color = settings.getNodeColor(); 321 } 322 } 323 324 final int size = Utils.max(selected ? settings.getSelectedNodeSize() : 0, 325 n.isTagged() ? settings.getTaggedNodeSize() : 0, 326 isConnection ? settings.getConnectionNodeSize() : 0, 327 settings.getUnselectedNodeSize()); 328 329 final boolean fill = (selected && settings.isFillSelectedNode()) || 330 (n.isTagged() && settings.isFillTaggedNode()) || 331 (isConnection && settings.isFillConnectionNode()) || 332 settings.isFillUnselectedNode(); 333 334 painter.drawNode(n, color, size, fill); 335 336 } 337 } else if (primitive instanceof Relation && mapImage != null) { 338 painter.drawRestriction((Relation) primitive, mapImage, painter.isInactiveMode() || primitive.isDisabled()); 339 } 340 } 341 342 public BoxProvider getBoxProvider() { 343 if (mapImage != null) 344 return mapImage.getBoxProvider(); 345 else if (symbol != null) 346 return new SimpleBoxProvider(new Rectangle(-symbol.size/2, -symbol.size/2, symbol.size, symbol.size)); 347 else { 348 // This is only executed once, so no performance concerns. 349 // However, it would be better, if the settings could be changed at runtime. 350 int size = Utils.max( 351 Main.pref.getInteger("mappaint.node.selected-size", 5), 352 Main.pref.getInteger("mappaint.node.unselected-size", 3), 353 Main.pref.getInteger("mappaint.node.connection-size", 5), 354 Main.pref.getInteger("mappaint.node.tagged-size", 3) 355 ); 356 return new SimpleBoxProvider(new Rectangle(-size/2, -size/2, size, size)); 357 } 358 } 359 360 @Override 361 public int hashCode() { 362 int hash = super.hashCode(); 363 hash = 17 * hash + (mapImage != null ? mapImage.hashCode() : 0); 364 hash = 17 * hash + (symbol != null ? symbol.hashCode() : 0); 365 hash = 17 * hash + (mapImageAngle != null ? mapImageAngle.hashCode() : 0); 366 return hash; 367 } 368 369 @Override 370 public boolean equals(Object obj) { 371 if (obj == null || getClass() != obj.getClass()) 372 return false; 373 if (!super.equals(obj)) 374 return false; 375 376 final NodeElemStyle other = (NodeElemStyle) obj; 377 // we should get the same image object due to caching 378 if (!Objects.equals(mapImage, other.mapImage)) 379 return false; 380 if (!Objects.equals(symbol, other.symbol)) 381 return false; 382 if (!Objects.equals(mapImageAngle, other.mapImageAngle)) 383 return false; 384 return true; 385 } 386 387 @Override 388 public String toString() { 389 StringBuilder s = new StringBuilder("NodeElemStyle{"); 390 s.append(super.toString()); 391 if (mapImage != null) { 392 s.append(" icon=[" + mapImage + ']'); 393 } 394 if (symbol != null) { 395 s.append(" symbol=[" + symbol + ']'); 396 } 397 if (mapImageAngle != null) { 398 s.append(" mapImageAngle=[" + mapImageAngle + ']'); 399 } 400 s.append('}'); 401 return s.toString(); 402 } 403}