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